import { Matrix } from "../common/matrix";
import { uid } from "../common/uid";

import type { Component } from "./component";

/**
 *  @hidden @deprecated
 * - 'in-pad': same as 'contain'
 * - 'in': similar to 'contain' without centering
 * - 'out-crop': same as 'cover'
 * - 'out': similar to 'cover' without centering
 */
export type LegacyFitMode = "in" | "out" | "out-crop" | "in-pad";

/**
 * - 'contain': contain within the provided space, maintain aspect ratio
 * - 'cover': cover the provided space, maintain aspect ratio
 * - 'fill': fill provided space without maintaining aspect ratio
 */
export type FitMode = "contain" | "cover" | "fill" | LegacyFitMode;

/** @internal */
export function isValidFitMode(value: string) {
  return (
    value &&
    (value === "cover" ||
      value === "contain" ||
      value === "fill" ||
      value === "in" ||
      value === "in-pad" ||
      value === "out" ||
      value === "out-crop")
  );
}

/** @internal */ let iid = 0;

/** @internal */ export function getIID() {
  return iid++;
}

export class Pin {
  /** @internal */ uid = "pin:" + uid();

  /** @internal */ _owner: Component;

  // todo: maybe this should be a getter instead?
  /** @internal */ _parent: Pin | null;

  /** @internal */ _relativeMatrix: Matrix;
  /** @internal */ _absoluteMatrix: Matrix;

  /** @internal */ _x: number;
  /** @internal */ _y: number;

  /** @internal */ _unscaled_width: number;
  /** @internal */ _unscaled_height: number;

  /** @internal */ _width: number;
  /** @internal */ _height: number;

  /** @internal */ _textureAlpha: number;
  /** @internal */ _alpha: number;

  /** @internal */ _scaleX: number;
  /** @internal */ _scaleY: number;

  /** @internal */ _skewX: number;
  /** @internal */ _skewY: number;
  /** @internal */ _rotation: number;

  /** @internal */ _pivoted: boolean;
  /** @internal */ _pivotX: number;
  /** @internal */ _pivotY: number;

  /** @internal */ _handled: boolean;
  /** @internal */ _handleX: number;
  /** @internal */ _handleY: number;

  /** @internal */ _aligned: boolean;
  /** @internal */ _alignX: number;
  /** @internal */ _alignY: number;

  /** @internal */ _offsetX: number;
  /** @internal */ _offsetY: number;

  /** @internal */ _boxX: number;
  /** @internal */ _boxY: number;
  /** @internal */ _boxWidth: number;
  /** @internal */ _boxHeight: number;

  /** @internal */ _ts_transform: number;
  /** @internal */ _ts_translate: number;
  /** @internal */ _ts_matrix: number;

  /** @internal */ _mo_handle: number;
  /** @internal */ _mo_align: number;
  /** @internal */ _mo_abs: number;
  /** @internal */ _mo_rel: number;

  /** @internal */ _directionX = 1;
  /** @internal */ _directionY = 1;

  /** @internal */
  constructor(owner: Component) {
    this._owner = owner;
    this._parent = null;

    // relative to parent
    this._relativeMatrix = new Matrix();

    // relative to stage
    this._absoluteMatrix = new Matrix();

    this.reset();
  }

  reset() {
    this._textureAlpha = 1;
    this._alpha = 1;

    this._width = 0;
    this._height = 0;

    this._scaleX = 1;
    this._scaleY = 1;
    this._skewX = 0;
    this._skewY = 0;
    this._rotation = 0;

    // scale/skew/rotate center
    this._pivoted = false;
    // todo: this used to be null
    this._pivotX = 0;
    this._pivotY = 0;

    // self pin point
    this._handled = false;
    this._handleX = 0;
    this._handleY = 0;

    // parent pin point
    this._aligned = false;
    this._alignX = 0;
    this._alignY = 0;

    // as seen by parent px
    this._offsetX = 0;
    this._offsetY = 0;

    this._boxX = 0;
    this._boxY = 0;
    this._boxWidth = this._width;
    this._boxHeight = this._height;

    // TODO: also set for owner
    this._ts_translate = ++iid;
    this._ts_transform = ++iid;
    this._ts_matrix = ++iid;
  }

