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

import { renderAxis } from "./debug";
import { Component } from "./component";
import { Pointer } from "./pointer";
import { fit, FitMode, getIID, isValidFitMode } from "./pin";

/** @internal */ const ROOTS: Root[] = [];

export function pause() {
  for (let i = ROOTS.length - 1; i >= 0; i--) {
    ROOTS[i].pause();
  }
}

export function resume() {
  for (let i = ROOTS.length - 1; i >= 0; i--) {
    ROOTS[i].resume();
  }
}

export function mount(configs: RootConfig = {}) {
  const root = new Root();
  // todo: root.use(new Pointer());
  root.mount(configs);
  // todo: maybe just pass root? or do root.use(pointer)
  root.pointer = new Pointer().mount(root, root.dom as HTMLElement);
  return root;
}

type RootConfig = {
  canvas?: string | HTMLCanvasElement;
};

/**
 * Geometry of the rectangular that the application takes on the screen.
 */
export type Viewport = {
  width: number;
  height: number;
  ratio: number;
};

/**
 * Geometry of a rectangular portion of the game that is projected on the screen.
 */
export type Viewbox = {
  x?: number;
  y?: number;
  width: number;
  height: number;
  mode?: FitMode;
};

let DEFAULT_CANVAS_MOUNTED = false;

export class Root extends Component {
  canvas: HTMLCanvasElement | null = null;
  dom: HTMLCanvasElement | null = null;
  context: CanvasRenderingContext2D | null = null;

  /** @internal */ clientWidth = -1;
  /** @internal */ clientHeight = -1;
  /** @internal */ pixelRatio = 1;
  /** @internal */ canvasWidth = 0;
  /** @internal */ canvasHeight = 0;

  mounted = false;
  paused = false;
  sleep = false;

  /** @internal */ devicePixelRatio: number;
  /** @internal */ backingStoreRatio: number;

  /** @internal */ pointer: Pointer;

  /** @internal */ _viewport: Viewport;
  /** @internal */ _viewbox: Viewbox;

  constructor() {
    super();
    this.label("Root");
  }

  mount = (configs: RootConfig = {}) => {
    if (typeof configs.canvas === "string") {
      this.canvas = document.getElementById(configs.canvas) as HTMLCanvasElement;
      if (!this.canvas) {
        console.error("Canvas element not found: ", configs.canvas);
      }
    } else if (configs.canvas instanceof HTMLCanvasElement) {
      this.canvas = configs.canvas;
    } else if (configs.canvas) {
      console.error("Unknown value for canvas:", configs.canvas);
    }

    if (!this.canvas) {
      this.canvas = (document.getElementById("cutjs") ||
        document.getElementById("stage")) as HTMLCanvasElement;
    }

    if (!this.canvas) {
      if (DEFAULT_CANVAS_MOUNTED) {
        throw new Error(
          "Default canvas element is already mounted. Please provide a canvas element or an id of a canvas element to mount.",
        );
      }
      DEFAULT_CANVAS_MOUNTED = true;

      console.debug && console.debug("Creating canvas element...");
      this.canvas = document.createElement("canvas");
      Object.assign(this.canvas.style, {
        position: "absolute",
        display: "block",
        top: "0",
        left: "0",
        bottom: "0",
        right: "0",
        width: "100%",
        height: "100%",
      });

      const body = document.body;
      body.insertBefore(this.canvas, body.firstChild);
    }

    if (this.canvas["__STAGE_MOUNTED"]) {
      console.error("Canvas element is already mounted: ", this.canvas);
    }
    this.canvas["__STAGE_MOUNTED"] = true;

    this.dom = this.canvas;

    this.context = this.canvas.getContext("2d");

    this.devicePixelRatio = window.devicePixelRatio || 1;
    this.backingStoreRatio =
      this.context["webkitBackingStorePixelRatio"] ||
      this.context["mozBackingStorePixelRatio"] ||
      this.context["msBackingStorePixelRatio"] ||
      this.context["oBackingStorePixelRatio"] ||
      this.context["backingStorePixelRatio"] ||
      1;

    this.pixelRatio = this.devicePixelRatio / this.backingStoreRatio;

    // resize();
    // window.addEventListener('resize', resize, false);
    // window.addEventListener('orientationchange', resize, false);

    this.mounted = true;
    ROOTS.push(this);
    this.requestFrame();
  };

  /** @internal */ frameRequested = false;

  /** @internal */
  requestFrame = () => {
    // one request at a time
    if (!this.frameRequested) {
      this.frameRequested = true;
      requestAnimationFrame(this.onFrame);
    }
  };

  /** @internal */ _lastFrameTime = 0;
  /** @internal */ _mo_touch: number | null = null; // monitor touch

  resizeCanvas() {
    const newClientWidth = this.canvas.clientWidth;
    const newClientHeight = this.canvas.clientHeight;

    // canvas display size is not changed
    if (this.clientWidth === newClientWidth && this.clientHeight === newClientHeight) return;

    this.clientWidth = newClientWidth;
    this.clientHeight = newClientHeight;

    const notStyled =
      this.canvas.clientWidth === this.canvas.width &&
      this.canvas.clientHeight === this.canvas.height;

    let pixelRatio: number;

    if (notStyled) {
      // If element is not styled, changing canvas rendering size will change its display size,
      // which creates a loop of resizing. So we ignore pixel ratio and keep current rendering size.
      pixelRatio = 1;
      this.canvasWidth = this.canvas.width;
      this.canvasHeight = this.canvas.height;
    } else {
      pixelRatio = this.pixelRatio;
      this.canvasWidth = this.clientWidth * pixelRatio;
      this.canvasHeight = this.clientHeight * pixelRatio;

      if (this.canvas.width !== this.canvasWidth || this.canvas.height !== this.canvasHeight) {
        // canvas rendering size is changed
        this.canvas.width = this.canvasWidth;
        this.canvas.height = this.canvasHeight;
      }
    }

    console.debug &&
      console.debug(
        "Resize: [" +
          this.canvasWidth +
          ", " +
          this.canvasHeight +
          "] = " +
          pixelRatio +
          " x [" +
          this.clientWidth +
          ", " +
          this.clientHeight +
          "]",
      );

    this.viewport({
      width: this.canvasWidth,
      height: this.canvasHeight,
      ratio: pixelRatio,
    });
  }

