import {meter, kilogram, second, unitless, newton, coulomb} from './units';

// Change the lables used for the basis to i, j, k.
// These lables were borrowed from Hamilton's quaternions.
UNITS.G3.BASIS_LABELS = UNITS.G3.BASIS_LABELS_HAMILTON

/////////////////////////////////////////////////////////////////////////////
// Lighting

/**
 * Ambient Lighting for the World.
 */
const ambLight = new EIGHT.AmbientLight(EIGHT.Color.white.scale(0.4))

/**
 * Directional Lighting for the World.
 */
const dirLight = new EIGHT.DirectionalLight()

/////////////////////////////////////////////////////////////////////////////
// Standard Colors

/**
 * The standard colors.
 */
export const color = {
  red: EIGHT.Color.red,
  green: EIGHT.Color.green,
  blue: EIGHT.Color.blue,
  yellow: EIGHT.Color.yellow,
  magenta: EIGHT.Color.magenta,
  cyan: EIGHT.Color.cyan,
  orange: EIGHT.Color.fromRGB(1, 102 / 255, 0),
  black: EIGHT.Color.black,
  white: EIGHT.Color.white
}

/////////////////////////////////////////////////////////////////////////////
// Physical Constants

/**
 * 
 */
export const ε0 = 8.854E-12 * (coulomb * coulomb) / (meter * meter * newton)

/////////////////////////////////////////////////////////////////////////////
// Validation

/**
 * Determines whether a multivector is admissable as a vector.
 */
function isVector(mv: EIGHT.GeometricE3): boolean {
  if (mv.a !== 0 || mv.b !== 0 || mv.yz !== 0 || mv.zx !== 0 || mv.xy !== 0) {
    return false;
  }
  else {
    return true;
  }
} 

/**
 * Determines whether a multivector is admissable as a scalar.
 */
function isScalar(mv: EIGHT.GeometricE3): boolean {
  if (mv.x !== 0 || mv.y !== 0 || mv.z !== 0 || mv.yz !== 0 || mv.zx !== 0 || mv.xy !== 0 || mv.b !== 0) {
    return false;
  }
  else {
    return true;
  }
}

/**
 * A camera is a frame of reference from which the scene is viewed.
 */
export interface Camera {
  /**
   * The position of the camera, a position vector, measured in meters.
   */
  eye: EIGHT.Vector3;

  /**
   * The point that the camera is looking at, a position vector, measured in meters.
   */
  look: UNITS.G3;
  /**
   * The desired up direction, a dimensionless vector.
   */
  up: UNITS.G3;
}

/**
 * A ready-to-go composite for EIGHT animations.
 */
export class World {
  /**
   * The scale factor for converting world units to dimensionless units.
   */
  public scaleFactor: UNITS.G3 = meter;
  /**
   * The frame of reference from which the world is viewed.
   */
  public camera: Camera;
  public engine:EIGHT.Engine;
  public scene: EIGHT.Scene;
  public ambients: EIGHT.Facet[] = [];
  private trackball:EIGHT.TrackballControls;
  private dimlessCamera: EIGHT.PerspectiveCamera;
  public framecounter: number = 0;
  public overlay: EIGHT.Diagram3D;
  constructor() {
    // Notice that the canvas is "burned in".
    this.engine = new EIGHT.Engine('canvas3D')
      .clearColor(0.2, 0.2, 0.2, 1.0)
      .enable(EIGHT.Capability.DEPTH_TEST)
//      .enable(EIGHT.Capability.BLEND)
//      .blendFunc(EIGHT.BlendingFactorSrc.SRC_ALPHA, EIGHT.BlendingFactorDest.ONE);

    this.scene = new EIGHT.Scene(this.engine)

    this.dimlessCamera = new EIGHT.PerspectiveCamera()
    this.dimlessCamera.eye.x = 0
    this.dimlessCamera.eye.y = 0
    this.dimlessCamera.eye.z = 3
    this.ambients.push(this.dimlessCamera)
    this.camera = worldCamera(this, this.dimlessCamera)

    this.ambients.push(ambLight)
    this.ambients.push(dirLight)
    
    this.trackball = new EIGHT.TrackballControls(this.dimlessCamera, window)
    // Workaround because Trackball no longer supports context menu for panning.
    this.trackball.noPan = true
    this.trackball.subscribe(this.engine.canvas)

    this.overlay = new EIGHT.Diagram3D('canvas2D', this.dimlessCamera)    

    windowResize(this.engine, this.overlay, this.dimlessCamera).resize()    
  }

