import JS from "./js";
import LiveSocket from "./live_socket";

type Transition = string | string[];

// Base options for commands involving transitions and timing
type BaseOpts = {
  /**
   * The CSS transition classes to set.
   * Accepts a string of classes or a 3-tuple like:
   * `["ease-out duration-300", "opacity-0", "opacity-100"]`
   */
  transition?: Transition;
  /** The transition duration in milliseconds. Defaults 200. */
  time?: number;
  /** Whether to block UI during transition. Defaults `true`. */
  blocking?: boolean;
};

type ShowOpts = BaseOpts & {
  /** The CSS display value to set. Defaults "block". */
  display?: string;
};

type ToggleOpts = {
  /** The CSS display value to set. Defaults "block". */
  display?: string;
  /**
   * The CSS transition classes for showing.
   * Accepts either the string of classes to apply when toggling in, or
   * a 3-tuple containing the transition class, the class to apply
   * to start the transition, and the ending transition class, such as:
   * `["ease-out duration-300", "opacity-0", "opacity-100"]`
   */
  in?: Transition;
  /**
   * The CSS transition classes for hiding.
   * Accepts either string of classes to apply when toggling out, or
   * a 3-tuple containing the transition class, the class to apply
   * to start the transition, and the ending transition class, such as:
   * `["ease-out duration-300", "opacity-100", "opacity-0"]`
   */
  out?: Transition;
  /** The transition duration in milliseconds. */
  time?: number;
  /** Whether to block UI during transition. Defaults `true`. */
  blocking?: boolean;
};

// Options specific to the 'transition' command
type TransitionCommandOpts = {
  /** The transition duration in milliseconds. */
  time?: number;
  /** Whether to block UI during transition. Defaults `true`. */
  blocking?: boolean;
};

type PushOpts = {
  /** Data to be merged into the event payload. */
  value?: any;
  /** For targeting a LiveComponent by its ID, a component ID (number), or a CSS selector string. */
  target?: HTMLElement | number | string;
  /** Indicates if a page loading state should be shown. */
  page_loading?: boolean;
  [key: string]: any; // Allow other properties like 'cid', 'redirect', etc.
};

type NavigationOpts = {
  /** Whether to replace the current history entry instead of pushing a new one. */
  replace?: boolean;
};

/**
 * Represents all possible JS commands that can be generated by the factory.
 * This is used as a base for LiveSocketJSCommands and HookJSCommands.
 */
interface AllJSCommands {
  /**
   * Executes encoded JavaScript in the context of the element.
   * This version is for general use via liveSocket.js().
   *
   * @param el - The element in whose context to execute the JavaScript.
   * @param encodedJS - The encoded JavaScript string to execute.
   */
  exec(el: HTMLElement, encodedJS: string): void;

  /**
   * Shows an element.
   *
   * @param el - The element to show.
   * @param {ShowOpts} [opts={}] - Optional settings.
   *   Accepts: `display`, `transition`, `time`, and `blocking`.
   */
  show(el: HTMLElement, opts?: ShowOpts): void;

  /**
   * Hides an element.
   *
   * @param el - The element to hide.
   * @param [opts={}] - Optional settings.
   *   Accepts: `transition`, `time`, and `blocking`.
   */
  hide(el: HTMLElement, opts?: BaseOpts): void;

  /**
   * Toggles the visibility of an element.
   *
   * @param el - The element to toggle.
   * @param [opts={}] - Optional settings.
   *   Accepts: `display`, `in`, `out`, `time`, and `blocking`.
   */
  toggle(el: HTMLElement, opts?: ToggleOpts): void;

  /**
   * Adds CSS classes to an element.
   *
   * @param el - The element to add classes to.
   * @param names - The class name(s) to add.
   * @param [opts={}] - Optional settings.
   *   Accepts: `transition`, `time`, and `blocking`.
   */
  addClass(el: HTMLElement, names: string | string[], opts?: BaseOpts): void;

