import { onWillStart, onWillUpdateProps, proxy, signal, useEffect, useProps } from "@odoo/owl";
import { deepEquals, range } from "../../helpers/misc";
import { Component, useLayoutEffect } from "../../owl3_compatibility_layer";
import { useLocalStore, useStore } from "../../store_engine/store_hooks";
import { DOMFocusableElementStore } from "../../stores/DOM_focus_store";
import { Color } from "../../types/misc";
import { PropsOf } from "../../types/props_of";
import { SpreadsheetChildEnv } from "../../types/spreadsheet_env";
import { Store } from "../../types/store_engine";
import { cssPropertiesToCss } from "../helpers/css";
import { useDragAndDropListItems } from "../helpers/drag_and_drop_dom_items_hook";
import { updateSelectionWithArrowKeys } from "../helpers/selection_helpers";
import { types } from "../props_validation";
import { RangeInputValue, SelectionInputStore } from "./selection_input_store";

interface State {
  isMissing: boolean;
}

interface SelectionRange extends Omit<RangeInputValue, "color"> {
  isFocused: boolean;
  isValidRange: boolean;
  color?: Color;
  disabled?: boolean;
}
/**
 * This component can be used when the user needs to input some
 * ranges. He can either input the ranges with the regular DOM `<input/>`
 * displayed or by selecting zones on the grid.
 *
 * onSelectionChanged is called every time the input value
 * changes.
 */
export class SelectionInput extends Component<SpreadsheetChildEnv> {
  static template = "o-spreadsheet-SelectionInput";

  protected props = useProps({
    ranges: types.array(types.string()),
    hasSingleRange: types.boolean().optional(),
    required: types.boolean().optional(),
    autofocus: types.boolean().optional(),
    isInvalid: types.boolean().optional(),
    class: types.string().optional(),
    onSelectionChanged: types.function<(ranges: string[]) => void>().optional(),
    onSelectionConfirmed: types.function().optional(),
    onSelectionReordered: types.function<(indexes: number[]) => void>().optional(),
    onSelectionRemoved: types.function<(index: number) => void>().optional(),
    onInputFocused: types.function().optional(),
    colors: types.ArrayOf<Color>().optional([]),
    disabledRanges: types.array(types.boolean()).optional([]),
    disabledRangeTitle: types.string().optional(),
  });
  private state: State = proxy({
    isMissing: false,
  });
  private dragAndDrop = useDragAndDropListItems();
  private focusedInputRef = signal.ref(HTMLInputElement);
  unfocusedInputRef = signal.ref(HTMLInputElement);
  private selectionRef = signal.ref();
  private DOMFocusableElementStore!: Store<DOMFocusableElementStore>;
  private store!: Store<SelectionInputStore>;

  get ranges(): SelectionRange[] {
    return this.store.selectionInputs;
  }

  get canAddRange(): boolean {
    return !this.props.hasSingleRange;
  }

  get isInvalid(): boolean {
    return this.props.isInvalid || this.state.isMissing;
  }

  get isConfirmable(): boolean {
    return this.store.isConfirmable;
  }

  get isResettable(): boolean {
    return this.store.isResettable;
  }

  get hasDisabledRanges(): boolean {
    return this.store.disabledRanges.some(Boolean);
  }

  setup() {
    /**
     * TODO Remove this empty onWillStart
     * This onWillStart empty is here following the update to OWL3
     * (due to this commit: https://github.com/odoo/owl/commit/16a65d545e1f8aa1b5f0a404f685a98ca6d6814a)
     */
    onWillStart(() => {});
    /**
     * TODO Remove this empty onWillUpdateProps
     * This onWillUpdateProps empty is here following the update to OWL3
     * (due to this commit: https://github.com/odoo/owl/commit/076b1f09079f8263a84afa4e412d395190a693db)
     */
    onWillUpdateProps(async () => {});

    this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
    this.store = useLocalStore(
      SelectionInputStore,
      this.props.ranges,
      this.props.hasSingleRange || false,
      this.props.colors,
      this.props.disabledRanges
    );
    if (this.props.autofocus) {
      this.store.focusById(this.store.selectionInputs[0]?.id);
      this.props.onInputFocused?.();
    }
    useEffect(() => {
      this.focusedInputRef()?.focus();
    });
    // Check on every render if the element is hidden (non-reactive DOM property)
    const checkVisibility = () => {
      const el = this.selectionRef();
      if ((this.store.isResettable || this.store.hasFocus) && el?.offsetParent === null) {
        this.reset();
      }
    };
    useLayoutEffect(checkVisibility);
    onWillUpdateProps((nextProps: PropsOf<SelectionInput>) => {
      if (nextProps.ranges.join() !== this.store.selectionInputValues.join()) {
        this.triggerChange();
      }
      if (
        nextProps.ranges.join() !== this.props.ranges.join() &&
        nextProps.ranges.join() !== this.store.selectionInputValues.join()
      ) {
        this.store.resetWithRanges(nextProps.ranges);
      }
      if (
        nextProps.colors?.join() !== this.props.colors?.join() &&
        nextProps.colors?.join() !== this.store.colors.join()
      ) {
        this.store.updateColors(nextProps.colors || []);
      }
      if (
        !deepEquals(nextProps.disabledRanges, this.props.disabledRanges) &&
        !deepEquals(nextProps.disabledRanges, this.store.disabledRanges)
      ) {
        this.store.updateDisabledRanges(nextProps.disabledRanges || []);
      }
    });
  }