  /**
   * The underlying HTML5 Canvas.
   */
  get canvas(): HTMLCanvasElement {
    return this.engine.canvas;
  }

  /**
   * The origin is fixed to be zero.
   */
  get origin(): EIGHT.Vector3 {
    return EIGHT.Vector3.e3().scale(0)
    // return 0 * this.scaleFactor
  }

  /**
   * Adds a drawable object to the world.
   */
  add(drawable: EIGHT.Renderable): void {
    if (drawable){
      this.scene.add(drawable)
    }
    else {
      // Throw Error 
    }
  }

  /**
   * Clears the WebGL canvas and keeps the directional light pointing in the camera direction.
   */ 
  clear(): void {
    this.engine.clear()
    this.overlay.clear()

    this.trackball.update()

    dirLight.direction.copy(this.dimlessCamera.look).sub(this.dimlessCamera.eye)
  }

  /**
   * Draws the objects that have been added to the world.
   */
  draw(): void {
    this.scene.draw(this.ambients)
  }
  
  drawText(text: string, X: EIGHT.Vector3): void {
    const where = {x: 0, y: 0, z: 0}
    // scale(text, X, this.scaleFactor, where)
    this.overlay.fillText(text, X)  
  }
}

/**
 * Divides the measure by the scaleFactor to produce a dimensionless quantity.
 */
function scale(name: string, measure: UNITS.G3, scaleFactor: UNITS.G3, out: EIGHT.VectorE3): void {
  if (!isScalar(scaleFactor)) {
    throw new Error(`scaleFactor must be a scalar. scale(${name}, ${measure}, ${scaleFactor})`)
  }
  // We are expecting the result of scaling to produce a dimensionless quantity.
  const dimless = measure / scaleFactor;
  const uom = dimless.uom
  if (!uom || uom.isOne()) {
    out.x = dimless.x
    out.y = dimless.y
    out.z = dimless.z
    // return EIGHT.Geometric3.copy(dimless)
  }
  else {
    throw new Error(`Units of ${name}, ${scaleFactor}, is not consistent with units of quantity, ${measure}.`)
  }
}

function scaleToNumber(name: string, measure: UNITS.G3, scaleFactor: UNITS.G3): number {
  if (!isScalar(scaleFactor)) {
    throw new Error(`scaleFactor must be a scalar. scale(${name}, ${measure}, ${scaleFactor})`)
  }
  // We are expecting the result of scaling to produce a dimensionless quantity.
  const dimless = measure / scaleFactor;
  const uom = dimless.uom
  if (!uom || uom.isOne()) {
    return dimless.a
  }
  else {
    throw new Error(`Units of ${name}, ${scaleFactor}, is not consistent with units of quantity, ${measure}.`)
  }
}

//////////////////////////////////////////////////////////////////////////////

/**
 * A type that is useful for representing vectors.
 */
export class Arrow {
  private _scaleFactor: UNITS.G3 = meter;
  private _label: string;
  private inner: EIGHT.Arrow;
  constructor(private world: World) {
    this.inner = new EIGHT.Arrow()
    world.add(this.inner)
  }  
  get color() {
    return this.inner.color;
  }
  set color(color: EIGHT.Color) {
    this.inner.color = color;
  }
  get label() {
    return this._label;
  }
  set label(value: string) {
    if (typeof value === 'string') {
      this._label = value
    }
    else {
      throw new Error(`Arrow.label property must be a string.`);
    }
  }
  get scaleFactor() {
    return this._scaleFactor;
  }
  set scaleFactor(value: UNITS.G3) {
    if (isScalar(value)) {
      this._scaleFactor = value;
    }
    else {
      throw new Error(`Arrow.scaleFactor property must be a scalar.`);
    }
  }
  get model() {
    return EIGHT.Vector3.copy(this.inner.h)
    // return UNITS.G3.copy(this.inner.h).mul(this.scaleFactor)
  }
  set model(value: EIGHT.Vector3) {
    this.inner.h.copyVector(value)
    /*
    if (isVector(value)) {
      scale('axis', value, this.scaleFactor, this.inner.h)
    }
    else {
      throw new Error(`Arrow.axis property must be a vector.`);
    }
    */
  }
  get position() {
    return EIGHT.Vector3.copy(this.inner.X)
    // return UNITS.G3.copy(this.inner.X).mul(this.world.scaleFactor);
  }
  set position(value: EIGHT.Vector3) {
    this.inner.X.copyVector(value)
    /*
    if (isVector(value)) {
      scale('pos', value, this.world.scaleFactor, this.inner.X)
    }
    else {
      throw new Error(`Arrow.pos property must be a vector.`);
    }
    */
  }
  draw() {
    this.inner.render(this.world.ambients)
    if (typeof this._label === 'string' && this._label.length > 0) {
        this.world.drawText(this._label, this.position + (this.model / 2))
//      this.world.drawText(this._label, this.position + (this.model / 2) * (this.world.scaleFactor / this.scaleFactor))
    }
  }
}

