import { mat4, vec3 } from "gl-matrix";

export type VaoBuffers<T extends string> = { [key in T]: WebGLBuffer };
export const updateVao = <T extends string>(
  gl: WebGL2RenderingContext,
  modal: { [key in T]: ArrayBufferView },
  pointers: Pointer<T>[],
  vao: WebGLVertexArrayObject
) => {
  gl.bindVertexArray(vao);

  const keys = pointers.map((p) => p.key);
  const buffers = Object.fromEntries(
    Object.entries(modal)
      .filter(([k]) => keys.includes(k as T))
      .map(([key, val]) => {
        const buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, val as ArrayBufferView, gl.STATIC_DRAW);
        return [key, buffer];
      })
  ) as VaoBuffers<T>;

  for (const pointer of pointers) {
    gl.bindBuffer(gl.ARRAY_BUFFER, buffers[pointer.key]);
    gl.enableVertexAttribArray(pointer.loc);
    gl.vertexAttribPointer(
      pointer.loc,
      pointer.size,
      pointer.type,
      false,
      pointer.stride,
      pointer.offset
    );

    if (pointer.divisor && "divisor" in pointer) {
      gl.vertexAttribDivisor(pointer.loc, pointer.divisor!);
    }
  }
  return buffers;
};

export type Pointer<T = string> = {
  loc: number;
  key: T;
  size: number;
  type: number;
  stride: number;
  offset: number;
  divisor?: number;
};
export const generateVao = <T extends string>(
  gl: WebGL2RenderingContext,
  modal: { [key in T]: ArrayBufferView },
  pointers: Pointer<T>[]
) => {
  const vao = gl.createVertexArray()!;
  updateVao(gl, modal, pointers, vao);

  if ("includes" in modal) {
    const eleBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, eleBuffer);
    gl.bufferData(
      gl.ELEMENT_ARRAY_BUFFER,
      (modal as any).includes,
      gl.STATIC_DRAW
    );
  }

  return vao;
};

export type Uniforms = {
  [key in string]:
    | number
    | number[]
    | vec3[]
    | number[][]
    | Float32Array
    | Float32Array[]
    | Uniforms
    | Uniforms[]
    | mat4[];
};

const setUniform = (
  gl: WebGL2RenderingContext,
  program: WebGLProgram,
  key: string,
  valR: number | number[] | Float32Array
) => {
  const val = (
    valR instanceof Float32Array || Array.isArray(valR) ? valR : [valR]
  ) as number[];

  const loc = gl.getUniformLocation(program, key);

  if (loc) {
    try {
      if (/(mat$)|(mats\[\d+\])/gi.test(key) && (valR as any).length) {
        (gl as any)[`uniformMatrix${Math.sqrt(val.length)}fv`](loc, false, val);
      } else if (key.includes("Tex") || key.includes("tex")) {
        gl.uniform1iv(loc, val);
      } else if (val.length > 4) {
        for (let i = 0; i < val.length; i++) {
          setUniform(gl, program, `${key}[${i}]`, val[i]);
        }
      } else {
        (gl as any)[`uniform${val.length}fv`](loc, val);
      }
    } catch (e) {
      console.error(`key in ${key} val in`, val);
      throw e;
    }
  } else {
    // console.log(key, loc);
  }
};

export const setUniforms = (
  gl: WebGL2RenderingContext,
  program: WebGLProgram,
  data: Uniforms,
  prefix = ""
) => {
  Object.entries(data).forEach(([k, v]) => {
    if (
      v instanceof Float32Array ||
      Array.isArray(v) ||
      typeof v !== "object"
    ) {
      if (Array.isArray(v) && typeof v[0] === "object") {
        v.forEach((vi, ndx) => {
          if (vi instanceof Float32Array) {
            setUniform(gl, program, prefix + k + `[${ndx}]`, vi);
          } else {
            setUniforms(gl, program, vi as Uniforms, prefix + k + `[${ndx}].`);
          }
        });
      } else {
        setUniform(gl, program, prefix + k, v as number);
      }
    } else {
      setUniforms(gl, program, v as Uniforms, k + ".");
    }
  });
};

export const generateProgram = (
  gl: WebGL2RenderingContext,
  ...shaders: WebGLShader[]
) => {
  const program = gl.createProgram();
  if (!program) throw "gl 无法创建程序";
  for (const shader of shaders) {
    gl.attachShader(program, shader);
  }
  gl.linkProgram(program);
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error(gl.getProgramInfoLog(program));
    throw "程序链接失败";
  }
  return program;
};