  /**
   * Removes CSS classes from an element.
   *
   * @param el - The element to remove classes from.
   * @param names - The class name(s) to remove.
   * @param [opts={}] - Optional settings.
   *   Accepts: `transition`, `time`, and `blocking`.
   */
  removeClass(el: HTMLElement, names: string | string[], opts?: BaseOpts): void;

  /**
   * Toggles CSS classes on an element.
   *
   * @param el - The element to toggle classes on.
   * @param names - The class name(s) to toggle.
   * @param [opts={}] - Optional settings.
   *   Accepts: `transition`, `time`, and `blocking`.
   */
  toggleClass(el: HTMLElement, names: string | string[], opts?: BaseOpts): void;

  /**
   * Applies a CSS transition to an element.
   *
   * @param el - The element to apply the transition to.
   * @param transition - The transition class(es) to apply.
   *   Accepts a string of classes to apply when transitioning or
   *   a 3-tuple containing the transition class, the class to apply
   *   to start the transition, and the ending transition class, such as:
   *
   *       ["ease-out duration-300", "opacity-100", "opacity-0"]
   *
   * @param [opts={}] - Optional settings for timing and blocking behavior.
   *   Accepts: `time` and `blocking`.
   */
  transition(
    el: HTMLElement,
    transition: string | string[],
    opts?: TransitionCommandOpts,
  ): void;

  /**
   * Sets an attribute on an element.
   *
   * @param el - The element to set the attribute on.
   * @param attr - The attribute name to set.
   * @param val - The value to set for the attribute.
   */
  setAttribute(el: HTMLElement, attr: string, val: string): void;

  /**
   * Removes an attribute from an element.
   *
   * @param el - The element to remove the attribute from.
   * @param attr - The attribute name to remove.
   */
  removeAttribute(el: HTMLElement, attr: string): void;

  /**
   * Toggles an attribute on an element between two values.
   *
   * @param el - The element to toggle the attribute on.
   * @param attr - The attribute name to toggle.
   * @param val1 - The first value to toggle between.
   * @param val2 - The second value to toggle between.
   */
  toggleAttribute(
    el: HTMLElement,
    attr: string,
    val1: string,
    val2: string,
  ): void;

  /**
   * Pushes an event to the server.
   *
   * @param el - An element that belongs to the target LiveView / LiveComponent or a component ID.
   *   To target a LiveComponent by its ID, pass a separate `target` in the options.
   * @param type - The event name to push.
   * @param [opts={}] - Optional settings.
   *   Accepts: `value`, `target`, `page_loading`.
   */
  push(el: HTMLElement, type: string, opts?: PushOpts): void;

  /**
   * Sends a navigation event to the server and updates the browser's pushState history.
   *
   * @param href - The URL to navigate to.
   * @param [opts={}] - Optional settings.
   *   Accepts: `replace`.
   */
  navigate(href: string, opts?: NavigationOpts): void;

  /**
   * Sends a patch event to the server and updates the browser's pushState history.
   *
   * @param href - The URL to patch to.
   * @param [opts={}] - Optional settings.
   *   Accepts: `replace`.
   */
  patch(href: string, opts?: NavigationOpts): void;

  /**
   * Mark attributes as ignored, skipping them when patching the DOM.
   *
   * @param el - The element to ignore attributes on.
   * @param attrs - The attribute name or names to ignore.
   */
  ignoreAttributes(el: HTMLElement, attrs: string | string[]): void;
}