/**
 * Constructor function for an Arrow.
 */
export function createArrow(world: World, options: {scaleFactor?: UNITS.G3; color?: EIGHT.Color} = {}): Arrow {
  const that = new Arrow(world)
    if (options.scaleFactor) {
      if (options.scaleFactor instanceof UNITS.G3) {
        that.scaleFactor = options.scaleFactor;
      }
      else {
        throw new Error("pos option must have type UNITS.G3");
      }
    }
    if (options.color) {
      if (options.color instanceof EIGHT.Color) {
        that.color = options.color;
      }
      else {
        throw new Error("color property must have type EIGHT.Color");
      }
    }
  return that;
}

/**
 * 
 */
export class Box {
  public scaleFactor: UNITS.G3 = meter;
  private inner: EIGHT.Box;
  constructor(private world: World) {
    this.inner = new EIGHT.Box({k: 1})
    this.scaleFactor = world.scaleFactor
    world.add(this.inner)
  }  
  get color() {
    return this.inner.color;
  }
  set color(color: EIGHT.Color) {
    this.inner.color = color;
  }
  get width() {
    return UNITS.G3.scalar(this.inner.width, this.scaleFactor.uom)
  }
  set width(value: UNITS.G3) {
    if (isScalar(value)) {
      this.inner.width = scaleToNumber('width', value, this.scaleFactor)
    }
    else {
      throw new Error(`Box.width property must be a scalar.`);
    }
  }
  get height() {
    return UNITS.G3.scalar(this.inner.height, this.scaleFactor.uom)
  }
  set height(value: UNITS.G3) {
    if (isScalar(value)) {
      this.inner.height = scaleToNumber('height', value, this.scaleFactor)
    }
    else {
      throw new Error(`Box.height property must be a scalar.`);
    }
  }
  get depth() {
    return UNITS.G3.scalar(this.inner.depth, this.scaleFactor.uom)
  }
  set depth(value: UNITS.G3) {
    if (isScalar(value)) {
      this.inner.depth = scaleToNumber('depth', value, this.scaleFactor)
    }
    else {
      throw new Error(`Box.depth property must be a scalar.`);
    }
  }
  get pos() {
    return UNITS.G3.copy(this.inner.X).mul(this.world.scaleFactor);
  }
  set pos(value: UNITS.G3) {
    if (isVector(value)) {
      scale('X', value, this.world.scaleFactor, this.inner.X)
    }
    else {
      throw new Error(`Box.pos property must be a vector.`);
    }
  }
  get visible() {
    return this.inner.visible
  }
  set visible(value: boolean) {
    this.inner.visible = false
  }
  draw() {
    this.inner.render(this.world.ambients)
  }
}

/**
 * Constructor function for a Box.
 */
export function createBox(world: World, options: {pos?: UNITS.G3; color?: EIGHT.Color} = {}): Box {
  if (world) {
    const that = new Box(world);
    if (options.pos) {
      if (options.pos instanceof UNITS.G3) {
        that.pos = options.pos;
      }
      else {
        throw new Error("pos option must have type UNITS.G3");
      }
    }
    if (options.color) {
      if (options.color instanceof EIGHT.Color) {
        that.color = options.color;
      }
      else {
        throw new Error("color property must have type EIGHT.Color");
      }
    }
    return that;
  }
  else {
    throw new Error("World has not yet been initialized.")
  }
}

/**
 * TODO
 */
