/*
 * XaoS.js
 * https://github.com/jblang/XaoS.js
 *
 * Copyright (C)2011 John B. Langston III
 * Copyright (C)2001, 2010 Andrea Medeghini
 * Copyright (C)1996, 1997 Jan Hubicka and Thomas Marsh
 *
 * Based on code from XaoS by Jan Hubicka (http://xaos.sf.net)
 * and from JAME by Andrea Medeghini (http://www.fractalwalk.net)
 *
 * This file is part of XaoS.js.
 *
 * XaoS.js is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * XaoS.js is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with XaoS.js.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
var xaos = xaos || {};

xaos.zoom = (function () {
  "use strict";

  const USE_XAOS = true; // Whether to use zooming or recalculate every frame
  const USE_SYMMETRY = true; // Whether to use symmetry when possible
  const USE_SOLIDGUESS = true; // Whether to use solid guessing to avoid calculations
  const RANGES = 2; // Number of ranges to use for sizing approximation data
  const RANGE = 4; // Maximum distance to use for approximation
  const MASK = 0x7; // Mask value for maximum potential source lines
  const DSIZE = RANGES + 1; // Shift value for target lines
  const FPMUL = 64; // Multiplication factor for fixed-point representation
  const FPRANGE = FPMUL * RANGE; // Fixed point range of approximation
  const MAX_PRICE = Number.MAX_VALUE; // Maximum price of uninitialized approximation
  const NEW_PRICE = FPRANGE * FPRANGE; // Price of calculating a new line
  const GUESS_RANGE = 4; // Range to use for solid guessing

  /** A price entry in the approximation table
   * @constructor
   */
  function Price() {
    this.previous = null; // Previous price calculated for the same line
    this.index = 0; // Index of the source for this approximation (-1 means new calculation)
    this.price = MAX_PRICE; // Price calculated for this line
  }

  /** A group of pixels to be moved
   * @constructor
   */
  function Move() {
    this.length = 0; // number of pixels to move
    this.from = 0; // starting offset of pixel source
    this.to = 0; // starting offset of pixel destination
  }

  /** A single row or column of pixels in the image
   * @constructor
   */
  function Line() {
    this.recalculate = false; // whether to recalculate this line
    this.dirty = false; // whether this line needs to be redrawn
    this.isRow = false; // whether this is a row (true) or column (false)
    this.index = 0; // index of row or column within the image
    this.symIndex = 0; // index of pixels to use for symmetry
    this.symTo = 0; // position of pixels this is symmetrical to
    this.symRef = 0; // position of pixels referring to this one
    this.oldPosition = 0.0; // line's old position in the fractal's complex plane
    this.newPosition = 0.0; // line's new position in the fractal's complex plane
    this.priority = 0.0; // calculation priority for this row/column
  }

  /** An image derived from an HTML5 canvas
   * @param canvas - the canvas used to display the image
   * @constructor
   */
  function CanvasImage(canvas) {
    let width = canvas.clientWidth;
    let height = canvas.clientHeight;
    if (canvas.width !== width || canvas.height !== height) {
      canvas.width = width;
      canvas.height = height;
    } else {
      ctx.clearRect(0, 0, width, height);
    }

    this.canvas = canvas;
    this.context = canvas.getContext("2d");
    this.width = canvas.width;
    this.height = canvas.height;
    this.newImageData = this.context.createImageData(this.width, this.height);
    this.oldImageData = this.context.createImageData(this.width, this.height);
    this.newBuffer = new Uint32Array(this.newImageData.data.buffer);
    this.oldBuffer = new Uint32Array(this.oldImageData.data.buffer);
  }

  /** Swap new and old buffers */
  CanvasImage.prototype.swapBuffers = function () {
    var tmp = this.oldBuffer;
    this.oldBuffer = this.newBuffer;
    this.newBuffer = tmp;
    tmp = this.oldImageData;
    this.newImageData = this.oldImageData;
    this.oldImageData = tmp;
  };

  /** Draw the current image */
  CanvasImage.prototype.paint = function () {
    this.context.putImageData(this.newImageData, 0, 0);
  };

  /** Utility function to make an array of the specified size
   * with the specified initial value. It will do the right thing
   * to create unique items, whether you pass in a prototype, a
   * constructor, or a primitive.
   * @param {number} size - the size of the array.
   * @param initial - the initial value for each entry.
   */
  function makeArray(size, initial) {
    var i,
      data = [];
    for (i = 0; i < size; i++) {
      if (typeof initial === "object") {
        // prototype object
        data[i] = Object.create(initial);
      } else if (typeof initial === "function") {
        // constructor
        data[i] = new initial();
      } else {
        // primitive
        data[i] = initial;
      }
    }
    return data;
  }

  /** Container for all zoom context data for a particular canvas.
   *
   * @param image {CanvasImage} Image on which to draw the fractal.
   * @param fractal {FractalContext} Fractal parameters.
   * @constructor
   */
  function ZoomContext(image, fractal) {
    var size = Math.max(image.width, image.height);
    this.image = image; // the image to draw the fractal on
    this.fractal = fractal; // the fractal formula used for the image
    this.columns = makeArray(image.width, Line); // columns in the fractal image
    this.rows = makeArray(image.height, Line); // rows in the fractal image
    this.sourcePos = makeArray(size + 1, 0); // fixed-point positions for source lines
    this.oldBest = makeArray(size, null); // best prices for previous line
    this.newBest = makeArray(size, null); // best prices for current line
    this.calcPrices = makeArray(size, Price); // prices for calculating new lines
    this.movePrices = makeArray(size << DSIZE, Price); // prices for approximating new lines from exsiting ones
    this.moveTable = makeArray(image.width + 1, Move); // table of pixels to be moved
    this.fillTable = makeArray(image.width + 1, Move); // table of pixels to be filled
    this.queue = makeArray(image.width + image.height, null); // queue of lines to calculate
    this.queueLength = 0; // length of the calculation queue
    this.startTime = 0; // time that the current frame was started
    this.minFPS = 60; // target FPS to maintain
    this.fudgeFactor = 0; // fudge factor used to achieve target FPS
    this.incomplete = false; // flag indicates incomplete calculation
    this.zooming = false; // flag indicates image is currently zooming
  }

  /** Swaps the old and new best prices in the this container. */
  ZoomContext.prototype.swapBest = function () {
    var tmpBest = this.oldBest;
    this.oldBest = this.newBest;
    this.newBest = tmpBest;
  };

  /** Convert fractal viewport from radius and center to x and y start to end ranges */
  ZoomContext.prototype.convertArea = function () {
    var radius = this.fractal.region.radius;
    var center = this.fractal.region.center;
    var aspect = this.image.width / this.image.height;
    var size = Math.max(radius.x, radius.y * aspect);
    return {
      begin: {
        x: center.x - size / 2,
        y: (center.y - size / 2) / aspect,
      },
      end: {
        x: center.x + size / 2,
        y: (center.y + size / 2) / aspect,
      },
    };
  };

  /** Resets line of pixels for fresh calculation
   *
   * @param line - row or column of pixels
   * @param begin - starting fractal cooridnate
   * @param end - ending coordinate
   * @param isRow - whether this is a row or column
   * @returns {number}
   */
  ZoomContext.prototype.initialize = function (lines, begin, end, isRow) {
    var i;
    var p;
    var step = (end - begin) / lines.length;
    var line = null;

    for (i = 0, p = begin; i < lines.length; i++, p += step) {
      line = lines[i];
      line.recalculate = true;
      line.dirty = true;
      line.isRow = isRow;
      line.index = i;
      line.oldPosition = p;
      line.newPosition = p;
      line.symIndex = i;
      line.symTo = -1;
      line.symRef = -1;
    }
    return step;
  };

  /** Calculate price of approximating one line from another
   *
   * @param p1 - position of first line
   * @param p2 - position of second line
   * @returns {number} - price of approximation
   */
  function calcPrice(p1, p2) {
    return (p1 - p2) * (p1 - p2);
  }

  /** Calculate fixed-point representation of each line's old position
   * @param lines - lines to use for calculation
   * @param begin - beginning of floating point range
   * @param end - end of floating point range
   */
  ZoomContext.prototype.calcFixedpoint = function (lines, begin, end) {
    var tofix = (lines.length * FPMUL) / (end - begin);
    var i;
    this.sourcePos[lines.length] = Number.MAX_VALUE;
    for (i = lines.length - 1; i >= 0; i--) {
      this.sourcePos[i] = ((lines[i].oldPosition - begin) * tofix) | 0;
      if (this.sourcePos[i] > this.sourcePos[i + 1]) {
        this.sourcePos[i] = this.sourcePos[i + 1];
      }
    }
  };

  /** Choose the best approximation for lines based on previous frame
   *
   * @param lines - relocation table for rows or columns
   * @param begin - beginning coordinate (x or y)
   * @param end - ending coordinate (x or y)
   * @param newPosition - array of newPosition coordinates on the complex plane
   * @returns {number}
   */
  ZoomContext.prototype.approximate = function (lines, begin, end) {
    var previous = null; // pointer to previous approximation
    var best = null; // pointer to best approximation
    var line = null; // pointer to current line
    var price = 0; // price of current approximation
    var dest; // index of the current destination line
    var idealPos = 0; // ideal position for the current destination
    var maxPos = 0; // maximum valid source position of the current destination
    var source = 0; // index of current source line
    var prevBegin = 0; // index of first potential source for current destination
    var prevEnd = 0; // index of last potential source for current destination
    var currBegin = 0; // index of first potential source for next destination
    var flag = 0;
    var size = lines.length;
    var step = (end - begin) / size;
    var sourcePos = this.sourcePos;

    // Calculate fixed-point positions of all source lines
    this.calcFixedpoint(lines, begin, end);

    for (dest = 0, idealPos = 0; dest < size; dest++, idealPos += FPMUL) {
      this.swapBest();
      maxPos = idealPos - FPRANGE;
      if (maxPos < -FPMUL) {
        maxPos = -FPMUL;
      }
      source = prevBegin;
      while (sourcePos[source] < maxPos) {
        source++;
      }
      currBegin = source;
      maxPos = idealPos + FPRANGE;

      // Find the previous approximation
      if (prevBegin !== prevEnd && source > prevBegin) {
        // Previous line had approximations; use them
        if (source < prevEnd) {
          previous = this.oldBest[source - 1];
        } else {
          previous = this.oldBest[prevEnd - 1];
        }
        price = previous.price;
      } else if (dest > 0) {
        // Previous line had no approximations
        // Use the price of calculating the previous line
        previous = this.calcPrices[dest - 1];
        price = previous.price;
      } else {
        // We're on the first line; no previous prices exists
        previous = null;
        price = 0;
      }

      // Add the price for calculating this line
      price += NEW_PRICE;
      best = this.calcPrices[dest];
      best.price = price;
      best.index = -1;
      best.previous = previous;

      // Try all possible approximations for this line and calculate the best one
      if (prevBegin !== prevEnd) {
        if (source === prevBegin) {
          // We're on the first line so there is no previous line
          if (sourcePos[source] !== sourcePos[source + 1]) {
            previous = this.calcPrices[dest - 1];
            price = previous.price + calcPrice(sourcePos[source], idealPos);
            if (price < best.price) {
              best = this.movePrices[(source << DSIZE) + (dest & MASK)];
              best.price = price;
              best.index = source;
              best.previous = previous;
            }
          }
          this.newBest[source++] = best;
        }
        previous = null;

        // Potential sources for the previous and current line overlap within
        // this range, so we have to calculate every possibility and find the best
        while (source < prevEnd) {
          if (sourcePos[source] !== sourcePos[source + 1]) {
            previous = this.oldBest[source - 1];
            price = previous.price + NEW_PRICE;
            if (price < best.price) {
              best = this.movePrices[((source - 1) << DSIZE) + (dest & MASK)];
              best.price = price;
              best.index = -1;
              best.previous = previous;
              this.newBest[source - 1] = best;
            }
            price = previous.price + calcPrice(sourcePos[source], idealPos);
            if (price < best.price) {
              best = this.movePrices[(source << DSIZE) + (dest & MASK)];
              best.price = price;
              best.index = source;
              best.previous = previous;
            } else if (sourcePos[source] > idealPos) {
              this.newBest[source++] = best;
              break;
            }
          }
          this.newBest[source++] = best;
        }

        // We are past the overlapping area
        if (source > prevBegin) {
          previous = this.oldBest[source - 1];
        } else {
          previous = this.calcPrices[dest - 1];
        }
        price = previous.price + NEW_PRICE;
        if (price < best.price && source > currBegin) {
          best = this.movePrices[((source - 1) << DSIZE) + (dest & MASK)];
          best.price = price;
          best.index = -1;
          best.previous = previous;
          this.newBest[source - 1] = best;
        }
        while (sourcePos[source] < maxPos) {
          if (sourcePos[source] !== sourcePos[source + 1]) {
            price = previous.price + calcPrice(sourcePos[source], idealPos);
            if (price < best.price) {
              best = this.movePrices[(source << DSIZE) + (dest & MASK)];
              best.price = price;
              best.index = source;
              best.previous = previous;
            } else if (sourcePos[source] > idealPos) {
              break;
            }
          }
          this.newBest[source++] = best;
        }
        while (sourcePos[source] < maxPos) {
          this.newBest[source++] = best;
        }
      } else if (sourcePos[source] < maxPos) {
        if (dest > 0) {
          previous = this.calcPrices[dest - 1];
          price = previous.price;
        } else {
          previous = null;
          price = 0;
        }
        while (sourcePos[source] < maxPos) {
          if (sourcePos[source] !== sourcePos[source + 1]) {
            price += calcPrice(sourcePos[source], idealPos);
            if (price < best.price) {
              best = this.movePrices[(source << DSIZE) + (dest & MASK)];
              best.price = price;
              best.index = source;
              best.previous = previous;
            } else if (sourcePos[source] > idealPos) {
              break;
            }
          }
          this.newBest[source++] = best;
        }
        while (sourcePos[source] < maxPos) {
          this.newBest[source++] = best;
        }
      }
      prevBegin = currBegin;
      currBegin = prevEnd;
      prevEnd = source;
    }
    if (begin > lines[0].oldPosition && end < lines[size - 1].oldPosition) {
      flag = 1;
    }
    if (sourcePos[0] > 0 && sourcePos[size - 1] < size * FPMUL) {
      flag = 2;
    }
    for (dest = size - 1; dest >= 0; dest--) {
      line = lines[dest];
      line.symTo = -1;
      line.symRef = -1;
      if (best.index < 0) {
        line.recalculate = true;
        line.dirty = true;
        line.symIndex = line.index;
      } else {
        line.symIndex = best.index;
        line.newPosition = lines[best.index].oldPosition;
        line.recalculate = false;
        line.dirty = false;
      }
      best = best.previous;
    }
    newPositions(lines, begin, end, step, flag);
    return step;
  };

  /** Choose new positions for lines based on calculated prices
   *
   * @param lines
   * @param size
   * @param begin1
   * @param end1
   * @param step
   * @param newPosition
   * @param flag
   */
  function newPositions(lines, begin1, end1, step, flag) {
    var delta = 0;
    var size = lines.length;
    var begin = 0;
    var end = 0;
    var s = -1;
    var e = -1;
    if (begin1 > end1) {
      begin1 = end1;
    }
    while (s < size - 1) {
      e = s + 1;
      if (lines[e].recalculate) {
        while (e < size) {
          if (!lines[e].recalculate) {
            break;
          }
          e++;
        }
        if (e < size) {
          end = lines[e].newPosition;
        } else {
          end = end1;
        }
        if (s < 0) {
          begin = begin1;
        } else {
          begin = lines[s].newPosition;
        }
        if (e === size && begin > end) {
          end = begin;
        }
        if (e - s === 2) {
          delta = (end - begin) * 0.5;
        } else {
          delta = (end - begin) / (e - s);
        }
        switch (flag) {
          case 1:
            for (s++; s < e; s++) {
              begin += delta;
              lines[s].newPosition = begin;
              lines[s].priority =
                1 / (1 + Math.abs(lines[s].oldPosition - begin) * step);
            }
            break;
          case 2:
            for (s++; s < e; s++) {
              begin += delta;
              lines[s].newPosition = begin;
              lines[s].priority = Math.abs(lines[s].oldPosition - begin) * step;
            }
            break;
          default:
            for (s++; s < e; s++) {
              begin += delta;
              lines[s].newPosition = begin;
              lines[s].priority = 1.0;
            }
            break;
        }
      }
      s = e;
    }
  }

  /** Populate symmetry data into relocation table
   *
   * @param lines
   * @param symi
   * @param symPosition
   * @param step
   */
  function prepareSymmetry(lines, symi, symPosition, step) {
    var i;
    var j = 0;
    var tmp;
    var abs;
    var distance;
    var newPosition;
    var size = lines.length;
    var max = size - RANGE - 1;
    var min = RANGE;
    var istart = 0;
    var line = null;
    var otherLine = null;
    var symj = 2 * symi - size;
    symPosition *= 2;
    if (symj < 0) {
      symj = 0;
    }
    distance = step * RANGE;
    for (i = symj; i < symi; i++) {
      line = lines[i];
      if (line.symTo !== -1) {
        continue;
      }
      newPosition = line.newPosition;
      line.symTo = 2 * symi - i;
      if (line.symTo > max) {
        line.symTo = max;
      }
      j = line.symTo - istart > RANGE ? -RANGE : -line.symTo + istart;
      if (line.recalculate) {
        while (j < RANGE && line.symTo + j < size - 1) {
          tmp = symPosition - lines[line.symTo + j].newPosition;
          abs = Math.abs(tmp - newPosition);
          if (abs < distance) {
            if (
              (i === 0 || tmp > lines[i - 1].newPosition) &&
              tmp < lines[i + 1].newPosition
            ) {
              distance = abs;
              min = j;
            }
          } else if (tmp < newPosition) {
            break;
          }
          j++;
        }
      } else {
        while (j < RANGE && line.symTo + j < size - 1) {
          if (line.recalculate) {
            tmp = symPosition - lines[line.symTo + j].newPosition;
            abs = Math.abs(tmp - newPosition);
            if (abs < distance) {
              if (
                (i === 0 || tmp > lines[i - 1].newPosition) &&
                tmp < lines[i + 1].newPosition
              ) {
                distance = abs;
                min = j;
              }
            } else if (tmp < newPosition) {
              break;
            }
          }
          j++;
        }
      }
      line.symTo += min;
      otherLine = lines[line.symTo];
      if (
        min === RANGE ||
        line.symTo <= symi ||
        otherLine.symTo !== -1 ||
        otherLine.symRef !== -1
      ) {
        line.symTo = -1;
        continue;
      }
      if (!line.recalculate) {
        line.symTo = -1;
        if (otherLine.symTo !== -1 || !otherLine.recalculate) {
          continue;
        }
        otherLine.symIndex = line.symIndex;
        otherLine.symTo = i;
        istart = line.symTo - 1;
        otherLine.recalculate = false;
        otherLine.dirty = true;
        line.symRef = line.symTo;
        otherLine.newPosition = symPosition - line.newPosition;
      } else {
        if (otherLine.symTo !== -1) {
          line.symTo = -1;
          continue;
        }
        line.symIndex = otherLine.symIndex;
        istart = line.symTo - 1;
        line.recalculate = false;
        line.dirty = true;
        otherLine.symRef = i;
        line.newPosition = symPosition - otherLine.newPosition;
      }
    }
  }

  /** Optimized array copy using Duff's Device.
   *
   * @param from {Array} source array
   * @param fromOffset {number} offset into source array
   * @param to {Array} idealPos array
   * @param toOffset {number} offset into idealPos array
   * @param length {number} elements to copy
   */
  function arrayCopy(from, fromOffset, to, toOffset, length) {
    var n = length % 8;
    while (n--) {
      to[toOffset++] = from[fromOffset++];
    }
    n = (length / 8) | 0;
    while (n--) {
      to[toOffset++] = from[fromOffset++];
      to[toOffset++] = from[fromOffset++];
      to[toOffset++] = from[fromOffset++];
      to[toOffset++] = from[fromOffset++];
      to[toOffset++] = from[fromOffset++];
      to[toOffset++] = from[fromOffset++];
      to[toOffset++] = from[fromOffset++];
      to[toOffset++] = from[fromOffset++];
    }
  }

  /** Apply previously calculated symmetry to image */
  ZoomContext.prototype.doSymmetry = function () {
    var from_offset = 0;
    var to_offset = 0;
    var i;
    var j = 0;
    var buffer = this.image.newBuffer;
    var bufferWidth = this.image.width;
    for (i = 0; i < this.rows.length; i++) {
      if (this.rows[i].symTo >= 0 && !this.rows[this.rows[i].symTo].dirty) {
        from_offset = this.rows[i].symTo * bufferWidth;
        arrayCopy(buffer, from_offset, buffer, to_offset, bufferWidth);
        this.rows[i].dirty = false;
      }
      to_offset += bufferWidth;
    }
    for (i = 0; i < this.columns.length; i++) {
      if (
        this.columns[i].symTo >= 0 &&
        !this.columns[this.columns[i].symTo].dirty
      ) {
        to_offset = i;
        from_offset = this.columns[i].symTo;
        for (j = 0; j < this.rows.length; j++) {
          buffer[to_offset] = buffer[from_offset];
          to_offset += bufferWidth;
          from_offset += bufferWidth;
        }
        this.columns[i].dirty = false;
      }
    }
  };

  /** Build an optimized move table based on relocation table */
  ZoomContext.prototype.prepareMove = function () {
    var move = null;
    var i = 0;
    var j = 0;
    var s = 0;
    while (i < this.columns.length) {
      if (!this.columns[i].dirty) {
        move = this.moveTable[s];
        move.to = i;
        move.length = 1;
        move.from = this.columns[i].symIndex;
        for (j = i + 1; j < this.columns.length; j++) {
          if (
            this.columns[j].dirty ||
            j - this.columns[j].symIndex !== move.to - move.from
          ) {
            break;
          }
          move.length++;
        }
        i = j;
        s++;
      } else {
        i++;
      }
    }
    move = this.moveTable[s];
    move.length = 0;
  };

  /** Execute moves defined in move table */
  ZoomContext.prototype.doMove = function () {
    var move = null;
    var newOffset = 0;
    var oldOffset = 0;
    var from = 0;
    var to = 0;
    var i;
    var s = 0;
    var length = 0;
    var newBuffer = this.image.newBuffer;
    var oldBuffer = this.image.oldBuffer;
    var bufferWidth = this.image.width;
    for (i = 0; i < this.rows.length; i++) {
      if (!this.rows[i].dirty) {
        s = 0;
        oldOffset = this.rows[i].symIndex * bufferWidth;
        while ((move = this.moveTable[s]).length > 0) {
          from = oldOffset + move.from;
          to = newOffset + move.to;
          length = move.length;
          arrayCopy(oldBuffer, from, newBuffer, to, length);
          s++;
        }
      }
      newOffset += bufferWidth;
    }
  };

  /** Shortcut for prepare and execute move */
  ZoomContext.prototype.movePixels = function () {
    this.prepareMove();
    this.doMove();
  };

  /** Prepare fill table based on relocation table */
  ZoomContext.prototype.prepareFill = function () {
    var fill = null;
    var i;
    var j = 0;
    var k = 0;
    var s = 0;
    var n = 0;
    for (i = 0; i < this.columns.length; i++) {
      if (this.columns[i].dirty) {
        j = i - 1;
        for (
          k = i + 1;
          k < this.columns.length && this.columns[k].dirty;
          k++
        ) {}
        while (i < this.columns.length && this.columns[i].dirty) {
          if (
            k < this.columns.length &&
            (j < i ||
              this.columns[i].newPosition - this.columns[j].newPosition >
                this.columns[k].newPosition - this.columns[i].newPosition)
          ) {
            j = k;
          } else if (j < 0) {
            break;
          }
          n = k - i;
          fill = this.fillTable[s];
          fill.length = n;
          fill.from = j;
          fill.to = i;
          while (n > 0) {
            this.columns[i].newPosition = this.columns[j].newPosition;
            this.columns[i].dirty = false;
            n--;
            i++;
          }
          s++;
        }
      }
    }
    fill = this.fillTable[s];
    fill.length = 0;
  };

  /** Apply fill table */
  ZoomContext.prototype.doFill = function () {
    var fill = null;
    var from_offset = 0;
    var to_offset = 0;
    var from = 0;
    var to = 0;
    var i;
    var j = 0;
    var k = 0;
    var t = 0;
    var s = 0;
    var d = 0;
    var buffer = this.image.newBuffer;
    var bufferWidth = this.image.width;
    for (i = 0; i < this.rows.length; i++) {
      if (this.rows[i].dirty) {
        j = i - 1;
        for (k = i + 1; k < this.rows.length && this.rows[k].dirty; k++) {}
        while (i < this.rows.length && this.rows[i].dirty) {
          if (
            k < this.rows.length &&
            (j < i ||
              this.rows[i].newPosition - this.rows[j].newPosition >
                this.rows[k].newPosition - this.rows[i].newPosition)
          ) {
            j = k;
          } else if (j < 0) {
            break;
          }
          to_offset = i * bufferWidth;
          from_offset = j * bufferWidth;
          if (!this.rows[j].dirty) {
            s = 0;
            while ((fill = this.fillTable[s]).length > 0) {
              from = from_offset + fill.from;
              to = from_offset + fill.to;
              for (t = 0; t < fill.length; t++) {
                d = to + t;
                buffer[d] = buffer[from];
              }
              s++;
            }
          }
          arrayCopy(buffer, from_offset, buffer, to_offset, bufferWidth);
          this.rows[i].newPosition = this.rows[j].newPosition;
          this.rows[i].dirty = true;
          i++;
        }
      } else {
        s = 0;
        from_offset = i * bufferWidth;
        while ((fill = this.fillTable[s]).length > 0) {
          from = from_offset + fill.from;
          to = from_offset + fill.to;
          for (t = 0; t < fill.length; t++) {
            d = to + t;
            buffer[d] = buffer[from];
          }
          s++;
        }
        this.rows[i].dirty = true;
      }
    }
  };

  /** Shortcut to prepare and apply fill table */
  ZoomContext.prototype.fill = function () {
    this.prepareFill();
    this.doFill();
  };

  /** Render line using solid guessing
   *
   * @param row
   */
  ZoomContext.prototype.renderRow = function (row) {
    var buffer = this.image.newBuffer;
    var bufferWidth = this.image.width;
    var newPosition = row.newPosition;
    var r = row.index;
    var offset = r * bufferWidth;
    var i;
    var j;
    var k;
    var n;
    var distl;
    var distr;
    var distu;
    var distd;
    var offsetu;
    var offsetd;
    var offsetl;
    var offsetul;
    var offsetur;
    var offsetdl;
    var offsetdr;
    var rend = r - GUESS_RANGE;
    var length;
    var current;
    if (rend < 0) {
      rend = 0;
    }
    for (i = r - 1; i >= rend && this.rows[i].dirty; i--) {}
    distu = r - i;
    rend = r + GUESS_RANGE;
    if (rend >= this.rows.length) {
      rend = this.rows.length - 1;
    }
    for (j = r + 1; j < rend && this.rows[j].dirty; j++) {}
    distd = j - r;
    if (
      !USE_SOLIDGUESS ||
      i < 0 ||
      j >= this.rows.length ||
      this.rows[i].dirty ||
      this.rows[j].dirty
    ) {
      for (k = 0, length = this.columns.length; k < length; k++) {
        current = this.columns[k];
        if (!this.columns[k].dirty) {
          buffer[offset] = this.fractal.formula(
            current.newPosition,
            newPosition
          );
        }
        offset++;
      }
    } else {
      distr = 0;
      distl = Number.MAX_VALUE / 2;
      offsetu = offset - distu * bufferWidth;
      offsetd = offset + distd * bufferWidth;
      for (k = 0, length = this.columns.length; k < length; k++) {
        current = this.columns[k];
        if (!this.columns[k].dirty) {
          if (distr <= 0) {
            rend = k + GUESS_RANGE;
            if (rend >= this.columns.length) {
              rend = this.columns.length - 1;
            }
            for (j = k + 1; j < rend && this.columns[j].dirty; j++) {
              distr = j - k;
            }
            if (j >= rend) {
              distr = Number.MAX_VALUE / 2;
            }
          }
          if (distr < Number.MAX_VALUE / 4 && distl < Number.MAX_VALUE / 4) {
            offsetl = offset - distl;
            offsetul = offsetu - distl;
            offsetdl = offsetd - distl;
            offsetur = offsetu + distr;
            offsetdr = offsetd + distr;
            n = buffer[offsetl];
            if (
              n == buffer[offsetu] &&
              n == buffer[offsetd] &&
              n == buffer[offsetul] &&
              n == buffer[offsetur] &&
              n == buffer[offsetdl] &&
              n == buffer[offsetdr]
            ) {
              buffer[offset] = n;
            } else {
              buffer[offset] = this.fractal.formula(
                current.newPosition,
                newPosition
              );
            }
          } else {
            buffer[offset] = this.fractal.formula(
              current.newPosition,
              newPosition
            );
          }
          distl = 0;
        }
        offset++;
        offsetu++;
        offsetd++;
        distr--;
        distl++;
      }
    }
    row.recalculate = false;
    row.dirty = false;
  };

  /** Render column using solid guessing
   *
   * @param column
   */
  ZoomContext.prototype.renderColumn = function (column) {
    var buffer = this.image.newBuffer;
    var bufferWidth = this.image.width;
    var newPosition = column.newPosition;
    var r = column.index;
    var offset = r;
    var rend = r - GUESS_RANGE;
    var i;
    var j;
    var k;
    var n;
    var distl;
    var distr;
    var distu;
    var distd;
    var offsetl;
    var offsetr;
    var offsetu;
    var offsetlu;
    var offsetru;
    var offsetld;
    var offsetrd;
    var sumu;
    var sumd;
    var length;
    var current;
    if (rend < 0) {
      rend = 0;
    }
    for (i = r - 1; i >= rend && this.columns[i].dirty; i--) {}
    distl = r - i;
    rend = r + GUESS_RANGE;
    if (rend >= this.columns.length) {
      rend = this.columns.length - 1;
    }
    for (j = r + 1; j < rend && this.columns[j].dirty; j++) {}
    distr = j - r;
    if (
      !USE_SOLIDGUESS ||
      i < 0 ||
      j >= this.columns.length ||
      this.columns[i].dirty ||
      this.columns[j].dirty
    ) {
      for (k = 0, length = this.rows.length; k < length; k++) {
        current = this.rows[k];
        if (!this.rows[k].dirty) {
          buffer[offset] = this.fractal.formula(
            newPosition,
            current.newPosition
          );
        }
        offset += bufferWidth;
      }
    } else {
      distd = 0;
      distu = Number.MAX_VALUE / 2;
      offsetl = offset - distl;
      offsetr = offset + distr;
      for (k = 0, length = this.rows.length; k < length; k++) {
        current = this.rows[k];
        if (!this.rows[k].dirty) {
          if (distd <= 0) {
            rend = k + GUESS_RANGE;
            if (rend >= this.rows.length) {
              rend = this.rows.length - 1;
            }
            for (j = k + 1; j < rend && this.rows[j].dirty; j++) {
              distd = j - k;
            }
            if (j >= rend) {
              distd = Number.MAX_VALUE / 2;
            }
          }
          if (distd < Number.MAX_VALUE / 4 && distu < Number.MAX_VALUE / 4) {
            sumu = distu * bufferWidth;
            sumd = distd * bufferWidth;
            offsetu = offset - sumu;
            offsetlu = offsetl - sumu;
            offsetru = offsetr - sumu;
            offsetld = offsetl + sumd;
            offsetrd = offsetr + sumd;
            n = buffer[offsetu];
            if (
              n == buffer[offsetl] &&
              n == buffer[offsetr] &&
              n == buffer[offsetlu] &&
              n == buffer[offsetru] &&
              n == buffer[offsetld] &&
              n == buffer[offsetrd]
            ) {
              buffer[offset] = n;
            } else {
              buffer[offset] = this.fractal.formula(
                newPosition,
                current.newPosition
              );
            }
          } else {
            buffer[offset] = this.fractal.formula(
              newPosition,
              current.newPosition
            );
          }
          distu = 0;
        }
        offset += bufferWidth;
        offsetl += bufferWidth;
        offsetr += bufferWidth;
        distd--;
        distu++;
      }
    }
    column.recalculate = false;
    column.dirty = false;
  };

  /** Calculate whether we're taking too long to render the fractal to meet the idealPos FPS */
  ZoomContext.prototype.tooSlow = function () {
    var newTime = new Date().getTime(),
      minFPS = this.zooming ? this.minFPS : 10;
    return 1000 / (newTime - this.startTime + this.fudgeFactor) < minFPS;
  };

  /** Prioritize calculation of lines between begin and end
   *
   * @param lines - rows or columns to prioritize
   * @param begin - index of first line to prioritize
   * @param end - index of last line to prioritize
   */
  function calcPriority(lines, begin, end) {
    var middle;
    while (begin < end) {
      middle = begin + ((end - begin) >> 1);
      lines[middle].priority =
        (lines[end].newPosition - lines[middle].newPosition) *
        lines[middle].priority;
      if (lines[middle].symRef !== -1) {
        lines[middle].priority /= 2.0;
      }
      calcPriority(lines, begin, middle);
      begin = middle + 1;
    }
  }

  /** Enqueue all the lines to be recalculated and set their priority
   *
   * @param lines - lines to enqueue for calculation
   */
  ZoomContext.prototype.enqueueCalculations = function (lines) {
    var i;
    var j = 0;
    for (i = 0; i < lines.length; i++) {
      if (lines[i].recalculate) {
        for (j = i; j < lines.length && lines[j].recalculate; j++) {
          this.queue[this.queueLength++] = lines[j];
        }
        if (j === lines.length) {
          j -= 1;
        }
        calcPriority(lines, i, j);
        i = j;
      }
    }
  };

  /** Sort calculation queue according to priority (using quicksort)
   *
   * @param queue
   * @param l
   * @param r
   */
  function sortQueue(queue, l, r) {
    var m = (queue[l].priority + queue[r].priority) / 2.0;
    var tmp = null;
    var i = l;
    var j = r;
    do {
      while (queue[i].priority > m) {
        i++;
      }
      while (queue[j].priority < m) {
        j--;
      }
      if (i <= j) {
        tmp = queue[i];
        queue[i] = queue[j];
        queue[j] = tmp;
        i++;
        j--;
      }
    } while (j >= i);
    if (l < j) {
      sortQueue(queue, l, j);
    }
    if (r > i) {
      sortQueue(queue, i, r);
    }
  }

  /** Process the relocation table */
  ZoomContext.prototype.calculate = function () {
    var i, newTime;
    this.incomplete = false;
    this.queueLength = 0;
    this.enqueueCalculations(this.columns);
    this.enqueueCalculations(this.rows);
    if (this.queueLength > 0) {
      if (this.queueLength > 1) {
        sortQueue(this.queue, 0, this.queueLength - 1);
      }
      for (i = 0; i < this.queueLength; i++) {
        if (this.queue[i].isRow) {
          this.renderRow(this.queue[i]);
        } else {
          this.renderColumn(this.queue[i]);
        }
        if (!this.recalculate && this.tooSlow() && i < this.queueLength) {
          this.incomplete = true;
          this.fill();
          break;
        }
      }
    }
  };

  /** Update newPosition array with newly calculated positions */
  ZoomContext.prototype.updatePosition = function () {
    var k;
    var len;
    for (k = 0, len = this.columns.length; k < len; k++) {
      this.columns[k].oldPosition = this.columns[k].newPosition;
    }
    for (k = 0, len = this.rows.length; k < len; k++) {
      this.rows[k].oldPosition = this.rows[k].newPosition;
    }
  };

  /** Calculate FPS achieved and determine if fudge factor needs adjustment for next frame */
  ZoomContext.prototype.updateFPS = function () {
    var fps = 1000 / (new Date().getTime() - this.startTime);
    if (fps < this.minFPS) {
      this.fudgeFactor++;
    } else if (fps > this.minFPS + 10 && this.fudgeFactor > 0) {
      this.fudgeFactor--;
    }
    console.log(fps + " fps");
  };

  /** Overall fractal drawing workflow, calls other functions */
  ZoomContext.prototype.drawFractal = function (recalculate) {
    var area = this.convertArea();
    var symx = this.fractal.symmetry && this.fractal.symmetry.x;
    var symy = this.fractal.symmetry && this.fractal.symmetry.y;
    var stepx, stepy;
    this.startTime = new Date().getTime();
    this.recalculate = recalculate;
    if (recalculate || !USE_XAOS) {
      stepx = this.initialize(this.columns, area.begin.x, area.end.x, false);
      stepy = this.initialize(this.rows, area.begin.y, area.end.y, true);
    } else {
      stepx = this.approximate(this.columns, area.begin.x, area.end.x);
      stepy = this.approximate(this.rows, area.begin.y, area.end.y);
    }
    if (
      USE_SYMMETRY &&
      typeof symy === "number" &&
      !(area.begin.y > symy || symy > area.end.y)
    ) {
      prepareSymmetry(
        this.rows,
        Math.floor((symy - area.begin.y) / stepy),
        symy,
        stepy
      );
    }
    if (
      USE_SYMMETRY &&
      typeof symx === "number" &&
      !(area.begin.x > symx || symx > area.end.x)
    ) {
      prepareSymmetry(
        this.columns,
        Math.floor((symx - area.begin.x) / stepx),
        symx,
        stepx
      );
    }
    this.image.swapBuffers();
    this.movePixels();
    this.calculate();
    if (
      (USE_SYMMETRY && typeof symx === "number") ||
      typeof symy === "number"
    ) {
      this.doSymmetry();
    }
    this.image.paint();
    this.updatePosition();
    this.updateFPS();
  };

  /** Adjust display region to zoom based on mouse buttons */
  ZoomContext.prototype.updateRegion = function (mouse) {
    var MAXSTEP = 0.008 * 3;
    var MUL = 0.3;
    var area = this.convertArea();
    var x =
      area.begin.x + mouse.x * ((area.end.x - area.begin.x) / this.image.width);
    var y =
      area.begin.y +
      mouse.y * ((area.end.y - area.begin.y) / this.image.height);
    var deltax =
      (mouse.oldx - mouse.x) * ((area.end.x - area.begin.x) / this.image.width);
    var deltay =
      (mouse.oldy - mouse.y) *
      ((area.end.y - area.begin.y) / this.image.height);
    var step;
    var mmul;
    if (mouse.button[1] || (mouse.button[0] && mouse.button[2])) {
      // Pan when middle or left+right buttons are pressed
      step = 0;
    } else if (mouse.button[0]) {
      // Zoom in when left button is pressed
      step = MAXSTEP * 2;
    } else if (mouse.button[2]) {
      // Zoom out when right button is pressed
      step = -MAXSTEP * 2;
    } else {
      this.zooming = false;
      return;
    }
    mmul = Math.pow(1 - step, MUL);
    area.begin.x = x + (area.begin.x - x) * mmul;
    area.end.x = x + (area.end.x - x) * mmul;
    area.begin.y = y + (area.begin.y - y) * mmul;
    area.end.y = y + (area.end.y - y) * mmul;
    this.fractal.region.radius.x = area.end.x - area.begin.x;
    this.fractal.region.radius.y = area.end.y - area.begin.y;
    this.fractal.region.center.x = (area.begin.x + area.end.x) / 2;
    this.fractal.region.center.y =
      ((area.begin.y + area.end.y) / 2) *
      (this.image.width / this.image.height);
    this.zooming = true;
  };

  /** Attaches zoomer to specified canvas */
  return function (canvas, fractal) {
    var image = new CanvasImage(canvas);
    var zoomer = new ZoomContext(image, fractal);
    var mouse = { x: 0, y: 0, button: [false, false, false] };

    function doZoom() {
      zoomer.updateRegion(mouse);
      if (zoomer.zooming || zoomer.incomplete) {
        requestAnimationFrame(doZoom);
        zoomer.drawFractal(false);
      }
    }

    canvas.ontouchstart = function (e) {
      if (e.touches.length < 3) {
        var touch = e.touches[0];
        e.touches.length == 2
          ? (mouse.button[2] = true)
          : (mouse.button[2] = false);
        var mouseEvent = new MouseEvent("mousedown", {
          clientX: touch.clientX,
          clientY: touch.clientY,
        });
        canvas.dispatchEvent(mouseEvent);
      }
    };

    canvas.ontouchend = function (e) {
      var mouseEvent = new MouseEvent("mouseup", {});
      canvas.dispatchEvent(mouseEvent);
    };

    canvas.ontouchmove = function (e) {
      var touch = e.touches[0];
      var mouseEvent = new MouseEvent("mousemove", {
        clientX: touch.clientX,
        clientY: touch.clientY,
      });
      canvas.dispatchEvent(mouseEvent);
    };

    canvas.onmousedown = function (e) {
      mouse.button[e.button] = true;
      mouse.x = e.offsetX || e.clientX - canvas.offsetLeft;
      mouse.y = e.offsetY || e.clientY - canvas.offsetTop;
      mouse.oldx = e.offsetX || e.clientX - canvas.offsetLeft;
      mouse.oldy = e.offsetY || e.clientY - canvas.offsetTop;
      doZoom();
    };

    canvas.onmouseup = function (e) {
      mouse.button[e.button] = false;
    };

    canvas.onmousemove = function (e) {
      mouse.x = e.offsetX || e.clientX - canvas.offsetLeft;
      mouse.y = e.offsetY || e.clientY - canvas.offsetTop;
    };

    canvas.oncontextmenu = function () {
      return false;
    };

    canvas.onmouseout = function () {
      mouse.button = [false, false, false];
    };

    zoomer.drawFractal(true);
  };
})();

