import { Arr, Fun, Optional, Optionals } from '@ephox/katamari';
import { Attribute, Compare, SugarElement } from '@ephox/sugar';

import { Generators, GeneratorsWrapper, SimpleGenerators } from '../api/Generators';
import * as ResizeBehaviour from '../api/ResizeBehaviour';
import { ResizeWire } from '../api/ResizeWire';
import * as Structs from '../api/Structs';
import * as TableLookup from '../api/TableLookup';
import { TableOperationResult } from '../api/TableOperations';
import { TableSection } from '../api/TableSection';
import { TableSize } from '../api/TableSize';
import { Warehouse } from '../api/Warehouse';
import * as Redraw from '../operate/Redraw';
import * as Bars from '../resize/Bars';
import * as LockedColumnUtils from '../util/LockedColumnUtils';
import * as Transitions from './Transitions';

type DetailExt = Structs.DetailExt;
type DetailNew = Structs.DetailNew;
type RowDetailNew<A extends Structs.Detail> = Structs.RowDetailNew<A>;

export interface OperationBehaviours {
  readonly sizing?: TableSize;
  readonly resize?: ResizeBehaviour.ResizeBehaviour;
  readonly section?: TableSection;
}

export interface RunOperationOutput {
  readonly cursor: Optional<SugarElement>;
  readonly newRows: SugarElement[];
  readonly newCells: SugarElement[];
}

export interface TargetElement {
  readonly element: SugarElement;
}

export interface TargetSelection {
  readonly selection: SugarElement[];
}

export interface TargetMergable {
  readonly mergable: Optional<ExtractMergable>;
}

export interface TargetUnmergable {
  readonly unmergable: Optional<SugarElement[]>;
}

// combines the above 4 interfaces because this is what data we actually get from TinyMCE
export interface CombinedTargets extends TargetElement, TargetSelection, TargetMergable, TargetUnmergable { }

export interface TargetPaste {
  readonly element: SugarElement;
  readonly generators: SimpleGenerators;
  readonly clipboard: SugarElement;
}

export interface TargetPasteRows {
  readonly selection: SugarElement[];
  readonly generators: SimpleGenerators;
  readonly clipboard: SugarElement[];
}

export interface ExtractMergable {
  readonly cells: SugarElement[];
  readonly bounds: Structs.Bounds;
}

export interface ExtractPaste extends Structs.DetailExt {
  readonly generators: SimpleGenerators;
  readonly clipboard: SugarElement;
}

export interface ExtractPasteRows {
  readonly cells: Structs.DetailExt[];
  readonly generators: SimpleGenerators;
  readonly clipboard: SugarElement[];
}

const fromWarehouse = (warehouse: Warehouse, generators: Generators) =>
  Transitions.toGrid(warehouse, generators, false);

const toDetailList = (grid: Structs.RowCells[]): RowDetailNew<DetailNew>[] =>
  Transitions.toDetails(grid, Compare.eq);

const findInWarehouse = (warehouse: Warehouse, element: SugarElement): Optional<DetailExt> => Arr.findMap(warehouse.all, (r) =>
  Arr.find(r.cells, (e) => Compare.eq(element, e.element))
);

const extractCells = (warehouse: Warehouse, target: TargetSelection, predicate: (detail: DetailExt) => boolean): Optional<DetailExt[]> => {
  const details = Arr.map(target.selection, (cell) => {
    return TableLookup.cell(cell)
      .bind((lc) => findInWarehouse(warehouse, lc))
      .filter(predicate);
  });
  const cells = Optionals.cat(details);
  return Optionals.someIf(cells.length > 0, cells);
};

type EqEle = (e1: SugarElement, e2: SugarElement) => boolean;
export type Operation<INFO, GW extends GeneratorsWrapper> = (model: Structs.RowCells[], info: INFO, eq: EqEle, w: GW, headers: TableSection) => TableOperationResult;
export type Extract<RAW, INFO> = (warehouse: Warehouse, target: RAW) => Optional<INFO>;
export type Adjustment<INFO> = <T extends Structs.DetailNew>(table: SugarElement, grid: Structs.RowDetailNew<T>[], info: INFO, behaviours: Required<OperationBehaviours>) => void;
export type PostAction = (e: SugarElement) => void;
export type GenWrap<GW extends GeneratorsWrapper> = (g: Generators) => GW;

export type OperationCallback<T> = (wire: ResizeWire, table: SugarElement<HTMLTableElement>, target: T, generators: Generators, behaviours?: OperationBehaviours) => Optional<RunOperationOutput>;

