/*
 * If not stated otherwise in this file or this component's LICENSE file the
 * following copyright and licenses apply:
 *
 * Copyright 2023 Comcast Cable Communications Management, LLC.
 *
 * 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.
 */
import type { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js';
import { Default } from '../../shaders/webgl/Default.js';
import type { QuadOptions } from '../CoreRenderer.js';
import type { CoreShaderProgram } from '../CoreShaderProgram.js';
import type { WebGlCtxTexture } from './WebGlCtxTexture.js';
import type { WebGlRenderOp } from './WebGlRenderOp.js';
import type { WebGlRenderer } from './WebGlRenderer.js';
import type { WebGlShaderType } from './WebGlShaderNode.js';
import type { BufferCollection } from './internal/BufferCollection.js';
import {
  createProgram,
  createShader,
  type UniformSet1Param,
  type UniformSet2Params,
  type UniformSet3Params,
  type UniformSet4Params,
} from './internal/ShaderUtils.js';

export class WebGlShaderProgram implements CoreShaderProgram {
  protected boundBufferCollection: BufferCollection | null = null;
  protected program: WebGLProgram;
  /**
   * Vertex Array Object
   *
   * @remarks
   * Used by WebGL2 Only
   */
  protected vao: WebGLVertexArrayObject | undefined;
  protected renderer: WebGlRenderer;
  protected glw: WebGlContextWrapper;
  protected attributeLocations: Record<string, number>;
  protected lifecycle: Pick<WebGlShaderType, 'update' | 'canBatch'>;
  protected useSystemAlpha = false;
  protected useSystemDimensions = false;
  supportsIndexedTextures = false;

  constructor(
    renderer: WebGlRenderer,
    config: WebGlShaderType,
    resolvedProps: Record<string, any>,
  ) {
    this.renderer = renderer;
    const glw = (this.glw = renderer.glw);

    // Check that extensions are supported
    const webGl2 = glw.isWebGl2();
    let requiredExtensions: string[] = [];

    this.supportsIndexedTextures =
      config.supportsIndexedTextures || this.supportsIndexedTextures;
    requiredExtensions =
      (webGl2 && config.webgl2Extensions) ||
      (!webGl2 && config.webgl1Extensions) ||
      [];

    const glVersion = webGl2 ? '2.0' : '1.0';
    requiredExtensions.forEach((extensionName) => {
      if (!glw.getExtension(extensionName)) {
        throw new Error(
          `Shader "${this.constructor.name}" requires extension "${extensionName}" for WebGL ${glVersion} but wasn't found`,
        );
      }
    });

    let vertexSource =
      config.vertex instanceof Function
        ? config.vertex(renderer, resolvedProps)
        : config.vertex;

    if (vertexSource === undefined) {
      vertexSource = Default.vertex as string;
    }

    const fragmentSource =
      config.fragment instanceof Function
        ? config.fragment(renderer, resolvedProps)
        : config.fragment;

    const vertexShader = createShader(glw, glw.VERTEX_SHADER, vertexSource);
    if (!vertexShader) {
      throw new Error('Vertex shader creation failed');
    }

    const fragmentShader = createShader(
      glw,
      glw.FRAGMENT_SHADER,
      fragmentSource,
    );

    if (!fragmentShader) {
      throw new Error('fragment shader creation failed');
    }

    const program = createProgram(glw, vertexShader, fragmentShader);
    if (!program) {
      throw new Error();
    }

    this.program = program;
    this.attributeLocations = glw.getAttributeLocations(program);

    this.useSystemAlpha =
      this.glw.getUniformLocation(program, 'u_alpha') !== null;
    this.useSystemDimensions =
      this.glw.getUniformLocation(program, 'u_dimensions') !== null;

    this.lifecycle = {
      update: config.update,
      canBatch: config.canBatch,
    };
  }

  disableAttribute(location: number) {
    this.glw.disableVertexAttribArray(location);
  }

  disableAttributes() {
    const { glw } = this;
    const attribs = Object.keys(this.attributeLocations);
    const attribLen = attribs.length;
    for (let i = 0; i < attribLen; i++) {
      glw.disableVertexAttribArray(i);
    }
  }

  reuseRenderOp(
    incomingQuad: QuadOptions,
    currentRenderOp: WebGlRenderOp,
  ): boolean {
    if (this.lifecycle.canBatch !== undefined) {
      return this.lifecycle.canBatch(incomingQuad, currentRenderOp);
    }

    if (this.useSystemAlpha === true) {
      if (incomingQuad.alpha !== currentRenderOp.alpha) {
        return false;
      }
    }

    if (this.useSystemDimensions === true) {
      if (
        incomingQuad.width !== currentRenderOp.width ||
        incomingQuad.height !== currentRenderOp.height
      ) {
        return false;
      }
    }
    let shaderPropsA: Record<string, unknown> | undefined = undefined;
    let shaderPropsB: Record<string, unknown> | undefined = undefined;

    if (incomingQuad.shader !== null) {
      shaderPropsA = incomingQuad.shader.resolvedProps;
    }
    if (currentRenderOp.shader !== null) {
      shaderPropsB = currentRenderOp.shader.resolvedProps;
    }

    if (
      (shaderPropsA === undefined && shaderPropsB !== undefined) ||
      (shaderPropsA !== undefined && shaderPropsB === undefined)
    ) {
      return false;
    }

    if (shaderPropsA !== undefined && shaderPropsB !== undefined) {
      for (const key in shaderPropsA) {
        if (shaderPropsA[key] !== shaderPropsB[key]) {
          return false;
        }
      }
    }

    return true;
  }

  bindRenderOp(renderOp: WebGlRenderOp) {
    this.bindBufferCollection(renderOp.buffers);
    this.bindTextures(renderOp.textures);

    const { parentHasRenderTexture } = renderOp;

    // Skip if the parent and current operation both have render textures
    if (renderOp.rtt === true && parentHasRenderTexture === true) {
      return;
    }

    // Bind render texture framebuffer dimensions as resolution
    // if the parent has a render texture
    if (parentHasRenderTexture === true) {
      const { width, height } = renderOp.framebufferDimensions!;
      // Force pixel ratio to 1.0 for render textures since they are always 1:1
      // the final render texture will be rendered to the screen with the correct pixel ratio
      this.glw.uniform1f('u_pixelRatio', 1.0);

      // Set resolution to the framebuffer dimensions
      this.glw.uniform2f('u_resolution', width, height);
    } else {
      this.glw.uniform1f('u_pixelRatio', renderOp.renderer.stage.pixelRatio);
      this.glw.uniform2f(
        'u_resolution',
        this.glw.canvas.width,
        this.glw.canvas.height,
      );
    }

    // if (this.useSystemAlpha) {
    this.glw.uniform1f('u_alpha', renderOp.alpha);
    // }

    // if (this.useSystemDimensions) {
    this.glw.uniform2f('u_dimensions', renderOp.width, renderOp.height);
    // }

    /**temporary fix to make sdf texts work */
    if (renderOp.sdfShaderProps !== undefined) {
      (renderOp.shader.shaderType as WebGlShaderType).onSdfBind?.call(
        this.glw,
        renderOp.sdfShaderProps,
      );
      return;
    }

    if (renderOp.shader.props !== undefined) {
      /**
       * loop over all precalculated uniform types
       */
      for (const key in renderOp.shader.uniforms.single) {
        const { method, value } = renderOp.shader.uniforms.single[key]!;
        this.glw[method as keyof UniformSet1Param](key, value as never);
      }

      for (const key in renderOp.shader.uniforms.vec2) {
        const { method, value } = renderOp.shader.uniforms.vec2[key]!;
        this.glw[method as keyof UniformSet2Params](key, value[0], value[1]);
      }

      for (const key in renderOp.shader.uniforms.vec3) {
        const { method, value } = renderOp.shader.uniforms.vec3[key]!;
        this.glw[method as keyof UniformSet3Params](
          key,
          value[0],
          value[1],
          value[2],
        );
      }

      for (const key in renderOp.shader.uniforms.vec4) {
        const { method, value } = renderOp.shader.uniforms.vec4[key]!;
        this.glw[method as keyof UniformSet4Params](
          key,
          value[0],
          value[1],
          value[2],
          value[3],
        );
      }
    }
  }

  bindBufferCollection(buffer: BufferCollection) {
    const { glw } = this;
    const attribs = Object.keys(this.attributeLocations);
    const attribLen = attribs.length;

    for (let i = 0; i < attribLen; i++) {
      const name = attribs[i]!;
      const resolvedBuffer = buffer.getBuffer(name);
      const resolvedInfo = buffer.getAttributeInfo(name);
      if (resolvedBuffer === undefined || resolvedInfo === undefined) {
        continue;
      }
      glw.enableVertexAttribArray(i);
      glw.vertexAttribPointer(
        resolvedBuffer,
        i,
        resolvedInfo.size,
        resolvedInfo.type,
        resolvedInfo.normalized,
        resolvedInfo.stride,
        resolvedInfo.offset,
      );
    }
  }

  bindTextures(textures: WebGlCtxTexture[]) {
    this.glw.activeTexture(0);
    this.glw.bindTexture(textures[0]!.ctxTexture);
  }

  attach(): void {
    this.glw.useProgram(this.program);
    if (this.glw.isWebGl2() && this.vao) {
      this.glw.bindVertexArray(this.vao);
    }
  }

  detach(): void {
    this.disableAttributes();
  }
}
