import { proxy, signal, useProps } from "@odoo/owl";
import { Component, useLayoutEffect } from "../../../owl3_compatibility_layer";
import { figureRegistry } from "../../../registries/figures_registry";
import { useStore } from "../../../store_engine/store_hooks";
import { ZoomStore } from "../../../stores/zoom_store";
import { UpdateFiguresPayload } from "../../../types/commands";
import { AnchorOffset, FigureUI, ResizeDirection } from "../../../types/figure";
import { CSSProperties, UID } from "../../../types/misc";
import { Rect } from "../../../types/rendering";
import { SpreadsheetChildEnv } from "../../../types/spreadsheet_env";
import { Store } from "../../../types/store_engine";
import { cssPropertiesToCss } from "../../helpers/css";
import { keyboardEventToShortcutString } from "../../helpers/dom_helpers";
import { MenuPopover, MenuState } from "../../menu_popover/menu_popover";
import { types } from "../../props_validation";

type ResizeAnchor =
  | "top left"
  | "top"
  | "top right"
  | "right"
  | "bottom right"
  | "bottom"
  | "bottom left"
  | "left";

// -----------------------------------------------------------------------------
// STYLE
// -----------------------------------------------------------------------------
const ANCHOR_SIZE = 8;
const ACTIVE_BORDER_WIDTH = 2;

export class FigureComponent extends Component<SpreadsheetChildEnv> {
  static template = "o-spreadsheet-FigureComponent";
  static components = { MenuPopover };

  protected props = useProps({
    figureUI: types.FigureUI(),
    style: types.string(),
    class: types.string(),
    onMouseDown: types.function<(ev: MouseEvent) => void>().optional(() => () => {}),
    onClickAnchor: types
      .function<(dirX: ResizeDirection, dirY: ResizeDirection, ev: MouseEvent) => void>()
      .optional(() => () => {}),
  });

  private menuState: MenuState = proxy({ isOpen: false, anchorRect: null, menuItems: [] });

  private figureRef = signal.ref();

  private zoomStore!: Store<ZoomStore>;

  get isSelected(): boolean {
    return (
      !this.env.model.getters.isDashboard() &&
      this.env.model.getters.getSelectedFigureIds().includes(this.props.figureUI.id)
    );
  }

  get isThemeDependant(): boolean {
    return figureRegistry.get(this.props.figureUI.tag).isThemeDependant;
  }

  get figureRegistry() {
    return figureRegistry;
  }

  get borderWidth() {
    return figureRegistry.get(this.props.figureUI.tag).borderWidth(this.env.model.getters);
  }

  get hasShadow() {
    return figureRegistry.get(this.props.figureUI.tag).hasShadow(this.env.model.getters);
  }

  get isRounded() {
    return figureRegistry.get(this.props.figureUI.tag).isRounded(this.env.model.getters);
  }

  get wrapperStyle() {
    const { x, y, width, height } = this.props.figureUI;
    const padding = this.hasShadow ? 2 : 0; // Leave some space for the shadow, so it's not cut by overflow: hidden
    return cssPropertiesToCss({
      left: `${x + padding}px`,
      top: `${y + padding}px`,
      width: `${width - padding * 2}px`,
      height: `${height - padding * 2}px`,
    });
  }

  get figureStyle() {
    const properties: CSSProperties = {
      "border-width": `${this.borderWidth}px`,
    };
    return cssPropertiesToCss(properties) + ";" + this.props.style;
  }

  get figureClass() {
    const classes = [this.props.class];
    if (this.hasShadow) {
      classes.push("o-figure-shadow");
    }
    if (this.isRounded) {
      classes.push("o-figure-rounded");
    }
    return classes.join(" ");
  }

  getResizerPosition(resizer: ResizeAnchor): string {
    const anchorCenteringOffset = (ANCHOR_SIZE - ACTIVE_BORDER_WIDTH) / 2;
    const style: CSSProperties = {};
    if (resizer.includes("top")) {
      style.top = `${-anchorCenteringOffset}px`;
    } else if (resizer.includes("bottom")) {
      style.bottom = `${-anchorCenteringOffset}px`;
    } else {
      style.bottom = `calc(50% - ${anchorCenteringOffset}px)`;
    }

    if (resizer.includes("left")) {
      style.left = `${-anchorCenteringOffset}px`;
    } else if (resizer.includes("right")) {
      style.right = `${-anchorCenteringOffset}px`;
    } else {
      style.right = `calc(50% - ${anchorCenteringOffset}px)`;
    }
    return cssPropertiesToCss(style);
  }

  setup() {
    this.zoomStore = useStore(ZoomStore);
    useLayoutEffect(
      () => {
        const selectedFigureIds = this.env.model.getters.getSelectedFigureIds();
        const thisFigureId = this.props.figureUI.id;
        const el = this.figureRef();
        if (selectedFigureIds.includes(thisFigureId)) {
          /** Scrolling on a newly inserted figure that overflows outside the viewport
           * will break the whole layout.
           * NOTE: `preventScroll`does not work on mobile but then again,
           * mobile is not really supported ATM.
           *
           * TODO: When implementing proper mobile, we will need to scroll the viewport
           * correctly (and render?) before focusing the element.
           */
          el?.focus({ preventScroll: true });
        }
      },
      () => [this.env.model.getters.getSelectedFigureIds(), this.props.figureUI.id]
    );
  }

  clickAnchor(dirX: ResizeDirection, dirY: ResizeDirection, ev: MouseEvent) {
    this.props.onClickAnchor(dirX, dirY, ev);
  }

  onMouseDown(ev: MouseEvent) {
    if (!this.env.isMobile()) {
      this.props.onMouseDown(ev);
    }
  }