const run = <RAW, INFO, GW extends GeneratorsWrapper>
(operation: Operation<INFO, GW>, extract: Extract<RAW, INFO>, adjustment: Adjustment<INFO>, postAction: PostAction, genWrappers: GenWrap<GW>): OperationCallback<RAW> =>
  (wire: ResizeWire, table: SugarElement<HTMLTableElement>, target: RAW, generators: Generators, behaviours?: OperationBehaviours): Optional<RunOperationOutput> => {
    const warehouse = Warehouse.fromTable(table);
    const tableSection = Optional.from(behaviours?.section).getOrThunk(TableSection.fallback);
    const output = extract(warehouse, target).map((info) => {
      const model = fromWarehouse(warehouse, generators);
      const result = operation(model, info, Compare.eq, genWrappers(generators), tableSection);
      const lockedColumns = LockedColumnUtils.getLockedColumnsFromGrid(result.grid);
      const grid = toDetailList(result.grid);
      return {
        info,
        grid,
        cursor: result.cursor,
        lockedColumns
      };
    });

    return output.bind((out) => {
      const newElements = Redraw.render(table, out.grid);
      const tableSizing = Optional.from(behaviours?.sizing).getOrThunk(() => TableSize.getTableSize(table));
      const resizing = Optional.from(behaviours?.resize).getOrThunk(ResizeBehaviour.preserveTable);
      adjustment(table, out.grid, out.info, { sizing: tableSizing, resize: resizing, section: tableSection });
      postAction(table);
      Bars.refresh(wire, table);
      // Update locked cols attribute
      Attribute.remove(table, LockedColumnUtils.LOCKED_COL_ATTR);
      if (out.lockedColumns.length > 0) {
        Attribute.set(table, LockedColumnUtils.LOCKED_COL_ATTR, out.lockedColumns.join(','));
      }
      return Optional.some({
        cursor: out.cursor,
        newRows: newElements.newRows,
        newCells: newElements.newCells
      });
    });
  };

const onCell = (warehouse: Warehouse, target: TargetElement): Optional<DetailExt> =>
  TableLookup.cell(target.element).bind((cell) => findInWarehouse(warehouse, cell));

const onPaste = (warehouse: Warehouse, target: TargetPaste): Optional<ExtractPaste> =>
  TableLookup.cell(target.element).bind((cell) => findInWarehouse(warehouse, cell).map((details) => {
    const value: ExtractPaste = {
      ...details,
      generators: target.generators,
      clipboard: target.clipboard
    };
    return value;
  }));

const onPasteByEditor = (warehouse: Warehouse, target: TargetPasteRows): Optional<ExtractPasteRows> =>
  extractCells(warehouse, target, Fun.always).map((cells) => ({
    cells,
    generators: target.generators,
    clipboard: target.clipboard
  }));

const onMergable = (_warehouse: Warehouse, target: TargetMergable): Optional<ExtractMergable> =>
  target.mergable;

const onUnmergable = (_warehouse: Warehouse, target: TargetUnmergable): Optional<SugarElement[]> =>
  target.unmergable;

const onCells = (warehouse: Warehouse, target: TargetSelection): Optional<DetailExt[]> =>
  extractCells(warehouse, target, Fun.always);

// Custom unlocked extractors

const onUnlockedCell = (warehouse: Warehouse, target: TargetElement): Optional<Structs.DetailExt> =>
  onCell(warehouse, target).filter((detail) => !detail.isLocked);

const onUnlockedCells = (warehouse: Warehouse, target: TargetSelection): Optional<Structs.DetailExt[]> =>
  extractCells(warehouse, target, (detail) => !detail.isLocked);

const isUnlockedTableCell = (warehouse: Warehouse, cell: SugarElement) => findInWarehouse(warehouse, cell).exists((detail) => !detail.isLocked);
const allUnlocked = (warehouse: Warehouse, cells: SugarElement[]) => Arr.forall(cells, (cell) => isUnlockedTableCell(warehouse, cell));

// If any locked columns are present in the selection, then don't want to be able to merge
const onUnlockedMergable = (warehouse: Warehouse, target: TargetMergable): Optional<ExtractMergable> =>
  onMergable(warehouse, target).filter((mergeable) => allUnlocked(warehouse, mergeable.cells));

// If any locked columns are present in the selection, then don't want to be able to unmerge
const onUnlockedUnmergable = (warehouse: Warehouse, target: TargetUnmergable): Optional<SugarElement[]> =>
  onUnmergable(warehouse, target).filter((cells) => allUnlocked(warehouse, cells));

export {
  run,
  toDetailList,
  onCell,
  onCells,
  onPaste,
  onPasteByEditor,
  onMergable,
  onUnmergable,
  onUnlockedCell,
  onUnlockedCells,
  onUnlockedMergable,
  onUnlockedUnmergable
};

