import '@maicol07/material-web-additions/data-table/data-table-row.js';
import '@maicol07/material-web-additions/data-table/data-table-cell.js';
import '@maicol07/material-web-additions/data-table/data-table-column.js';
import '@maicol07/material-web-additions/data-table/data-table-footer.js';

import {
  DataTable as MdDataTable,
  RowSelectionChangedDetail
} from '@maicol07/material-web-additions/data-table/lib/data-table';
import {DataTableCell} from '@maicol07/material-web-additions/data-table/lib/data-table-cell';
import {mdiDeleteOutline} from '@mdi/js';
import collect, {Collection} from 'collect.js';
import {
  ToManyRelation,
  ToOneRelation
} from 'coloquent';
import {
  Children,
  Vnode,
  VnodeDOM
} from 'mithril';
import {Class} from 'type-fest';

import DataTable, {DataTableAttributes} from '~/Components/DataTable/DataTable';
import DataTableColumn, {DataTableColumnAttributes} from '~/Components/DataTable/DataTableColumn';
import RecordsTableColumn from '~/Components/DataTable/RecordsTableColumn';
import MdIcon from '~/Components/MdIcon';
import Model from '~/Models/Model';
import {isVnode} from '~/utils/misc';

export interface RecordsTableColumnAttributes extends DataTableColumnAttributes {
  label?: string;
}

export interface RecordsTableAttributes<M extends Model<any, any>> extends DataTableAttributes {
  cols: Collection<Children> | Collection<RecordsTableColumnAttributes> | Collection<Children | RecordsTableColumnAttributes>;
  records: Map<string, M>;
  readonly?: boolean;

  onTableRowClick?(recordId: string, event: MouseEvent): void;

  onDeleteRecordButtonClick?(recordId: string, event: MouseEvent): void;

  onDeleteSelectedRecordsButtonClick?(recordsIds: string[], event: MouseEvent): void;

  onRowSelectionChanged?(selectedRecordsIds: string[], event: CustomEventInit<RowSelectionChangedDetail>): void;

  valueModifier?(value: any, attribute: string, record: M): any;

  selectable?: boolean;
}

export default class RecordsTable<M extends Model<any, any>, A extends RecordsTableAttributes<M> = RecordsTableAttributes<M>> extends DataTable<A> {
  element!: MdDataTable;
  selectedRecordsIds: string[] = [];

  oninit(vnode: Vnode<A, this>) {
    super.oninit(vnode);

    vnode.attrs.paginated ??= true;
    vnode.attrs.currentPageSize ??= 10;
  }

  onupdate(vnode: VnodeDOM<A, this>) {
    super.onupdate(vnode);

    // Clear selected rows if new records are not included
    for (const id of this.selectedRecordsIds) {
      if (!vnode.attrs.records.has(id)) {
        this.selectedRecordsIds.splice(this.selectedRecordsIds.indexOf(id), 1);
      }
    }
  }

  contents(vnode: Vnode<A>) {
    return [
      this.tableColumns(vnode).values<Children>().all(),
      vnode.attrs.records.size === 0 ? this.noRecordsContent(vnode) : this.tableRows(vnode),
      this.tableFooter(vnode)
    ];
  }

  protected tableColumns(vnode: Vnode<A>) {
    let columns: Collection<Children> = collect({});
    if (vnode.attrs.selectable) {
      columns.put('checkbox', <md-data-table-column key="checkbox" type="checkbox"></md-data-table-column>);
    }

    columns = columns.merge(vnode.attrs.cols.map<Children>((column: Children | RecordsTableColumnAttributes, attribute: string) => {
      // If the column is a vnode, and it is a DataTableColumn or a string that matches the tag name of a DataTableColumn, then use it as is.
      if (isVnode<Record<string, any>>(column) && (column.tag === 'md-data-table-column' || (typeof column.tag !== 'string' && (column.tag as Class<any>).prototype instanceof DataTableColumn))) {
        column.key ??= attribute;
        column.attrs['data-model-attribute'] ??= attribute;
        return column;
      }

      let attributes: DataTableColumnAttributes = {};
      let children: Children | RecordsTableColumnAttributes = column;

      if (this.isRecordTableColumnAttributes(column)) {
        children = column.label ?? attribute;
        attributes = column;
      }

      attributes['data-model-attribute'] ??= attribute;

      // Otherwise, wrap it in a DataTableColumn
      return <RecordsTableColumn key={attribute} {...attributes}>{children}</RecordsTableColumn>;
    }).all());

    if (!vnode.attrs.readonly) {
      columns.put('actions', this.tableActionsColumn());
    }

    return columns;
  }

  protected tableActionsColumn(): Children {
    return (
      <md-data-table-column key="actions">{__('Azioni')}</md-data-table-column>
    );
  }

  noRecordsContent(vnode: Vnode<A>): Children {
    const colspan = vnode.attrs.cols.count() + (vnode.attrs.readonly ? 0 : 1);
    return (
      <md-data-table-row>
        <td colspan={colspan} style={{textAlign: 'center'}}>{__('Nessun record trovato')}</td>
      </md-data-table-row>
    );
  }

