import { AccountAvatarRecord } from "src/app/shared/server-services/query-records/account-records";

// incoming types from the server
export interface ResourceNode {
  id: string;
  name: string;
  avatar: ArrayBuffer | AccountAvatarRecord;
  owner: string;
  news: boolean;
  lastSeenMessageId: string;
  topMessageId: string;
  ordering: number;
  type: ResourceType;
  error: boolean;
  nanoSessions: { [key: string]: number };
}

export interface ResourceGroupNode {
  id: string;
  name: string;
  closed: boolean;
  ordering: number;
  error: boolean;
  resources: ResourceNode[];
}

export enum ResourceType {
  ROOM = "room",
  DIALOGUE = "dialogue",
}

type ResourceIndex = {
  groupIndex: number;
  resourceIndex: number;
};

interface Orderable {
  ordering: number;
}

interface OrderableUnique {
  id;
  ordering: number;
}

export class SidebarData {
  private data: ResourceGroupNode[] = [];
  private ungroupedData: ResourceNode[] = [];

  private isSortOn = true;

  construct() {}

  /**
   * Return the data as a reference. You shouldn't modify the data directly
   * because it can ruin the ordering system. Use this class as an api instead.
   * @returns ResourceGroupNode[]
   */
  public getData() {
    return this.data;
  }

  /**
   * Return the ungrouped data as a reference. You shouldn't modify it directly.
   * @returns ResourceNode[]
   */
  public getUngroupedData() {
    return this.ungroupedData;
  }

  /**
   * iterate over all resource
   * @param cb
   */
  public forEachResources(cb: (res: ResourceNode)=>void) {
    // TODO test
    this.data.forEach((group) => {
      group.resources.forEach((el) => {
        cb(el);
      });
    });
    this.ungroupedData.forEach((el) => {
      cb(el);
    });
  }

  // group functions ---------------------------

  /**
   * Push the group into the data in the right position using its ordering attribute
   * @param group
   * @returns
   */
  public pushGroup(group: ResourceGroupNode) {
    this.push(group, this.data);
  }

  /**
   * Push the group at the first position in the array. Return with the new ordering
   * @param group
   * @returns the ordering of the pushed element
   */
  public pushGroupBefore(group: ResourceGroupNode): number {
    let ordering = this.calcOrderingByPosition(this.data, 0, group["id"]);
    group.ordering = ordering;
    this.push(group, this.data);
    return ordering;
  }

  public removeGroup(groupId) {
    let i = this.getGroupIndexById(groupId);
    if (i > -1) {
      if (this.data[i].resources.length > 0) {
        this.pushResourcesIntoUngrouped(this.data[i].resources);
      }
      this.data.splice(i, 1);
    }
  }

  public editGroupName(groupId, name) {
    let i = this.getGroupIndexById(groupId);
    if (i > -1) {
      this.data[i].name = name;
      // somewhy the group component cannod detect only the name changes
      this.data[i] = Object.assign({}, this.data[i]);
    }
  }

  public editGroupOrdering(groupId, ordering) {
    let i = this.getGroupIndexById(groupId);
    if (i > -1) {
      return this.editOrdering(this.data[i], this.data, ordering);
    }
    console.error("can not find group", groupId);
  }

  /**
   *
   * @param groupId
   * @param closed is the element closed (isClosed)
   */
  public editGroupClosed(groupId, closed: boolean) {
    let i = this.getGroupIndexById(groupId);
    if (i > -1) {
      this.data[i].closed = closed;
    }
  }

  public setGroupError(groupId, error: boolean) {
    let i = this.getGroupIndexById(groupId);
    if (i > -1) {
      this.data[i].error = error;
    }
  }

  /**
   * Push the group into the data at the specific position into the array.
   * It will calculate its new ordering and return with that.
   * @param groupId
   * @param position new destination in the array (from 0 to array length-1)
   * @returns new ordering
   */
  public moveGroup(groupId: string, position: number): number {
    let index = this.getGroupIndexById(groupId);
    if (index != position) {
      let ordering = this.calcOrderingByPosition(this.data, position, groupId);
      this.editGroupOrdering(groupId, ordering);
      return ordering;
    }

    return this.data[index].ordering; // else nothing to change
  }

  // resource funtions -----------------------
  /**
   * Push resource into the group, or into the ungrouped array
   * @param groupId id, or null if ungrouped
   * @param resource
   */
  public pushResource(groupId, resource: ResourceNode) {
    if (groupId != null) {
      let groupIndex = this.getGroupIndexById(groupId);
      if (groupIndex > -1) {
        this.push(resource, this.data[groupIndex].resources);
      } else {
        throw "Can not find group: " + groupId;
      }
    } else {
      this.push(resource, this.ungroupedData);
    }
  }

