/*
 * Copyright (c) Joshua John Lawrence 2012-2014, all rights reserved.
 */

/*
 * A class for rendering Mandelbrot sets.
 */
function Mandelbrot( maxIterations ) {

  /*
   * Private helper. Translate the number of iterations performed on a
   * point in the complex plane into a color. An optimisation would be
   * to store the colors in an array and reuse them, rather than create
   * a new one each time. An extension would be to present an API to
   * allow the color mapping to be changed.
   *
   * Argument should be numeric. Returns a Color.
   */
  function lookupColor( iterations ) {
    if( iterations == maxIterations ) return Color.BLACK;

    return new Color(
      210 - iterations % 200,
      220 - iterations % 200,
      255 - ( iterations % 200 ) / 3
    );
  }

  /*
   * Private helper. Iterate a point in the complex plane using Mandelbrot's formula.
   *
   * Argument should be numeric. Returns the number of iterations.
   */
  function countIterations( c ) {
    var iterations = 1;
    var z = c.copy();   // Seeing as we are using mutators below, ensure z and c are not aliases.
    while( z.modulusSquared() < 4 && iterations < maxIterations ) {
//    This optimisation gains an order of magnitude speed increase.
//      z = z.squared().plus( c );
      z.inlineSquaredPlus( c );
      ++iterations;
    }

    return iterations;
  }

  /*
   * Private helper. Find the color a point in the complex plane should be drawn.
   *
   * Argument is Complex. Returns a Color.
   */
  function findColor( c ) {
    return lookupColor( countIterations( c ) );
  }

  /*
   * Set the maximum number of iterations to attempt. Argument should be numeric.
   */
  this.setMaxIterations = function( newMaxIterations ) {
    maxIterations = newMaxIterations;
  };

  /*
   * Draw a Mandelbrot set filling the PixelCanvas and using the Transform to
   * map pixels to points in the complex plane. I'm using my own Transform
   * class instead of the built-in canvas transform operations because I need
   * to apply the transform manually when working out how to zoom.
   */
  this.draw = function( transform, pixelCanvas ) {
    var x = 0;
//  var t0 = new Date().getTime();

    // Like a for loop over x, but with a break in between each column of
    // pixels to allow the screen to update. Doing it this way provides visual
    // feedback of progress, but does create some overhead. An extension
    // would be to do blocks of columns at a time, adapting the block width
    // depending on image complexity.
    var interval = setInterval(
      function() {
        for( var y = 0; y < pixelCanvas.getHeight(); ++y ) {
          var screenPos = new Vector( x, y );
          var worldPos = transform.screenToWorld( screenPos );
          pixelCanvas.drawPixel(
            screenPos,
            findColor( new Complex( worldPos.getX(), worldPos.getY() ) )
          );
        }
        pixelCanvas.paint();

        ++x;
        if( x >= pixelCanvas.getWidth() ) {
          clearInterval( interval );
//          alert( "Done in " + ( new Date().getTime() - t0 ) );
        }
      },
      0
    );
  }
}

/*
 * The user interface and starting mandelbrot set.
 */
