/*****************************************************************************
 * Open MCT, Copyright (c) 2014-2024, United States Government
 * as represented by the Administrator of the National Aeronautics and Space
 * Administration. All rights reserved.
 *
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 * Open MCT includes source code licensed under additional open source
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 * this source code distribution or the Licensing information page available
 * at runtime from the About dialog for additional information.
 *****************************************************************************/

import { EventEmitter } from 'eventemitter3';

import eventHelpers from '../lib/eventHelpers.js';
import { MARKER_SHAPES } from './MarkerShapes.js';

// WebGL shader sources (for drawing plain colors)
const FRAGMENT_SHADER = `
        precision mediump float;
        uniform vec4 uColor;
        uniform int uMarkerShape;
        
        void main(void) {
            gl_FragColor = uColor;

            if (uMarkerShape > 1) {
                vec2 clipSpacePointCoord = 2.0 * gl_PointCoord - 1.0;

                if (uMarkerShape == 2) { // circle
                    float distance = length(clipSpacePointCoord);

                    if (distance > 1.0) {
                        discard;
                    }
                } else if (uMarkerShape == 3) { // diamond
                    float distance = abs(clipSpacePointCoord.x) + abs(clipSpacePointCoord.y);

                    if (distance > 1.0) {
                        discard;
                    }
                } else if (uMarkerShape == 4) { // triangle
                    float x = clipSpacePointCoord.x;
                    float y = clipSpacePointCoord.y;
                    float distance = 2.0 * x - 1.0;
                    float distance2 = -2.0 * x - 1.0;

                    if (distance > y || distance2 > y) {
                        discard;
                    }
                }

            }
        }
    `;

const VERTEX_SHADER = `
        attribute vec2 aVertexPosition;
        uniform vec2 uDimensions;
        uniform vec2 uOrigin;
        uniform float uPointSize;
        
        void main(void) {
            gl_Position = vec4(2.0 * ((aVertexPosition - uOrigin) / uDimensions) - vec2(1,1), 0, 1);
            gl_PointSize = uPointSize;
        }
    `;

/**
 * Create a draw api utilizing WebGL.
 *
 * @constructor
 * @param {CanvasElement} canvas the canvas object to render upon
 * @throws {Error} an error is thrown if WebGL is unavailable.
 */
