import { debounce } from "lodash"
import { EventEmitter } from "./emitter"
import { isIOS, isMacintosh } from "./platform"
import {
  active,
  disabled,
  horizontal,
  hover,
  mac,
  maximum,
  minimum,
  sash,
  vertical,
} from "./index.module.scss"

export interface SashOptions {
  /** Whether a sash is horizontal or vertical. */
  readonly orientation: Orientation

  /** The width or height of a vertical or horizontal sash, respectively. */
  readonly size?: number
}

export interface SashEvent {
  startX: number
  currentX: number
  startY: number
  currentY: number
}

export const enum Orientation {
  Vertical,
  Horizontal,
}

export const enum SashState {
  /** Disable any UI interaction. */
  Disabled,

  /**
   * Allow dragging down or to the right, depending on the sash orientation.
   *
   * Some OSs allow customizing the mouse cursor differently whenever
   * some resizable component can't be any smaller, but can be larger.
   */
  Minimum,

  /**
   * Allow dragging up or to the left, depending on the sash orientation.
   *
   * Some OSs allow customizing the mouse cursor differently whenever
   * some resizable component can't be any larger, but can be smaller.
   */
  Maximum,

  /** Enable dragging. */
  Enabled,
}

let globalSize = isIOS ? 20 : 8

const onDidChangeGlobalSize = new EventEmitter<{ change: [size: number] }>()

export function setGlobalSashSize(size: number): void {
  globalSize = size
  onDidChangeGlobalSize.emit("change", size)
}

type SashLayoutProvider = unknown

/** A vertical sash layout provider provides position and height for a sash. */
export interface VerticalSashLayoutProvider {
  getVerticalSashLeft(sash: Sash): number
  getVerticalSashTop?(sash: Sash): number
  getVerticalSashHeight?(sash: Sash): number
}

/** A horizontal sash layout provider provides position and width for a sash. */
export interface HorizontalSashLayoutProvider {
  getHorizontalSashTop(sash: Sash): number
  getHorizontalSashLeft?(sash: Sash): number
  getHorizontalSashWidth?(sash: Sash): number
}

/**
 * The {@link Sash} is the UI component which allows the user to resize other
 * components. It's usually an invisible horizontal or vertical line which, when
 * hovered, becomes highlighted and can be dragged along the perpendicular dimension
 * to its direction.
 */
