import { mat4, vec3, vec4 } from 'gl-matrix'

type Geometry = {
  vertexBuffer: WebGLBuffer
  indexBuffer: WebGLBuffer
  indexCount: number
  vao: WebGLVertexArrayObject | null
}

/**
 * Object rendered with WebGL
 **/
export class NiivueObject3D {
  static BLEND = 1
  static CULL_FACE = 2
  static CULL_FRONT = 4
  static CULL_BACK = 8
  static ENABLE_DEPTH_TEST = 16

  sphereIdx: number[] = []
  sphereVtx: number[] = []
  renderShaders: number[] = []
  isVisible = true
  isPickable = true
  vertexBuffer: WebGLVertexArrayObject
  indexCount: number
  indexBuffer: WebGLVertexArrayObject | null
  vao: WebGLVertexArrayObject | null
  mode: number
  glFlags = 0
  id: number
  colorId: [number, number, number, number]

  modelMatrix = mat4.create()
  scale = [1, 1, 1]
  position = [0, 0, 0]
  rotation = [0, 0, 0]
  rotationRadians = 0.0

  extentsMin: number[] = []
  extentsMax: number[] = []

  // TODO needed through NVImage
  furthestVertexFromOrigin?: number
  originNegate?: vec3
  fieldOfViewDeObliqueMM?: vec3

  // TODO needed through crosshairs in NiiVue
  mm?: vec4

  constructor(
    id: number,
    vertexBuffer: WebGLBuffer,
    mode: number,
    indexCount: number,
    indexBuffer: WebGLVertexArrayObject | null = null,
    vao: WebGLVertexArrayObject | null = null
  ) {
    this.vertexBuffer = vertexBuffer
    this.indexCount = indexCount
    this.indexBuffer = indexBuffer
    this.vao = vao
    this.mode = mode

    this.id = id
    this.colorId = [
      ((id >> 0) & 0xff) / 255.0,
      ((id >> 8) & 0xff) / 255.0,
      ((id >> 16) & 0xff) / 255.0,
      ((id >> 24) & 0xff) / 255.0
    ]
  }

  static generateCrosshairs = function (
    gl: WebGL2RenderingContext,
    id: number,
    xyzMM: vec4,
    xyzMin: vec3,
    xyzMax: vec3,
    radius: number,
    sides = 20,
    gap = 0
  ): NiivueObject3D {
    const geometry = NiivueObject3D.generateCrosshairsGeometry(gl, xyzMM, xyzMin, xyzMax, radius, sides, gap)
    return new NiivueObject3D(
      id,
      geometry.vertexBuffer,
      gl.TRIANGLES,
      geometry.indexCount,
      geometry.indexBuffer,
      geometry.vao
    )
  }