const typeNameMap: { [key in string]: string } = {
  35633: "顶点着色器",
  35632: "片段着色器",
};
export const createShader = (
  gl: WebGL2RenderingContext,
  type: number,
  source: string
) => {
  const shader = gl.createShader(type);
  if (!shader) throw `gl 无法创建${typeNameMap[type]}着色器`;
  gl.shaderSource(shader, source);
  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error(gl.getShaderInfoLog(shader));
    throw `${typeNameMap[type]}着色器编译失败`;
  }

  return shader;
};

export const createProgram = (
  gl: WebGL2RenderingContext,
  vSource: string,
  fSource: string
) => {
  const program = generateProgram(
    gl,
    createShader(gl, gl.VERTEX_SHADER, vSource),
    createShader(gl, gl.FRAGMENT_SHADER, fSource)
  );
  return program;
};

import { ReadonlyVec3, vec2 } from "gl-matrix";
import { mergeFuns } from ".";

export const createFPSCamera = (
  mount: HTMLElement,
  onChange: (viewMat: mat4, eys: vec3, front: vec3) => void,
  worldUp = vec3.fromValues(0, 1, 0),
  initEys: ReadonlyVec3 = vec3.fromValues(0, 0, 3),
  initView: { pitch?: number; yaw?: number } = {},
  maxDis = Infinity
) => {
  const up = vec3.fromValues(0, 1, 0);
  const eys = vec3.fromValues(initEys[0], initEys[1], initEys[2]);
  // 向量，前沿方向
  const front = vec3.fromValues(0, 0, -1);
  // 俯仰视角
  let pitch = initView.pitch || 0;
  // 偏航角
  let yaw = initView.yaw || -Math.PI / 2;

  const cameraMat = mat4.identity(mat4.create());
  const updateCameraMat = () => {
    const target = vec3.add(vec3.create(), eys, front);
    mat4.lookAt(cameraMat, eys, target, up);
    onChange(cameraMat, eys, front);
  };
  const updateFront = () => {
    front[0] = Math.cos(pitch) * Math.cos(yaw);
    front[1] = Math.sin(pitch);
    front[2] = Math.cos(pitch) * Math.sin(yaw);
    // console.log(pitch, yaw);
    vec3.normalize(front, front);
    vec3.cross(up, vec3.cross(vec3.create(), front, worldUp), front);
  };

  const start = vec2.create();
  const mousedownHandler = (ev: MouseEvent) => {
    start[0] = ev.offsetX;
    start[1] = ev.offsetY;

    mount.addEventListener("mousemove", mouseMoveHandler);
    mount.addEventListener("mouseup", mouseUpHandler);
  };

  const rotatePixelAmount = 1500;
  const mouseMoveHandler = (ev: MouseEvent) => {
    const end = vec2.fromValues(ev.offsetX, ev.offsetY);
    const move = vec2.sub(vec2.create(), end, start);
    pitch += (move[1] / rotatePixelAmount) * Math.PI;
    if (pitch > 89) {
      pitch = 89;
    } else if (pitch < -89) {
      pitch = -89;
    }
    yaw -= (move[0] / rotatePixelAmount) * Math.PI;
    start[0] = end[0];
    start[1] = end[1];
    updateFront();
    updateCameraMat();
  };

  const mouseUpHandler = () => {
    mount.removeEventListener("mousemove", mouseMoveHandler);
    mount.removeEventListener("moseup", mouseUpHandler);
  };
  const wheelHandler = (ev: WheelEvent) => {
    const amount = ev.deltaY * -0.01;
    const move = vec3.scale(vec3.create(), front, amount);
    const neys = vec3.create();
    vec3.add(neys, eys, move);
    if (vec3.length(neys) <= maxDis) {
      vec3.copy(eys, neys);
      updateCameraMat();
    }
  };

  mount.addEventListener("mousedown", mousedownHandler);
  document.addEventListener("wheel", wheelHandler);

  setTimeout(() => {
    updateFront();
    updateCameraMat();
  });

  return {
    recovery() {
      vec3.copy(eys, vec3.fromValues(initEys[0], initEys[1], initEys[2]));
      pitch = initView.pitch || 0;
      yaw = initView.yaw || -Math.PI / 2;
      updateFront();
      updateCameraMat();
    },
    destory: mergeFuns(mouseUpHandler, () =>
      document.removeEventListener("wheel", wheelHandler)
    ),
  };
};

export const useTex = (
  gl: WebGL2RenderingContext,
  tex: WebGLTexture,
  target: number = gl.TEXTURE_2D,
  offset = 0
) => {
  gl.activeTexture(gl.TEXTURE0 + offset);
  gl.bindTexture(target, tex);
  return offset;
};
