var PrintPreview = function(jqCanvas) {
  var canvas = jqCanvas[0];

  // === 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)

  console.log('torus',torus);

  function draw() {
    // 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
      // }

      var angle = 0;
      var alpha = 1;

      draw_shape(canvas, 
        concat_n([
          rotate(0),
          transpose_axes(1, 2),
          rotate(0),  // to reduce periodicity
          transpose_axes(1, 2),
          translate(0, 0, 6),
          // transpose_axes(1, 2),
          // rotate(1), 
        ]),
        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)

  return {
    draw: draw,
  }
}