  /** @internal */
  _update() {
    this._parent = this._owner._parent && this._owner._parent._pin;

    // if handled and transformed then be translated
    if (this._handled && this._mo_handle != this._ts_transform) {
      this._mo_handle = this._ts_transform;
      this._ts_translate = ++iid;
    }

    if (this._aligned && this._parent && this._mo_align != this._parent._ts_transform) {
      this._mo_align = this._parent._ts_transform;
      this._ts_translate = ++iid;
    }

    return this;
  }

  toString() {
    return this._owner + " (" + (this._parent ? this._parent._owner : null) + ")";
  }

  // TODO: ts fields require refactoring
  absoluteMatrix() {
    this._update();
    const ts = Math.max(
      this._ts_transform,
      this._ts_translate,
      this._parent ? this._parent._ts_matrix : 0,
    );
    if (this._mo_abs == ts) {
      return this._absoluteMatrix;
    }
    this._mo_abs = ts;

    const abs = this._absoluteMatrix;
    abs.reset(this.relativeMatrix());

    this._parent && abs.concat(this._parent._absoluteMatrix);
    this._owner._xf && abs.concat(this._owner._xf);

    this._ts_matrix = ++iid;

    return abs;
  }

  relativeMatrix() {
    this._update();
    const ts = Math.max(
      this._ts_transform,
      this._ts_translate,
      this._parent ? this._parent._ts_transform : 0,
    );
    if (this._mo_rel == ts) {
      return this._relativeMatrix;
    }
    this._mo_rel = ts;

    const rel = this._relativeMatrix;

    rel.identity();
    if (this._pivoted) {
      rel.translate(-this._pivotX * this._width, -this._pivotY * this._height);
    }
    rel.scale(this._scaleX * this._directionX, this._scaleY * this._directionY);
    rel.skew(this._skewX, this._skewY);
    rel.rotate(this._rotation);
    if (this._pivoted) {
      rel.translate(this._pivotX * this._width, this._pivotY * this._height);
    }

    // calculate effective box
    if (this._pivoted) {
      // origin
      this._boxX = 0;
      this._boxY = 0;
      this._boxWidth = this._width;
      this._boxHeight = this._height;
    } else {
      // aabb
      let p;
      let q;
      if ((rel.a > 0 && rel.c > 0) || (rel.a < 0 && rel.c < 0)) {
        p = 0;
        q = rel.a * this._width + rel.c * this._height;
      } else {
        p = rel.a * this._width;
        q = rel.c * this._height;
      }
      if (p > q) {
        this._boxX = q;
        this._boxWidth = p - q;
      } else {
        this._boxX = p;
        this._boxWidth = q - p;
      }
      if ((rel.b > 0 && rel.d > 0) || (rel.b < 0 && rel.d < 0)) {
        p = 0;
        q = rel.b * this._width + rel.d * this._height;
      } else {
        p = rel.b * this._width;
        q = rel.d * this._height;
      }
      if (p > q) {
        this._boxY = q;
        this._boxHeight = p - q;
      } else {
        this._boxY = p;
        this._boxHeight = q - p;
      }
    }

    this._x = this._offsetX;
    this._y = this._offsetY;

    this._x -= this._boxX + this._handleX * this._boxWidth * this._directionX;
    this._y -= this._boxY + this._handleY * this._boxHeight * this._directionY;

    if (this._aligned && this._parent) {
      this._parent.relativeMatrix();
      this._x += this._alignX * this._parent._width;
      this._y += this._alignY * this._parent._height;
    }

    rel.translate(this._x, this._y);

    return this._relativeMatrix;
  }

  /** @internal */
  get(key: string) {
    if (typeof getters[key] === "function") {
      return getters[key](this);
    }
  }