export class Cylinder {
  private inner: EIGHT.Cylinder;
  public scaleFactor: UNITS.G3 = meter;
  constructor(private world: World) {
    this.inner = new EIGHT.Cylinder();
    world.add(this.inner)
  }
  get color() {
    return this.inner.color;
  }
  set color(color: EIGHT.Color) {
    this.inner.color = color;
  }
  get length() {
    return UNITS.G3.copy(this.inner.length, void 0).mul(this.scaleFactor);
  }
  set length(length: UNITS.G3) {
    scale('length', length, this.scaleFactor, this.inner.length)
  }
  get radius() {
    return UNITS.G3.copy(this.inner.radius, void 0).mul(this.scaleFactor);
  }
  set radius(radius: UNITS.G3) {
    scale('radius', radius, this.scaleFactor, this.inner.radius)
  }
  get axis() {
    return UNITS.G3.copy(this.inner.axis, void 0)
  }
  set axis(axis: UNITS.G3) {
    scale('axis', axis, unitless, this.inner.axis)
  }
  get transparent() {
    return this.inner.transparent;
  }
  set transparent(transparent: boolean) {
    this.inner.transparent = transparent;
  }
  get X() {
    return UNITS.G3.copy(this.inner.X, void 0).mul(this.world.scaleFactor);
  }
  set X(X: UNITS.G3) {
    scale('X', X, this.world.scaleFactor, this.inner.X)
  }
}

/**
 *
 */
export class Grid {
  constructor(private world: World, private inner: EIGHT.Grid) {
    world.add(inner)
  }
  get color() {
    return this.inner.color;
  }
  set color(color: EIGHT.Color) {
    this.inner.color = color;
  }
  draw() {
    this.inner.render(this.world.ambients)
  }
}

export function createGridXY(world: World) {
  const that = new Grid(world, new EIGHT.GridXY())
  return that;
}

export function createGridZX(world: World) {
  const that = new Grid(world, new EIGHT.GridZX())
  return that;
}

/**
 * 
 */
export class Sphere {
  public scaleFactor: UNITS.G3 = meter;
  public trail: Curve;
  private inner: EIGHT.Sphere;
  private _velocity: UNITS.G3 = 0 * meter / second;
  constructor(private world: World) {
    this.inner = new EIGHT.Sphere()
    this.scaleFactor = world.scaleFactor;
    this.inner.transparent = false
    this.inner.opacity = 1
    world.add(this.inner)
  }  
  get color() {
    return this.inner.color;
  }
  set color(color: EIGHT.Color) {
    this.inner.color = color;
  }
  get radius() {
    return UNITS.G3.scalar(this.inner.radius, this.scaleFactor.uom)
  }
  set radius(value: UNITS.G3) {
    this.inner.radius = scaleToNumber('radius', value, this.scaleFactor)
  }
  get velocity() {
    return this._velocity;
  }
  set velocity(value: UNITS.G3) {
    if (isVector(value)) {
      this._velocity = value;
    }
    else {
      throw new Error(`Sphere.velocity property must be a vector.`);
    }
  }
  get pos() {
    return UNITS.G3.copy(this.inner.X).mul(this.world.scaleFactor);
  }
  set pos(value: UNITS.G3) {
    if (isVector(value)) {
      scale('X', value, this.world.scaleFactor, this.inner.X)
    }
    else {
      throw new Error(`Sphere.pos property must be a vector.`);
    }
  }
}

/**
 * Constructor function for a Sphere.
 */
export function sphere(world: World, options: {pos?: UNITS.G3; radius?: UNITS.G3; color?: EIGHT.Color} = {}): Sphere {
  if (world) {
    const that = new Sphere(world);
    if (options.pos) {
      if (options.pos instanceof UNITS.G3) {
        that.pos = options.pos;
      }
      else {
        throw new Error("pos option must have type UNITS.G3");
      }
    }
    if (options.radius) {
      if (options.radius instanceof UNITS.G3) {
        that.radius = options.radius;
      }
      else {
        throw new Error("radius option must have type UNITS.G3");
      }
    }
    if (options.color) {
      if (options.color instanceof EIGHT.Color) {
        that.color = options.color;
      }
      else {
        throw new Error("color option must have type EIGHT.Color");
      }
    }
    return that;
  }
  else {
    throw new Error("World has not yet been initialized.")
  }
}

