import { computedFrom } from "aurelia-framework";
import { UIInternal } from "./../../utils/ui-internal";

let NODE_ID = 0;

export class UITreeModel {
  public children: UITreeNode[] = [];
  public nodes: UITreeNode[] = [];
  private autoSort: boolean | ((nodeA: UITreeNode, nodeB: UITreeNode) => number);

  constructor(children: KeyValue[], private maxNodes = 0, autoCheckable = true, autoSort: boolean | ((nodeA: UITreeNode, nodeB: UITreeNode) => number) = true) {
    this.autoSort = autoSort;
    this.children = children.map(child => new UITreeNode(child, null, autoCheckable));
    this.nodes = this.getExpandedTree(this.applyAutoSort(this.children));
  }

  private applyAutoSort(items: UITreeNode[]): UITreeNode[] {
    if (this.autoSort === true) {
      return items.sortBy('label');
    } else if (this.autoSort === false) {
      return items;
    } else if (typeof this.autoSort == 'function') {
      return items.sort(this.autoSort);
    }
    return items;
  }

  public filter(query: string) {
    this.nodes = this.getExpandedTree(this.applyAutoSort(this.filterNodes(this.children, query)));
  }

  public expandNode(nodeId: string) {
    let hierarchy = this.findNodeHierarchy(nodeId, this.nodes);
    hierarchy?.forEach(x => {
      if (x.expanded) {
        return;
      }
      let index = this.nodes.indexOf(x);
      if (index >= 0) {
        this.toggleExpand(index);
      }
    })
  }

  private findNodeHierarchy(nodeId: string, nodes: UITreeNode[]): UITreeNode[] {
    let node = nodes.find(x => x.id == nodeId);
    if (node != null) {
      return [node];
    }
    for (let x of nodes) {
      if (x.children?.length == 0) {
        continue;
      }
      let foundChildren = this.findNodeHierarchy(nodeId, x.children);
      if (foundChildren?.length > 0) {
        foundChildren.splice(0, 0, x);
        return foundChildren;
      }
    }
    return null;
  }

  public toggleExpand(index: number): void {
    const node = this.nodes[index];
    if (false == node.isExpandable) {
      return;
    }

    node.expanded = !node.expanded;

    if (node.expanded) {
      const injectedChildren = this.getExpandedTree([node]);
      this.nodes = [
        ...this.nodes.slice(0, index),
        ...injectedChildren,
        ...this.nodes.slice(index + 1)
      ];
    } else {
      let lastIndex = this.nodes.lastIndex(node.id, "parentId");
      while (this.nodes[lastIndex].expanded) {
        lastIndex = this.nodes.lastIndex(this.nodes[lastIndex].id, "parentId");
      }
      this.nodes = [...this.nodes.slice(0, index + 1), ...this.nodes.slice(lastIndex + 1)];
    }
  }

  public toggleMore(index: number): void {
    const node = this.nodes[index];
    node.showingMore = !node.showingMore;

    const parentIndex = this.nodes.index(node.parentId, "id");
    let injectedChildren = this.applyAutoSort(this.nodes[parentIndex].children);
    if (!node.showingMore) {
      if (injectedChildren.length === 0) {
        injectedChildren.push(new UITreeNode({ id: "node-empty", leaf: true }, node));
      }
      if (this.maxNodes > 0 && injectedChildren.length > this.maxNodes) {
        injectedChildren = [...injectedChildren.slice(0, this.maxNodes)];
      }
    }
    this.nodes = [
      ...this.nodes.slice(0, parentIndex + 1),
      ...injectedChildren,
      this.nodes[index],
      ...this.nodes.slice(index + 1)
    ];
  }

  public getChecked(): UITreeNode[] {
    const checked = [];
    this.getCheckedNodes(this.children, checked);
    return checked;
  }

  private getCheckedNodes(nodes, checked) {
    nodes.forEach(node => {
      if (node.checked === 1) {
        checked.push(node);
      }
      if (node.children) {
        this.getCheckedNodes(node.childNodes, checked);
      }
    });
  }

  private getExpandedTree(children: UITreeNode[]) {
    const nodes = [];
    children.forEach(child => {
      nodes.push(child);
      if (child.expanded) {
        nodes.push(...this.getExpandedTree(this.getChildren(child)));
      }
    });
    return nodes;
  }

  private filterNodes(nodes: UITreeNode[], query: string) {
    let queryNotEmpty = (query || '').length > 0;
    return nodes.filter(child => {
      let retVal = this.nodeMatchesQuery(child, query);
      // Mute node
      child.muted = queryNotEmpty && false == retVal;
      if (!child.leaf) {
        child.filtered = this.filterNodes(child.childNodes, query);
        // Auto expand node if filtered children is not empty ONLY if there is a query
        if (queryNotEmpty && child.filtered.length > 0) {
          child.expanded = true;
        }
        retVal = retVal || child.filtered.length > 0;
      }
      return retVal;
    });
  }

  private nodeMatchesQuery(node: UITreeNode, query: string): boolean {
    return !query || (node.label != null && (node.label as string).ascii().toLocaleLowerCase().includes(query.ascii().toLocaleLowerCase()));
  }

  private getChildren(node: UITreeNode) {
    let injectedChildren = this.applyAutoSort(node.children);
    if (injectedChildren.length === 0) {
      injectedChildren.push(new UITreeNode({ id: "node-empty", leaf: true }, node));
    }
    if (this.maxNodes > 0 && injectedChildren.length > this.maxNodes) {
      injectedChildren = [
        ...injectedChildren.slice(0, this.maxNodes),
        new UITreeNode({ id: "node-more", leaf: true }, node)
      ];
    }
    return injectedChildren;
  }
}