  // TODO: Use defineProperty instead? What about multi-field pinning?
  /** @internal */
  set(a, b?) {
    if (typeof a === "string") {
      if (typeof setters[a] === "function" && typeof b !== "undefined") {
        setters[a](this, b);
      }
    } else if (typeof a === "object") {
      for (b in a) {
        if (typeof setters[b] === "function" && typeof a[b] !== "undefined") {
          setters[b](this, a[b], a);
        }
      }
    }
    if (this._owner) {
      this._owner._ts_pin = ++iid;
      this._owner.touch();
    }
    return this;
  }

  // todo: should this be public?
  /** @internal */
  fit(width: number | null, height: number | null, mode?: FitMode) {
    this._ts_transform = ++iid;
    if (mode === "contain") {
      mode = "in-pad";
    }
    if (mode === "cover") {
      mode = "out-crop";
    }
    if (typeof width === "number") {
      this._scaleX = width / this._unscaled_width;
      this._width = this._unscaled_width;
    }
    if (typeof height === "number") {
      this._scaleY = height / this._unscaled_height;
      this._height = this._unscaled_height;
    }
    if (typeof width === "number" && typeof height === "number" && typeof mode === "string") {
      if (mode === "fill") {
      } else if (mode === "out" || mode === "out-crop") {
        this._scaleX = this._scaleY = Math.max(this._scaleX, this._scaleY);
      } else if (mode === "in" || mode === "in-pad") {
        this._scaleX = this._scaleY = Math.min(this._scaleX, this._scaleY);
      }
      if (mode === "out-crop" || mode === "in-pad") {
        this._width = width / this._scaleX;
        this._height = height / this._scaleY;
      }
    }
  }
}

/** @internal */
const fitted = {};

/** @internal */
export function fit(
  this: unknown,
  inWidth: number,
  inHeight: number,
  outWidth: number | null,
  outHeight: number | null,
  mode?: FitMode,
) {
  if (mode === "contain") mode = "in-pad";
  if (mode === "cover") mode = "out-crop";

  let scaleX: number;
  let scaleY: number;

  let width: number;
  let height: number;

  if (typeof outWidth === "number") {
    scaleX = outWidth / inWidth;
    width = inWidth;
  }
  if (typeof outHeight === "number") {
    scaleY = outHeight / inHeight;
    height = inHeight;
  }
  if (typeof outWidth === "number" && typeof outHeight === "number" && typeof mode === "string") {
    if (mode === "fill") {
    } else if (mode === "out" || mode === "out-crop") {
      scaleX = scaleY = Math.max(scaleX, scaleY);
    } else if (mode === "in" || mode === "in-pad") {
      scaleX = scaleY = Math.min(scaleX, scaleY);
    }
    if (mode === "out-crop" || mode === "in-pad") {
      width = outWidth / scaleX;
      height = outHeight / scaleY;
    }
  }

  return { scaleX, scaleY, width, height };
}

/** @internal */ const getters = {
  alpha: function (pin: Pin) {
    return pin._alpha;
  },

  textureAlpha: function (pin: Pin) {
    return pin._textureAlpha;
  },

  width: function (pin: Pin) {
    return pin._width;
  },

  height: function (pin: Pin) {
    return pin._height;
  },

  boxWidth: function (pin: Pin) {
    return pin._boxWidth;
  },

  boxHeight: function (pin: Pin) {
    return pin._boxHeight;
  },

  // scale : function(pin: Pin) {
  // },

  scaleX: function (pin: Pin) {
    return pin._scaleX;
  },

  scaleY: function (pin: Pin) {
    return pin._scaleY;
  },

  // skew : function(pin: Pin) {
  // },

  skewX: function (pin: Pin) {
    return pin._skewX;
  },

  skewY: function (pin: Pin) {
    return pin._skewY;
  },

  rotation: function (pin: Pin) {
    return pin._rotation;
  },

  // pivot : function(pin: Pin) {
  // },

  pivotX: function (pin: Pin) {
    return pin._pivotX;
  },

  pivotY: function (pin: Pin) {
    return pin._pivotY;
  },

  // offset : function(pin: Pin) {
  // },

  offsetX: function (pin: Pin) {
    return pin._offsetX;
  },

  offsetY: function (pin: Pin) {
    return pin._offsetY;
  },

  // align : function(pin: Pin) {
  // },

  alignX: function (pin: Pin) {
    return pin._alignX;
  },

  alignY: function (pin: Pin) {
    return pin._alignY;
  },

  // handle : function(pin: Pin) {
  // },

  handleX: function (pin: Pin) {
    return pin._handleX;
  },

  handleY: function (pin: Pin) {
    return pin._handleY;
  },
};

