import { Logger } from '../..';
import { Shader, VertexAttributeDefinition } from './shader';
import { VertexBuffer } from './vertex-buffer';
import { getGLTypeFromSource, getGlTypeSizeBytes, isAttributeInSource } from './webgl-util';


export interface VertexLayoutOptions {
  /**
   * WebGL2RenderingContext this layout will be attached to, these cannot be reused across contexts.
   */
  gl: WebGL2RenderingContext,
  /**
   * Shader that this layout will be for, if null you must set a shader before using it.
   */
  shader?: Shader;
  /**
   * Vertex buffer to use for vertex data
   */
  vertexBuffer: VertexBuffer,
  /**
   * Specify the attributes that will exist in the vertex buffer
   *
   * **Important** must specify them in the order that they will be in the vertex buffer!!
   */
  attributes: [name: string, numberOfComponents: number][],
  /**
   * Optionally suppress any warnings out of vertex layouts
   *
   * **BEWARE** this may cause you to have issues go unnoticed
   */
  suppressWarnings?: boolean
}

/**
 * Helper around creating vertex attributes in a given [[VertexBuffer]], this is useful for describing
 * the memory layout for your vertices inside a particular buffer
 *
 * Note: This helper assumes interleaved attributes in one [[VertexBuffer]], not many.
 *
 * Working with `gl.vertexAttribPointer` can be tricky, and this attempts to double check you
 */
export class VertexLayout {
  private _gl: WebGL2RenderingContext;
  private _logger = Logger.getInstance();
  private _suppressWarnings = false;
  private _shader: Shader;
  private _layout: VertexAttributeDefinition[] = [];
  private _attributes: [name: string, numberOfComponents: number][] = [];
  private _vertexBuffer: VertexBuffer;
  private _vao: WebGLVertexArrayObject;

  public get vertexBuffer() {
    return this._vertexBuffer;
  }

  public get attributes(): readonly [name: string, numberOfComponents: number][] {
    return this._attributes;
  }

  constructor(options: VertexLayoutOptions) {
    const {gl, shader, vertexBuffer, attributes, suppressWarnings } = options;
    this._gl = gl;
    this._vertexBuffer = vertexBuffer;
    this._attributes = attributes;
    this._shader = shader;
    this._suppressWarnings = suppressWarnings;
    if (shader) {
      this.initialize();
    }
  }

  private _vertexTotalSizeBytes = 0;
  /**
   * Total number of bytes that the vertex will take up
   */
  public get totalVertexSizeBytes(): number {
    return this._vertexTotalSizeBytes;
  }

  public set shader(shader: Shader) {
    if (shader && this._shader !== shader) {
      this._shader = shader;
      this.initialize();
    }
  }

  public get shader() {
    return this._shader;
  }

  private _initialized = false;

  /**
   * Layouts need shader locations and must be bound to a shader
   */
  initialize() {
    if (this._initialized) {
      return;
    }
    if (!this._shader) {
      return;
    }

    if (!this._shader.compiled) {
      throw Error('Shader not compiled, shader must be compiled before defining a vertex layout');
    }
    this._vertexTotalSizeBytes = 0;
    this._layout.length = 0;
    const shaderAttributes = this._shader.attributes;
    for (const attribute of this._attributes) {
      const attrib = shaderAttributes[attribute[0]];
      if (!attrib) {
        if (!isAttributeInSource(this._shader.vertexSource, attribute[0])) {
          throw Error(`The attribute named: ${attribute[0]} size ${attribute[1]}`+
          ` not found in the shader source code:\n ${this._shader.vertexSource}`);
        }

        if (!this._suppressWarnings) {
          this._logger.warn(`The attribute named: ${attribute[0]} size ${attribute[1]}`+
          ` not found in the compiled shader. This is possibly a bug:\n` +
          ` 1. Not a bug, but should remove unused code - attribute "${attribute[0]}" is unused in` +
          ` vertex/fragment and has been automatically removed by glsl compiler.\n` +
          ` 2. Definitely a bug, attribute "${attribute[0]}" in layout has been mistyped or is missing` +
          ` in shader, check vertex/fragment source.`);
        }

        const glType = getGLTypeFromSource(this._gl, this._shader.vertexSource, attribute[0]);

        // Unused attribute placeholder
        this._layout.push({
          name: attribute[0],
          glType,
          size: attribute[1],
          location: -1,
          normalized: false
        });
      }
      if (attrib) {
        if (attrib.size !== attribute[1]) {
          throw Error(`VertexLayout size definition for attribute: [${attribute[0]}, ${attribute[1]}],`
          +` doesn\'t match shader source size ${attrib.size}:\n ${this._shader.vertexSource}`);
        }
        this._layout.push(attrib);
      }
    }

    // calc size
    let componentsPerVertex = 0;
    for (const vertAttribute of this._layout) {
      const typeSize = getGlTypeSizeBytes(this._gl, vertAttribute.glType);
      this._vertexTotalSizeBytes += typeSize * vertAttribute.size;
      componentsPerVertex += vertAttribute.size;
    }

    if (this._vertexBuffer.bufferData.length % componentsPerVertex !== 0) {
      this._logger.warn(`The vertex component size (${componentsPerVertex})  does NOT divide evenly into the specified vertex buffer`
      +` (${this._vertexBuffer.bufferData.length})`);
    }


    // create VAO
    const gl = this._gl;
    this._vao = gl.createVertexArray();
    gl.bindVertexArray(this._vao);
    this._vertexBuffer.bind();

    let offset = 0;
    for (const vert of this._layout) {
      if (vert.location !== -1) { // skip unused attributes
        gl.vertexAttribPointer(vert.location, vert.size, vert.glType, vert.normalized, this.totalVertexSizeBytes, offset);
        gl.enableVertexAttribArray(vert.location);
      }
      offset += getGlTypeSizeBytes(gl, vert.glType) * vert.size;
    }
    gl.bindVertexArray(null);
    this._vertexBuffer.unbind();

    this._initialized = true;
  }

  /**
   * Bind this layout with it's associated vertex buffer
   * @param uploadBuffer Optionally indicate you wish to upload the buffer to the GPU associated with this layout
   */
  use(uploadBuffer = false, count?: number) {
    if (!this._shader) {
      throw Error('No shader is associated with this vertex layout, a shader must be set');
    }

    const gl = this._gl;
    if (!this._shader.isCurrentlyBound()) {
      throw Error('Shader associated with this vertex layout is not active! Call shader.use() before layout.use()');
    }
    this._vertexBuffer.bind();
    if (uploadBuffer) {
      this._vertexBuffer.upload(count);
    }
    gl.bindVertexArray(this._vao);
  }
}