class DrawWebGL extends EventEmitter {
  constructor(canvas, overlay) {
    super();
    eventHelpers.extend(this);
    this.canvas = canvas;
    this.gl =
      this.canvas.getContext('webgl', { preserveDrawingBuffer: true }) ||
      this.canvas.getContext('experimental-webgl', { preserveDrawingBuffer: true });

    this.overlay = overlay;
    this.c2d = overlay.getContext('2d');
    if (!this.c2d) {
      throw new Error('No canvas 2d!');
    }

    // Ensure a context was actually available before proceeding
    if (!this.gl) {
      throw new Error('WebGL unavailable.');
    }

    this.initContext();

    this.listenTo(this.canvas, 'webglcontextlost', this.onContextLost, this);
  }
  onContextLost(event) {
    this.emit('error');
    this.isContextLost = true;
    this.destroy();
    // TODO re-initialize and re-draw on context restored
  }
  initContext() {
    // Initialize shaders
    this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
    this.gl.shaderSource(this.vertexShader, VERTEX_SHADER);
    this.gl.compileShader(this.vertexShader);

    this.fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
    this.gl.shaderSource(this.fragmentShader, FRAGMENT_SHADER);
    this.gl.compileShader(this.fragmentShader);

    // Assemble vertex/fragment shaders into programs
    this.program = this.gl.createProgram();
    this.gl.attachShader(this.program, this.vertexShader);
    this.gl.attachShader(this.program, this.fragmentShader);
    this.gl.linkProgram(this.program);
    this.gl.useProgram(this.program);

    // Get locations for attribs/uniforms from the
    // shader programs (to pass values into shaders at draw-time)
    this.aVertexPosition = this.gl.getAttribLocation(this.program, 'aVertexPosition');
    this.uColor = this.gl.getUniformLocation(this.program, 'uColor');
    this.uMarkerShape = this.gl.getUniformLocation(this.program, 'uMarkerShape');
    this.uDimensions = this.gl.getUniformLocation(this.program, 'uDimensions');
    this.uOrigin = this.gl.getUniformLocation(this.program, 'uOrigin');
    this.uPointSize = this.gl.getUniformLocation(this.program, 'uPointSize');

    this.gl.enableVertexAttribArray(this.aVertexPosition);

    // Create a buffer to holds points which will be drawn
    this.buffer = this.gl.createBuffer();

    // Enable blending, for smoothness
    this.gl.enable(this.gl.BLEND);
    this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
  }
  destroy() {
    // Lose the context and delete all associated resources
    // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#lose_contexts_eagerly
    this.gl?.getExtension('WEBGL_lose_context')?.loseContext();
    this.gl?.deleteBuffer(this.buffer);
    this.buffer = undefined;
    this.gl?.deleteProgram(this.program);
    this.program = undefined;
    this.gl?.deleteShader(this.vertexShader);
    this.vertexShader = undefined;
    this.gl?.deleteShader(this.fragmentShader);
    this.fragmentShader = undefined;
    this.gl = undefined;

    this.stopListening();
    this.canvas = undefined;
    this.overlay = undefined;
  }
  // Convert from logical to physical x coordinates
  x(v) {
    return ((v - this.origin[0]) / this.dimensions[0]) * this.width;
  }
  // Convert from logical to physical y coordinates
  y(v) {
    return this.height - ((v - this.origin[1]) / this.dimensions[1]) * this.height;
  }
  doDraw(drawType, buf, color, points, shape) {
    if (this.isContextLost) {
      return;
    }

    const shapeCode = MARKER_SHAPES[shape] ? MARKER_SHAPES[shape].drawWebGL : 0;

    this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
    this.gl.bufferData(this.gl.ARRAY_BUFFER, buf, this.gl.DYNAMIC_DRAW);
    this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0);
    this.gl.uniform4fv(this.uColor, color);
    this.gl.uniform1i(this.uMarkerShape, shapeCode);
    if (points !== 0) {
      this.gl.drawArrays(drawType, 0, points);
    }
  }
  clear() {
    if (this.isContextLost) {
      return;
    }

    this.height = this.canvas.height = this.canvas.offsetHeight;
    this.width = this.canvas.width = this.canvas.offsetWidth;
    this.overlay.height = this.overlay.offsetHeight;
    this.overlay.width = this.overlay.offsetWidth;
    // Set the viewport size; note that we use the width/height
    // that our WebGL context reports, which may be lower
    // resolution than the canvas we requested.
    this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);
    this.gl.clear(this.gl.COLOR_BUFFER_BIT + this.gl.DEPTH_BUFFER_BIT);
  }
  /**
   * Set the logical boundaries of the chart.
   * @param {number[]} dimensions the horizontal and
   *        vertical dimensions of the chart
   * @param {number[]} origin the horizontal/vertical
   *        origin of the chart
   */
  setDimensions(dimensions, origin) {
    this.dimensions = dimensions;
    this.origin = origin;
    if (this.isContextLost) {
      return;
    }

    if (dimensions && dimensions.length > 0 && origin && origin.length > 0) {
      this.gl?.uniform2fv(this.uDimensions, dimensions);
      this.gl?.uniform2fv(this.uOrigin, origin);
    }
  }
  /**
   * Draw the supplied buffer as a line strip (a sequence
   * of line segments), in the chosen color.
   * @param {Float32Array} buf the line strip to draw,
   *        in alternating x/y positions
   * @param {number[]} color the color to use when drawing
   *        the line, as an RGBA color where each element
   *        is in the range of 0.0-1.0
   * @param {number} points the number of points to draw
   */
  drawLine(buf, color, points) {
    if (this.isContextLost) {
      return;
    }

    this.doDraw(this.gl.LINE_STRIP, buf, color, points);
  }
  /**
   * Draw the buffer as points.
   *
   */
  drawPoints(buf, color, points, pointSize, shape) {
    if (this.isContextLost) {
      return;
    }

    this.gl.uniform1f(this.uPointSize, pointSize);
    this.doDraw(this.gl.POINTS, buf, color, points, shape);
  }
  /**
   * Draw a rectangle extending from one corner to another,
   * in the chosen color.
   * @param {number[]} min the first corner of the rectangle
   * @param {number[]} max the opposite corner
   * @param {number[]} color the color to use when drawing
   *        the rectangle, as an RGBA color where each element
   *        is in the range of 0.0-1.0
   */
  drawSquare(min, max, color) {
    if (this.isContextLost) {
      return;
    }

    this.doDraw(
      this.gl.TRIANGLE_FAN,
      new Float32Array(min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]])),
      color,
      4
    );
  }
  drawLimitPoint(x, y, size) {
    this.c2d.fillRect(x + size, y, size, size);
    this.c2d.fillRect(x, y + size, size, size);
    this.c2d.fillRect(x - size, y, size, size);
    this.c2d.fillRect(x, y - size, size, size);
  }
  drawLimitPoints(points, color, pointSize) {
    const limitSize = pointSize * 2;
    const offset = limitSize / 2;

    const mappedColor = color
      .map(function (c, i) {
        return i < 3 ? Math.floor(c * 255) : c;
      })
      .join(',');
    this.c2d.strokeStyle = 'rgba(' + mappedColor + ')';
    this.c2d.fillStyle = 'rgba(' + mappedColor + ')';

    for (let i = 0; i < points.length; i++) {
      this.drawLimitPoint(this.x(points[i].x) - offset, this.y(points[i].y) - offset, limitSize);
    }
  }
}
export default DrawWebGL;
