import { inline_chunk, pad_chunk, now } from "efreet/utils";
import { FElement } from "./renderer";

export type RawValue = number | string | boolean;
export type MaybeProperty<T> = T | Compilable<T>;
export type MapToMaybeProperties<T> = { [K in keyof T]: MaybeProperty<T[K]> };

export function embed<Val extends Compilable<any> | any>(val: Val) {
  return val === undefined
    ? "undefined"
    : val instanceof Compilable
    ? val.compile()
    : JSON.stringify(val);
}

//----------------------------------------------------------------------
// Event Binder (statically generated event handlers)
//----------------------------------------------------------------------

export class EventBinder {
  protected _when?: string;
  protected _thens: string[] = [];

  protected _preventer?: string;
  protected _prevent?: (...args: any[]) => boolean;

  protected _stopper?: string;
  protected _stop?: (...args: any[]) => boolean;

  // @NOTE: Working around a gnarly issue in the type system.
  constructor(
    protected _handle?: (...args: any[]) => void,
    protected _handler?: string,
    protected _prior_clause?: EventBinder
  ) {}

  compile_inner() {
    // pad_chunk(2)
    let inner = `
      ${this._handler}
      ${this._preventer}
      ${this._stopper}

      ${this._thens.join("\n")}
    `;
    if (this._when) {
      // pad_chunk(2)
      inner = `
        if(${this._when}) {
          ${inner}
        }
      `;
    }
    if (this._prior_clause) {
      // The inverted order here is important because we're unwinding the linked-list :/
      inner +=
        "\n\n  // OR\n  binder = binder._prior_clause;\n" +
        this._prior_clause.compile_inner();
    }
    return inner;
  }

  compile() {
    let start = now();
    let inner = this.compile_inner();
    let compiled = new Function(
      "binder",
      // pad_chunk(2)
      `
      return function catalyst_compiled_handler(event, elem) {
        var target = event.currentTarget || event.target;
        ${inner}
      };
    `
    )(this) as (event: Event, elem: FElement) => unknown;
    // console.log("COMPILED", this._handle && this._handle.name, now() - start);
    // console.log(compiled);
    return compiled;
  }

  /** Trigger the handler with the given properties when this event fires. */
  static handle<U extends any[]>(
    handler: (...args: U) => unknown,
    ...args: MapToMaybeProperties<U>
  ) {
    return new EventBinder(
      handler,
      // inline_chunk
      `
      // ${handler.name}
      binder._handle(${args.map(embed).join(", ")});
    `
    );
  }

  /** Add another non-exclusive handling clause. Useful in conjunction with `when()`. */
  or<U extends any[]>(
    handler: (...args: U) => unknown,
    ...args: MapToMaybeProperties<U>
  ) {
    return new EventBinder(
      handler,
      // inline_chunk
      `
      // ${handler.name}
      binder._handle(${args.map(embed).join(", ")});
    `,
      this
    );
  }

  /** Add a condition which must be true for the handler to fire. */
  when(prop: Compilable<boolean> | boolean) {
    this._when = embed(prop);
    return this;
  }

  /** Add a side effect to be triggered after the handler fires. */
  then(effect: Compilable<any>) {
    this._thens.push(effect.compile());
    return this;
  }

  /** Always stop propagation on this event. */
  stop() {
    this._stopper = `event.stopPropagation();`;
    return this;
  }

  /** Always prevent default on this event. */
  prevent() {
    this._preventer = `event.preventDefault();`;
    return this;
  }

  /** Stop propagation on this event if the predicate returns true. */
  stop_if<U extends RawValue[]>(
    predicate: (...args: U) => boolean,
    ...args: MapToMaybeProperties<U>
  ) {
    this._stop = predicate;
    // inline_chunk
    this._stopper = `
      // ${predicate.name}
      if(binder._stop(${args.map(embed).join(", ")})) {
        event.stopPropagation();
      }
    `;
    return this;
  }

  /** Prevent default on this event if the predicate returns true. */
  prevent_if<U extends RawValue[]>(
    predicate: (...args: U) => boolean,
    ...args: MapToMaybeProperties<U>
  ) {
    this._prevent = predicate;
    // inline_chunk
    this._preventer = `
      // ${predicate.name}
      if(binder._prevent(${args.map(embed).join(", ")})) {
        event.preventDefault();
      }
    `;
    return this;
  }
}

//----------------------------------------------------------------------
// Compilables (statically defined snippets for generating event handlers)
//----------------------------------------------------------------------

export abstract class Compilable<Returns = any> {
  public returns: Returns;
  abstract compile(): string;

  equals<Rhs extends Compilable<any> | any>(rhs: Rhs) {
    return new Compilable.Infix("===", this, rhs);
  }

  is_not<Rhs extends Compilable<any> | any>(rhs: Rhs) {
    return new Compilable.Infix("!==", this, rhs);
  }

  dot<Prop extends Returns extends {} ? keyof Returns : never>(property: Prop) {
    return new Compilable.Dot<Returns, Prop>(this, property);
  }

  and(rhs: Compilable<any>) {
    return new Compilable.Infix("&&", this, rhs);
  }
  or(rhs: Compilable<any>) {
    return new Compilable.Infix("||", this, rhs);
  }

  to_string() {
    return new Compilable.Computed<string>(`""+$1`, this);
  }

  to_number() {
    return new Compilable.Computed<number>(
      `!isNaN(+$1) ? +$1 : undefined`,
      this
    );
  }
}

export namespace Compilable {
  export class Snippet<Returns = any> extends Compilable<Returns> {
    constructor(public code: string) {
      super();
    }