type ResizeParams = {
  resizeMode: FitMode;
  resizeWidth: number;
  resizeHeight: number;
};

type ScaleParams = {
  scaleMode: FitMode;
  scaleWidth: number;
  scaleHeight: number;
};

/** @internal */ const setters = {
  alpha: function (pin: Pin, value: number) {
    pin._alpha = value;
  },

  textureAlpha: function (pin: Pin, value: number) {
    pin._textureAlpha = value;
  },

  width: function (pin: Pin, value: number) {
    pin._unscaled_width = value;
    pin._width = value;
    pin._ts_transform = ++iid;
  },

  height: function (pin: Pin, value: number) {
    pin._unscaled_height = value;
    pin._height = value;
    pin._ts_transform = ++iid;
  },

  scale: function (pin: Pin, value: number) {
    pin._scaleX = value;
    pin._scaleY = value;
    pin._ts_transform = ++iid;
  },

  scaleX: function (pin: Pin, value: number) {
    pin._scaleX = value;
    pin._ts_transform = ++iid;
  },

  scaleY: function (pin: Pin, value: number) {
    pin._scaleY = value;
    pin._ts_transform = ++iid;
  },

  skew: function (pin: Pin, value: number) {
    pin._skewX = value;
    pin._skewY = value;
    pin._ts_transform = ++iid;
  },

  skewX: function (pin: Pin, value: number) {
    pin._skewX = value;
    pin._ts_transform = ++iid;
  },

  skewY: function (pin: Pin, value: number) {
    pin._skewY = value;
    pin._ts_transform = ++iid;
  },

  rotation: function (pin: Pin, value: number) {
    pin._rotation = value;
    pin._ts_transform = ++iid;
  },

  pivot: function (pin: Pin, value: number) {
    pin._pivotX = value;
    pin._pivotY = value;
    pin._pivoted = true;
    pin._ts_transform = ++iid;
  },

  pivotX: function (pin: Pin, value: number) {
    pin._pivotX = value;
    pin._pivoted = true;
    pin._ts_transform = ++iid;
  },

  pivotY: function (pin: Pin, value: number) {
    pin._pivotY = value;
    pin._pivoted = true;
    pin._ts_transform = ++iid;
  },

  offset: function (pin: Pin, value: number) {
    pin._offsetX = value;
    pin._offsetY = value;
    pin._ts_translate = ++iid;
  },

  offsetX: function (pin: Pin, value: number) {
    pin._offsetX = value;
    pin._ts_translate = ++iid;
  },

  offsetY: function (pin: Pin, value: number) {
    pin._offsetY = value;
    pin._ts_translate = ++iid;
  },

  align: function (pin: Pin, value: number) {
    this.alignX(pin, value);
    this.alignY(pin, value);
  },

  alignX: function (pin: Pin, value: number) {
    pin._alignX = value;
    pin._aligned = true;
    pin._ts_translate = ++iid;

    this.handleX(pin, value);
  },

  alignY: function (pin: Pin, value: number) {
    pin._alignY = value;
    pin._aligned = true;
    pin._ts_translate = ++iid;

    this.handleY(pin, value);
  },

  handle: function (pin: Pin, value: number) {
    this.handleX(pin, value);
    this.handleY(pin, value);
  },

  handleX: function (pin: Pin, value: number) {
    pin._handleX = value;
    pin._handled = true;
    pin._ts_translate = ++iid;
  },

  handleY: function (pin: Pin, value: number) {
    pin._handleY = value;
    pin._handled = true;
    pin._ts_translate = ++iid;
  },

  resizeMode: function (pin: Pin, value: FitMode, all: ResizeParams) {
    if (all) {
      if (value == "in") {
        value = "in-pad";
      } else if (value == "out") {
        value = "out-crop";
      }
      pin.fit(all.resizeWidth, all.resizeHeight, value);
    }
  },

  resizeWidth: function (pin: Pin, value: number, all: ResizeParams) {
    if (!all || !all.resizeMode) {
      pin.fit(value, null);
    }
  },

  resizeHeight: function (pin: Pin, value: number, all: ResizeParams) {
    if (!all || !all.resizeMode) {
      pin.fit(null, value);
    }
  },

  scaleMode: function (pin: Pin, value: FitMode, all: ScaleParams) {
    if (all) {
      pin.fit(all.scaleWidth, all.scaleHeight, value);
    }
  },

  scaleWidth: function (pin: Pin, value: number, all: ScaleParams) {
    if (!all || !all.scaleMode) {
      pin.fit(value, null);
    }
  },

  scaleHeight: function (pin: Pin, value: number, all: ScaleParams) {
    if (!all || !all.scaleMode) {
      pin.fit(null, value);
    }
  },

  matrix: function (pin: Pin, value: Matrix) {
    this.scaleX(pin, value.a);
    this.skewX(pin, value.c / value.d);
    this.skewY(pin, value.b / value.a);
    this.scaleY(pin, value.d);
    this.offsetX(pin, value.e);
    this.offsetY(pin, value.f);
    this.rotation(pin, 0);
  },
};