  protected tableRows(vnode: Vnode<A>) {
    return [...this.tableRowsData(vnode)].map(([recordId, row]): Children => (
      <md-data-table-row key={recordId} data-model-id={recordId} onclick={this.onTableRowClick.bind(this, vnode, recordId)} style={{cursor: vnode.attrs.readonly ? undefined : 'pointer'}}>
        {vnode.attrs.selectable && <md-data-table-cell type="checkbox"></md-data-table-cell>}
        {row.map((cell, attribute: string) => <md-data-table-cell key={attribute}>{cell}</md-data-table-cell>).values<Children>().all()}
      </md-data-table-row>
    ));
  }

  protected tableRowsData(vnode: Vnode<A>) {
    const rows = new Map<string, Collection<string>>();

    for (const record of vnode.attrs.records.values()) {
      const cells = collect<string>({});

      for (const attribute of vnode.attrs.cols.keys()) {
        const value = this.getModelValue(record, attribute, vnode);
        cells.put(attribute, value);
      }

      if (!vnode.attrs.readonly) {
        cells.put('actions', this.tableRowActions(vnode, record).values<Children>().all());
      }

      rows.set(record.getId()!, cells);
    }

    return rows;
  }

  protected tableRowActions(vnode: Vnode<A>, record: M) {
    return collect<Children>({
      delete: (
        <md-standard-icon-button onclick={this.onDeleteRecordButtonClicked.bind(this, vnode, record)}>
          <MdIcon icon={mdiDeleteOutline}/>
        </md-standard-icon-button>
      )
    });
  }

  protected tableFooter(vnode: Vnode<A>) {
    return (
      <md-data-table-footer slot="footer" style={{
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'right',
        gap: '4px'
      }}>
        {__('Seleziona un\'azione per la selezione multipla:')}
        {this.tableFooterActions(vnode).values<Children>().all()}
      </md-data-table-footer>
    );
  }

  protected tableFooterActions(vnode: Vnode<A>) {
    return collect<Children>({
      delete: (
        <md-text-button onclick={this.onDeleteSelectedRecordsButtonClicked.bind(this, vnode)} disabled={this.selectedRecordsIds.length === 0}>
          {__('Elimina selezionati')}
          <MdIcon icon={mdiDeleteOutline} slot="icon"/>
        </md-text-button>
      )
    });
  }

  oncreate(vnode: VnodeDOM<A, this>) {
    super.oncreate(vnode);

    this.element.addEventListener('rowSelectionChanged', this.onRowSelectionChanged.bind(this, vnode));
  }

  protected onRowSelectionChanged(vnode: Vnode<A>, event: CustomEventInit<RowSelectionChangedDetail>) {
    const recordId = event.detail!.row.dataset.modelId!;
    if (event.detail!.selected && !this.selectedRecordsIds.includes(recordId)) {
      this.selectedRecordsIds.push(recordId);
    } else if (!event.detail!.selected && this.selectedRecordsIds.includes(recordId)) {
      this.selectedRecordsIds.splice(this.selectedRecordsIds.indexOf(recordId), 1);
    }
    vnode.attrs.onRowSelectionChanged?.(this.selectedRecordsIds, event);
  }

  protected onTableRowClick(vnode: Vnode<A>, recordId: string, event: MouseEvent) {
    if (event.target instanceof DataTableCell && event.target.type === 'checkbox') {
      event.target.dispatchEvent(new CustomEvent('checked', {
        detail: {
          checked: !event.target?.checkbox?.checked
        }
      }));
      return;
    }
    vnode.attrs.onTableRowClick?.(recordId, event);
  }

  protected onDeleteRecordButtonClicked(vnode: Vnode<A>, record: M, event: MouseEvent) {
    event.stopPropagation();
    vnode.attrs.onDeleteRecordButtonClick?.(record.getId()!, event);
  }

  protected onDeleteSelectedRecordsButtonClicked(vnode: Vnode<A>, event: MouseEvent) {
    vnode.attrs.onDeleteSelectedRecordsButtonClick?.(this.selectedRecordsIds, event);
  }

  protected getModelValue(record: M, attribute: string, vnode: Vnode<A>): unknown {
    // Check if is a relation
    let value: unknown = this.getModelRelationValue(record, attribute);

    if (!value) {
      value = record.getAttribute(attribute);
    }
    if (attribute === 'id') {
      value = record.getId()!;
    }

    let fallback = value;
    if (value instanceof ToOneRelation || value instanceof ToManyRelation) {
      fallback = value.getReferringObject().getApiId();
    }

    return vnode.attrs.valueModifier?.(value, attribute, record) ?? fallback;
  }

  protected getModelRelationValue(record: M, attribute: string) {
    const relation = Reflect.get(record, attribute) as unknown;
    if (relation && typeof relation === 'function') {
      const relationship = relation() as unknown;
      if (relationship instanceof ToOneRelation || relationship instanceof ToManyRelation) {
        return relationship;
      }
    }
  }

  private isRecordTableColumnAttributes(column: Children | RecordsTableColumnAttributes): column is RecordsTableColumnAttributes {
    return typeof column === 'object' && 'label' in (column ?? {});
  }
}
