/**
 * Convert world coordinates to screen normalized (-1, 1)
 *
 * @param viewMatrix Camera world matrix
 * @param projectionMatrix Camera projection matrix
 * @param worldPoint World point
 */
export function worldToScreenNormalized(
  viewMatrix: Array<number>,
  projectionMatrix: Array<number>,
  worldPoint: [x: number, y: number, z: number]
): [x: number, y: number, z: number] {
  function applyMatrixToPoint3(matrix, point: [x: number, y: number, z: number]): [x: number, y: number, z: number] {
    const [x, y, z] = point;
    const e = matrix;
    const w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15]);

    return [
      (e[0] * x + e[4] * y + e[8] * z + e[12]) * w,
      (e[1] * x + e[5] * y + e[9] * z + e[13]) * w,
      (e[2] * x + e[6] * y + e[10] * z + e[14]) * w,
    ];
  }

  const [x, y, z] = applyMatrixToPoint3(projectionMatrix, applyMatrixToPoint3(invertMatrix(viewMatrix), worldPoint));

  return [x, y, z];
}

export function multiplyMatrices(a: Array<number>, b: Array<number>): Array<number> {
  const ae = a;
  const be = b;
  const te = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];

  const a11 = ae[0],
    a12 = ae[4],
    a13 = ae[8],
    a14 = ae[12];
  const a21 = ae[1],
    a22 = ae[5],
    a23 = ae[9],
    a24 = ae[13];
  const a31 = ae[2],
    a32 = ae[6],
    a33 = ae[10],
    a34 = ae[14];
  const a41 = ae[3],
    a42 = ae[7],
    a43 = ae[11],
    a44 = ae[15];

  const b11 = be[0],
    b12 = be[4],
    b13 = be[8],
    b14 = be[12];
  const b21 = be[1],
    b22 = be[5],
    b23 = be[9],
    b24 = be[13];
  const b31 = be[2],
    b32 = be[6],
    b33 = be[10],
    b34 = be[14];
  const b41 = be[3],
    b42 = be[7],
    b43 = be[11],
    b44 = be[15];

  te[0] = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41;
  te[4] = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42;
  te[8] = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43;
  te[12] = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44;

  te[1] = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41;
  te[5] = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42;
  te[9] = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43;
  te[13] = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44;

  te[2] = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41;
  te[6] = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42;
  te[10] = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43;
  te[14] = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44;

  te[3] = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41;
  te[7] = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42;
  te[11] = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43;
  te[15] = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44;

  return te;
}

/**
 * @param matrix
 * @returns Matrix
 */
