// === basic wheel reinvention stuff ===

function $(id) { return document.getElementById(id) }

// comparison function using a key, to pass to .sort()
function keycomp(key) {
  return function(a, b) {
    var ka = key(a)
    var kb = key(b)
    if (ka < kb) return -1
    if (ka > kb) return 1
    return 0 
  }
}

// return a list transformed by a function
function map(f, list) {
  var rv = []
  for (var ii = 0; ii < list.length; ii++) rv.push(f(list[ii]))
  return rv
}

// === 3d transforms ===

// We represent transforms as a 3x4 list of lists (ahem, array of arrays):
// [[x_from_x, x_from_y, x_from_z, x_off],
//  [y_from_x, y_from_y, y_from_z, y_off],
//  [z_from_x, z_from_y, z_from_z, z_off]]
// And we only actually multiply points through them in xform.
function translate(x, y, z) {
  return [[1, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z]]
}
function identity() { return translate(0, 0, 0) }
// rotation around the Z-axis
function rotate(theta) {
  var s = Math.sin(theta)
  var c = Math.cos(theta)
  return [[c, -s, 0, 0], [s, c, 0, 0], [0, 0, 1, 0]]
}
// exchange two of the X, Y, Z axes --- useful for making rotate() go around
// another axis :)
function transpose_axes(a, b) {
  var rv = identity()
  var tmp = rv[a]
  rv[a] = rv[b]
  rv[b] = tmp
  return rv
}
// you'd think we'd have a scale() function too, but I haven't needed it yet.
// concatenate two transforms --- the magic that makes it all possible
function concat(x1, x2) {
  var rv = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
  for (var ii = 0; ii < 3; ii++) {
    rv[ii][3] = x2[ii][3]
    for (var jj = 0; jj < 3; jj++) {
      rv[ii][3] += x1[jj][3] * x2[ii][jj]
      for (var kk = 0; kk < 3; kk++) {
        rv[ii][jj] += x1[kk][jj] * x2[ii][kk]
      }
    }
  }
  return rv
}
// concatenate N transforms.  I'd insert a special case for 0 transforms,
// but amusingly this function isn't all that performance-critical.
function concat_n(xforms) {
  var rv = identity()
  for (var ii = 0; ii < xforms.length; ii++) rv = concat(rv, xforms[ii]) 
  return rv
}
// transform a point.
function xform(xform, p) {
  var result_vec = []
  for (var ii = 0; ii < 3; ii++) {
    var rv = xform[ii][3]
    for (var jj = 0; jj < 3; jj++) rv += xform[ii][jj] * p[jj]
    result_vec.push(rv)
  }
  return result_vec
}
// transform multiple points.
function xform_points(xf, points) {
  var xp = []
  for (var ii = 0; ii < points.length; ii++) {
    xp.push(xform(xf, points[ii]))
  }
  return xp
}
// perspective-transform a point --- into 2d.
function persp(p) { return [p[0] / p[2], p[1] / p[2]] }
// perspective-transform multiple points
function persp_points(points) {
  return map(persp, points)
}

// return the normal of a triangle defined by three points.
function normal(p1, p2, p3) {
  var v1 = [p1[0]-p2[0], p1[1]-p2[1], p1[2]-p2[2]]
  var v2 = [p2[0]-p3[0], p2[1]-p3[1], p2[2]-p3[2]]
  var n = [v1[1]*v2[2]-v1[2]*v2[1], 
      	   v1[2]*v2[0]-v1[0]*v2[2],
           v1[0]*v2[1]-v1[1]*v2[0]]
  var mag = Math.sqrt(n[0]*n[0] + n[1]*n[1] + n[2]*n[2])
  return [n[0]/mag, n[1]/mag, n[2]/mag]
}

// === 3d shapes ===
// We represent these as an array of three arrays
// [points, lines, polies] where each line is two indices into the points array
// and each poly is three indices into the points array

function dup(array) {
  var newarray = new Array(array.length)
  for (var ii = 0; ii < array.length; ii++) newarray[ii] = array[ii]
  return newarray
}

// transform a shape, returning a new shape
function xform_shape(xf, shape) {
  // de-alias new lines and polies
  return [xform_points(xf, shape[0]), dup(shape[1]), dup(shape[2])]
}

