import noUiSlider, { API as NoUiSliderInstance } from "nouislider";
import wnumb, { Instance as WNumbInstance } from "wnumb";

import defaultConfig from "./data";

export interface RangeSliderConfig {
  format?: any;
  [key: string]: any;
}

interface Output {
  element: HTMLElement;
  format?: WNumbInstance;
}

export default class RangeSlider {
  public element: HTMLElement;
  public config: RangeSliderConfig;
  public instance?: NoUiSliderInstance;
  public inputs: HTMLInputElement[];
  public outputs: (Output | false)[];

  constructor(element: HTMLElement, config?: Partial<RangeSliderConfig>) {
    this.element = element;
    this.config = { ...defaultConfig, ...config };
    this.inputs = [];
    this.outputs = [];
    this.handleSliderUpdate = this.handleSliderUpdate.bind(this);
    (this.element as any).ODS_RangeSlider = this;
    this.init();
    return this;
  }

  public init(): void {
    const configAttr = this.element.getAttribute("data-rangeslider-config");
    let elementConfig: Partial<RangeSliderConfig> = {};
    if (configAttr) {
      try {
        elementConfig = JSON.parse(configAttr);
      } catch {
        // ignore parse error
      }
    }
    this.config = { ...this.config, ...elementConfig };
    this.config.format = wnumb(this.config.format);

    if ((this.element as any).noUiSlider) {
      (this.element as any).noUiSlider = undefined;
      if (this.instance?.destroy) this.instance.destroy();
    }

    // Ensure config matches required Options type
    if (!this.config.range) {
      throw new Error("RangeSlider config must include 'range'.");
    }
    this.instance = noUiSlider.create(this.element, this.config as any);
    this.inputs = Array.from(
      document.querySelectorAll(
        `input[name^=${this.element.getAttribute("id")}-]`,
      ),
    ) as HTMLInputElement[];

    this.outputs = this.inputs.map((input) => {
      const outputElement = document.getElementById(
        `${input.getAttribute("id")}-output`,
      );
      if (outputElement) {
        const formatAttr = outputElement.getAttribute("data-format");
        return {
          element: outputElement,
          format:
            outputElement.hasAttribute("data-format") && formatAttr
              ? wnumb(JSON.parse(formatAttr))
              : undefined,
        };
      }
      return false;
    });

    this.instance.on(
      "update",
      (values: (string | number)[], handle: number) => {
        this.handleSliderUpdate(values.map(String), handle);
      },
    );
  }

  public update(): void {
    this.instance?.destroy();
    this.init();
  }

  public destroy(): void {
    this.instance?.off("update");
    this.instance?.destroy();
    (this.element as any).ODS_RangeSlider = null;
  }

  static getInstance(el: HTMLElement): RangeSlider | null {
    return el && (el as any).ODS_RangeSlider
      ? (el as any).ODS_RangeSlider
      : null;
  }

  private handleSliderUpdate = (values: string[], handle: number): void => {
    const value = values[handle];
    if (this.inputs[handle]) {
      this.inputs[handle].value = value;
    }
    if (this.outputs[handle]) {
      const output = this.outputs[handle] as Output;
      if (output && output.format) {
        output.element.innerText = output.format.to(
          this.config.format.from(value),
        );
      } else if (output) {
        output.element.innerText = value;
      }
    }
  };
}
