import type {
  ActionListener,
  CellAddress,
  EventListenerId,
  GetRadioEditorGroup,
  LayoutObjectId,
  RadioEditorOption,
} from "../../ts-types";
import { bindCellClickAction, bindCellKeyAction } from "./actionBind";
import {
  cellEquals,
  event,
  extend,
  isPromise,
  obj,
  then,
} from "../../internal/utils";
import {
  isDisabledRecord,
  isReadOnlyRecord,
  toggleValue,
} from "./action-utils";
import { DG_EVENT_TYPE } from "../../core/DG_EVENT_TYPE";
import { Editor } from "./Editor";
import type { GridInternal } from "../../ts-types-internal";
import type { RangePasteContext } from "./BaseAction";
import { animate } from "../../internal/animate";
import { getRadioColumnStateId } from "../../internal/symbolManager";
import { toBoolean } from "../utils";

const RADIO_COLUMN_STATE_ID = getRadioColumnStateId();

export class RadioEditor<T> extends Editor<T> {
  protected _group: GetRadioEditorGroup<T> | undefined;
  private _checkAction: ActionListener | undefined;
  constructor(option: RadioEditorOption<T> = {}) {
    super(option);
    this._group = option.group;
    this._checkAction = option.checkAction;
  }
  clone(): RadioEditor<T> {
    return new RadioEditor(this);
  }
  /** @deprecated Use checkAction instead. */
  get group(): GetRadioEditorGroup<T> | undefined {
    return this._group;
  }
  /** @deprecated Use checkAction instead. */
  set group(group: GetRadioEditorGroup<T> | undefined) {
    this._group = group;
  }
  get checkAction(): ActionListener | undefined {
    return this._checkAction;
  }
  set checkAction(checkAction: ActionListener | undefined) {
    this._checkAction = checkAction;
  }
  bindGridEvent(
    grid: GridInternal<T>,
    cellId: LayoutObjectId
  ): EventListenerId[] {
    let _state = grid[RADIO_COLUMN_STATE_ID];
    if (!_state) {
      _state = { block: {}, elapsed: {} };
      obj.setReadonly(grid, RADIO_COLUMN_STATE_ID, _state);
    }
    const state = _state;

    const action = (cell: CellAddress): void => {
      this._action(grid, cell);
    };

    function isTarget(col: number, row: number): boolean {
      return grid.getLayoutCellId(col, row) === cellId;
    }
    return [
      ...bindCellClickAction(grid, cellId, {
        action,
        mouseOver: (e) => {
          if (isDisabledRecord(this.disabled, grid, e.row)) {
            return false;
          }
          state.mouseActiveCell = {
            col: e.col,
            row: e.row,
          };
          const range = grid.getCellRange(e.col, e.row);
          grid.invalidateCellRange(range);
          return true;
        },
        mouseOut: (e) => {
          delete state.mouseActiveCell;
          const range = grid.getCellRange(e.col, e.row);
          grid.invalidateCellRange(range);
        },
      }),
      ...bindCellKeyAction(grid, cellId, {
        action,
      }),

      // paste value
      grid.listen(DG_EVENT_TYPE.PASTE_CELL, (e) => {
        if (e.multi) {
          // ignore multi cell values
          return;
        }
        const selectionRange = grid.selection.range;
        if (!cellEquals(selectionRange.start, selectionRange.end)) {
          // ignore multi paste values
          return;
        }
        if (!isTarget(e.col, e.row)) {
          return;
        }
        event.cancel(e.event);

        const pasteValue = e.normalizeValue.trim();
        if (isRejectValue(pasteValue)) {
          // Not a boolean
          const record = grid.getRowRecord(e.row);
          if (!isPromise(record)) {
            grid.fireListeners("rejected_paste_values", {
              detail: [
                {
                  col: e.col,
                  row: e.row,
                  record,
                  define: grid.getColumnDefine(e.col, e.row),
                  pasteValue,
                },
              ],
            });
          }
          return;
        }
        if (!toBoolean(pasteValue)) {
          return;
        }

        action({
          col: e.col,
          row: e.row,
        });
      }),
    ];
  }
  onPasteCellRangeBox(
    grid: GridInternal<T>,
    cell: CellAddress,
    value: string,
    context: RangePasteContext
  ): void {
    if (
      isReadOnlyRecord(this.readOnly, grid, cell.row) ||
      isDisabledRecord(this.disabled, grid, cell.row)
    ) {
      return;
    }
    const pasteValue = value.trim();
    if (isRejectValue(pasteValue)) {
      // Not a boolean
      context.reject();
      return;
    }
    if (!toBoolean(pasteValue)) {
      return;
    }
    this._action(grid, {
      col: cell.col,
      row: cell.row,
    });
  }
  onDeleteCellRangeBox(): void {
    // noop
  }
  private _action(grid: GridInternal<T>, cell: CellAddress): void {
    const state = grid[RADIO_COLUMN_STATE_ID]!;
    const range = grid.getCellRange(cell.col, cell.row);
    const cellKey = `${range.start.col}:${range.start.row}`;

    if (
      isReadOnlyRecord(this.readOnly, grid, cell.row) ||
      isDisabledRecord(this.disabled, grid, cell.row) ||
      state.block[cellKey]
    ) {
      return;
    }

    grid.doGetCellValue(cell.col, cell.row, (value) => {
      if (toBoolean(value)) {
        return;
      }
      if (this._checkAction) {
        // User behavior
        const record = grid.getRowRecord(cell.row);
        this._checkAction(record, extend(cell, { grid }));
        return;
      }
      if (this._group) {
        // Backward compatibility
        const state = grid[RADIO_COLUMN_STATE_ID]!;

        const targets = this._group({ grid, col: cell.col, row: cell.row });
        targets.forEach(({ col, row }) => {
          const range = grid.getCellRange(col, row);
          const cellKey = `${range.start.col}:${range.start.row}`;

          if (
            isReadOnlyRecord(this.readOnly, grid, cell.row) ||
            isDisabledRecord(this.disabled, grid, cell.row) ||
            state.block[cellKey]
          ) {
            return;
          }

          actionCell(grid, col, row, col === cell.col && row === cell.row);
        });
        return;
      }

      // default behavior
      const field = grid.getField(cell.col, cell.row)!;
      const recordStartRow = grid.getRecordStartRowByRecordIndex(
        grid.getRecordIndexByRow(cell.row)
      );

      /** Original DataSource */
      const { dataSource } = grid.dataSource;

      const girdRecords = getAllRecordsFromGrid(grid);

      for (let index = 0; index < dataSource.length; index++) {
        const record = dataSource.get(index);
        const showData = girdRecords.find((d) => d.record === record);
        if (showData) {
          actionCell(
            grid,
            cell.col,
            showData.row,
            showData.row === recordStartRow
          );
        } else {
          // Hidden record
          then(dataSource.getField(index, field), (value) => {
            if (!toBoolean(value)) {
              return;
            }
            dataSource.setField(index, field, toggleValue(value));
          });
        }
      }
    });
  }
}