// tslint:disable-next-line
export class UITreeNode {
  @computedFrom("leaf", "icon", "expanded", "iconOpen", "iconClosed")
  get nodeIcon() {
    return (this.leaf ? this.icon : this.expanded ? this.iconOpen : this.iconClosed) || this.icon;
  }

  @computedFrom("checked")
  get checkIcon() {
    return this.checked === 2
      ? "tree-check-half"
      : this.checked
        ? "tree-check-on"
        : "tree-check-off";
  }

  @computedFrom("expanded")
  get expandIcon() {
    return this.expanded ? "digi-move-line-down" : "digi-move-line";
  }

  public id: string;
  public icon: string;
  public label: string;
  public model: KeyValue;

  public iconOpen: string;
  public iconClosed: string;

  public childNodes: UITreeNode[] = [];
  public filtered: UITreeNode[] = null;

  public leaf: boolean;
  public parentId: string;
  public level: number = 0;
  public checked: number = 0;
  public expanded: boolean;
  public selected: boolean;
  public isSelectable: boolean = true;
  public isExpandable: boolean = true;
  public disabled: boolean;
  public showingMore: boolean;
  public muted: boolean = true;

  public actions: UITreeAction[];

  constructor(node: KeyValue, public parent?: UITreeNode, autoCheckable = true) {
    this.id = node.id || `node__${NODE_ID++}`;
    this.label = node.label;
    this.model = node.model;

    this.icon = node.icon;
    this.iconOpen = node.iconOpen;
    this.iconClosed = node.iconClosed;

    this.leaf = !!node.leaf;
    this.expanded = node.expanded;
    // Default selectable to true
    this.isSelectable = node.isSelectable == null ? true : node.isSelectable;
    if (this.isSelectable) {
      this.selected = node.selected;
      if (this.selected) {
        this.toggleCheck(false, autoCheckable);
      }
    } else {
      this.selected = false;
    }
    // Default expandable to true
    this.isExpandable = node.isExpandable == null ? true : node.isExpandable;
    this.disabled = node.disabled;
    this.muted = node.muted;

    this.actions = node.actions;

    if (parent) {
      this.level = parent.level + 1;
      this.parentId = parent.id;
    }

    if (node.children) {
      this.childNodes = node.children.map(child => new UITreeNode(child, this, autoCheckable));
    }
  }

  @computedFrom("filtered", "childNodes")
  get children() {
    return this.filtered || this.childNodes;
  }

  public hasSelectedChild() {
    return this.children.find(c => c.selected || c.hasSelectedChild()) != null;
  }

  public toggleCheck(isInitial: boolean = true, autoCheckable: boolean = true) {
    this.checked = this.checked ? 0 : 1;
    if (isInitial) {
      window.dispatchEvent(UIInternal.createEvent("nodeselection", this.checked));
    }
    if (autoCheckable) {
      this.children.forEach((c: UITreeNode) => {
        c.updateChild("checked", this.checked);
      });
      if (this.parent && this.parent.updateCheckedPartial) {
        this.parent.updateCheckedPartial();
      }
    }
  }

  private updateCheckedPartial() {
    const allChecked = this.children.every(node => node.checked === 1);
    const allUnchecked = this.children.every(node => node.checked === 0);

    this.checked = allChecked ? 1 : allUnchecked ? 0 : 2;
    if (this.parent && this.parent.updateCheckedPartial) {
      this.parent.updateCheckedPartial();
    }
  }

  private updateChild(prop, v) {
    this[prop] = v;
    this.children.forEach((c: UITreeNode) => {
      c.updateChild(prop, v);
    });
  }

  private mainActionsLength = 0;
  private get _mainActions(): UITreeAction[] {
    let actions: UITreeAction[] = [];
    if (this.actions != null) {
      actions = this.actions.filter(action => false == action.asDropdown && (action.display === true || (typeof action.display == 'function' && action.display(this))));
    }
    this.mainActionsLength = actions.length;
    return actions;
  }
  @computedFrom('mainActionsLength')
  public get mainActions(): UITreeAction[] {
    return this._mainActions;
  }

  private dropdownActionsLength = 0;
  private get _dropdownActions(): UITreeAction[] {
    let actions: UITreeAction[] = [];
    if (this.actions != null) {
      actions = this.actions.filter(action => action.asDropdown && (action.display === true || (typeof action.display == 'function' && action.display(this))));
    }
    this.dropdownActionsLength = actions.length;
    return actions;
  }
  @computedFrom('dropdownActionsLength')
  public get dropdownActions(): UITreeAction[] {
    return this._dropdownActions;
  }

  public assignActions(actions: UITreeAction[]): UITreeNode {
    this.actions = actions.map(action => {
      let copy = new UITreeAction(action);
      copy.label = action.getLabel(this);
      return copy;
    });
    return this;
  }
}

export class UITreeAction {
  public label: string;
  public generateLabel: (node: UITreeNode) => string;
  public icon: string;
  public iconColor: string;
  public color: string;
  public classes: string;
  public asDropdown: boolean = true;
  public this: any;
  public fn: (node: UITreeNode) => void;
  public display: boolean | ((node: UITreeNode) => boolean) = true;

  public getLabel(node: UITreeNode): string {
    return this.generateLabel ? this.generateLabel(node) : this.label;
  }

  constructor(init?: Partial<UITreeAction>) {
    Object.assign(this, init);
  }
}