  /** @internal */
  onFrame = (now: number) => {
    this.frameRequested = false;

    if (!this.mounted || !this.canvas || !this.context) {
      return;
    }

    this.requestFrame();
    this.resizeCanvas();

    const last = this._lastFrameTime || now;
    const elapsed = now - last;

    if (!this.mounted || this.paused || this.sleep) {
      return;
    }

    this._lastFrameTime = now;

    this.prerender();

    const tickRequest = this._tick(elapsed, now, last);
    if (this._mo_touch != this._ts_touch) {
      // something changed since last call
      this._mo_touch = this._ts_touch;
      this.sleep = false;

      if (this.canvasWidth > 0 && this.canvasHeight > 0) {
        this.context.setTransform(1, 0, 0, 1, 0, 0);
        this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
        this.render(this.context);
      }
    } else if (tickRequest) {
      // nothing changed, but a component requested next tick
      this.sleep = false;
    } else {
      // nothing changed, and no component requested next tick
      this.sleep = true;
    }

    stats.fps = elapsed ? 1000 / elapsed : 0;
  };

  renderDebug(ctx: CanvasRenderingContext2D, m: Matrix) {
    if (!this._debug) return;
    ctx.setTransform(m.a, m.b, m.c, m.d, m.e, m.f);
    ctx.lineWidth = 3 / m.a;
    renderAxis(ctx, 10);
  }

  resume() {
    if (this.sleep || this.paused) {
      this.requestFrame();
    }
    this.paused = false;
    this.sleep = false;
    this.publish("resume");
    return this;
  }

  pause() {
    if (!this.paused) {
      this.publish("pause");
    }
    this.paused = true;
    return this;
  }

  /** @internal */
  touch() {
    if (this.sleep || this.paused) {
      this.requestFrame();
    }
    this.sleep = false;
    return super.touch();
  }

  unmount() {
    this.mounted = false;
    const index = ROOTS.indexOf(this);
    if (index >= 0) {
      ROOTS.splice(index, 1);
    }

    this.pointer?.unmount();
    return this;
  }

  background(color: string) {
    if (this.dom) {
      this.dom.style.backgroundColor = color;
    }
    return this;
  }

  /**
   * Set/Get viewport.
   * This is used along with viewbox to determine the scale and position of the viewbox within the viewport.
   * Viewport is the size of the container, for example size of the canvas element.
   * Viewbox is provided by the user, and is the ideal size of the content.
   */
  viewport(): Viewport;
  viewport(width: number, height: number, ratio?: number): this;
  viewport(viewbox: Viewport): this;
  viewport(width?: number | Viewport, height?: number, ratio?: number) {
    if (typeof width === "undefined") {
      // todo: return readonly object instead
      return Object.assign({}, this._viewport);
    }

    if (typeof width === "object") {
      const options = width;
      width = options.width;
      height = options.height;
      ratio = options.ratio;
    }

    if (typeof width === "number" && typeof height === "number") {
      this._viewport = {
        width: width,
        height: height,
        ratio: typeof ratio === "number" ? ratio : 1,
      };
      this.rescale();
      const data = Object.assign({}, this._viewport);
      this.visit({
        start: function (component) {
          if (!component._flag("viewport")) {
            return true;
          }
          component.publish("viewport", [data]);
        },
      });
    }

    return this;
  }

  /**
   * Set viewbox.
   */
  viewbox(viewbox: Viewbox): this;
  viewbox(width?: number, height?: number, mode?: FitMode): this;
  viewbox(width?: number | Viewbox, height?: number, mode?: FitMode): this {
    // TODO: static/fixed viewbox
    if (typeof width === "number" && typeof height === "number") {
      this._viewbox = {
        width,
        height,
        mode,
      };
    } else if (typeof width === "object" && width !== null) {
      this._viewbox = {
        ...width,
      };
    }

    this.rescale();

    return this;
  }

  /** @hidden */
  camera(matrix: Matrix) {
    this._xf = matrix.clone();
    this._pin._ts_transform = getIID();
    this.touch();
    return this;
  }

  /** @internal */
  rescale() {
    const viewbox = this._viewbox;
    const viewport = this._viewport;
    if (viewport && viewbox) {
      const viewboxMode = isValidFitMode(viewbox.mode) ? viewbox.mode : "in-pad";
      const fitted = fit(
        viewbox.width,
        viewbox.height,
        viewport.width,
        viewport.height,
        viewboxMode,
      );
      this.pin({
        width: fitted.width,
        height: fitted.height,
        scaleX: fitted.scaleX,
        scaleY: fitted.scaleY,
        offsetX: -(viewbox.x || 0) * fitted.scaleX,
        offsetY: -(viewbox.y || 0) * fitted.scaleY,
      });
    } else if (viewport) {
      this.pin({
        width: viewport.width,
        height: viewport.height,
      });
    }

    return this;
  }

  /** @hidden */
  flipX(x: boolean) {
    this._pin._directionX = x ? -1 : 1;
    return this;
  }

  /** @hidden */
  flipY(y: boolean) {
    this._pin._directionY = y ? -1 : 1;
    return this;
  }
}
