/**
 * @module ol/render/canvas/ZIndexContext
 */

import {getSharedCanvasContext2D} from '../../dom.js';

/** @typedef {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D & {globalAlpha: any}} ZIndexContextProxy */

/**
 * @extends {CanvasRenderingContext2D}
 */
class ZIndexContext {
  constructor() {
    /**
     * @private
     * @type {Array<Array<*>>}
     */
    this.instructions_ = [];
    /**
     * @type {number}
     */
    this.zIndex = 0;
    /**
     * @private
     * @type {number}
     */
    this.offset_ = 0;

    /**
     * @private
     * @type {ZIndexContextProxy}
     */
    this.context_ = /** @type {ZIndexContextProxy} */ (
      new Proxy(getSharedCanvasContext2D(), {
        get: (target, property) => {
          if (
            typeof (/** @type {*} */ (getSharedCanvasContext2D())[property]) !==
            'function'
          ) {
            // we only accept calling functions on the proxy, not accessing properties
            return undefined;
          }
          this.push_(property);
          return this.pushMethodArgs_;
        },
        set: (target, property, value) => {
          this.push_(property, value);
          return true;
        },
      })
    );
  }

  /**
   * @param {...*} args Arguments to push to the instructions array.
   * @private
   */
  push_(...args) {
    const instructions = this.instructions_;
    const index = this.zIndex + this.offset_;
    if (!instructions[index]) {
      instructions[index] = [];
    }
    instructions[index].push(...args);
  }

  /**
   * @private
   * @param {...*} args Args.
   * @return {ZIndexContext} This.
   */
  pushMethodArgs_ = (...args) => {
    this.push_(args);
    return this;
  };

  /**
   * Push a function that renders to the context directly.
   * @param {function(CanvasRenderingContext2D): void} render Function.
   */
  pushFunction(render) {
    this.push_(render);
  }

  /**
   * Get a proxy for CanvasRenderingContext2D which does not support getting state
   * (e.g. `context.globalAlpha`, which will return `undefined`). To set state, if it relies on a
   * previous state (e.g. `context.globalAlpha = context.globalAlpha / 2`), set a function,
   * e.g. `context.globalAlpha = (context) => context.globalAlpha / 2`.
   * @return {ZIndexContextProxy} Context.
   */
  getContext() {
    return this.context_;
  }

  /**
   * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
   */
  draw(context) {
    this.instructions_.forEach((instructionsAtIndex) => {
      for (let i = 0, ii = instructionsAtIndex.length; i < ii; ++i) {
        const property = instructionsAtIndex[i];
        if (typeof property === 'function') {
          property(context);
          continue;
        }
        const instructionAtIndex = instructionsAtIndex[++i];
        if (typeof (/** @type {*} */ (context)[property]) === 'function') {
          /** @type {*} */ (context)[property](...instructionAtIndex);
        } else {
          if (typeof instructionAtIndex === 'function') {
            /** @type {*} */ (context)[property] = instructionAtIndex(context);
            continue;
          }
          /** @type {*} */ (context)[property] = instructionAtIndex;
        }
      }
    });
  }

  clear() {
    this.instructions_.length = 0;
    this.zIndex = 0;
    this.offset_ = 0;
  }

  /**
   * Offsets the zIndex by the highest current zIndex. Useful for rendering multiple worlds or tiles, to
   * avoid conflicting context.clip() or context.save()/restore() calls.
   */
  offset() {
    this.offset_ = this.instructions_.length;
    this.zIndex = 0;
  }
}

export default ZIndexContext;