  startDragAndDrop(rangeId: number, event: MouseEvent) {
    if (event.button !== 0 || (event.target as HTMLElement).tagName === "SELECT") {
      return;
    }

    const rects = this.getRangeElementsRects();
    const draggableIds = this.ranges.map((range) => range.id);
    const draggableItems = draggableIds.map((id, index) => ({
      id: id.toString(),
      size: rects[index].height,
      position: rects[index].y,
    }));
    this.dragAndDrop.start("vertical", {
      draggedItemId: rangeId.toString(),
      initialMousePosition: event.clientY,
      items: draggableItems,
      scrollableContainerEl: this.selectionRef()!,
      onDragEnd: (dimensionName, finalIndex) => {
        const originalIndex = draggableIds.findIndex((id) => id === rangeId);
        if (originalIndex === finalIndex) {
          return;
        }
        const indexes = range(0, draggableIds.length);
        indexes.splice(originalIndex, 1);
        indexes.splice(finalIndex, 0, originalIndex);
        this.props.onSelectionReordered?.(indexes);
        this.props.onSelectionConfirmed?.();
        this.store.confirm();
      },
    });
  }

  getRangeElementsRects() {
    return Array.from(this.selectionRef()!.children).map((el) => el.getBoundingClientRect());
  }

  getColor(range: SelectionRange) {
    const properties = {};
    if (range.color) {
      properties["color"] = range.color;
    }
    if (this.store.mode === "select-range") {
      properties["caret-color"] = "transparent";
    }
    return cssPropertiesToCss(properties);
  }

  private triggerChange() {
    const ranges = this.store.selectionInputValues;
    this.props.onSelectionChanged?.(ranges);
  }

  onKeydown(ev: KeyboardEvent) {
    if (ev.key === "F2") {
      ev.preventDefault();
      ev.stopPropagation();
      this.store.toggleEditMode();
    } else if (ev.key.startsWith("Arrow")) {
      ev.stopPropagation();
      if (this.store.mode === "select-range") {
        ev.preventDefault();
        updateSelectionWithArrowKeys(ev, this.env.model.selection);
      }
    } else if (ev.key === "Enter") {
      const target = ev.target as HTMLInputElement;
      target.blur();
      if (this.isConfirmable) {
        this.confirm();
      }
    } else if (ev.key === "Escape") {
      this.reset();
      this.DOMFocusableElementStore.focus();
    }
  }

  private extractRanges(value: string): string {
    return this.props.hasSingleRange ? value.split(",")[0] : value;
  }

  focus(rangeId: number) {
    this.state.isMissing = false;
    this.store.focusById(rangeId);
    this.props.onInputFocused?.();
  }

  addEmptyInput() {
    this.store.addEmptyRange();
  }

  removeInput(rangeId: number) {
    const index = this.store.selectionInputs.findIndex((range) => range.id === rangeId);
    if (this.ranges.find((range) => range.id === rangeId)?.xc) {
      this.props.onSelectionRemoved?.(index);
      this.props.onSelectionConfirmed?.();
    }
    this.store.removeRange(rangeId);
  }

  onInputChanged(rangeId: number, ev: InputEvent) {
    const target = ev.target as HTMLInputElement;
    const value = this.extractRanges(target.value);
    this.store.changeRange(rangeId, value);
    this.triggerChange();
  }

  reset() {
    this.store.reset();
    this.triggerChange();
  }

  confirm() {
    this.store.confirm();
    const anyValidInput = this.store.selectionInputs.some((range) =>
      this.env.model.getters.isRangeValid(range.xc)
    );
    if (this.props.required && !anyValidInput) {
      this.state.isMissing = true;
    }
    this.props.onSelectionChanged?.(this.store.selectionInputValues);
    this.props.onSelectionConfirmed?.();
  }
}