  onClick(ev: MouseEvent) {
    if (this.env.isMobile()) {
      this.props.onMouseDown(ev);
    }
  }

  onKeyDown(ev: KeyboardEvent) {
    const keyDownShortcut = keyboardEventToShortcutString(ev);
    switch (keyDownShortcut) {
      case "Delete":
      case "Backspace":
        this.env.model.dispatch("DELETE_FIGURES", {
          sheetId: this.env.model.getters.getActiveSheetId(),
          figureIds: this.env.model.getters.getSelectedFigureIds(),
        });
        ev.preventDefault();
        ev.stopPropagation();
        break;
      case "Shift+ArrowDown":
      case "Shift+ArrowLeft":
      case "Shift+ArrowRight":
      case "Shift+ArrowUp":
      case "ArrowDown":
      case "ArrowLeft":
      case "ArrowRight":
      case "ArrowUp":
        const sheetId = this.env.model.getters.getActiveSheetId();
        const figureIds = this.env.model.getters.getSelectedFigureIds();
        const figures: UpdateFiguresPayload[] = [];
        for (const figureId of figureIds) {
          const figure = this.env.model.getters.getFigure(sheetId, figureId);
          if (!figure) {
            continue;
          }
          figures.push({
            sheetId,
            figureId,
            ...this.positionInBoundary(
              sheetId,
              this.env.model.getters.getFigureUI(sheetId, figure),
              ev.key,
              ev.shiftKey
            ),
          });
        }
        this.env.model.dispatch("UPDATE_FIGURES", { figures });
        ev.preventDefault();
        ev.stopPropagation();
        break;
      case "Ctrl+A":
        // Maybe in the future we will implement a way to select all figures
        ev.preventDefault();
        ev.stopPropagation();
        break;
      case "Ctrl+Y":
      case "Ctrl+Z":
        if (keyDownShortcut === "Ctrl+Y") {
          this.env.model.dispatch("REQUEST_REDO");
        } else if (keyDownShortcut === "Ctrl+Z") {
          this.env.model.dispatch("REQUEST_UNDO");
        }
        ev.preventDefault();
        ev.stopPropagation();
        break;
    }
  }

  private positionInBoundary(
    sheetId: UID,
    figure: FigureUI,
    key: string,
    shift: boolean
  ): AnchorOffset {
    const shiftAmount = shift ? 1 : 5;
    let { col, row, offset } = figure;
    offset = { ...offset };
    const maxAnchor = this.env.model.getters.getMaxAnchorOffset(
      sheetId,
      figure.height,
      figure.width
    );
    switch (key) {
      case "ArrowUp":
        if (offset.y < shiftAmount) {
          row--;
          while (row > 0 && this.env.model.getters.isRowHiddenByUser(sheetId, row)) {
            row--;
          }
          offset.y = this.env.model.getters.getRowSize(sheetId, row) - shiftAmount + offset.y;
        } else {
          offset.y -= shiftAmount;
        }
        break;
      case "ArrowLeft":
        if (offset.x < shiftAmount) {
          col--;
          while (col > 0 && this.env.model.getters.isColHiddenByUser(sheetId, col)) {
            col--;
          }
          offset.x = this.env.model.getters.getColSize(sheetId, col) - shiftAmount + offset.x;
        } else {
          offset.x -= shiftAmount;
        }
        break;
      case "ArrowDown":
        const rowSize = this.env.model.getters.getRowSize(sheetId, row);
        if (offset.y + shiftAmount >= rowSize) {
          row++;
          while (row <= maxAnchor.row && this.env.model.getters.isRowHiddenByUser(sheetId, row)) {
            row++;
          }
          offset.y = offset.y + shiftAmount - rowSize;
        } else {
          offset.y += shiftAmount;
        }
        break;
      case "ArrowRight":
        const colSize = this.env.model.getters.getColSize(sheetId, col);
        if (offset.x + shiftAmount >= colSize) {
          col++;
          while (col <= maxAnchor.col && this.env.model.getters.isColHiddenByUser(sheetId, col)) {
            col++;
          }
          offset.x = offset.x + shiftAmount - colSize;
        } else {
          offset.x += shiftAmount;
        }
    }
    if (col < 0) {
      col = 0;
      offset.x = 0;
    } else if (col > maxAnchor.col || (col === maxAnchor.col && offset.x > maxAnchor.offset.x)) {
      col = maxAnchor.col;
      offset.x = maxAnchor.offset.x;
    }
    if (row < 0) {
      row = 0;
      offset.y = 0;
    } else if (row > maxAnchor.row || (row === maxAnchor.row && offset.y > maxAnchor.offset.y)) {
      row = maxAnchor.row;
      offset.y = maxAnchor.offset.y;
    }
    return { col, row, offset };
  }

  onContextMenu(ev: MouseEvent) {
    if (this.env.model.getters.isDashboard()) {
      return;
    }
    const zoomedMouseEvent = this.zoomStore.getZoomedEvent(ev);
    this.openContextMenu({
      x: zoomedMouseEvent.clientX,
      y: zoomedMouseEvent.clientY,
      width: 0,
      height: 0,
    });
  }

  openContextMenu(anchorRect: Rect) {
    this.menuState.isOpen = true;
    this.menuState.anchorRect = anchorRect;
    this.menuState.menuItems = figureRegistry
      .get(this.props.figureUI.tag)
      .menuBuilder(this.props.figureUI.id, this.env);
  }

  get isFigureResizable(): boolean {
    return (
      this.isSelected &&
      !this.env.isMobile() &&
      !this.env.model.getters.isDashboard() &&
      !this.env.model.getters.isCurrentSheetLocked()
    );
  }
}