export function invertMatrix(matrix: Array<number>): Array<number> {
  const te = matrix,
    n11 = te[0],
    n21 = te[1],
    n31 = te[2],
    n41 = te[3],
    n12 = te[4],
    n22 = te[5],
    n32 = te[6],
    n42 = te[7],
    n13 = te[8],
    n23 = te[9],
    n33 = te[10],
    n43 = te[11],
    n14 = te[12],
    n24 = te[13],
    n34 = te[14],
    n44 = te[15],
    t11 = n23 * n34 * n42 - n24 * n33 * n42 + n24 * n32 * n43 - n22 * n34 * n43 - n23 * n32 * n44 + n22 * n33 * n44,
    t12 = n14 * n33 * n42 - n13 * n34 * n42 - n14 * n32 * n43 + n12 * n34 * n43 + n13 * n32 * n44 - n12 * n33 * n44,
    t13 = n13 * n24 * n42 - n14 * n23 * n42 + n14 * n22 * n43 - n12 * n24 * n43 - n13 * n22 * n44 + n12 * n23 * n44,
    t14 = n14 * n23 * n32 - n13 * n24 * n32 - n14 * n22 * n33 + n12 * n24 * n33 + n13 * n22 * n34 - n12 * n23 * n34;

  const det = n11 * t11 + n21 * t12 + n31 * t13 + n41 * t14;

  if (det === 0) return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];

  const detInv = 1 / det;

  return [
    t11 * detInv,
    (n24 * n33 * n41 - n23 * n34 * n41 - n24 * n31 * n43 + n21 * n34 * n43 + n23 * n31 * n44 - n21 * n33 * n44) *
      detInv,
    (n22 * n34 * n41 - n24 * n32 * n41 + n24 * n31 * n42 - n21 * n34 * n42 - n22 * n31 * n44 + n21 * n32 * n44) *
      detInv,
    (n23 * n32 * n41 - n22 * n33 * n41 - n23 * n31 * n42 + n21 * n33 * n42 + n22 * n31 * n43 - n21 * n32 * n43) *
      detInv,

    t12 * detInv,
    (n13 * n34 * n41 - n14 * n33 * n41 + n14 * n31 * n43 - n11 * n34 * n43 - n13 * n31 * n44 + n11 * n33 * n44) *
      detInv,
    (n14 * n32 * n41 - n12 * n34 * n41 - n14 * n31 * n42 + n11 * n34 * n42 + n12 * n31 * n44 - n11 * n32 * n44) *
      detInv,
    (n12 * n33 * n41 - n13 * n32 * n41 + n13 * n31 * n42 - n11 * n33 * n42 - n12 * n31 * n43 + n11 * n32 * n43) *
      detInv,

    t13 * detInv,
    (n14 * n23 * n41 - n13 * n24 * n41 - n14 * n21 * n43 + n11 * n24 * n43 + n13 * n21 * n44 - n11 * n23 * n44) *
      detInv,
    (n12 * n24 * n41 - n14 * n22 * n41 + n14 * n21 * n42 - n11 * n24 * n42 - n12 * n21 * n44 + n11 * n22 * n44) *
      detInv,
    (n13 * n22 * n41 - n12 * n23 * n41 - n13 * n21 * n42 + n11 * n23 * n42 + n12 * n21 * n43 - n11 * n22 * n43) *
      detInv,

    t14 * detInv,
    (n13 * n24 * n31 - n14 * n23 * n31 + n14 * n21 * n33 - n11 * n24 * n33 - n13 * n21 * n34 + n11 * n23 * n34) *
      detInv,
    (n14 * n22 * n31 - n12 * n24 * n31 - n14 * n21 * n32 + n11 * n24 * n32 + n12 * n21 * n34 - n11 * n22 * n34) *
      detInv,
    (n12 * n23 * n31 - n13 * n22 * n31 + n13 * n21 * n32 - n11 * n23 * n32 - n12 * n21 * n33 + n11 * n22 * n33) *
      detInv,
  ];
}

/**
 * Convert world coordinates to screen (screenWidth, screenHeight)
 *
 * @param viewMatrix Camera world matrix
 * @param projectionMatrix Camera projection matrix
 * @param worldPoint World point
 * @param screenWidth Screen width
 * @param screenHeight Screen height
 */
export function worldToScreen(
  viewMatrix: Array<number>,
  projectionMatrix: Array<number>,
  worldPoint: [x: number, y: number, z: number],
  screenWidth: number,
  screenHeight: number
): number[] {
  const widthHalf = screenWidth / 2;
  const heightHalf = screenHeight / 2;

  const [x, y] = worldToScreenNormalized(viewMatrix, projectionMatrix, worldPoint);
  const p = [x * widthHalf + widthHalf, -(y * heightHalf) + heightHalf];
  return p;
}

/**
 * @param a
 * @returns
 */
function lengthVector(a: { x: number; y: number; z: number }): number {
  return Math.sqrt(a.x * a.x + a.y * a.y + a.z * a.z);
}

function normalizeVector(a: { x: number; y: number; z: number }): { x: number; y: number; z: number } {
  const len = lengthVector(a);
  if (len === 0) return a;
  return {
    x: a.x / len,
    y: a.y / len,
    z: a.z / len,
  };
}

function crossVectors(a, b) {
  const ax = a.x,
    ay = a.y,
    az = a.z;
  const bx = b.x,
    by = b.y,
    bz = b.z;

  return {
    x: ay * bz - az * by,
    y: az * bx - ax * bz,
    z: ax * by - ay * bx,
  };
}

