import { UITreePanelConfig } from './ui-tree-panel-config';
import { I18N } from 'aurelia-i18n';
import { bindable, bindingMode, customElement, inlineView, inject } from "aurelia-framework";
import { UITreeModel, UITreeAction, UITreeNode } from "../ui-tree-model";
import { UIInternal } from "../../../utils/ui-internal";
import { BindingEngine, Disposable } from "aurelia-binding";

@customElement("ui-tree-panel")
@inject(Element, BindingEngine, I18N)
export class UITreePanel<TTargetEntity> {
  @bindable({ defaultBindingMode: bindingMode.twoWay })
  public value: Array<AnyObject> | AnyObject;
  @bindable({ defaultBindingMode: bindingMode.twoWay })
  public model: Array<AnyObject> | AnyObject;

  @bindable({ defaultBindingMode: bindingMode.toView })
  public options: KeyValue[];
  protected treeOptions: UITreeNode[] = [];
  @bindable({ defaultBindingMode: bindingMode.toView })
  public treeConfig: UITreePanelConfig<TTargetEntity>;

  @bindable({ defaultBindingMode: bindingMode.toView })
  public singleLineHeader: boolean = true;
  @bindable({ defaultBindingMode: bindingMode.toView })
  public showCollapseAll: boolean = false;
  @bindable
  public showNodeExpand: boolean = true;

  @bindable()
  public labelSearch: string = this.i18n.tr('treepanel.search', { ns: 'common' });
  @bindable()
  public labelDeselectAll: string = this.i18n.tr('treepanel.deselectAll', { ns: 'common' });
  @bindable()
  public labelEmpty: string = this.i18n.tr('treepanel.empty', { ns: 'common' });
  @bindable()
  public labelLess: string = this.i18n.tr('treepanel.less', { ns: 'common' });
  @bindable()
  public labelMore: string = this.i18n.tr('treepanel.more', { ns: 'common' });

  @bindable() public maxNodes: number;
  @bindable() public checkable: boolean | number = false;
  @bindable() public searchable: boolean = false;
  @bindable() public canRefresh: boolean = false;
  @bindable() public autoCheckable: boolean = true;
  @bindable() public checkableWhenEmpty: boolean = false;
  @bindable() public maxValues: number;

  @bindable({ defaultBindingMode: bindingMode.oneTime }) public exportToCsv: () => void;

  @bindable({ defaultBindingMode: bindingMode.oneTime }) autoSort: boolean | ((nodeA: UITreeNode, nodeB: UITreeNode) => number) = true;

  private rootNode: UITreeModel;
  private disposable: Disposable;
  private valueDisposable: Disposable;
  private searchText: string;

  constructor(protected element: Element, private bindingEngine: BindingEngine, private i18n: I18N) {
    this.checkable = element.hasAttribute("checkable");
    this.searchable = element.hasAttribute("searchable");
  }

  public select(node: UITreeNode, event = null) {
    if (false == node.isSelectable) {
      return;
    }
    if (this.checkable !== false) {
      if (typeof this.checkable == 'number' && node.level >= this.checkable) {
        if (this.maxValues != null && this.rootNode.nodes.filter(x => x.checked).length == this.maxValues) {
          this.internalToggleCheck(this.rootNode.nodes.filter(x => x.checked && this.value[0] == x.id)[0], true);
        }
        this.toggleCheck(node, event);
        this.getCheckedValues();
        this.element.dispatchEvent(UIInternal.createEvent("checked", this.value));
      }
    } else {
      return UIInternal.fireCallbackEvent(this, "beforeselect").then(b =>
        b ? this.changeSelection(node) : false
      );
    }
  }

  protected bind(): void {
    this.parseOptionsAsTree();
    this.selectOptionsFromValue(this.treeOptions);
    this.rootNode = new UITreeModel(this.treeOptions, this.maxNodes, this.autoCheckable, this.autoSort);
    this.disposable = this.bindingEngine.collectionObserver(this.options).subscribe(() => this.optionsChanged());
    this.valueDisposable = this.bindingEngine.collectionObserver(this.value).subscribe(() => this.valueChanged());
  }

  protected optionsChanged() {
    this.parseOptionsAsTree();
    this.rootNode = new UITreeModel(this.treeOptions, this.maxNodes, this.autoCheckable, this.autoSort);

    // If a search is active on options changed, re-apply search
    if (this.searchText?.length > 0) {
      this.searchTextChanged(this.searchText);
    }
  }

  protected valueChanged() {
    this.selectOptionsFromValue(this.treeOptions);
  }


  public refreshSelectedTreeValues() {
    this.value.splice(0);
    this.rootNode?.nodes.forEach(node => {
      node.checked = 0;
    })
  }

  protected selectOptionsFromValue(options: UITreeNode[]) {
    if (this.value?.length > 0) {
      options.forEach(option => {
        if (option.id) {
          option.selected = this.value.indexOf(option.id) > -1;
          if (option.selected) {
            this.rootNode?.expandNode(option.id);
          }
        }

        if (option.children) {
          this.selectOptionsFromValue(option.children);
        }
      });
    }
  }

  protected unbind() {
    if (this.disposable) this.disposable.dispose();
    if (this.valueDisposable) this.valueDisposable.dispose();
  }

  protected toggleExpand(index: number): void {
    this.rootNode?.toggleExpand(index);
  }

  protected toggleMore(index: number): void {
    this.rootNode?.toggleMore(index);
  }

  lastCheck: UITreeNode;