  // not included in public docs
  static generateCrosshairsGeometry = function (
    gl: WebGL2RenderingContext,
    xyzMM: vec4,
    xyzMin: vec3,
    xyzMax: vec3,
    radius: number,
    sides = 20,
    gap = 0
  ): Geometry {
    const vertices: number[] = []
    const indices: number[] = []
    const gapX = radius * gap
    if (gapX <= 0) {
      // left-right
      let start = vec3.fromValues(xyzMin[0], xyzMM[1], xyzMM[2])
      let dest = vec3.fromValues(xyzMax[0], xyzMM[1], xyzMM[2])
      NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides)
      // anterior-posterior
      start = vec3.fromValues(xyzMM[0], xyzMin[1], xyzMM[2])
      dest = vec3.fromValues(xyzMM[0], xyzMax[1], xyzMM[2])
      NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides)
      // superior-inferior
      start = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMin[2])
      dest = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMax[2])
      NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides)
    } else {
      // left-right
      let start = vec3.fromValues(xyzMin[0], xyzMM[1], xyzMM[2])
      let dest = vec3.fromValues(xyzMM[0] - gapX, xyzMM[1], xyzMM[2])
      NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)
      start = vec3.fromValues(xyzMM[0] + gapX, xyzMM[1], xyzMM[2])
      dest = vec3.fromValues(xyzMax[0], xyzMM[1], xyzMM[2])
      NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)
      // anterior-posterior
      start = vec3.fromValues(xyzMM[0], xyzMin[1], xyzMM[2])
      dest = vec3.fromValues(xyzMM[0], xyzMM[1] - gapX, xyzMM[2])
      NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)
      start = vec3.fromValues(xyzMM[0], xyzMM[1] + gapX, xyzMM[2])
      dest = vec3.fromValues(xyzMM[0], xyzMax[1], xyzMM[2])
      NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)
      // superior-inferior
      start = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMin[2])
      dest = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMM[2] - gapX)
      NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)
      start = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMM[2] + gapX)
      dest = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMax[2])
      NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false)
    }
    // console.log('i:',indices.length / 3, 'v:',vertices.length / 3);
    const vertexBuffer = gl.createBuffer()
    if (vertexBuffer === null) {
      throw new Error('could not instantiate vertex buffer')
    }

    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW)

    // index buffer allocated in parent class
    const indexBuffer = gl.createBuffer()
    if (indexBuffer === null) {
      throw new Error('could not instantiate index buffer')
    }
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW)

    const vao = gl.createVertexArray()
    gl.bindVertexArray(vao)
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
    // vertex position: 3 floats X,Y,Z
    gl.enableVertexAttribArray(0)
    gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0)
    gl.bindVertexArray(null) // https://stackoverflow.com/questions/43904396/are-we-not-allowed-to-bind-gl-array-buffer-and-vertex-attrib-array-to-0-in-webgl

    return {
      vertexBuffer,
      indexBuffer,
      indexCount: indices.length,
      vao
    }
  }

  static getFirstPerpVector = function (v1: vec3): vec3 {
    const v2 = vec3.fromValues(0.0, 0.0, 0.0)
    if (v1[0] === 0.0) {
      v2[0] = 1.0
    } else if (v1[1] === 0.0) {
      v2[1] = 1.0
    } else if (v1[2] === 0.0) {
      v2[2] = 1.0
    } else {
      // If xyz is all set, we set the z coordinate as first and second argument .
      // As the scalar product must be zero, we add the negated sum of x and y as third argument
      v2[0] = v1[2] // scalp = z*x
      v2[1] = v1[2] // scalp = z*(x+y)
      v2[2] = -(v1[0] + v1[1]) // scalp = z*(x+y)-z*(x+y) = 0
      vec3.normalize(v2, v2)
    }
    return v2
  }

  static subdivide = function (verts: number[], faces: number[]): void {
    // Subdivide each triangle into four triangles, pushing verts to the unit sphere"""
    let nv = verts.length / 3
    let nf = faces.length / 3
    const n = nf
    const vNew = vec3.create()
    const nNew = vec3.create()
    for (let faceIndex = 0; faceIndex < n; faceIndex++) {
      // setlength(verts, nv + 3);
      const fx = faces[faceIndex * 3 + 0]
      const fy = faces[faceIndex * 3 + 1]
      const fz = faces[faceIndex * 3 + 2]
      const vx = vec3.fromValues(verts[fx * 3 + 0], verts[fx * 3 + 1], verts[fx * 3 + 2])
      const vy = vec3.fromValues(verts[fy * 3 + 0], verts[fy * 3 + 1], verts[fy * 3 + 2])
      const vz = vec3.fromValues(verts[fz * 3 + 0], verts[fz * 3 + 1], verts[fz * 3 + 2])
      vec3.add(vNew, vx, vy)
      vec3.normalize(nNew, vNew)
      verts.push(...nNew)

      vec3.add(vNew, vy, vz)
      vec3.normalize(nNew, vNew)
      verts.push(...nNew)

      vec3.add(vNew, vx, vz)
      vec3.normalize(nNew, vNew)
      verts.push(...nNew)
      // Split the current triangle into four smaller triangles:
      let face = [nv, nv + 1, nv + 2]
      faces.push(...face)
      face = [fx, nv, nv + 2]
      faces.push(...face)
      face = [nv, fy, nv + 1]
      faces.push(...face)
      faces[faceIndex * 3 + 0] = nv + 2
      faces[faceIndex * 3 + 1] = nv + 1
      faces[faceIndex * 3 + 2] = fz
      nf = nf + 3
      nv = nv + 3
    }
  }

  static weldVertices = function (verts: number[], faces: number[]): number[] {
    // unify identical vertices
    const nv = verts.length / 3
    // yikes: bubble sort! TO DO: see Surfice for more efficient solution
    let nUnique = 0 // first vertex is unique
    // var remap = new Array();
    const remap = new Int32Array(nv)
    for (let i = 0; i < nv - 1; i++) {
      if (remap[i] !== 0) {
        continue
      } // previously tested
      remap[i] = nUnique
      let v = i * 3
      const x = verts[v]
      const y = verts[v + 1]
      const z = verts[v + 2]
      for (let j = i + 1; j < nv; j++) {
        v += 3
        if (x === verts[v] && y === verts[v + 1] && z === verts[v + 2]) {
          remap[j] = nUnique
        }
      }
      nUnique++ // another new vertex
    } // for i
    if (nUnique === nv) {
      return verts
    }
    // console.log('welding vertices removed redundant positions ', nv, '->', nUnique);
    const nf = faces.length
    for (let f = 0; f < nf; f++) {
      faces[f] = remap[faces[f]]
    }
    const vtx = verts.slice(0, nUnique * 3 - 1)
    for (let i = 0; i < nv - 1; i++) {
      const v = i * 3
      const r = remap[i] * 3
      vtx[r] = verts[v]
      vtx[r + 1] = verts[v + 1]
      vtx[r + 2] = verts[v + 2]
    }
    return vtx
  }

  static makeSphere = function (
    vertices: number[],
    indices: number[],
    radius: number,
    origin: vec3 | vec4 = [0, 0, 0]
  ): void {
    let vtx = [
      0.0, 0.0, 1.0, 0.894, 0.0, 0.447, 0.276, 0.851, 0.447, -0.724, 0.526, 0.447, -0.724, -0.526, 0.447, 0.276, -0.851,
      0.447, 0.724, 0.526, -0.447, -0.276, 0.851, -0.447, -0.894, 0.0, -0.447, -0.276, -0.851, -0.447, 0.724, -0.526,
      -0.447, 0.0, 0.0, -1.0
    ]
    // let idx = new Uint16Array([
    const idx = [
      0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 5, 0, 5, 1, 7, 6, 11, 8, 7, 11, 9, 8, 11, 10, 9, 11, 6, 10, 11, 6, 2, 1, 7, 3, 2,
      8, 4, 3, 9, 5, 4, 10, 1, 5, 6, 7, 2, 7, 8, 3, 8, 9, 4, 9, 10, 5, 10, 6, 1
    ]
    NiivueObject3D.subdivide(vtx, idx)
    NiivueObject3D.subdivide(vtx, idx)
    vtx = NiivueObject3D.weldVertices(vtx, idx)

    for (let i = 0; i < vtx.length; i++) {
      vtx[i] = vtx[i] * radius
    }
    const nvtx = vtx.length / 3
    let j = 0
    for (let i = 0; i < nvtx; i++) {
      vtx[j] = vtx[j] + origin[0]
      j++
      vtx[j] = vtx[j] + origin[1]
      j++
      vtx[j] = vtx[j] + origin[2]
      j++
    }
    const idx0 = Math.floor(vertices.length / 3) // first new vertex will be AFTER previous vertices
    for (let i = 0; i < idx.length; i++) {
      idx[i] = idx[i] + idx0
    }

    indices.push(...idx)
    vertices.push(...vtx)
  }

  static makeCylinder = function (
    vertices: number[],
    indices: number[],
    start: vec3,
    dest: vec3,
    radius: number,
    sides = 20,
    endcaps = true
  ): void {
    if (sides < 3) {
      sides = 3
    } // prism is minimal 3D cylinder
    const v1 = vec3.create()
    vec3.subtract(v1, dest, start)
    vec3.normalize(v1, v1) // principle axis of cylinder
    const v2 = NiivueObject3D.getFirstPerpVector(v1) // a unit length vector orthogonal to v1
    // Get the second perp vector by cross product
    const v3 = vec3.create()
    vec3.cross(v3, v1, v2) // a unit length vector orthogonal to v1 and v2
    vec3.normalize(v3, v3)
    let num_v = 2 * sides
    let num_f = 2 * sides
    if (endcaps) {
      num_f += 2 * sides
      num_v += 2
    }
    const idx0 = Math.floor(vertices.length / 3) // first new vertex will be AFTER previous vertices
    const idx = new Uint32Array(num_f * 3)
    const vtx = new Float32Array(num_v * 3)
    function setV(i: number, vec3: vec3): void {
      vtx[i * 3 + 0] = vec3[0]
      vtx[i * 3 + 1] = vec3[1]
      vtx[i * 3 + 2] = vec3[2]
    }
    function setI(i: number, a: number, b: number, c: number): void {
      idx[i * 3 + 0] = a + idx0
      idx[i * 3 + 1] = b + idx0
      idx[i * 3 + 2] = c + idx0
    }
    const startPole = 2 * sides
    const destPole = startPole + 1
    if (endcaps) {
      setV(startPole, start)
      setV(destPole, dest)
    }
    const pt1 = vec3.create()
    const pt2 = vec3.create()
    for (let i = 0; i < sides; i++) {
      const c = Math.cos((i / sides) * 2 * Math.PI)
      const s = Math.sin((i / sides) * 2 * Math.PI)
      pt1[0] = radius * (c * v2[0] + s * v3[0])
      pt1[1] = radius * (c * v2[1] + s * v3[1])
      pt1[2] = radius * (c * v2[2] + s * v3[2])
      vec3.add(pt2, start, pt1)
      setV(i, pt2)
      vec3.add(pt2, dest, pt1)
      setV(i + sides, pt2)
      let nxt = 0
      if (i < sides - 1) {
        nxt = i + 1
      }
      setI(i * 2, i, nxt, i + sides)
      setI(i * 2 + 1, nxt, nxt + sides, i + sides)
      if (endcaps) {
        setI(sides * 2 + i, i, startPole, nxt)
        setI(sides * 2 + i + sides, destPole, i + sides, nxt + sides)
      }
    }
    indices.push(...idx)
    vertices.push(...vtx)
  }

  static makeColoredCylinder = function (
    vertices: number[],
    indices: number[],
    colors: number[],
    start: vec3,
    dest: vec3,
    radius: number,
    rgba255 = [192, 0, 0, 255],
    sides = 20,
    endcaps = false
  ): void {
    let nv = vertices.length / 3
    NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, endcaps)
    nv = vertices.length / 3 - nv
    const clrs = []
    for (let i = 0; i < nv * 4 - 1; i += 4) {
      clrs[i] = rgba255[0]
      clrs[i + 1] = rgba255[1]
      clrs[i + 2] = rgba255[2]
      clrs[i + 3] = rgba255[3]
    }
    colors.push(...clrs)
  }

  static makeColoredSphere = function (
    vertices: number[],
    indices: number[],
    colors: number[],
    radius: number,
    origin: vec3 | vec4 = [0, 0, 0],
    rgba255 = [0, 0, 192, 255]
  ): void {
    let nv = vertices.length / 3
    NiivueObject3D.makeSphere(vertices, indices, radius, origin)
    nv = vertices.length / 3 - nv
    const clrs = []
    for (let i = 0; i < nv * 4 - 1; i += 4) {
      clrs[i] = rgba255[0]
      clrs[i + 1] = rgba255[1]
      clrs[i + 2] = rgba255[2]
      clrs[i + 3] = rgba255[3]
    }
    colors.push(...clrs)
  }
}