/** Create the default XaoS color palette */
xaos.defaultPalette = function () {
  var MAXENTRIES = 65536;
  var segmentsize = 8;
  var setsegments = Math.floor((MAXENTRIES + 3) / segmentsize);
  var nsegments = Math.floor(255 / segmentsize);
  var segments = [
    [0, 0, 0],
    [120, 119, 238],
    [24, 7, 25],
    [197, 66, 28],
    [29, 18, 11],
    [135, 46, 71],
    [24, 27, 13],
    [241, 230, 128],
    [17, 31, 24],
    [240, 162, 139],
    [11, 4, 30],
    [106, 87, 189],
    [29, 21, 14],
    [12, 140, 118],
    [10, 6, 29],
    [50, 144, 77],
    [22, 0, 24],
    [148, 188, 243],
    [4, 32, 7],
    [231, 146, 14],
    [10, 13, 20],
    [184, 147, 68],
    [13, 28, 3],
    [169, 248, 152],
    [4, 0, 34],
    [62, 83, 48],
    [7, 21, 22],
    [152, 97, 184],
    [8, 3, 12],
    [247, 92, 235],
    [31, 32, 16],
  ];
  var i, y;
  var r, g, b;
  var rs, gs, bs;
  var palette = [];

  for (i = 0; i < setsegments; i++) {
    r = segments[i % nsegments][0];
    g = segments[i % nsegments][1];
    b = segments[i % nsegments][2];
    rs = (segments[((i + 1) % setsegments) % nsegments][0] - r) / segmentsize;
    gs = (segments[((i + 1) % setsegments) % nsegments][1] - g) / segmentsize;
    bs = (segments[((i + 1) % setsegments) % nsegments][2] - b) / segmentsize;
    for (y = 0; y < segmentsize; y++) {
      palette.push((255 << 24) | (b << 16) | (g << 8) | r);
      r += rs;
      g += gs;
      b += bs;
    }
  }
  return new Uint32Array(palette);
};

xaos.mandelbrot = {
  symmetry: { x: null, y: 0 },
  region: {
    center: { x: -0.75, y: 0.0 },
    radius: { x: 2.5, y: 2.5 },
    angle: 0,
  },
  z0: { x: 0, y: 0 },
  maxiter: 512,
  bailout: 4,
  formula: function (cr, ci) {
    var maxiter = this.maxiter,
      bailout = this.bailout,
      zr = this.z0.x,
      zi = this.z0.y,
      i = maxiter;

    while (i--) {
      var zr2 = zr * zr;
      var zi2 = zi * zi;

      if (zr2 + zi2 > bailout) {
        return this.palette[(maxiter - i) % this.palette.length];
      }

      zi = ci + 2 * zr * zi;
      zr = cr + zr2 - zi2;
    }

    return this.palette[0];
  },
  palette: xaos.defaultPalette(),
};

xaos.zoom(document.getElementById("canvas"), xaos.mandelbrot);
