import { onMounted, onWillUnmount, signal, useProps } from "@odoo/owl";
import { rectIntersection } from "../../helpers/rectangle";
import { Component, useLayoutEffect } from "../../owl3_compatibility_layer";
import { CSSProperties } from "../../types/misc";
import { DOMCoordinates, DOMDimension, Rect } from "../../types/rendering";
import { SpreadsheetChildEnv } from "../../types/spreadsheet_env";
import { usePopoverContainer, useSpreadsheetRect } from "../helpers/position_hook";
import { types } from "../props_validation";

type PopoverPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right";
type DisplayValue = "none" | "block";

export class Popover extends Component<SpreadsheetChildEnv> {
  static template = "o-spreadsheet-Popover";

  protected props = useProps({
    anchorRect: types.Rect(),
    positioning: types
      .or([types.literal("top-right"), types.literal("bottom-left")])
      .optional("bottom-left"),
    maxWidth: types.Pixel().optional(),
    maxHeight: types.Pixel().optional(),
    verticalOffset: types.number().optional(0),
    onMouseWheel: types.function().optional(() => () => {}),
    onPopoverHidden: types.function().optional(() => () => {}),
    onPopoverMoved: types.function().optional(() => () => {}),
    zIndex: types.number().optional(),
    class: types.string().optional(),
  });

  private popoverRef = signal.ref();
  private popoverContentRef = signal.ref();
  private currentPosition: PopoverPosition | undefined = undefined;
  private currentDisplayValue: DisplayValue | undefined = undefined;

  private spreadsheetRect = useSpreadsheetRect();
  private containerRect: Rect | undefined;

  setup() {
    this.containerRect = usePopoverContainer();

    const resizeObserver = new ResizeObserver(this.computePopoverPosition.bind(this));
    onMounted(() => {
      const contentEl = this.popoverContentRef();
      if (contentEl) {
        resizeObserver.observe(contentEl);
      }
    });
    onWillUnmount(() => {
      resizeObserver.disconnect();
    });

    useLayoutEffect(this.computePopoverPosition.bind(this));
  }

  private computePopoverPosition() {
    if (!this.containerRect) {
      throw new Error("Popover container is not defined");
    }
    const el = this.popoverRef();
    const contentEl = this.popoverContentRef();
    if (!el || !contentEl) {
      return;
    }

    const anchor = rectIntersection(this.props.anchorRect, this.containerRect);
    const newDisplay: DisplayValue = anchor ? "block" : "none";
    if (this.currentDisplayValue !== "none" && newDisplay === "none") {
      this.props.onPopoverHidden?.();
    }
    el.style.display = newDisplay;
    this.currentDisplayValue = newDisplay;

    if (!anchor) {
      return;
    }

    const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
    let elDims = {
      width: contentEl.getBoundingClientRect().width,
      height: contentEl.getBoundingClientRect().height,
    };

    const spreadsheetRect = this.spreadsheetRect;

    const popoverPositionHelper =
      this.props.positioning === "bottom-left"
        ? new BottomLeftPopoverContext(
            anchor,
            this.containerRect,
            propsMaxSize,
            spreadsheetRect,
            this.currentPosition
          )
        : new TopRightPopoverContext(
            anchor,
            this.containerRect,
            propsMaxSize,
            spreadsheetRect,
            this.currentPosition
          );

    el.style["max-height"] = popoverPositionHelper.getMaxHeight(elDims.height) + "px";
    el.style["max-width"] = popoverPositionHelper.getMaxWidth(elDims.width) + "px";
    // Re-compute the dimensions after setting the max-width and max-height
    elDims = {
      width: el.getBoundingClientRect().width,
      height: el.getBoundingClientRect().height,
    };

    const style = popoverPositionHelper.getCss(elDims, this.props.verticalOffset);
    for (const property of Object.keys(style)) {
      el.style[property] = style[property];
    }

    const newPosition = popoverPositionHelper.getCurrentPosition(elDims);
    if (this.currentPosition && newPosition !== this.currentPosition) {
      this.props.onPopoverMoved?.();
    }
    this.currentPosition = newPosition;
  }
}

abstract class PopoverPositionContext {
  constructor(
    protected anchorRect: Rect,
    protected containerRect: Rect,
    private propsMaxSize: Partial<DOMDimension>,
    private spreadsheetOffset: DOMCoordinates,
    private lastPosition?: PopoverPosition
  ) {}

  protected abstract get availableHeightUp(): number;
  protected abstract get availableHeightDown(): number;
  protected abstract get availableWidthRight(): number;
  protected abstract get availableWidthLeft(): number;

  protected abstract getTopCoordinate(elementHeight: number, shouldRenderAtBottom: boolean): number;
  protected abstract getLeftCoordinate(elementWidth: number, shouldRenderAtRight: boolean): number;

  /**
   * Check if the popover should be rendered at the bottom of the anchorRect or at the top.
   * Try to keep the same position as this.lastPosition if possible.
   */
  private shouldRenderAtBottom(elementHeight: number): boolean {
    if (this.lastPosition === "top-left" || this.lastPosition === "top-right") {
      const shouldRenderAtTop =
        elementHeight <= this.availableHeightUp ||
        this.availableHeightUp >= this.availableHeightDown;
      return !shouldRenderAtTop;
    }
    const shouldRenderAtBottom =
      elementHeight <= this.availableHeightDown ||
      this.availableHeightDown >= this.availableHeightUp;
    return shouldRenderAtBottom;
  }

