import { action, computed, makeObservable, observable } from 'mobx';
import type { CellBorder } from './types';
import type { FiltersMap } from '@wix/bex-core';
import type { EditableTableState } from './EditableTableState';
import { cellKey, parseCellKey, noBorder } from './utils';

export class ClipboardState<T, F extends FiltersMap> {
  copiedCells = new Set<string>();
  isCut = false;
  private _copiedDataString = '';
  private _copiedValues = new Map<string, any>();
  private readonly _parent: EditableTableState<T, F>;

  constructor(parent: EditableTableState<T, F>) {
    this._parent = parent;
    makeObservable(this, {
      copiedCells: observable,
      isCut: observable,
      hasCopiedCells: computed,
      copy: action,
      cut: action,
      paste: action,
      clear: action,
    });
  }

  /**
   * Copy selected cells. Returns serialized TSV string for clipboard.
   */
  copy(): string {
    this.clear();
    return this._storeCells();
  }

  /**
   * Cut selected cells. Returns serialized TSV string for clipboard.
   */
  cut(): string {
    this.clear();
    this.isCut = true;
    return this._storeCells();
  }

  /**
   * Paste data starting at target position.
   * If the clipboard text matches what we stored at copy time, uses the rich
   * typed values instead of deserializing from TSV (avoids lossy round-trip).
   */
  paste(rawData: string) {
    const { cellValue, cellInteraction } = this._parent;
    const target = cellInteraction.focusedCell;
    if (!target) {
      return;
    }
    const targetPos = this._parent._toIndices(target);
    if (!targetPos) {
      return;
    }

    const isInternal =
      this._copiedDataString !== '' && this._copiedDataString === rawData;

    const pastedKeys = this._pasteGrid(
      rawData,
      targetPos,
      isInternal,
      cellValue,
    );

    if (this.isCut && isInternal) {
      this._clearCutSources(pastedKeys, cellValue);
    }

    this.clear();
  }

  private _pasteGrid(
    rawData: string,
    targetPos: { row: number; col: number },
    useRichValues: boolean,
    cellValue: EditableTableState<T, F>['cellValue'],
  ): Set<string> {
    const pastedKeys = new Set<string>();
    const richChanges: { rowKey: string; columnId: string; value: any }[] = [];
    const rawChanges: { rowKey: string; columnId: string; rawValue: string }[] =
      [];
    const rows = rawData.split(/\r?\n/).filter((r) => r.length > 0);

    for (let r = 0; r < rows.length; r++) {
      const cols = rows[r].split('\t');
      for (let c = 0; c < cols.length; c++) {
        const dest = this._parent._toPosition(
          targetPos.row + r,
          targetPos.col + c,
        );
        if (!dest) {
          continue;
        }

        const richValue = useRichValues
          ? this._copiedValues.get(`${r}:${c}`)
          : undefined;

        if (richValue !== undefined) {
          richChanges.push({
            rowKey: dest.rowKey,
            columnId: dest.columnId,
            value: richValue,
          });
        } else {
          rawChanges.push({
            rowKey: dest.rowKey,
            columnId: dest.columnId,
            rawValue: cols[c],
          });
        }

        pastedKeys.add(cellKey(dest.rowKey, dest.columnId));
      }
    }

    if (richChanges.length > 0) {
      cellValue.applyBulkValues(richChanges);
    }
    if (rawChanges.length > 0) {
      cellValue.applyBulkRawValues(rawChanges);
    }

    return pastedKeys;
  }

  private _clearCutSources(
    pastedKeys: Set<string>,
    cellValue: EditableTableState<T, F>['cellValue'],
  ) {
    const rawChanges: {
      rowKey: string;
      columnId: string;
      rawValue: string;
    }[] = [];
    for (const key of this.copiedCells) {
      if (pastedKeys.has(key)) {
        continue;
      }
      const pos = parseCellKey(key);
      if (pos) {
        rawChanges.push({
          rowKey: pos.rowKey,
          columnId: pos.columnId,
          rawValue: '',
        });
      }
    }

    cellValue.applyBulkRawValues(rawChanges);
  }

  clear() {
    this.copiedCells.clear();
    this.isCut = false;
    this._copiedDataString = '';
    this._copiedValues.clear();
  }

  isCellCopied(rowKey: string, columnId: string): boolean {
    return this.copiedCells.has(cellKey(rowKey, columnId));
  }

  getCopiedBorder(rowKey: string, columnId: string): CellBorder {
    if (!this.isCellCopied(rowKey, columnId)) {
      return noBorder;
    }

    const pos = this._parent._toIndices({ rowKey, columnId });
    if (!pos) {
      return noBorder;
    }

    const isNeighborCopied = (row: number, col: number) => {
      const neighbor = this._parent._toPosition(row, col);
      return (
        neighbor !== null &&
        this.copiedCells.has(cellKey(neighbor.rowKey, neighbor.columnId))
      );
    };

    return {
      top: !isNeighborCopied(pos.row - 1, pos.col),
      bottom: !isNeighborCopied(pos.row + 1, pos.col),
      left: !isNeighborCopied(pos.row, pos.col - 1),
      right: !isNeighborCopied(pos.row, pos.col + 1),
    };
  }

  get hasCopiedCells(): boolean {
    return this.copiedCells.size > 0;
  }

  private _storeCells(): string {
    const cells = this._parent.cellInteraction.selectedCellsList;
    if (cells.length === 0) {
      return '';
    }

    const { cellValue } = this._parent;

    for (const cell of cells) {
      this.copiedCells.add(cellKey(cell.rowKey, cell.columnId));
    }

    const indices = cells
      .map((c) => this._parent._toIndices(c))
      .filter((idx): idx is { row: number; col: number } => idx !== null);

    if (indices.length === 0) {
      return '';
    }

    const minRow = Math.min(...indices.map((i) => i.row));
    const maxRow = Math.max(...indices.map((i) => i.row));
    const minCol = Math.min(...indices.map((i) => i.col));
    const maxCol = Math.max(...indices.map((i) => i.col));

    const data: string[][] = [];
    for (let r = minRow; r <= maxRow; r++) {
      const row: string[] = [];
      for (let c = minCol; c <= maxCol; c++) {
        const pos = this._parent._toPosition(r, c);
        if (pos && this.copiedCells.has(cellKey(pos.rowKey, pos.columnId))) {
          const serialized = cellValue.serializeCellValue(
            pos.rowKey,
            pos.columnId,
          );
          row.push(serialized);
          this._copiedValues.set(
            `${r - minRow}:${c - minCol}`,
            cellValue.getCellDisplayValue(pos.rowKey, pos.columnId),
          );
        } else {
          row.push('');
        }
      }
      data.push(row);
    }

    const tsv = data.map((row) => row.join('\t')).join('\n');
    this._copiedDataString = tsv;
    return tsv;
  }
}