    compile() {
      return this.code;
    }
  }

  export class Computed<Returns = any> extends Compilable<Returns> {
    terms: any[];
    constructor(public template: string, ...terms: any[]) {
      super();
      this.terms = terms;
    }

    compile() {
      let code = this.template;
      for (let ix = 0; ix < this.terms.length; ix += 1) {
        code = code.replace(
          new RegExp(`\\$${ix + 1}`, "g"),
          embed(this.terms[ix])
        );
      }
      return code;
    }
  }

  export class Infix<
    Lhs extends Compilable<any> | any,
    Rhs extends Compilable<any> | any
  > extends Compilable<boolean> {
    constructor(public op: string, public lhs: Lhs, public rhs: Rhs) {
      super();
    }

    compile() {
      return `(${embed(this.lhs)} ${this.op} ${embed(this.rhs)})`;
    }
  }

  export class Dot<
    Returns extends {},
    Prop extends keyof Returns
  > extends Compilable<
    Returns extends null
      ? null
      : Returns extends undefined
      ? undefined
      : Returns[Prop]
  > {
    constructor(public parent: Compilable<Returns>, public property: Prop) {
      super();
    }

    compile() {
      return `(${this.parent.compile()})["${this.property}"]`;
    }
  }

  export class Property<
    Obj extends {},
    Prop extends keyof Obj
  > extends Compilable<Obj[Prop]> {
    static create<Obj extends {}, Prop extends keyof Obj = keyof Obj>(
      prefix: string,
      property: Prop
    ) {
      return new Property<Obj, Prop>(prefix, property);
    }

    static create_event<Obj extends {}, Prop extends keyof Obj>(
      eventCtor: new (...args: any[]) => Obj,
      property: Prop
    ) {
      return new Property<Obj, Prop>("event", property);
    }

    constructor(public prefix: string, public property: Prop) {
      super();
    }

    compile() {
      return `${this.prefix}["${this.property}"]`;
    }
  }

  export class CategoryProperty<
    Obj extends {},
    Prop extends keyof Obj
  > extends Compilable<Obj[Prop]> {
    static create<
      Obj extends {},
      Prop extends keyof Obj,
      Categories extends { [name: string]: Obj[Prop] }
    >(prefix: string, property: Prop, categories: Categories) {
      let prop = new CategoryProperty(prefix, property, categories);
      return prop as typeof prop &
        { [K in keyof Categories]: Compilable<boolean> };
    }

    static create_event<
      Obj extends {},
      Prop extends keyof Obj,
      Categories extends { [name: string]: Obj[Prop] }
    >(
      obj: new (...args: any[]) => Obj,
      property: Prop,
      categories: Categories
    ) {
      let prop = new CategoryProperty("event", property, categories);
      return prop as typeof prop &
        { [K in keyof Categories]: Compilable<boolean> };
    }

    constructor(
      public prefix: string,
      public property: Prop,
      public categories: { [name: string]: Obj[Prop] }
    ) {
      super();
      for (let name in categories) {
        this[name] = this.equals(categories[name]);
      }
    }

    compile() {
      return `${this.prefix}["${this.property}"]`;
    }
  }
}

//----------------------------------------------------------------------
// Public API
//----------------------------------------------------------------------

/** Trigger the handler with the given properties when this event fires. */
export let handle = EventBinder.handle;

/** Helper objects for retrieving event and element properties in catalyst helpers. */

interface Target {
  value: string;
  textContent: string;
  selectedOptions: HTMLOptionElement[];
}

export const get = {
  elem: (prop: string) => {
    return Compilable.Property.create<any>("elem", prop);
  },
  target: {
    value: Compilable.Property.create<Target, "value">("target", "value"),
    content: Compilable.Property.create<Target, "textContent">(
      "target",
      "textContent"
    ),
    selected: new Compilable.Computed<string[]>(
      `[].slice.apply($1).map((opt) => opt.value)`,
      Compilable.Property.create<Target, "selectedOptions">(
        "target",
        "selectedOptions"
      )
    ),
    bounds: new Compilable.Computed<ClientRect>(
      `target.getBoundingClientRect()`
    )
  },
  node: new Compilable.Computed<HTMLElement>(`event.target`),
  event: new Compilable.Computed<Event>(`event`),
  mouse: {
    button: Compilable.CategoryProperty.create_event(MouseEvent, "button", {
      left: 0,
      middle: 1,
      right: 2
    }),
    x: Compilable.Property.create_event(MouseEvent, "clientX"),
    y: Compilable.Property.create_event(MouseEvent, "clientY"),
    wheel: {
      delta_mode: Compilable.Property.create_event(WheelEvent, "deltaMode"),
      delta_x: Compilable.Property.create_event(WheelEvent, "deltaX"),
      delta_y: Compilable.Property.create_event(WheelEvent, "deltaY"),
      delta_z: Compilable.Property.create_event(WheelEvent, "deltaZ")
    }
  },
  keyboard: {
    key: Compilable.Property.create_event(KeyboardEvent, "key"),
    mod: (mod: string) =>
      new Compilable.Computed<boolean>(`event.getModifierState("${mod}")`)
  },

  do: {
    target: {
      clear: new Compilable.Snippet(`target.value = ""`),
      style: (property: MaybeProperty<string>, value: MaybeProperty<any>) =>
        new Compilable.Snippet(
          `target.style.setProperty(${embed(property)}, ${embed(value)})`
        )
    }
  }
};

/** Registry for compiled event handlers (automatically maintained when compiled). */
export let handlers: { [id: string]: ReturnType<EventBinder["compile"]> } = {};
