import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
import {
  css,
  CSSResultGroup,
  html,
  LitElement,
  PropertyValues,
  TemplateResult,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";

declare global {
  interface HASSDomEvents {
    "slider-moved": { value?: number };
  }
}

const A11Y_KEY_CODES = new Set([
  "ArrowRight",
  "ArrowUp",
  "ArrowLeft",
  "ArrowDown",
  "PageUp",
  "PageDown",
  "Home",
  "End",
]);

@customElement("ha-control-slider")
export class HaControlSlider extends LitElement {
  @property({ type: Boolean, reflect: true })
  public disabled = false;

  @property()
  public mode?: "start" | "end" | "cursor" = "start";

  @property({ type: Boolean, reflect: true })
  public vertical = false;

  @property({ type: Boolean, attribute: "show-handle" })
  public showHandle = false;

  @property({ type: Number })
  public value?: number;

  @property({ type: Number })
  public step = 1;

  @property({ type: Number })
  public min = 0;

  @property({ type: Number })
  public max = 100;

  private _mc?: HammerManager;

  @property({ type: Boolean, reflect: true })
  public pressed = false;

  valueToPercentage(value: number) {
    return (this.boundedValue(value) - this.min) / (this.max - this.min);
  }

  percentageToValue(value: number) {
    return (this.max - this.min) * value + this.min;
  }

  steppedValue(value: number) {
    return Math.round(value / this.step) * this.step;
  }

  boundedValue(value: number) {
    return Math.min(Math.max(value, this.min), this.max);
  }

  protected firstUpdated(changedProperties: PropertyValues): void {
    super.firstUpdated(changedProperties);
    this.setupListeners();
    this.setAttribute("role", "slider");
    if (!this.hasAttribute("tabindex")) {
      this.setAttribute("tabindex", "0");
    }
  }

  protected updated(changedProps: PropertyValues) {
    super.updated(changedProps);
    if (changedProps.has("value")) {
      const valuenow = this.steppedValue(this.value ?? 0);
      this.setAttribute("aria-valuenow", valuenow.toString());
    }
    if (changedProps.has("min")) {
      this.setAttribute("aria-valuemin", this.min.toString());
    }
    if (changedProps.has("max")) {
      this.setAttribute("aria-valuemax", this.max.toString());
    }
    if (changedProps.has("vertical")) {
      const orientation = this.vertical ? "vertical" : "horizontal";
      this.setAttribute("aria-orientation", orientation);
    }
  }

  connectedCallback(): void {
    super.connectedCallback();
    this.setupListeners();
  }

  disconnectedCallback(): void {
    super.disconnectedCallback();
    this.destroyListeners();
  }

  @query("#slider")
  private slider;

  setupListeners() {
    if (this.slider && !this._mc) {
      this._mc = new Manager(this.slider, {
        touchAction: this.vertical ? "pan-x" : "pan-y",
      });
      this._mc.add(
        new Pan({
          threshold: 10,
          direction: DIRECTION_ALL,
          enable: true,
        })
      );

      this._mc.add(new Tap({ event: "singletap" }));

      let savedValue;
      this._mc.on("panstart", () => {
        if (this.disabled) return;
        this.pressed = true;
        savedValue = this.value;
      });
      this._mc.on("pancancel", () => {
        if (this.disabled) return;
        this.pressed = false;
        this.value = savedValue;
      });
      this._mc.on("panmove", (e) => {
        if (this.disabled) return;
        const percentage = this._getPercentageFromEvent(e);
        this.value = this.percentageToValue(percentage);
        const value = this.steppedValue(this.value);
        fireEvent(this, "slider-moved", { value });
      });
      this._mc.on("panend", (e) => {
        if (this.disabled) return;
        this.pressed = false;
        const percentage = this._getPercentageFromEvent(e);
        this.value = this.steppedValue(this.percentageToValue(percentage));
        fireEvent(this, "slider-moved", { value: undefined });
        fireEvent(this, "value-changed", { value: this.value });
      });

      this._mc.on("singletap", (e) => {
        if (this.disabled) return;
        const percentage = this._getPercentageFromEvent(e);
        this.value = this.steppedValue(this.percentageToValue(percentage));
        fireEvent(this, "value-changed", { value: this.value });
      });

      this.addEventListener("keydown", this._handleKeyDown);
      this.addEventListener("keyup", this._handleKeyUp);
    }
  }

  destroyListeners() {
    if (this._mc) {
      this._mc.destroy();
      this._mc = undefined;
    }
    this.removeEventListener("keydown", this._handleKeyDown);
    this.removeEventListener("keyup", this._handleKeyDown);
  }

  private get _tenPercentStep() {
    return Math.max(this.step, (this.max - this.min) / 10);
  }

  _handleKeyDown(e: KeyboardEvent) {
    if (!A11Y_KEY_CODES.has(e.code)) return;
    e.preventDefault();
    switch (e.code) {
      case "ArrowRight":
      case "ArrowUp":
        this.value = this.boundedValue((this.value ?? 0) + this.step);
        break;
      case "ArrowLeft":
      case "ArrowDown":
        this.value = this.boundedValue((this.value ?? 0) - this.step);
        break;
      case "PageUp":
        this.value = this.steppedValue(
          this.boundedValue((this.value ?? 0) + this._tenPercentStep)
        );
        break;
      case "PageDown":
        this.value = this.steppedValue(
          this.boundedValue((this.value ?? 0) - this._tenPercentStep)
        );
        break;
      case "Home":
        this.value = this.min;
        break;
      case "End":
        this.value = this.max;
        break;
    }
    fireEvent(this, "slider-moved", { value: this.value });
  }

  _handleKeyUp(e: KeyboardEvent) {
    if (!A11Y_KEY_CODES.has(e.code)) return;
    e.preventDefault();
    fireEvent(this, "value-changed", { value: this.value });
  }

  private _getPercentageFromEvent = (e: HammerInput) => {
    if (this.vertical) {
      const y = e.center.y;
      const offset = e.target.getBoundingClientRect().top;
      const total = e.target.clientHeight;
      return Math.max(Math.min(1, 1 - (y - offset) / total), 0);
    }
    const x = e.center.x;
    const offset = e.target.getBoundingClientRect().left;
    const total = e.target.clientWidth;
    return Math.max(Math.min(1, (x - offset) / total), 0);
  };

  protected render(): TemplateResult {
    return html`
      <div
        id="slider"
        class="slider"
        style=${styleMap({
          "--value": `${this.valueToPercentage(this.value ?? 0)}`,
        })}
      >
        <div class="slider-track-background"></div>
        ${this.mode === "cursor"
          ? this.value != null
            ? html`
                <div
                  class=${classMap({
                    "slider-track-cursor": true,
                  })}
                ></div>
              `
            : null
          : html`
              <div
                class=${classMap({
                  "slider-track-bar": true,
                  [this.mode ?? "start"]: true,
                  "show-handle": this.showHandle,
                })}
              ></div>
            `}
      </div>
    `;
  }

  static get styles(): CSSResultGroup {
    return css`
      :host {
        display: block;
        --control-slider-color: var(--primary-color);
        --control-slider-background: var(--disabled-color);
        --control-slider-background-opacity: 0.2;
        --control-slider-thickness: 40px;
        --control-slider-border-radius: 10px;
        height: var(--control-slider-thickness);
        width: 100%;
        border-radius: var(--control-slider-border-radius);
        outline: none;
        transition: box-shadow 180ms ease-in-out;
      }
      :host(:focus-visible) {
        box-shadow: 0 0 0 2px var(--control-slider-color);
      }
      :host([vertical]) {
        width: var(--control-slider-thickness);
        height: 100%;
      }
      .slider {
        position: relative;
        height: 100%;
        width: 100%;
        border-radius: var(--control-slider-border-radius);
        transform: translateZ(0);
        overflow: hidden;
        cursor: pointer;
      }
      .slider * {
        pointer-events: none;
      }
      .slider .slider-track-background {
        position: absolute;
        top: 0;
        left: 0;
        height: 100%;
        width: 100%;
        background: var(--control-slider-background);
        opacity: var(--control-slider-background-opacity);
      }
      .slider .slider-track-bar {
        --border-radius: var(--control-slider-border-radius);
        --handle-size: 4px;
        --handle-margin: calc(var(--control-slider-thickness) / 8);
        --slider-size: 100%;
        position: absolute;
        height: 100%;
        width: 100%;
        background-color: var(--control-slider-color);
        transition: transform 180ms ease-in-out,
          background-color 180ms ease-in-out;
      }
      .slider .slider-track-bar.show-handle {
        --slider-size: calc(
          100% - 2 * var(--handle-margin) - var(--handle-size)
        );
      }
      .slider .slider-track-bar::after {
        display: block;
        content: "";
        position: absolute;
        margin: auto;
        border-radius: var(--handle-size);
        background-color: white;
      }
      .slider .slider-track-bar {
        top: 0;
        left: 0;
        transform: translate3d(
          calc((var(--value, 0) - 1) * var(--slider-size)),
          0,
          0
        );
        border-radius: 0 var(--border-radius) var(--border-radius) 0;
      }
      .slider .slider-track-bar:after {
        top: 0;
        bottom: 0;
        right: var(--handle-margin);
        height: 50%;
        width: var(--handle-size);
      }
      .slider .slider-track-bar.end {
        right: 0;
        left: initial;
        transform: translate3d(
          calc(var(--value, 0) * var(--slider-size)),
          0,
          0
        );
        border-radius: var(--border-radius) 0 0 var(--border-radius);
      }
      .slider .slider-track-bar.end::after {
        right: initial;
        left: var(--handle-margin);
      }

      :host([vertical]) .slider .slider-track-bar {
        bottom: 0;
        left: 0;
        transform: translate3d(
          0,
          calc((1 - var(--value, 0)) * var(--slider-size)),
          0
        );
        border-radius: var(--border-radius) var(--border-radius) 0 0;
      }
      :host([vertical]) .slider .slider-track-bar:after {
        top: var(--handle-margin);
        right: 0;
        left: 0;
        bottom: initial;
        width: 50%;
        height: var(--handle-size);
      }
      :host([vertical]) .slider .slider-track-bar.end {
        top: 0;
        bottom: initial;
        transform: translate3d(
          0,
          calc((0 - var(--value, 0)) * var(--slider-size)),
          0
        );
        border-radius: 0 0 var(--border-radius) var(--border-radius);
      }
      :host([vertical]) .slider .slider-track-bar.end::after {
        top: initial;
        bottom: var(--handle-margin);
      }

      .slider .slider-track-cursor:after {
        display: block;
        content: "";
        background-color: var(--secondary-text-color);
        position: absolute;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
        margin: auto;
        border-radius: var(--handle-size);
      }

      .slider .slider-track-cursor {
        --cursor-size: calc(var(--control-slider-thickness) / 4);
        --handle-size: 4px;
        position: absolute;
        background-color: white;
        border-radius: var(--handle-size);
        transition: left 180ms ease-in-out, bottom 180ms ease-in-out;
        top: 0;
        bottom: 0;
        left: calc(var(--value, 0) * (100% - var(--cursor-size)));
        width: var(--cursor-size);
      }
      .slider .slider-track-cursor:after {
        height: 50%;
        width: var(--handle-size);
      }

      :host([vertical]) .slider .slider-track-cursor {
        top: initial;
        right: 0;
        left: 0;
        bottom: calc(var(--value, 0) * (100% - var(--cursor-size)));
        height: var(--cursor-size);
        width: 100%;
      }
      :host([vertical]) .slider .slider-track-cursor:after {
        height: var(--handle-size);
        width: 50%;
      }

      :host([pressed]) .slider-track-bar,
      :host([pressed]) .slider-track-cursor {
        transition: none;
      }
      :host(:disabled) .slider {
        cursor: not-allowed;
      }
    `;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    "ha-control-slider": HaControlSlider;
  }
}