  /**
   * Push the resource in the first position of the array. Return with its new ordering
   * @param groupId
   * @param resource
   * @returns new ordering of the element
   */
  public pushResourceBefore(groupId, resource: ResourceNode) {
    let groupIndex = this.getGroupIndexById(groupId);
    if (groupIndex > -1) {
      let ordering = this.calcOrderingByPosition(
        this.data[groupIndex].resources,
        0,
        resource["id"]
      );
      this.push(resource, this.data[groupIndex].resources);
      return ordering;
    } else {
      throw "Can not find group: " + groupId;
    }
  }

  public editResourceName(resourceId, name) {
    this.getResourceById(resourceId).name = name;
  }

  public editResourceAvatar(
    resourceId,
    avatar: ArrayBuffer | AccountAvatarRecord
  ) {
    this.getResourceById(resourceId).avatar = avatar;
  }

  public editResourceLastSeenMessage(resourceId, lastSeenMessageId) {
    let res = this.getResourceById(resourceId);
    res.lastSeenMessageId = lastSeenMessageId;
    res.news = res.lastSeenMessageId != res.topMessageId;
  }

  public editResourceTopMessageId(resourceId, topMessageId) {
    let res = this.getResourceById(resourceId);
    res.topMessageId = topMessageId;
    res.news = res.lastSeenMessageId != res.topMessageId;
  }

  public setResourceError(resourceId, error: boolean) {
    this.getResourceById(resourceId).error = error;
  }

  public setResourceNanoSession(
    resourceId,
    nanoSessions: { [key: string]: number }
  ) {
    this.getResourceById(resourceId).nanoSessions = nanoSessions;
  }

  /**
   * Get the resource, remove from the older array, and put into the destination
   * @param resourceId
   * @param groupId new destination group (null if ungrouped)
   * @param ordering new ordering number
   * @returns
   */
  public editResourceOrdering(resourceId, groupId: any, ordering) {
    let index = this.getResourceIndexById(resourceId);

    if (index !== null) {
      // we can find in a group

      let resource = this.data[index.groupIndex].resources[index.resourceIndex];
      if (groupId != this.data[index.groupIndex].id) {
        // remove it from the old container
        this.data[index.groupIndex].resources.splice(index.resourceIndex, 1)[0];
      }

      if (groupId !== null) {
        // put into a group
        return this.editOrdering(
          resource,
          this.data[this.getGroupIndexById(groupId)].resources,
          ordering
        );
      } else {
        // put into ungroup
        return this.editOrdering(resource, this.ungroupedData, ordering);
      }
    } else {
      // we can find it in the ungrouped array
      let index = this.getResourceIndexFromUngroupedById(resourceId);
      if (index > -1) {
        let resource = this.ungroupedData[index];
        if (groupId != null) {
          // the destination is not the ungrouped array
          // remove it from the old container
          this.ungroupedData.splice(index, 1);
          return this.editOrdering(
            resource,
            this.data[this.getGroupIndexById(groupId)].resources,
            ordering
          );
        } else {
          // the destination is the ungrouped array
          return this.editOrdering(resource, this.ungroupedData, ordering);
        }
      }
    }
    console.error("can not find resource", resourceId);
  }

  /**
   *
   * @param resourceId
   * @param groupId
   * @param position
   * @returns new ordering
   */
  public moveResource(resourceId, groupId, position): number {
    let ordering;
    if (groupId == null) {
      ordering = this.calcOrderingByPosition(
        this.ungroupedData,
        position,
        resourceId
      );
    } else {
      let groupIndex = this.getGroupIndexById(groupId);
      if (groupIndex > -1) {
        ordering = this.calcOrderingByPosition(
          this.data[groupIndex].resources,
          position,
          resourceId
        );
      } else {
        throw "Can not find destination group:" + groupId;
      }
    }

    this.editResourceOrdering(resourceId, groupId, ordering);
    return ordering;
  }

  public removeResource(resourceId) {
    let index = this.getResourceIndexById(resourceId);
    if (index !== null) {
      this.data[index.groupIndex].resources.splice(index.resourceIndex, 1);
    } else {
      let index = this.getResourceIndexFromUngroupedById(resourceId);
      if (index > -1) {
        this.ungroupedData.splice(index, 1);
      }
    }
  }

  // ungrouped functions
  private pushResourcesIntoUngrouped(resources: ResourceNode[]) {
    for (let i = 0; i < resources.length; i++) {
      this.push(resources[i], this.ungroupedData);
    }
  }

  // helper functions -----------------------