// add a new shape onto an old shape, mutating the old one
function augment(shape1, shape2) {
  var s1p = shape1[0]
  var off = s1p.length
  for (var ii = 0; ii < shape2[0].length; ii++) s1p.push(shape2[0][ii])
  var s2ll = shape2[1].length  // in case of aliasing
  for (var ii = 0; ii < s2ll; ii++) 
    shape1[1].push([shape2[1][ii][0] + off, shape2[1][ii][1] + off])
  var s2pl = shape2[2].length
  for (var ii = 0; ii < s2pl; ii++) {
    var tri = shape2[2][ii]
    shape1[2].push([tri[0]+off, tri[1]+off, tri[2]+off])
  }
}

// given a shape, make a more complicated shape by copying it through transform
// xf n times, and connecting the corresponding points.  This is more powerful
// than the usual kind of extrusion, and can be used to create fairly 
// interesting shapes --- a snail shell from a circle, for instance.
function extrude_shape(xf, shape, n) {
  if (n == null) n = 1
  var new_part = shape
  var old_line_base = 0 // where the lines to attach the triangles start
  for (var ii = 0; ii < n; ii++) {
    var new_part = xform_shape(xf, new_part)
    var shape_length = shape[0].length
    var new_line_base = shape[1].length  // for triangles later
    augment(shape, new_part)
    var new_part_length = new_part[0].length
    // connect corresponding points
    for (var jj = 0; jj < new_part_length; jj++) {
      shape[1].push([shape_length + jj - new_part_length, shape_length + jj])
    }
    // make triangles
    var nlines = new_part[1].length
    // var old_line_base = new_line_base - nlines
    for (var jj = 0; jj < nlines; jj++) {
      var old_line = shape[1][old_line_base + jj]
      var new_line = shape[1][new_line_base + jj]
      shape[2].push([old_line[0], old_line[1], new_line[0]])
      shape[2].push([new_line[1], new_line[0], old_line[1]])
    }
    old_line_base = new_line_base
  }
}
// a shape consisting of a single point
function point_shape(x, y, z) { return [[[x, y, z]], [], []] }
// approximate a circle in the x-y plane around the origin; radius r and n points
function circle(r, n) {
  var shape = point_shape(r, 0, 0)
  extrude_shape(rotate(Math.atan(1)*8/n), shape, n)
  return shape
}
// approximate a torus with major radius r2 and minor radius r1,
// with correspondingly n2 and n1 points around each axis
function make_torus(r1, r2, n1, n2) {
  var c = xform_shape(translate(r2, 0, 0), circle(r1, n1))
  extrude_shape(concat_n([transpose_axes(1, 2), 
    	                  rotate(Math.atan(1)*8/n2),
		          transpose_axes(1, 2)]),
		c, n2)
  return c
}

// === drawing code ===

