import { BindingSignaler } from 'aurelia-templating-resources';
import { UIColumn } from './ui-column';
import { CSSBreakpoints, SignalBindings } from '../../../constants/constants';
import { bindable, BindingEngine, bindingMode, children, computedFrom, customElement, Disposable, ICollectionObserverSplice, observable, viewResources } from "aurelia-framework";
import { UIDataSource } from "./ui-data-source";
import { BodyCell } from "./body-cell";
import { HeaderCell } from "./header-cell";
import { UIInternal } from "../../../utils/ui-internal";
import { UITableAction } from './ui-table-action';
import dragula from 'dragula';
import { DateUtils } from '../../../utils/date-utils';

@customElement("ui-table")
@children({ name: 'columns', selector: ['ui-column', 'ui-table-action'] })
@viewResources(HeaderCell, BodyCell)
export class UITable {
  //#region Bindable properties
  @bindable({ defaultBindingMode: bindingMode.toView })
  public data: KeyValue[];
  @bindable()
  public defaultSortProperty?: string;
  @bindable()
  public defaultSortOrder?: "asc" | "desc";
  @bindable()
  public filter: (record: KeyValue) => boolean = null;

  @bindable()
  protected dragDropFunction: (obj: { record: KeyValue, oldVal: number, newVal: number }) => void | Promise<void> | boolean | Promise<boolean>;
  @bindable()
  protected showCounter: boolean;
  @bindable()
  public checkable: boolean;
  @bindable()
  public selected: KeyValue[] = [];
  @bindable()
  public nbSelectedMax: number;
  @bindable customColumns: UIColumn[];
  @bindable()
  public designWithBorder: boolean = false;
  @bindable()
  public compact: boolean = false;

  @bindable({ defaultBindingMode: bindingMode.oneTime })
  public rowClassComputedFrom: string | string[] = [];
  @bindable()
  public rowClass: string | ((record: KeyValue, isDesktopScreen: boolean) => string);
  @bindable()
  public onRowClicked: (obj: { record: KeyValue }) => void;
  @bindable()
  public rowRoute: (obj: { record: KeyValue }) => string;
  @bindable
  public cellAlign: "start" | "center" | "end" = "start";
  @bindable
  public headerAlign: "start" | "center" | "end" = "start";
  @bindable
  public useVirtualization: boolean = true;
  @bindable()
  public onRowSelected: (obj: object) => boolean;
  //#endregion 

  @observable()
  protected focusData: { data: any, colIndex: number } | null = null;
  @observable()
  protected isRendered = false;
  @observable()
  protected isLoadingData = false;

  @computedFrom("headersVisible", "headersVisible.length", "checkable", "showCounter")
  protected get nbHeadersVisible() {
    return this.headersVisible.length + (this.checkable ? 1 : 0) + (this.showCounter ? 1 : 0);
  }
  @computedFrom('designWithBorder', 'compact')
  protected get tableClasses(): string {
    return [
      this.designWithBorder ? 'ui-table-with-border' : '',
      this.compact ? 'ui-table-compact' : ''
    ].join(' ');
  }
  private get canDragAndDrop() {
    return this.isDesktopScreen && this.dragDropFunction != null;
  }

  //#region private props
  protected ds: UIDataSource;
  private drake;
  protected disposables: Array<Disposable> = [];
  private isDesktopScreen: boolean = true;
  private isColumnsInitialized = false;
  private auTargetId: number;
  private columns: Array<UIColumn | UITableAction> = [];
  private headersVisible: Array<UIColumn | UITableAction> = [];
  private columnsVisible: Array<UIColumn | UITableAction> = [];
  private isDrangAndDropPending = false;
  private computedFromSubscriptions: { subscription: Disposable, record: KeyValue }[] = [];
  private readonly draggableHandleClassName = "ui-table__draggable-handle";
  private readonly recordChangedSignal = 'ui-table-record-changed';
  //#endregion

  constructor(private element: Element, private bindingEngine: BindingEngine, private signaler: BindingSignaler, private dateUtils: DateUtils) {
    this.isDesktopScreen = window.innerWidth > CSSBreakpoints.md;
  }

  public filterChanged() {
    this.ds?.deboucePerformFilter();
  }

  public refresh() {
    this.isRendered = false;
    setTimeout(() => {
      UIInternal.broadcast(UIInternal.EVT_UI_TABLE_BODY_CELL_CHANGED + `_${this.auTargetId}`);
    }, 200);
  }

  protected attached() {
    if (this.element) {
      this.auTargetId = parseInt($(this.element).attr('au-target-id'));
    }
    this.computeTemplateWidth();
    this.disposables.push(
      UIInternal.subscribe(UIInternal.EVT_VIEWPORT_CSS_BREAKPOINT_CHANGE, (data) => {
        this.isDesktopScreen = data > CSSBreakpoints.md;
        this.computeTemplateWidth();
        this.initDrake();
      }),
      UIInternal.subscribe(UIInternal.EVT_UI_TABLE_REFRESH + `_${this.auTargetId}`, () => {
        this.computeTemplateWidth();
      }),
      this.bindingEngine.collectionObserver(this.columns).subscribe(changeRecords => {
        if (changeRecords.length > 0) {
          this.columnsChanged();
        }
      }),
    );
    UIInternal.queueMicroTask(() => {
      this.initDrake();
    });
  }

