import { setBuffersAndAttributes } from "twgl.js";
import VERTEX_SHADER from "./link.vertex.glsl.js";
import FRAGMENT_SHADER from "./link.fragment.glsl.js";
import COMMON_SHADER from "./link.common.glsl.js";
import { LinkVertexBuilder } from "../gl/dataToVertices.js";

import Mark from "./mark.js";
import { isChannelDefWithScale } from "../encoder/encoder.js";

const LINK_SHAPES = ["arc", "dome", "diagonal", "line"];
const ORIENTS = ["vertical", "horizontal"];

/**
 * @extends {Mark<import("../spec/mark.js").LinkProps>}
 */
export default class LinkMark extends Mark {
    /**
     * @param {import("../view/unitView.js").default} unitView
     */
    constructor(unitView) {
        super(unitView);
        /**
         * Only available if "WebGL Draft Extensions" is enabled in chrome://flags
         * But seems to work.
         *
         * @private
         */
        this._baseInstanceExt = undefined;
    }

    /**
     * Returns the default hit test mode for this mark.
     * @returns {import("./mark.js").HitTestMode}
     */
    get defaultHitTestMode() {
        return "endpoints";
    }

    /**
     * @returns {import("../spec/channel.js").Channel[]}
     */
    getAttributes() {
        return [
            "uniqueId",
            "facetIndex",
            "x",
            "x2",
            "y",
            "y2",
            "size",
            "color",
            "opacity",
        ];
    }

    /** @return {import("../spec/channel.js").Channel[]} */
    getSupportedChannels() {
        return [...super.getSupportedChannels(), "x2", "y2", "size"];
    }

    /**
     * @param {import("../spec/channel.js").Encoding} encoding
     * @returns {import("../spec/channel.js").Encoding}
     */
    fixEncoding(encoding) {
        if (!encoding.x2) {
            if (isChannelDefWithScale(encoding.x)) {
                encoding.x2 = { datum: 0.0 };
            } else {
                encoding.x2 = encoding.x;
            }
        }

        if (!encoding.y2) {
            if (isChannelDefWithScale(encoding.y)) {
                encoding.y2 = { datum: 0.0 };
            } else {
                encoding.y2 = encoding.y;
            }
        }

        return encoding;
    }

    async initializeGraphics() {
        await super.initializeGraphics();

        this._baseInstanceExt = this.gl.getExtension(
            "WEBGL_draw_instanced_base_vertex_base_instance"
        );

        this.createAndLinkShaders(VERTEX_SHADER, FRAGMENT_SHADER, [
            COMMON_SHADER,
        ]);
    }

    finalizeGraphicsInitialization() {
        super.finalizeGraphicsInitialization();
        this.gl.useProgram(this.programInfo.program);

        const props = this.properties;

        this.registerMarkUniformValue(
            "uArcFadingDistance",
            props.arcFadingDistance,
            (x) => x || /** @type {[number, number]} */ ([0, 0])
        );
        this.registerMarkUniformValue(
            "uArcHeightFactor",
            props.arcHeightFactor
        );
        this.registerMarkUniformValue("uMinArcHeight", props.minArcHeight);
        this.registerMarkUniformValue("uMinPickingSize", props.minPickingSize);
        this.registerMarkUniformValue("uShape", props.linkShape, (linkShape) =>
            LINK_SHAPES.indexOf(linkShape)
        );
        this.registerMarkUniformValue("uOrient", props.orient, (orient) =>
            ORIENTS.indexOf(orient)
        );
        this.registerMarkUniformValue(
            "uClampApex",
            props.clampApex,
            (x) => !!x
        );
        this.registerMarkUniformValue("uMaxChordLength", props.maxChordLength);
        this.registerMarkUniformValue(
            "uSegmentBreaks",
            props.segments,
            (x) => x
        );
        this.registerMarkUniformValue(
            "uNoFadingOnPointSelection",
            props.noFadingOnPointSelection,
            (x) => !!x
        );
    }

    updateGraphicsData() {
        const collector = this.unitView.getCollector();
        if (!collector) {
            console.debug("No collector");
            return;
        }
        const itemCount = collector.getItemCount();

        const builder = new LinkVertexBuilder({
            encoders: this.encoders,
            attributes: this.getAttributes(),
            numItems: itemCount,
        });

        builder.addBatches(collector.facetBatches);

        const vertexData = builder.toArrays();

        this.rangeMap.migrateEntries(vertexData.rangeMap);

        this.arrays = Object.fromEntries(
            Object.entries(vertexData.arrays).map(([k, v]) => [
                k,
                { ...v, data: /** @type {any} */ (undefined) },
            ])
        );

        this.updateBufferInfo(vertexData);
    }

    /**
     * @param {import("../types/rendering.js").GlobalRenderingOptions} options
     */
    prepareRender(options) {
        const ops = super.prepareRender(options);

        ops.push(() => this.bindOrSetMarkUniformBlock());

        if (this._baseInstanceExt) {
            ops.push(() =>
                setBuffersAndAttributes(
                    this.gl,
                    this.programInfo,
                    this.vertexArrayInfo
                )
            );
        } else {
            ops.push(() => this.gl.bindVertexArray(null));
        }

        return ops;
    }

    /**
     * @param {import("./mark.js").MarkRenderingOptions} options
     */
    render(options) {
        const gl = this.gl;

        const getInstanceVertexCount = () => {
            const breaks = /** @type {Float32Array} */ (
                this.markUniformInfo.uniforms.uSegmentBreaks
            )[0];

            return (breaks + 1) * 2;
        };

        return this._baseInstanceExt
            ? this.createRenderCallback((offset, count) => {
                  // Using the following extension, which, however, is only a draft and
                  // available if "WebGL Draft Extensions" is enabled in chrome://flags
                  // https://www.khronos.org/registry/webgl/extensions/WEBGL_draw_instanced_base_vertex_base_instance/

                  this._baseInstanceExt.drawArraysInstancedBaseInstanceWEBGL(
                      gl.TRIANGLE_STRIP,
                      0,
                      getInstanceVertexCount(),
                      count,
                      offset
                  );
              }, options)
            : this.createRenderCallback((offset, count) => {
                  // Because vanilla WebGL 2 does not provide glDrawArraysInstancedBaseInstance,
                  // we have to hack with offsets in vertexAttribPointer
                  //
                  // TODO: Use VAOs more intelligently to reduce WebGL calls. In other words,
                  // reserve one VAO for each facet/sample.

                  for (const attribInfoObject of Object.entries(
                      this.bufferInfo.attribs
                  )) {
                      const [attribute, attribInfo] = attribInfoObject;
                      if (
                          attribInfo.buffer &&
                          attribInfo.numComponents &&
                          attribInfo.divisor
                      ) {
                          attribInfo.offset =
                              offset *
                              this.arrays[attribute].numComponents *
                              this.bytesPerElement.get(attribute);
                      }
                  }
                  setBuffersAndAttributes(
                      gl,
                      this.programInfo,
                      this.bufferInfo
                  );

                  gl.drawArraysInstanced(
                      gl.TRIANGLE_STRIP,
                      0,
                      getInstanceVertexCount(),
                      count
                  );
              }, options);
    }
}