export default (
  liveSocket: LiveSocket,
  eventType: string | null,
): AllJSCommands => {
  return {
    exec(el, encodedJS) {
      liveSocket.execJS(el, encodedJS, eventType);
    },
    show(el, opts = {}) {
      const owner = liveSocket.owner(el);
      JS.show(
        eventType,
        owner,
        el,
        opts.display,
        JS.transitionClasses(opts.transition),
        opts.time,
        opts.blocking,
      );
    },
    hide(el, opts = {}) {
      const owner = liveSocket.owner(el);
      JS.hide(
        eventType,
        owner,
        el,
        null,
        JS.transitionClasses(opts.transition),
        opts.time,
        opts.blocking,
      );
    },
    toggle(el, opts = {}) {
      const owner = liveSocket.owner(el);
      const inTransition = JS.transitionClasses(opts.in);
      const outTransition = JS.transitionClasses(opts.out);
      JS.toggle(
        eventType,
        owner,
        el,
        opts.display,
        inTransition,
        outTransition,
        opts.time,
        opts.blocking,
      );
    },
    addClass(el, names, opts = {}) {
      const classNames = Array.isArray(names) ? names : names.split(" ");
      const owner = liveSocket.owner(el);
      JS.addOrRemoveClasses(
        el,
        classNames,
        [],
        JS.transitionClasses(opts.transition),
        opts.time,
        owner,
        opts.blocking,
      );
    },
    removeClass(el, names, opts = {}) {
      const classNames = Array.isArray(names) ? names : names.split(" ");
      const owner = liveSocket.owner(el);
      JS.addOrRemoveClasses(
        el,
        [],
        classNames,
        JS.transitionClasses(opts.transition),
        opts.time,
        owner,
        opts.blocking,
      );
    },
    toggleClass(el, names, opts = {}) {
      const classNames = Array.isArray(names) ? names : names.split(" ");
      const owner = liveSocket.owner(el);
      JS.toggleClasses(
        el,
        classNames,
        JS.transitionClasses(opts.transition),
        opts.time,
        owner,
        opts.blocking,
      );
    },
    transition(el, transition, opts = {}) {
      const owner = liveSocket.owner(el);
      JS.addOrRemoveClasses(
        el,
        [],
        [],
        JS.transitionClasses(transition),
        opts.time,
        owner,
        opts.blocking,
      );
    },
    setAttribute(el, attr, val) {
      JS.setOrRemoveAttrs(el, [[attr, val]], []);
    },
    removeAttribute(el, attr) {
      JS.setOrRemoveAttrs(el, [], [attr]);
    },
    toggleAttribute(el, attr, val1, val2) {
      JS.toggleAttr(el, attr, val1, val2);
    },
    push(el, type, opts = {}) {
      liveSocket.withinOwners(el, (view) => {
        const data = opts.value || {};
        delete opts.value;
        let e = new CustomEvent("phx:exec", { detail: { sourceElement: el } });
        JS.exec(e, eventType, type, view, el, ["push", { data, ...opts }]);
      });
    },
    navigate(href, opts = {}) {
      const customEvent = new CustomEvent("phx:exec");
      liveSocket.historyRedirect(
        customEvent,
        href,
        opts.replace ? "replace" : "push",
        null,
        null,
      );
    },
    patch(href, opts = {}) {
      const customEvent = new CustomEvent("phx:exec");
      liveSocket.pushHistoryPatch(
        customEvent,
        href,
        opts.replace ? "replace" : "push",
        null,
      );
    },
    ignoreAttributes(el, attrs) {
      JS.ignoreAttrs(el, Array.isArray(attrs) ? attrs : [attrs]);
    },
  };
};

/**
 * JSCommands for use with `liveSocket.js()`.
 * Includes the general `exec` command that requires an element.
 */
export type LiveSocketJSCommands = AllJSCommands;

/**
 * JSCommands for use within a Hook.
 * The `exec` command is tailored for hooks, not requiring an explicit element.
 */
export interface HookJSCommands extends Omit<AllJSCommands, "exec"> {
  /**
   * Executes encoded JavaScript in the context of the hook's element.
   *
   * @param {string} encodedJS - The encoded JavaScript string to execute.
   */
  exec(encodedJS: string): void;
}