  private push(content: Orderable, container: Orderable[]) {
    for (let i = 0; i < container.length; i++) {
      if (content.ordering < container[i].ordering) {
        // we should insert it before the next element
        if (i == 0) {
          container.unshift(content);
        } else {
          container.splice(i, 0, content);
        }
        return true;
      }
    }

    // can not fit anywhere, so put it at the end
    container.push(content);
    return true;
  }

  private editOrdering(
    content: Orderable,
    container: Orderable[],
    newOrdering: number
  ) {
    content.ordering = newOrdering;
    let index = container.indexOf(content);
    if (index > -1) {
      // if the element is in the container, we have to remove it first
      container.splice(index, 1);
    }

    this.push(content, container);
  }

  /**
   * Calculate the ordering number by the position where do you want to insert the element
   * @param container array with elements which have ordering attribute
   * @param pos Where do you want to put it in the list (0 means start, arraylength-1 means last)
   * @param ignoreId use the moveable object's id here, the algorithm should ignore that during the calculation
   * @returns ordering number
   */
  private calcOrderingByPosition(
    container: OrderableUnique[],
    pos: number,
    ignoreId
  ): number {
    if (container.length == 0) {
      return 1; // default when you have only 1 group
    }

    if (pos >= container.length) {
      // push after the last element roundup(last)+1
      return Math.ceil(container[container.length - 1].ordering + 1);
    }

    // decide where to put (calculate the ordering)
    if (container[pos].id == ignoreId) {
      return container[pos].ordering; // its already on the right position
    }

    if (pos == 0) {
      // push between 0 and the first element ordering
      return container[0].ordering / 2;
    }

    // push somewhere in the middle (halfway between to elements)

    // check if the element is exist before the position
    for (let i = 0; i < pos; i++) {
      if (container[i].id == ignoreId) {
        pos++; // we have to shift the whole process, or we will move 1 level lower
        break;
      }
    }

    // check the new position too
    if (pos >= container.length) {
      return Math.ceil(container[container.length - 1].ordering + 1);
    }

    let posBefore = container[pos - 1].ordering;
    let posAfter = container[pos].ordering;

    return (posAfter - posBefore) / 2 + posBefore;
  }

  /**
   *
   * @param groupId
   * @returns ResourceGroupNode as a reference or null if not found
   */
  /*public getGroupById(groupId){
        for(let i = 0; i < this.data.length; i++){
            if (this.data[i].id = groupId){
                return this.data[i];
            }
        }
        return null;
    }*/

  private getGroupIndexById(groupId) {
    for (let i = 0; i < this.data.length; i++) {
      if (this.data[i].id == groupId) {
        return i;
      }
    }
    return -1;
  }

  /**
   * get the locaiton info for the resource in the this.data array
   * @param resourceId
   * @returns ResourceIndex if found, null if not found
   */
  private getResourceIndexById(resourceId): ResourceIndex {
    for (let i = 0; i < this.data.length; i++) {
      for (let j = 0; j < this.data[i].resources.length; j++) {
        if (resourceId == this.data[i].resources[j].id) {
          return {
            groupIndex: i,
            resourceIndex: j,
          };
        }
      }
    }
    return null;
  }

  private getResourceIndexFromUngroupedById(resourceId): number {
    for (let i = 0; i < this.ungroupedData.length; i++) {
      if (this.ungroupedData[i].id == resourceId) {
        return i;
      }
    }
    return -1;
  }

  /**
   *
   * @param resourceId
   * @returns reference of resource or null if not found
   */

  public getResourceById(resourceId) {
    let index = this.getResourceIndexById(resourceId);
    if (index !== null) {
      return this.data[index.groupIndex].resources[index.resourceIndex];
    } else {
      let index = this.getResourceIndexFromUngroupedById(resourceId);

      if (index > -1) {
        return this.ungroupedData[index];
      }
    }
    return null;
  }

  /**
   *
   * @param groupId
   * @returns reference of group or null if not found
   */

  public getGroupById(groupId) {
    let index = this.getGroupIndexById(groupId);
    if (index > -1) {
      return this.data[index];
    }

    return null;
  }

  /**
   * We will merge the editResourceOrdering and editResourceGroup callback subscription
   * but now we need these information
   * @param resourceId
   */
  public getResourceParentIdByResourceId(resourceId) {
    let index = this.getResourceIndexById(resourceId);
    if (index !== null) {
      return this.data[index.groupIndex].id;
    } else {
      let index = this.getResourceIndexFromUngroupedById(resourceId);

      if (index > -1) {
        return null; // this is the ID of the ungroup
      }
    }
    throw "can not find resource: " + resourceId;
  }
}