export interface Curve {
  append(point: UNITS.G3): void
}

/**
 * 
 */
export function curve(world: World, options: {color?: EIGHT.Color} = {}): Curve {
  const track = new EIGHT.Track({engine: world.engine, color: options.color})
  world.scene.add(track)
  const that: Curve = {
    append(point: UNITS.G3): void {
      track.addPoint(point)
    }
  }
  return that;
}

///////////////////////////////////////////////////////////////////////

/**
 * Wrapper object for the PerspectiveCamera so that the eye, look (vector)
 * properties use the units of the World (usually meters).
 */
function worldCamera(world: World, camera: EIGHT.PerspectiveCamera): Camera {
  const that: Camera = {
    get eye() {
      return EIGHT.Vector3.copy(camera.eye) 
      // return UNITS.G3.copy(camera.eye).mul(world.scaleFactor);
    },
    set eye(value: EIGHT.Vector3) {
      camera.eye.copyVector(value)
      /*
      if (isVector(value)) {
        scale('eye', value, world.scaleFactor, camera.eye)
      }
      else {
        throw new Error(`Camera.eye property must be a vector.`);
      }
      */
    },
    get look() {
      return UNITS.G3.copy(camera.look).mul(world.scaleFactor);
    },
    set look(value: UNITS.G3) {
      if (isVector(value)) {
        scale('look', value, world.scaleFactor, camera.look)
      }
      else {
        throw new Error(`Camera.look property must be a vector.`);
      }
    },
    get up() {
      return UNITS.G3.copy(camera.up).mul(unitless);
    },
    set up(value: UNITS.G3) {
      if (isVector(value)) {
        scale('up', value, unitless, camera.up)
      }
      else {
        throw new Error(`Camera.up property must be a vector.`);
      }
    }
  }
  return that;
}

///////////////////////////////////////////////////////////////////////////////

/**
 * Displays an exception by writing it to a <pre> element.
 */
function displayError(e: any) {
  const stderr = <HTMLPreElement>document.getElementById('error')
  stderr.style.color = "#FF0000"
  stderr.innerHTML = `${e}`
}

/**
 * Calls the callback argument when the Document Object Model (DOM) has been loaded.
 * Exceptions thrown by the callback function are caught and displayed.
 */
export function domReady(callback: () => any): void {
  DomReady.ready(function() {
    try
    {
      callback()
    }
    catch(e) {
      displayError(e)
    }
  })

}

/**
 * Catches exceptions thrown in the animation callback and displays them.
 * This function will have a slight performance impact owing to the try...catch statement.
 * This function may be bypassed for production use by using window.requestAnimationFrame directly.
 */
export function requestFrame(callback: FrameRequestCallback): number {
  const wrapper: FrameRequestCallback = function(time: number) {
    try {
      callback(time)
    }
    catch(e) {
      displayError(e)
    }
  }
  return window.requestAnimationFrame(wrapper)
}


/**
 * Creates an object that manages resizing of the output to fit the window.
 */
function windowResize(engine: EIGHT.Engine, overlay: EIGHT.Diagram3D, camera: EIGHT.PerspectiveCamera){
	const callback	= function() {
		engine.size(window.innerWidth, window.innerHeight);
	  // engine.viewport(0, 0, window.innerWidth, window.innerHeight)
	  // engine.canvas.width = window.innerWidth
	  // engine.canvas.height = window.innerHeight
		engine.canvas.style.width = `${window.innerWidth}px`
		engine.canvas.style.height = `${window.innerHeight}px`
		camera.aspect	= window.innerWidth / window.innerHeight;
	  overlay.canvas.width = window.innerWidth
	  overlay.canvas.height = window.innerHeight
		overlay.canvas.style.width = `${window.innerWidth}px`
		overlay.canvas.style.height = `${window.innerHeight}px`
		const ctxt = overlay.canvas.getContext('2d')
		ctxt.font = '24px Helvetica'
		ctxt.fillStyle = '#FFFFFF'
  }
	window.addEventListener('resize', callback, false);

	const that = {
    /**
     *
     */
	  resize: function() {
	    callback();
	    return that;
	  },

		/**
		 * Stop watching window resize
		 */
		stop	: function() {
			window.removeEventListener('resize', callback);
			return that;
		}
	};
	return that;
}