export class Sash
  extends EventEmitter<{
    enablementChange: [state: SashState]
    start: [startEvent: SashEvent]
    change: [changeEvent: SashEvent]
    end: []
    reset: []
  }>
  implements Disposable
{
  private el: HTMLElement
  private layoutProvider: SashLayoutProvider
  private orientation!: Orientation
  private size: number
  private hoverDelay = 300
  private hoverDelayer = debounce(el => el.classList.add(hover), this.hoverDelay)
  private stack = new DisposableStack()

  private _state: SashState = SashState.Enabled

  get state(): SashState {
    return this._state
  }

  /**
   * The state of a sash defines whether it can be interacted with by the user
   * as well as what mouse cursor to use, when hovered.
   */
  set state(state: SashState) {
    if (this._state === state) {
      return
    }

    this.el.classList.toggle(disabled, state === SashState.Disabled)
    this.el.classList.toggle(minimum, state === SashState.Minimum)
    this.el.classList.toggle(maximum, state === SashState.Maximum)

    this._state = state

    this.emit("enablementChange", state)
  }

  /**
   * Create a new vertical sash.
   *
   * @param container A DOM node to append the sash to.
   * @param verticalLayoutProvider A vertical layout provider.
   * @param options The options.
   */
  constructor(
    container: HTMLElement,
    layoutProvider: VerticalSashLayoutProvider,
    options: SashOptions,
  )

  /**
   * Create a new horizontal sash.
   *
   * @param container A DOM node to append the sash to.
   * @param horizontalLayoutProvider A horizontal layout provider.
   * @param options The options.
   */
  constructor(
    container: HTMLElement,
    layoutProvider: HorizontalSashLayoutProvider,
    options: SashOptions,
  )

  constructor(
    container: HTMLElement,
    layoutProvider: SashLayoutProvider,
    options: SashOptions,
  ) {
    super()
    this.el = document.createElement("div")
    this.el.classList.add(sash)
    this.el.dataset.testid = "sash"
    container.append(this.el)

    if (isMacintosh) {
      this.el.classList.add(mac)
    }

    this.el.addEventListener("pointerdown", this.#onPointerStart)
    this.el.addEventListener("dblclick", this.#onPointerDoublePress)
    this.el.addEventListener("mouseenter", this.#onMouseEnter)
    this.el.addEventListener("mouseleave", this.onMouseLeave)

    if (typeof options.size === "number") {
      this.size = options.size

      if (options.orientation === Orientation.Vertical) {
        this.el.style.width = `${this.size}px`
      } else {
        this.el.style.height = `${this.size}px`
      }
    } else {
      this.size = globalSize

      this.stack.use(
        onDidChangeGlobalSize.on("change", size => {
          this.size = size
          this.layout()
        }),
      )
    }

    this.layoutProvider = layoutProvider

    this.orientation = options.orientation ?? Orientation.Vertical

    if (this.orientation === Orientation.Horizontal) {
      this.el.classList.add(horizontal)
      this.el.classList.remove(vertical)
    } else {
      this.el.classList.remove(horizontal)
      this.el.classList.add(vertical)
    }

    this.layout()
  }

  #onPointerStart = (event: PointerEvent) => {
    const startX = event.pageX
    const startY = event.pageY

    const startEvent: SashEvent = {
      startX,
      currentX: startX,
      startY,
      currentY: startY,
    }

    this.el.classList.add(active)

    this.emit("start", startEvent)

    this.el.setPointerCapture(event.pointerId)

    const onPointerMove = (event: PointerEvent) => {
      event.preventDefault()

      const moveEvent: SashEvent = {
        startX,
        currentX: event.pageX,
        startY,
        currentY: event.pageY,
      }

      this.emit("change", moveEvent)
    }

    const onPointerUp = (event: PointerEvent): void => {
      event.preventDefault()

      this.el.classList.remove(active)
      this.hoverDelayer.cancel()
      this.emit("end")

      this.el.releasePointerCapture(event.pointerId)

      window.removeEventListener("pointermove", onPointerMove)
      window.removeEventListener("pointerup", onPointerUp)
    }

    window.addEventListener("pointermove", onPointerMove)
    window.addEventListener("pointerup", onPointerUp)
  }

  #onPointerDoublePress = (): void => {
    this.emit("reset")
  }

  #onMouseEnter = (): void => {
    if (this.el.classList.contains(active)) {
      this.hoverDelayer.cancel()
      this.el.classList.add(hover)
    } else {
      this.hoverDelayer(this.el)
    }
  }

  private onMouseLeave = (): void => {
    this.hoverDelayer.cancel()
    this.el.classList.remove(hover)
  }

  /**
   * Layout the sash. The sash will size and position itself
   * based on its provided {@link SashLayoutProvider layout provider}.
   */
  public layout(): void {
    if (this.orientation === Orientation.Vertical) {
      const verticalProvider = this.layoutProvider as VerticalSashLayoutProvider

      this.el.style.left =
        verticalProvider.getVerticalSashLeft(this) - this.size / 2 + "px"

      if (verticalProvider.getVerticalSashTop) {
        this.el.style.top = verticalProvider.getVerticalSashTop(this) + "px"
      }

      if (verticalProvider.getVerticalSashHeight) {
        this.el.style.height = verticalProvider.getVerticalSashHeight(this) + "px"
      }
    } else {
      const horizontalProvider = this.layoutProvider as HorizontalSashLayoutProvider

      this.el.style.top =
        horizontalProvider.getHorizontalSashTop(this) - this.size / 2 + "px"

      if (horizontalProvider.getHorizontalSashLeft) {
        this.el.style.left = horizontalProvider.getHorizontalSashLeft(this) + "px"
      }

      if (horizontalProvider.getHorizontalSashWidth) {
        this.el.style.width = horizontalProvider.getHorizontalSashWidth(this) + "px"
      }
    }
  }

  [Symbol.dispose](): void {
    this.el.removeEventListener("pointerdown", this.#onPointerStart)
    this.el.removeEventListener("dblclick", this.#onPointerDoublePress)
    this.el.removeEventListener("mouseenter", this.#onMouseEnter)
    this.el.removeEventListener("mouseleave", () => this.onMouseLeave)

    this.el.remove()
    this.stack.dispose()
  }
}