function getAllRecordsFromGrid<T>(grid: GridInternal<T>) {
  const result = [];
  const { rowCount, recordRowCount } = grid;
  for (
    let targetRow = grid.frozenRowCount;
    targetRow < rowCount;
    targetRow += recordRowCount
  ) {
    const record = grid.getRowRecord(targetRow);
    result.push({ row: targetRow, record });
  }
  return result;
}

function actionCell<T>(
  grid: GridInternal<T>,
  col: number,
  row: number,
  flag: boolean
): void {
  grid.doGetCellValue(col, row, (value) => {
    if (toBoolean(value) === flag) {
      return;
    }

    const state = grid[RADIO_COLUMN_STATE_ID]!;
    const range = grid.getCellRange(col, row);
    const cellKey = `${range.start.col}:${range.start.row}`;
    const ret = grid.doChangeValue(col, row, toggleValue);
    if (ret) {
      const onChange = (): void => {
        // checkbox animation
        animate(200, (point) => {
          if (point === 1) {
            delete state.elapsed[cellKey];
          } else {
            state.elapsed[cellKey] = point;
          }
          grid.invalidateCellRange(range);
        });
      };
      if (isPromise(ret)) {
        state.block[cellKey] = true;
        ret.then(() => {
          delete state.block[cellKey];
          onChange();
        });
      } else {
        onChange();
      }
    }
  });
}

function isRejectValue(pasteValue: string) {
  return toggleValue(toggleValue(pasteValue)) !== pasteValue;
}