function normalizedProjection(
  width,
  height,
  nearClipPlaneDist,
  farClipPlaneDist,
  normalizedCenter,
  normalizedWidth,
  normalizedHeight,
  normalizedDepth
) {
  const mtx = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];

  mtx[0] = -normalizedWidth / width;
  mtx[1] = 0;
  mtx[2] = 0.0;
  mtx[3] = normalizedCenter.x;

  mtx[4] = 0.0;
  mtx[5] = -normalizedHeight / height;
  mtx[6] = 0.0;
  mtx[7] = normalizedCenter.y;

  mtx[8] = 0;
  mtx[9] = 0.0;
  mtx[10] = normalizedDepth / (farClipPlaneDist - nearClipPlaneDist);
  mtx[11] = -mtx[10] * nearClipPlaneDist;

  mtx[12] = mtx[13] = mtx[14] = 0.0;
  mtx[15] = 1.0;

  return mtx;
}

export function createOrthoProjectionMatrix(camera, width, height) {
  const nearClipPlaneDist = 0.1;
  const farClipPlaneDist = 1000;
  const fieldWidth = camera.field_width;
  const fieldHeight = camera.field_height;
  let viewportNormalizedWidth = 2;
  let viewportNormalizedHeight = 2;

  if (width > height) {
    const aspect = height / width;
    const aspectFiled = fieldHeight / fieldWidth;
    viewportNormalizedWidth = (2 / aspectFiled) * aspect;
    viewportNormalizedHeight = 2;
  } else {
    const aspect = width / height;
    const aspectFiled = fieldWidth / fieldHeight;
    viewportNormalizedWidth = 2;
    viewportNormalizedHeight = (2 / aspectFiled) * aspect;
  }

  const viewportNormalizedCenter = {
    x: 0,
    y: 0,
  };

  const projectionMatrix = normalizedProjection(
    fieldWidth,
    fieldHeight,
    nearClipPlaneDist,
    farClipPlaneDist,
    viewportNormalizedCenter,
    viewportNormalizedWidth,
    viewportNormalizedHeight,
    1.0
  );

  return projectionMatrix;
}

export function createViewMatrix(camera) {
  const position = camera.view_point;

  const viewMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, position.x, position.y, position.z, 1];

  const target = addVectors(camera.direction, position);

  matrixLookAt(viewMatrix, position, target, camera.up_vector);

  return viewMatrix;
}

export function bcfWorldToScreenFromCamera(
  camera,
  canvas: HTMLCanvasElement,
  point: [x: number, y: number, z: number]
) {
  const { width, height } = canvas;

  if (lengthVector(camera.direction) >= 1.0001) {
    camera = {
      ...camera,
      direction: normalizeVector(subVectors(camera.direction, camera.view_point)),
    };
  }

  const projectionMatrix = createOrthoProjectionMatrix(camera, width, height);
  const viewMatrix = createViewMatrix(camera);

  return worldToScreen(viewMatrix, projectionMatrix, point, width, height);
}

function subVectors(
  a: { x: number; y: number; z: number },
  b: { x: number; y: number; z: number }
): { x: number; y: number; z: number } {
  return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
}

function addVectors(
  a: { x: number; y: number; z: number },
  b: { x: number; y: number; z: number }
): { x: number; y: number; z: number } {
  return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
}

export function matrixLookAt(matrix, eye, target, up) {
  let z = subVectors(eye, target);

  if (lengthVector(z) === 0) {
    z.z = 1;
  }
  z = normalizeVector(z);

  let x = crossVectors(z, up);
  if (lengthVector(x) === 0) {
    if (Math.abs(up.z) === 1) {
      z.x += 0.0001;
    } else {
      z.z += 0.0001;
    }

    z = normalizeVector(z);

    x = crossVectors(up, z);
  }

  x = normalizeVector(x);

  const y = crossVectors(z, x);

  const m = matrix;

  m[0] = x.x;
  m[4] = y.x;
  m[8] = z.x;
  m[1] = x.y;
  m[5] = y.y;
  m[9] = z.y;
  m[2] = x.z;
  m[6] = y.z;
  m[10] = z.z;

  return m;
}