// draw a 3d shape on a canvas
// 95% of the run time is in this function and its kids
function draw_shape(canvas, xf, shape, alpha) {
  var ctx = canvas.getContext('2d')
  var w = canvas.width
  var h = canvas.height

  // set up coordinate system so canvas is (-1, -1) to (1, 1)
  ctx.save()
  ctx.translate(w/2, h/2)
  ctx.scale(w/2, h/2)  

  // 1/3 of the time is in these two lines (when not doing polies)
  var points3d = xform_points(xf, shape[0])
  var points = persp_points(points3d)
  var lines = shape[1]
  // 2/3 of the time is in this loop (when we're not doing polies)
  if (alpha == null) {
    ctx.strokeStyle = 'grey'
    ctx.lineWidth = 1/(w/2)
    ctx.beginPath()
    var p1, p2
    for (var ii = 0; ii < lines.length; ii++) {
      p1 = points[lines[ii][0]]
      p2 = points[lines[ii][1]]
      ctx.moveTo(p1[0], p1[1])
      ctx.lineTo(p2[0], p2[1])
    }
    ctx.stroke()
  }

  // when we're doing polies, 90% of our time is spent doing polies
  if (alpha != null) {
    // Sort polygons by depth so we draw the farthest-away stuff first
    // ("painter's algorithm")
    var minusdepth = function(p) {
      return [-(points3d[p[0]][2] + points3d[p[1]][2] + points3d[p[2]][2]), p] 
    }
    var polies = map(minusdepth, shape[2])
    polies.sort(keycomp(function(p) { return p[0] }))

    // draw all the polygons
    var tri, p1, p2, p3, n, bright
    for (var ii = 0; ii < polies.length; ii++) {
      tri = polies[ii][1]
      if (alpha == '1') {
        // light surface
	n = normal(points3d[tri[0]], points3d[tri[1]], points3d[tri[2]])
	// I'm not sure how to make backface removal work with perspective: 
	// if (n[2] > 0 && alpha == '1') continue // backface removal

	// lighting from (1, -1, -1) direction
	bright = parseInt(((n[0]-n[1]-n[2]) / Math.sqrt(3) * 255))
	if (bright < 20) bright = 20
      } else {
        // lighting doesn't make sense if the object is transparent,
        // so we color by depth to have some variation in color...
        var maxd = polies[polies.length-1][0]
	var mind = polies[0][0]
	var d = polies[ii][0]
	bright = parseInt((d-mind)/(maxd-mind) * 255)
      }
      ctx.fillStyle = 'rgba(' + bright + ',' + bright + ',' + bright + 
                      ',' + alpha + ')'
      ctx.beginPath()
      p1 = points[tri[0]]
      p2 = points[tri[1]]
      p3 = points[tri[2]]
      ctx.moveTo(p1[0], p1[1])
      ctx.lineTo(p2[0], p2[1])
      ctx.lineTo(p3[0], p3[1])
      // ctx.closePath()  seems to be unnecessary
      ctx.fill()
    }
  }

  ctx.restore()
}
// clear a canvas
function cls(canvas) {
  var ctx = canvas.getContext('2d')
  ctx.fillStyle = 'black'
  ctx.fillRect(0, 0, canvas.width, canvas.height)
}

// === drawing of particular shapes. also DOM. ===
angle = 0
function unit_cube() {
  var shape = point_shape(0, 0, 0)
  extrude_shape(translate(0,0,1), shape)
  extrude_shape(translate(0,1,0), shape)
  extrude_shape(translate(1,0,0), shape)
  return shape
}

// this was where I tested stuff as I wrote this
function make_some_junk() {
  // make a unit cube centered on the origin
  var shape = xform_shape(translate(-0.5, -0.5, -0.5), unit_cube())

  // add some circles
  augment(shape, circle(0.707, 16))
  augment(shape, xform_shape(transpose_axes(0, 2), circle(0.707, 16)))
  augment(shape, xform_shape(transpose_axes(1, 2), circle(0.707, 16)))
  augment(shape, circle(1, 15))

  // add a disc
  var big_disc = circle(2, 20)
  extrude_shape(translate(0, 0, 0.5), big_disc, 2)
  augment(shape, big_disc)
  return shape
}
var some_junk = make_some_junk()

function draw_some_junk(canvas) {
  var xf = concat_n([transpose_axes(1, 2),
                     rotate(angle),
      	     	     transpose_axes(1, 2),
		     rotate(angle*1.618),
		     translate(0, 0, 2.5)])

  draw_shape(canvas, xf, some_junk)
}
var torus = make_torus(1, 3, 12, 12)
function draw_torus(canvas) {
  var start = new Date()
  var alpha = null
  if ($('fill').checked) alpha = ($('translucent').checked ? '0.5' : 1)
  if ($('trails').checked) {
    $('canvas').getContext('2d').globalAlpha = 0.33
  } else {
    $('canvas').getContext('2d').globalAlpha = 1
  }
  draw_shape(canvas, concat_n([rotate(angle), 
  		               transpose_axes(1, 2),
                               rotate(angle / 1.618),  // to reduce periodicity
                               transpose_axes(1, 2),
                               translate(0, 0, 6)]),
             torus, alpha)
  var end = new Date()
  var ms = $('ms')
  if (ms) {
    var msvalue = ms.value + ' ' + (end.getTime() - start.getTime())
    if (msvalue.length > 25) msvalue = msvalue.substr(msvalue.length - 25)
    ms.value = msvalue
  }
}
function update() {
  if (!$('go').checked) return
  angle += 3.14159 / 30
  cls($('canvas'))
  draw_torus($('canvas'))
}
function init(ev) {
  setInterval(update, 100)
  // this doesn't work: $('fill').addEventListener('change', update, true)
  // how do you do what I want to do there?
  cls($('canvas'))
  draw_torus($('canvas'))
}
window.addEventListener('load', init, true)
