import { Matrix } from './matrix';

/**
 * Utility class for generating 3-D transformations. All trnasformations assume
 * a left-handed coordinate system.
 */
export class Transform3D {
  static readonly identity = Matrix.identity(4);
  static readonly right = new Matrix([1, 0, 0]);
  static readonly up = new Matrix([0, 1, 0]);
  static readonly in = new Matrix([0, 0, 1]);

  static translate(x: number, y: number, z: number) {
    const result = Transform3D.identity.copy();
    result.set(3, 0, x);
    result.set(3, 1, y);
    result.set(3, 2, z);
    return result;
  }

  static rotateX(angle: number) {
    const result = Transform3D.identity.copy();
    const radians = (angle * Math.PI) / 180;
    const cos = Math.cos(radians);
    const sin = Math.sin(radians);
    result.set(1, 1, cos);
    result.set(1, 2, sin);
    result.set(2, 1, -sin);
    result.set(2, 2, cos);
    return result;
  }

  static rotateY(angle: number) {
    const result = Transform3D.identity.copy();
    const radians = (angle * Math.PI) / 180;
    const cos = Math.cos(radians);
    const sin = Math.sin(radians);
    result.set(0, 0, cos);
    result.set(0, 2, -sin);
    result.set(2, 0, sin);
    result.set(2, 2, cos);
    return result;
  }

  static rotateZ(angle: number) {
    const result = Transform3D.identity.copy();
    const radians = (angle * Math.PI) / 180;
    const cos = Math.cos(radians);
    const sin = Math.sin(radians);
    result.set(0, 0, cos);
    result.set(0, 1, sin);
    result.set(1, 0, -sin);
    result.set(1, 1, cos);
    return result;
  }

  static scale(factor: number) {
    const result = Transform3D.identity.copy();
    result.set(0, 0, factor);
    result.set(1, 1, factor);
    result.set(2, 2, factor);
    return result;
  }

  static lookAt(eye: Matrix, at: Matrix) {
    const zUnnormed = Matrix.subtract(at, eye);
    const zNonZero = zUnnormed.magnitude() !== 0;
    const z = zNonZero ? zUnnormed.normalize() : Transform3D.in;
    const xUnnormed = Matrix.cross(Transform3D.up, z);
    const xNonZero = xUnnormed.magnitude() !== 0;
    const x = xNonZero ? xUnnormed.normalize() : Transform3D.right;
    const y = Matrix.cross(z, x);
    const result = Matrix.zerosLike(Transform3D.identity);
    result.set(0, 0, x.at(0));
    result.set(0, 1, y.at(0));
    result.set(0, 2, z.at(0));
    result.set(1, 0, x.at(1));
    result.set(1, 1, y.at(1));
    result.set(1, 2, z.at(1));
    result.set(2, 0, x.at(2));
    result.set(2, 1, y.at(2));
    result.set(2, 2, z.at(2));
    result.set(3, 0, -Matrix.dot(x, eye));
    result.set(3, 1, -Matrix.dot(y, eye));
    result.set(3, 2, -Matrix.dot(z, eye));
    result.set(3, 3, 1);
    return result;
  }

  static inverseLookAt(lookAt: Matrix) {
    const x = lookAt.slice(0, 1, 0, 3);
    const y = lookAt.slice(1, 2, 0, 3);
    const z = lookAt.slice(2, 3, 0, 3);
    const eye = lookAt.slice(3, 4, 0, 3);
    const result = Matrix.zerosLike(Transform3D.identity);
    result.set(0, 0, x.at(0));
    result.set(0, 1, y.at(0));
    result.set(0, 2, z.at(0));
    result.set(1, 0, x.at(1));
    result.set(1, 1, y.at(1));
    result.set(1, 2, z.at(1));
    result.set(2, 0, x.at(2));
    result.set(2, 1, y.at(2));
    result.set(2, 2, z.at(2));
    result.set(3, 0, -Matrix.dot(x, eye));
    result.set(3, 1, -Matrix.dot(y, eye));
    result.set(3, 2, -Matrix.dot(z, eye));
    result.set(3, 3, 1);
    return result;
  }

  static perspective(near: number, far: number, vfov = 90, aspect = 1) {
    const yScale = 1 / Math.tan((vfov * Math.PI) / 360);
    const xScale = yScale / aspect;
    const zScale = far / (far - near);
    const zTranslation = -near * zScale;
    const result = Matrix.zerosLike(Transform3D.identity);
    result.set(0, 0, xScale);
    result.set(1, 1, yScale);
    result.set(2, 2, zScale);
    result.set(3, 2, zTranslation);
    result.set(2, 3, 1);
    return result;
  }

  static viewport(width: number, height: number) {
    const xScale = width / 2;
    const xTranslation = width / 2;
    const yScale = -height / 2;
    const yTranslation = height / 2;
    const result = Matrix.zerosLike(Transform3D.identity);
    result.set(0, 0, xScale);
    result.set(1, 1, yScale);
    result.set(2, 2, 1);
    result.set(3, 3, 1);
    result.set(3, 0, xTranslation);
    result.set(3, 1, yTranslation);
    return result;
  }
}