  /**
   * Check if the popover should be rendered at the right of the anchorRect or at the left.
   * Try to keep the same position as this.lastPosition if possible.
   */
  private shouldRenderAtRight(elementWidth: number): boolean {
    if (this.lastPosition === "bottom-left" || this.lastPosition === "top-left") {
      const shouldRenderAtLeft =
        elementWidth <= this.availableWidthLeft ||
        this.availableWidthLeft >= this.availableWidthRight;
      return !shouldRenderAtLeft;
    }
    const shouldRenderAtRight =
      elementWidth <= this.availableWidthRight ||
      this.availableWidthRight >= this.availableWidthLeft;
    return shouldRenderAtRight;
  }

  getMaxHeight(elementHeight: number) {
    const shouldRenderAtBottom = this.shouldRenderAtBottom(elementHeight);
    const availableHeight = shouldRenderAtBottom
      ? this.availableHeightDown
      : this.availableHeightUp;

    return this.propsMaxSize.height
      ? Math.min(availableHeight, this.propsMaxSize.height)
      : availableHeight;
  }

  getMaxWidth(elementWidth: number) {
    const shouldRenderAtRight = this.shouldRenderAtRight(elementWidth);
    const availableWidth = shouldRenderAtRight ? this.availableWidthRight : this.availableWidthLeft;

    return this.propsMaxSize.width
      ? Math.min(availableWidth, this.propsMaxSize.width)
      : availableWidth;
  }

  getCss(elDims: DOMDimension, verticalOffset: number): CSSProperties {
    const maxHeight = this.getMaxHeight(elDims.height);
    const maxWidth = this.getMaxWidth(elDims.width);

    const actualHeight = Math.min(maxHeight, elDims.height);
    const actualWidth = Math.min(maxWidth, elDims.width);

    const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height);
    const shouldRenderAtRight = this.shouldRenderAtRight(elDims.width);

    verticalOffset = shouldRenderAtBottom ? verticalOffset : -verticalOffset;
    return {
      top:
        this.getTopCoordinate(actualHeight, shouldRenderAtBottom) -
        this.spreadsheetOffset.y -
        verticalOffset +
        "px",
      left:
        this.getLeftCoordinate(actualWidth, shouldRenderAtRight) - this.spreadsheetOffset.x + "px",
    };
  }

  getCurrentPosition(elDims: DOMDimension): PopoverPosition {
    const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height);
    const shouldRenderAtRight = this.shouldRenderAtRight(elDims.width);

    if (shouldRenderAtBottom && shouldRenderAtRight) {
      return "bottom-right";
    }
    if (shouldRenderAtBottom && !shouldRenderAtRight) {
      return "bottom-left";
    }
    if (!shouldRenderAtBottom && shouldRenderAtRight) {
      return "top-right";
    }
    return "top-left";
  }
}

class BottomLeftPopoverContext extends PopoverPositionContext {
  protected get availableHeightUp() {
    return this.anchorRect.y - this.containerRect.y;
  }

  protected get availableHeightDown() {
    return this.containerRect.height - this.availableHeightUp - this.anchorRect.height;
  }

  protected get availableWidthRight() {
    return this.containerRect.x + this.containerRect.width - this.anchorRect.x;
  }

  protected get availableWidthLeft() {
    return this.anchorRect.x + this.anchorRect.width - this.containerRect.x;
  }

  protected getTopCoordinate(elementHeight: number, shouldRenderAtBottom: boolean): number {
    if (shouldRenderAtBottom) {
      return this.anchorRect.y + this.anchorRect.height;
    } else {
      return this.anchorRect.y - elementHeight;
    }
  }

  protected getLeftCoordinate(elementWidth: number, shouldRenderAtRight: boolean): number {
    if (shouldRenderAtRight) {
      return this.anchorRect.x;
    } else {
      return this.anchorRect.x + this.anchorRect.width - elementWidth;
    }
  }
}

class TopRightPopoverContext extends PopoverPositionContext {
  protected get availableHeightUp() {
    return this.anchorRect.y + this.anchorRect.height - this.containerRect.y;
  }

  protected get availableHeightDown() {
    return this.containerRect.y + this.containerRect.height - this.anchorRect.y;
  }

  protected get availableWidthRight() {
    return this.containerRect.width - this.anchorRect.width - this.availableWidthLeft;
  }

  protected get availableWidthLeft() {
    return this.anchorRect.x - this.containerRect.x;
  }

  protected getTopCoordinate(elementHeight: number, shouldRenderAtBottom: boolean): number {
    if (shouldRenderAtBottom) {
      return this.anchorRect.y;
    } else {
      return this.anchorRect.y + this.anchorRect.height - elementHeight;
    }
  }

  protected getLeftCoordinate(elementWidth: number, shouldRenderAtRight: boolean): number {
    if (shouldRenderAtRight) {
      return this.anchorRect.x + this.anchorRect.width;
    } else {
      return this.anchorRect.x - elementWidth;
    }
  }
}
