/// 
/**
 * Distance Path algorithm implementation
 */
class DistancePath extends Algorithm {
  /**
   * Pixels updated by last iteration. Value = x<<16 + y
   */
  private last: number[];
  /**
   * Pixels updated by current iteration. Value = x<<16 + y
   */
  private next: number[];
  /**
   * Weight of a block (Pixel with value 0)
   */
  private weightBlock: number;
  /**
   * Weight of a path (Pixel with value 255)
   */
  private weightPath: number;
  /**
   * Treshold value when a pixel becomes a path (0...255)
   */
  private treshold: number;
  /**
   * Maximum distance before path becomes no longer traversable
   */
  private maxDistance: number;
  /**
   * Input pixel data (RGBA)
   */
  private inRAW: Uint8ClampedArray;
  /**
   * Temporary distance map
   */
  private outRAW: number[];
  /**
   * Constructor
   * @param config Parameters from UI
   */
  constructor(config: Object) {
    super();
    this.last = [];
    this.next = [];
    this.weightBlock = parseFloat(config['weightblock']);
    this.weightPath = parseFloat(config['weightpath']);
    this.treshold = parseInt(config['treshold'], 10);
    this.maxDistance = parseFloat(config['maxdistance']);
    // start pixel
    var start = config['startpoints'].split(" ");
    for (var i = 0; i < start.length; ++i) {
      var pos = start[i].split(",");
      this.last.push((parseInt(pos[0]) << 16) + parseInt(pos[1]));
    }
  }
  /**
   * Process a single pixel.
   * @param x X-coordinate
   * @param y Y-coordinate
   * @param lastValue Last value of the pixel that triggered (re)calculation
   * @param width image width
   */
  processPixel(x: number, y: number, lastValue: number, width: number) {
    var offset = (y * width + x);
    var inOffset = offset * 4;
    var avg = (this.inRAW[inOffset] + this.inRAW[inOffset + 1] + this.inRAW[inOffset + 2]) / 765;
    var newValue = lastValue + (1 - avg) * (this.weightBlock - this.weightPath) + this.weightPath;
    if (newValue > this.maxDistance) newValue = this.maxDistance;
    if (newValue < this.outRAW[offset]) {
      this.next.push((x << 16) + y);
      this.outRAW[offset] = newValue;
    }
  }
  /**
   * Run the algorithm
   */
  run() {
    var width = this.input.getWidth();
    var height = this.input.getHeight();
    this.inRAW = this.input.getPixels8();
    var end = width * height;
    // allocate output storage and set to m_maxDistance
    this.outRAW = new Array(width * height);
    for (var i = 0; i < end; i++) this.outRAW[i] = this.maxDistance; 
    // set start pixels to distance of 0
    this.last.forEach((coordinate) => {
      var x = coordinate >> 16;
      var y = coordinate & 0xFFFF;
      this.outRAW[(width * y + x)] = 0;
    });
  
    var outPixels = this.output.getPixels8();
    var iteration = 0;
    do {
      // process pixel from last time
      for (var i = 0; i < this.last.length; ++i) {
        var coordinate = this.last[i];
        var x = coordinate >> 16;
        var y = coordinate & 0xFFFF;
        var lastValue = this.outRAW[y * width + x];
        if (y > 0) {  // process row on top
          if (x > 0) this.processPixel(x - 1, y - 1, lastValue, width);
          this.processPixel(x, y - 1, lastValue, width);
          if (x < (width - 1)) this.processPixel(x + 1, y - 1, lastValue, width);
        }
        if (x > 0) this.processPixel(x - 1, y, lastValue, width);
        if (x < (width - 1)) this.processPixel(x + 1, y, lastValue, width);
        if (y < (height - 1)) {  // process row on bottom
          if (x > 0) this.processPixel(x - 1, y + 1, lastValue, width);
          this.processPixel(x, y + 1, lastValue, width);
          if (x < (width - 1)) this.processPixel(x + 1, y + 1, lastValue, width);
        }
      }
      ++iteration;
      // clear out old list
      this.last = this.next;
      this.next = [];
      if (iteration % 1000 == 0) {
        this.callback.messageCallback("iteration: " + iteration, false);
      }
      if (iteration > 10000) break;
    }  while (this.last.length > 0);
    // find max value
    var max = 0;
    var threshold = this.treshold * 3;  // multiply by 3 here saves many division from sum to average in the loops below
    // could be optimized, but it is only run once for every pixel
    for (var i = 0; i < end; ++i) {
      var offset = i * 4;
      var sum = (this.inRAW[offset] + this.inRAW[offset + 1] + this.inRAW[offset + 2]);
      // only update max if it is larger than previous, but still smaller than our max distance and larger as the threshhold (visible path cheat)
      if (this.outRAW[i] > max && this.outRAW[i] < this.maxDistance && (sum > threshold)) max = this.outRAW[i];
    }
    this.callback.messageCallback("took " + iteration + " iterations max value: " + max, false);
    max = 65535.0 / max;  // results in 0 ... 65535 range
    // write result to output
    var inOffset = 0;
    var outOffset = 0;
    for (var i = 0; i < end; ++i) {
      var sum = (this.inRAW[inOffset++] + this.inRAW[inOffset++] + this.inRAW[inOffset++]);
      ++inOffset;
      if (sum > threshold) {
        var value = this.outRAW[i] * max;
        outPixels[outOffset++] = value >> 8;    // red = distance high byte
        outPixels[outOffset++] = value & 0xFF;  // green = distance low byte
        outPixels[outOffset++] = 255;           // blue= a pathable pixel
      } else {
        outPixels[outOffset++] = 0;
        outPixels[outOffset++] = 0;
        outPixels[outOffset++] = 0;
      }
    }
  }
}