export interface SetPinType {
  alpha?: number;
  textureAlpha?: number;

  width?: number;
  height?: number;

  scale?: number;
  scaleX?: number;
  scaleY?: number;

  skew?: number;
  skewX?: number;
  skewY?: number;

  rotation?: number;

  /** Center of scale/skew/rotate, 0 is start, 1 is end  */
  pivot?: number;
  /** Center of scale/skew/rotate, 0 is start, 1 is end  */
  pivotX?: number;
  /** Center of scale/skew/rotate, 0 is start, 1 is end  */
  pivotY?: number;

  /** Offset in parent coordination */
  offset?: number;
  /** Offset in parent coordination */
  offsetX?: number;
  /** Offset in parent coordination */
  offsetY?: number;

  /** A point on parent where this component is offset from, 0 is start, 1 is end  */
  align?: number;
  /** A point on parent where this component is offset from, 0 is start, 1 is end */
  alignX?: number;
  /** A point on parent where this component is offset from, 0 is start, 1 is end */
  alignY?: number;

  /** A point on this component which is offset from parent, 0 is start, 1 is end */
  handle?: number;
  /** A point on this component which is offset from parent, 0 is start, 1 is end */
  handleX?: number;
  /** A point on this component which is offset from parent, 0 is start, 1 is end */
  handleY?: number;

  /** @hidden @deprecated Use component.fit() */
  resizeMode?: FitMode;
  /** @hidden @deprecated Use component.fit() */
  resizeWidth?: number;
  /** @hidden @deprecated Use component.fit() */
  resizeHeight?: number;

  /** @hidden @deprecated Use component.fit() */
  scaleMode?: FitMode;
  /** @hidden @deprecated Use component.fit() */
  scaleWidth?: number;
  /** @hidden @deprecated Use component.fit() */
  scaleHeight?: number;

  matrix?: Matrix;
}

export interface GetPinType {
  alpha: number;
  textureAlpha: number;

  width: number;
  height: number;

  boxWidth: number;
  boxHeight: number;

  scaleX: number;
  scaleY: number;

  skewX: number;
  skewY: number;

  rotation: number;

  pivotX: number;
  pivotY: number;

  offsetX: number;
  offsetY: number;

  alignX: number;
  alignY: number;

  handleX: number;
  handleY: number;
}

export type SetPinKeys = keyof SetPinType;
export type GetPinKeys = keyof GetPinType;