  protected toggleCheck(node: UITreeNode, event: MouseEvent) {
    if (event.shiftKey === false) {
      this.internalToggleCheck(node, true);
    }
    else { // User press shift key, me must select a sequence of node
      if (!this.lastCheck) {
        this.internalToggleCheck(node, true);
      }
      else {
        let lastCheckNodeIndex = this.rootNode.nodes.findIndex(n => n.id == this.lastCheck.id);
        let currentCheckNodeIndex = this.rootNode.nodes.indexOf(node);
        if (lastCheckNodeIndex > -1 && currentCheckNodeIndex > -1) {
          if (currentCheckNodeIndex > lastCheckNodeIndex) {
            // ie: lastCheckNodeIndex = 2 & currentCheckNodeIndex = 6
            for (let index = lastCheckNodeIndex + 1; index <= currentCheckNodeIndex; index++) {
              let node = this.rootNode.nodes[index];
              this.internalToggleCheck(node, true);
            }
          }
          else {
            // ie: lastCheckNodeIndex = 8 & currentCheckNodeIndex = 3
            // we must move up the array
            for (let index = lastCheckNodeIndex - 1; index >= currentCheckNodeIndex; index--) {
              let node = this.rootNode.nodes[index];
              this.internalToggleCheck(node, true);
            }
          }
        }
        else {
          this.internalToggleCheck(node, true);
        }
      }
    }
  }

  private internalToggleCheck(node: UITreeNode, isInitial: boolean = true): void {
    if (isInitial) {
      node.toggleCheck(true, this.autoCheckable);
    }
    this.lastCheck = node;
  }

  protected getCheckedValues() {
    if (!this.value) this.value = [];
    if (!this.model) this.model = [];
    this.rootNode?.nodes.forEach(node => {
      if (node.checked == 1) {
        if (node.id) {
          if (this.value.indexOf(node.id) < 0) this.value.push(node.id);
        }
        if (node.model) {
          if (this.model.indexOf(node.model) < 0) this.model.push(node.model);
        }
      }
      else {
        if (node.id) {
          this.value.remove(node.id);
        }
        if (node.model) {
          this.model.remove(node.model);
        }
      }
    });
  }

  protected searchTextChanged(query) {
    this.rootNode?.filter(query);
  }

  private changeSelection(node: UITreeNode) {
    this.value = [node.id];
    this.model = [node.model];
    this.element.dispatchEvent(UIInternal.createEvent("select", node.id));
  }

  protected parseOptionsAsTree() {
    let actions: UITreeAction[] = [];
    if (Array.isArray(this.treeConfig?.actions) && this.treeConfig.actions.length > 0) {
      actions = this.treeConfig.actions.map(action => action.transformToTreeAction());
    }
    let options = this.treeConfig ? this.getChildren(null, 0, actions) : this.options;
    this.treeOptions.splice(0, this.treeOptions.length, ...(options as []));
  }

  private getChildren(id: number | string, level: number, actions: UITreeAction[]): UITreeNode[] {
    let nodes: UITreeNode[] = [];
    let defaultExpanded: boolean = true;
    if (this.treeConfig) {
      defaultExpanded = isNumber(this.treeConfig.autoExpand) ? level < this.treeConfig.autoExpand : this.treeConfig.autoExpand;
    }
    this.options.filter(entry => entry[this.treeConfig.parentId] == id).forEach(entry => {
      let id = entry[this.treeConfig.id];
      let children = this.getChildren(id, level + 1, actions);
      let uiTreeNode = new UITreeNode({
        id,
        label: typeof this.treeConfig.label === 'string' ? entry[this.treeConfig.label] : this.treeConfig.label(entry),
        icon: this.treeConfig.icon == null ? null : entry[this.treeConfig.icon],
        model: entry,
        expanded: false,
        isSelectable: this.treeConfig.isNodeSelectable ? this.treeConfig.isNodeSelectable(entry as TTargetEntity) : true,
        isExpandable: this.treeConfig.isNodeExpandable ? this.treeConfig.isNodeExpandable(entry as TTargetEntity) : true,
        selected: (this.value || []).find(v => v == id) != null,
        leaf: children.length === 0,
        children,
      }).assignActions(actions);

      // Determine expanded state. If has selected child, expand to show selected child
      if (uiTreeNode.hasSelectedChild()) {
        uiTreeNode.expanded = true;
      } else {
        // Find existing expanded state. If not found, use default auto expand option
        let existing = this.findNodeInStructure(id, this.rootNode?.children);
        uiTreeNode.expanded = existing ? existing.expanded : defaultExpanded;
      }

      nodes.push(uiTreeNode);
    }, []);
    return nodes;
  }

  private findNodeInStructure(id: number | string, nodes?: UITreeNode[]): UITreeNode | null {
    return nodes?.reduce((found: UITreeNode | null, node: UITreeNode) => {
      if (found != null) {
        return found;
      } else if (node.id == id) {
        return node;
      } else {
        return this.findNodeInStructure(id, node.children);
      }
    }, null);
  }

  public get showExpandAll(): boolean {
    return null != (this.rootNode?.nodes || []).find((node: UITreeNode) => {
      return node.leaf == false && node.expanded == false;
    })
  }

  public toggleExpandAll() {
    if (this.rootNode) {
      let expanded = this.showExpandAll;
      // For loop because after each expand the rootNode list of nodes changes, rendering a for-each unusable
      for (let i = 0; i < this.rootNode.nodes.length; ++i) {
        if (this.rootNode.nodes[i].leaf == false && this.rootNode.nodes[i].expanded != expanded) {
          this.toggleExpand(i);
        }
      }
    }
  }

  public get showExportCsv(): boolean {
    return this.exportToCsv != null;
  }
}
