import { RenderSource } from './render-source';

declare global {
  interface WebGL2RenderingContext {
    /**
     * Experimental only in chrome
     */
    drawingBufferFormat?: number;
  }
}


export interface RenderTargetOptions {
  gl: WebGL2RenderingContext;
  width: number;
  height: number;
  transparency: boolean;
  /**
   * Optionally enable render buffer multisample anti-aliasing
   *
   * By default false
   */
  antialias?: boolean;
  /**
   * Optionally specify number of anti-aliasing samples to use
   *
   * By default the max for the platform is used if antialias is on.
   */
  samples?: number;
}

export class RenderTarget {
  width: number;
  height: number;
  transparency: boolean;
  antialias: boolean = false;
  samples: number = 1;
  private _gl: WebGL2RenderingContext;
  public readonly bufferFormat: number;
  constructor(options: RenderTargetOptions) {
    this._gl = options.gl;
    this.width = options.width;
    this.height = options.height;
    this.transparency = options.transparency;
    this.antialias = options.antialias ?? this.antialias;
    this.samples = options.samples ?? this._gl.getParameter(this._gl.MAX_SAMPLES);

    const gl = this._gl;
    // Determine current context format for blitting later needs to match
    if (gl.drawingBufferFormat) {
      this.bufferFormat = gl.drawingBufferFormat;
    } else {
      // Documented in webgl spec
      // https://registry.khronos.org/webgl/specs/latest/1.0/
      if (this.transparency) {
        this.bufferFormat = gl.RGBA8;
      } else {
        this.bufferFormat = gl.RGB8;
      }
    }

    this._setupRenderBuffer();
    this._setupFramebuffer();
  }

  setResolution(width: number, height: number) {
    const gl = this._gl;
    this.width = width;
    this.height = height;

    // update backing texture size
    gl.bindTexture(gl.TEXTURE_2D, this._frameTexture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

    // update render buffer size
    if (this._renderBuffer) {
      gl.bindRenderbuffer(gl.RENDERBUFFER, this._renderBuffer);
      gl.renderbufferStorageMultisample(
        gl.RENDERBUFFER,
        Math.min(this.samples, gl.getParameter(gl.MAX_SAMPLES)),
        this.bufferFormat,
        this.width,
        this.height);
    }
  }

  private _renderBuffer: WebGLRenderbuffer;
  public get renderBuffer() {
    return this._renderBuffer;
  }
  private _renderFrameBuffer: WebGLFramebuffer;
  public get renderFrameBuffer() {
    return this._renderFrameBuffer;
  }

  private _frameBuffer: WebGLFramebuffer;
  public get frameBuffer() {
    return this._frameBuffer;
  }
  private _frameTexture: WebGLTexture;
  public get frameTexture() {
    return this._frameTexture;
  }

  private _setupRenderBuffer() {
    if (this.antialias) {
      const gl = this._gl;
      // Render buffers can be used as an input to a shader
      this._renderBuffer = gl.createRenderbuffer();
      this._renderFrameBuffer = gl.createFramebuffer();
      gl.bindRenderbuffer(gl.RENDERBUFFER, this._renderBuffer);
      gl.renderbufferStorageMultisample(
        gl.RENDERBUFFER,
        Math.min(this.samples, gl.getParameter(gl.MAX_SAMPLES)),
        this.bufferFormat,
        this.width,
        this.height);
      gl.bindFramebuffer(gl.FRAMEBUFFER, this._renderFrameBuffer);
      gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, this._renderBuffer);
    }
  }

  private _setupFramebuffer() {
    // Allocates frame buffer
    const gl = this._gl;
    this._frameTexture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, this._frameTexture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

    // set the filtering so we don't need mips
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);


    // attach the texture as the first color attachment
    const attachmentPoint = gl.COLOR_ATTACHMENT0;

    // After this bind all draw calls will draw to this framebuffer texture
    this._frameBuffer = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, this._frameBuffer);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, this._frameTexture, 0);

    // Reset after initialized
    this.disable();
  }

  public toRenderSource() {
    if (this.renderBuffer) {
      this.blitRenderBufferToFrameBuffer();
    }
    const source = new RenderSource(this._gl, this._frameTexture);
    return source;
  }

  public blitToScreen() {
    const gl = this._gl;
    // set to size of canvas's drawingBuffer
    if (this._renderBuffer) {
      gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.renderFrameBuffer);
      gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
      gl.clearBufferfv(gl.COLOR, 0, [0.0, 0.0, 1.0, 1.0]);
      gl.blitFramebuffer(
        0, 0, this.width, this.height,
        0, 0, this.width, this.height,
        gl.COLOR_BUFFER_BIT, gl.LINEAR);
    } else {
      gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.frameBuffer);
      gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
      gl.clearBufferfv(gl.COLOR, 0, [0.0, 0.0, 1.0, 1.0]);
      gl.blitFramebuffer(
        0, 0, this.width, this.height,
        0, 0, this.width, this.height,
        gl.COLOR_BUFFER_BIT, gl.LINEAR);
    }
  }

  public blitRenderBufferToFrameBuffer() {
    if (this._renderBuffer) {
      const gl = this._gl;
      gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.renderFrameBuffer);
      gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.frameBuffer);
      gl.clearBufferfv(gl.COLOR, 0, [0.0, 0.0, 1.0, 1.0]);
      gl.blitFramebuffer(
        0, 0, this.width, this.height,
        0, 0, this.width, this.height,
        gl.COLOR_BUFFER_BIT, gl.LINEAR);
    }
  }

  public copyToTexture(texture: WebGLTexture) {
    const gl = this._gl;
    if (this._renderBuffer) {
      this.blitRenderBufferToFrameBuffer();
    }
    gl.bindFramebuffer(gl.FRAMEBUFFER, this._frameBuffer);
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, this.width, this.height, 0);
  }

  /**
   * When called, all drawing gets redirected to this render target
   */
  public use() {
    const gl = this._gl;
    if (this.antialias) {
      gl.bindFramebuffer(gl.FRAMEBUFFER, this._renderFrameBuffer);
    } else {
      gl.bindFramebuffer(gl.FRAMEBUFFER, this._frameBuffer);
    }

    // very important to set the viewport to the size of the framebuffer texture
    gl.viewport(0, 0, this.width, this.height);
  }

  /**
   * When called, all drawing is sent back to the canvas
   */
  public disable() {
    const gl = this._gl;
    // passing null switches rendering back to the canvas
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.bindTexture(gl.TEXTURE_2D, null);
  }
}