  protected detached() {
    this.disposables?.forEach(disposable => disposable.dispose());
    this.drake?.destroy();
  }

  protected bind() {
    this.checkable = this.checkable || this.element.hasAttribute("checkable");
    this.showCounter = this.showCounter || this.element.hasAttribute("counter");
    this.dataHasChanged();
    this.customColumnsChanged();

    this.disposables.push(
      this.bindingEngine.collectionObserver(this.data).subscribe((changeRecords) => {
        this.isLoadingData = true;
        this.signaler.signal(SignalBindings.RefreshUiTableLoadingData);
        this.signaler.signal(SignalBindings.RefreshUiTableCheck);
        this.dataHasChanged(changeRecords);
        this.initDrake();
        this.isLoadingData = false;
        this.signaler.signal(SignalBindings.RefreshUiTableLoadingData);
      }),
      this.bindingEngine.propertyObserver(this, "showCounter").subscribe(() => {
        this.computeTemplateWidth();
      }),
      this.bindingEngine.propertyObserver(this, "checkable").subscribe(() => {
        this.computeTemplateWidth();
      }),
    );
  }

  protected toggleSelection($event, record: KeyValue) {
    if (this.selected == null) {
      this.selected = [];
    }
    if ($event.detail.checked) {
      this.selected.push(record);
      if (this.nbSelectedMax != null) {
        if (this.selected.length > this.nbSelectedMax) {
          this.selected.splice(0, this.selected.length - this.nbSelectedMax);
        }
      }
    } else {
      this.selected.remove(record);
    }
  }

  protected toggleSelectionAll() {
    if (this.selected.length === 0) {
      this.selected.splice(0);
      this.selected.push(...this.ds.data.filter(x => this.onRowSelected == null || this.onRowSelected({ x })));
    } else {
      this.selected.splice(0);
    }
    this.signaler.signal(SignalBindings.RefreshUiTableCheck);
  }

  protected getRowClass(record: KeyValue) {
    if (this.rowClass != null) {
      if (typeof this.rowClass == 'string') {
        return this.rowClass;
      } else {
        return this.rowClass(record, this.isDesktopScreen);
      }
    }
  }

  dataHasChanged(splices?: ICollectionObserverSplice<any, any>[]) {
    if (isArray(this.data)) {
      this.ds?.dispose();
      this.ds = new UIDataSource(this.data, {
        defaultSortProperty: this.defaultSortProperty,
        defaultSortOrder: this.defaultSortOrder,
        filter: this.filter
      }, this.dateUtils);
    }

    this.setupRowClassObservers(splices);

    if (this.ds.data == null || this.ds.data.length == 0) {
      this.isRendered = true;
    }
  }

  protected setupRowClassObservers(splices?: ICollectionObserverSplice<any, any>[]) {
    if (this.rowClass && typeof (this.rowClass) == 'function' && this.rowClassComputedFrom) {
      let computedFrom = typeof (this.rowClassComputedFrom) == 'string' ?
        [this.rowClassComputedFrom] : this.rowClassComputedFrom;

      if (splices != null) {
        for (var i = 0; i < splices.length; i++) {
          var splice: ICollectionObserverSplice<KeyValue> = splices[i];

          var valuesAdded = this.data.slice(splice.index, splice.index + splice.addedCount);
          if (valuesAdded.length > 0) {
            this.observeRecordChanges(computedFrom, valuesAdded);
          }

          if (splice.removed.length > 0) {
            this.disposeRecordChanges(splice.removed)
          }
        }
      } else {
        this.observeRecordChanges(computedFrom, this.data);
      }
    }
  }

  protected observeRecordChanges(computedFrom: string[], records: KeyValue[]) {
    records.forEach(record =>
      computedFrom.forEach(prop =>
        this.computedFromSubscriptions.push({
          subscription: this.bindingEngine.expressionObserver(record, prop).subscribe(() =>
            this.signaler.signal(this.recordChangedSignal),
          ),
          record,
        })
      )
    );
  }

  protected disposeRecordChanges(records: KeyValue[]) {
    for (var i = this.computedFromSubscriptions.length - 1; i >= 0; --i) {
      var x = this.computedFromSubscriptions[i];
      if (records.some(y => y == x.record)) {
        x.subscription.dispose();
        this.computedFromSubscriptions.splice(i, 1);
      }
    };
  }

  columnsChanged() {
    if (!this.isColumnsInitialized) {
      if (this.showCounter) {
        this.createColumn('counter');
      }
      if (this.checkable) {
        this.createColumn('checkbox');
      }
      if (this.canDragAndDrop) {
        this.createColumn('drag');
      }
    }
    this.isColumnsInitialized = true;
    this.computeTemplateWidth();

    this.columns.forEach(c => {
      if (!c.tableAuTargetId) {
        c.tableAuTargetId = this.auTargetId;
      }
    })
  }