try {

  // The canvases.
  var canvas = document.getElementById( "theCanvas" );
  var uiOverlay = document.getElementById( "theUiOverlay" );

  // Sanity checking.
  if( !canvas ) {
    throw "Could not find the canvas";
  }
  if( !canvas.getContext ) {
    throw "Could not find getContext in canvas";
  }
  if( !uiOverlay ) {
    throw "Could not find the UI overlay";
  }
  if( !uiOverlay.getContext ) {
    throw "Could not find getContext in the UI overlay";
  }

  // Find 2D contexts.
  var context = canvas.getContext( "2d" );
  if( !context ) {
    throw "Could not get a 2d context for the canvas";
  }
  var overlayContext = uiOverlay.getContext( "2d" );
  if( !overlayContext ) {
    throw "Could not get a 2d context for the UI overlay";
  }


  /*
   * The UI.
   */

  // UI variables.
  var highlight = false;
  overlayContext.strokeStyle = "#505050";
  overlayContext.lineWidth = 0;
  
  /*
   * Helper for mouse event handlers. Get the current mouse position in pixels
   * from the corner of the UI canvas. The argument is a mouse event.
   */
  function getMousePosition( event ) {
    if( event.offsetX || event.offsetX == 0 ) {
      return new Vector( Math.floor( event.offsetX ), Math.floor( event.offsetY ) );
    } else if( event.layerX || event.layerX == 0 ) {
      return new Vector( Math.floor( event.layerX ), Math.floor( event.layerY ) );
    }
  }

  /*
   * Handle mouse down. Start dragging a rectangle, storing the size and position
   * in highlight. The argument is a mouse event.
   */
  function mouseDown( event ) {
    var mousePosition = getMousePosition( event );
    highlight = { x: mousePosition.getX(), y: mousePosition.getY(), w: 0, h: 0 };
    
    // Mouse events.
    uiOverlay.removeEventListener( "mousedown", mouseDown, true );
    uiOverlay.addEventListener( "mousemove", mouseMove, true );
    uiOverlay.addEventListener( "mouseup", mouseUp, true );
    
    // Touch events.
    uiOverlay.removeEventListener( "touchstart", mouseDown, true );
    uiOverlay.addEventListener( "touchmove", mouseMove, true );
    uiOverlay.addEventListener( "touchend", mouseUp, true );

    // Prevent scrolling on mobile browsers.
    event.preventDefault();
  }

  /*
   * Handle mouse move. Update highlight and the display.
   */
  function mouseMove( event ) {
    var mousePosition = getMousePosition( event );
    if( highlight ) {
      highlight.w = mousePosition.getX() - highlight.x;
      highlight.h = mousePosition.getY() - highlight.y;
    }
    updateOverlay();

    // Prevent scrolling on mobile browsers.
    event.preventDefault();
  }

  /*
   * Handle mouse up. Clear highlight, update the trandform and redraw the Mandelbrot set.
   */
  function mouseUp( event ) {
    var a = transform.screenToWorld( new Vector( highlight.x, highlight.y ) );
    var b = transform.screenToWorld( new Vector( highlight.x + highlight.w, highlight.y + highlight.h ) );
    transform = new Transform(
      new Vector( Math.min( a.getX(), b.getX() ), Math.min( a.getY(), b.getY() ) ),
      new Vector( Math.max( a.getX(), b.getX() ), Math.max( a.getY(), b.getY() ) ),
      new Vector( 0, pixelCanvas.getHeight() ),
      new Vector( pixelCanvas.getWidth(), 0 )
    );
    mandelbrot.draw( transform, pixelCanvas );
    highlight = false;
    updateOverlay();

    // Mouse events.
    uiOverlay.addEventListener( "mousedown", mouseDown, true );
    uiOverlay.removeEventListener( "mousemove", mouseMove, true );
    uiOverlay.removeEventListener( "mouseup", mouseUp, true );
    
    // Touch events.
    uiOverlay.addEventListener( "touchstart", mouseDown, true );
    uiOverlay.removeEventListener( "touchmove", mouseMove, true );
    uiOverlay.removeEventListener( "touchend", mouseUp, true );

    // Prevent scrolling on mobile browsers.
    event.preventDefault();
  }

  // Initialize the UI event handlers.
  uiOverlay.addEventListener( "mousedown", mouseDown, true );
  uiOverlay.addEventListener( "touchstart", mouseDown, true );

  /*
   * Redraw the dragged rectangle.
   */
  function updateOverlay() {
    overlayContext.clearRect( 0, 0, uiOverlay.width, uiOverlay.height );
    if( highlight ) {
      overlayContext.strokeRect( highlight.x + 0.5, highlight.y + 0.5, highlight.w, highlight.h );
    }
  }

  /*
   * Update the maximum number of iterations to attempt for a pixel and redraw the
   * Mandelbrot set. Argument should be numeric.
   */
  function changeMaxIterations( newMaxNumIterations ) {
    mandelbrot.setMaxIterations( Number( newMaxNumIterations ) );
    mandelbrot.draw( transform, pixelCanvas );
  }

  /*
   * Reset the state of and redraw the Mandelbrot set.
   */
  function reset() {
    var mandelbrotIterations = document.getElementById( "mandelbrotIterations" );
    if( !pixelCanvas ) {
      // The UI code probably hit an exception, which means we are probably in Internet Explorer, and
      // nothing can save us.
      return;
    }
    transform = new Transform(
        new Vector( -2.2, -1.2 ),
        new Vector( 1.0, 1.2 ),
        new Vector( 0, pixelCanvas.getHeight() ),
        new Vector( pixelCanvas.getWidth(), 0 )
    );
    mandelbrotIterations.value = DEFAULT_ITERATIONS;   // This doesn't actually call the onchange handler.
    changeMaxIterations( mandelbrotIterations.value );
  }

  // Initialise and draw a starting mandelbrot set.
  var DEFAULT_ITERATIONS = 100;
  var pixelCanvas = new PixelCanvas( context );
  var transform;
  var mandelbrot = new Mandelbrot( DEFAULT_ITERATIONS );
} catch( e ) {
  // We are probably in Internet Explorer, and nothing can save us.
}