  createColumn(type: "text" | "number" | "drag" | "checkbox" | "counter") {
    const newColumn = new UIColumn(this.element, null, null, null, this.bindingEngine);
    newColumn.type = type;
    newColumn.order = 'auto';
    newColumn.width = 'fit-content';
    this.columns.splice(0, 0, newColumn);
  }

  initDrake() {
    setTimeout(() => {
      if (this.canDragAndDrop) {
        var allContainers = $(this.element).find('.draggable');
        if (this.drake != null) {
          this.drake.destroy();
          this.drake = null;
        }
        this.drake = new dragula(allContainers.toArray(),
          {
            copy: false,
            moves: (el: Element, source, handle, sibling) => {
              return handle != null && handle.classList.contains(this.draggableHandleClassName) && !this.isDrangAndDropPending;
            },
            revertOnSpill: true
          })
          .on('drop', async (el: Element & { auTargetId: number }, target: Element, source: Element, sibling: Element, itemVM, siblingVM) => {
            try {
              this.isDrangAndDropPending = true;
              const initialIndex = el['currentIndex'];
              const initialOrder = initialIndex + 1;
              const previousElement = el.previousElementSibling;

              let finalOrder = 0;
              if (previousElement == null || previousElement['currentIndex'] == null) {
                // first row in table
                finalOrder = 1;
              }
              else {
                const previousElementIndex = previousElement['currentIndex'];
                if (initialIndex < previousElementIndex) {
                  // move from top to bottom
                  finalOrder = previousElement['currentIndex'] + 1;
                }
                else {
                  // move from bottom to top
                  finalOrder = previousElement['currentIndex'] + 2
                }
              }
              this.drake.cancel(true);
              const dragAndDropResult = await this.dragDropFunction({ record: el['viewModel'], oldVal: initialOrder, newVal: finalOrder });
            }
            catch (error) {
              console.error('error when dropping element', error);
            }
            finally {
              this.isDrangAndDropPending = false;
            }
          });
      } else {
        this.drake?.destroy();
      }
    }, 500);
  }

  computeTemplateWidth() {
    if (this.isColumnsInitialized === false) return;
    if (this.checkable != null && this.showCounter != null && this.columns != null) {
      const orderedColumns = this.sortColumns(this.columns);
      this.columnsVisible = orderedColumns.filter(col => col.isVisible && (col instanceof UITableAction || ((this.isDesktopScreen && !col.hideOnLarge) || (!this.isDesktopScreen && !col.hideOnSmall))));
      this.headersVisible = this.columnsVisible.filter(col => col instanceof UITableAction || ((this.isDesktopScreen && !col.noHeaderOnLarge) || (!this.isDesktopScreen && !col.noHeaderOnSmall)));
      let sumProportionalWidths = 0;
      let fixedWidths: string[] = [];

      this.headersVisible.forEach(col => {
        if (!(col instanceof UITableAction) && col.width != null && col.width != 0 && col.width != 'fit-content') {
          let width = col.width.toString();
          if (isNaN(width)) {
            fixedWidths.push(width);
          } else {
            sumProportionalWidths += parseFloat(width);
          }
        }
      });

      let remainingWidthForProportional = fixedWidths.length == 0 ? '100%'
        : `calc(100% - ${fixedWidths.join(' - ')})`;
      this.headersVisible.forEach(col => {
        if (col.headerAlign == null) {
          col.headerAlign = this.headerAlign;
        }
        if (col instanceof UITableAction) {
          col.computedWidth = "auto";
        } else {
          if (col.width == null || col.width == 0) {
            col.computedWidth = "auto";
          } else if (col.width == 'fit-content') {
            col.computedWidth = "auto";
          } else {
            let width = col.width.toString();
            if (isNaN(width)) {
              // col.computedWidth = "auto";
              col.computedWidth = width;
            } else {
              col.computedWidth = `calc(${remainingWidthForProportional} * ${parseFloat(width) / sumProportionalWidths})`;
            }
          }
        }
      });

      this.columnsVisible.forEach(col => {
        if (col.cellAlign == null) {
          col.cellAlign = this.cellAlign;
        }
      })
    }
  }

  sortColumns(columnsVisible: (UIColumn | UITableAction)[]) {
    const autoCols = columnsVisible.filter(c => c.order == 'auto');
    const fixedCols = columnsVisible.filter(c => c.order != 'auto');
    fixedCols.forEach(c => {
      autoCols.splice(c.order as number, 0, c)
    });
    return autoCols;
  }

  renderCell(cell) {
    if (this.isRendered === false) {
      setTimeout(() => {
        this.isRendered = true;
      }, 50);
    }
  }

  customColumnsChanged() {
    this.addCustomColumns();
  }

  addCustomColumns() {
    if (this.customColumns?.length > 0) {
      this.columns ??= [];
      this.columns.push(...this.customColumns);
    }
  }
}

