'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _index = require('../../index.js');

var _index2 = _interopRequireDefault(_index);

var _array = require('../../array.js');

var _array2 = _interopRequireDefault(_array);

var _dom = require('../../dom.js');

var _dom2 = _interopRequireDefault(_dom);

var _extent = require('../../extent.js');

var _extent2 = _interopRequireDefault(_extent);

var _transform = require('../../geom/flat/transform.js');

var _transform2 = _interopRequireDefault(_transform);

var _obj = require('../../obj.js');

var _obj2 = _interopRequireDefault(_obj);

var _replaygroup = require('../replaygroup.js');

var _replaygroup2 = _interopRequireDefault(_replaygroup);

var _replaytype = require('../replaytype.js');

var _replaytype2 = _interopRequireDefault(_replaytype);

var _replay = require('../canvas/replay.js');

var _replay2 = _interopRequireDefault(_replay);

var _imagereplay = require('../canvas/imagereplay.js');

var _imagereplay2 = _interopRequireDefault(_imagereplay);

var _linestringreplay = require('../canvas/linestringreplay.js');

var _linestringreplay2 = _interopRequireDefault(_linestringreplay);

var _polygonreplay = require('../canvas/polygonreplay.js');

var _polygonreplay2 = _interopRequireDefault(_polygonreplay);

var _textreplay = require('../canvas/textreplay.js');

var _textreplay2 = _interopRequireDefault(_textreplay);

var _replay3 = require('../replay.js');

var _replay4 = _interopRequireDefault(_replay3);

var _transform3 = require('../../transform.js');

var _transform4 = _interopRequireDefault(_transform3);

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj };
}

/**
 * @constructor
 * @extends {ol.render.ReplayGroup}
 * @param {number} tolerance Tolerance.
 * @param {ol.Extent} maxExtent Max extent.
 * @param {number} resolution Resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {boolean} overlaps The replay group can have overlapping geometries.
 * @param {?} declutterTree Declutter tree
 * for declutter processing in postrender.
 * @param {number=} opt_renderBuffer Optional rendering buffer.
 * @struct
 */
var _ol_render_canvas_ReplayGroup_ = function (tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree, opt_renderBuffer) {
  _replaygroup2.default.call(this);

  /**
   * Declutter tree.
   * @private
   */
  this.declutterTree_ = declutterTree;

  /**
   * @type {ol.DeclutterGroup}
   * @private
   */
  this.declutterGroup_ = null;

  /**
   * @private
   * @type {number}
   */
  this.tolerance_ = tolerance;

  /**
   * @private
   * @type {ol.Extent}
   */
  this.maxExtent_ = maxExtent;

  /**
   * @private
   * @type {boolean}
   */
  this.overlaps_ = overlaps;

  /**
   * @private
   * @type {number}
   */
  this.pixelRatio_ = pixelRatio;

  /**
   * @private
   * @type {number}
   */
  this.resolution_ = resolution;

  /**
   * @private
   * @type {number|undefined}
   */
  this.renderBuffer_ = opt_renderBuffer;

  /**
   * @private
   * @type {!Object.<string,
   *        Object.<ol.render.ReplayType, ol.render.canvas.Replay>>}
   */
  this.replaysByZIndex_ = {};

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.hitDetectionContext_ = _dom2.default.createCanvasContext2D(1, 1);

  /**
   * @private
   * @type {ol.Transform}
   */
  this.hitDetectionTransform_ = _transform4.default.create();
};

_index2.default.inherits(_ol_render_canvas_ReplayGroup_, _replaygroup2.default);

/**
 * This cache is used for storing calculated pixel circles for increasing performance.
 * It is a static property to allow each Replaygroup to access it.
 * @type {Object.<number, Array.<Array.<(boolean|undefined)>>>}
 * @private
 */
_ol_render_canvas_ReplayGroup_.circleArrayCache_ = {
  0: [[true]]
};

/**
 * This method fills a row in the array from the given coordinate to the
 * middle with `true`.
 * @param {Array.<Array.<(boolean|undefined)>>} array The array that will be altered.
 * @param {number} x X coordinate.
 * @param {number} y Y coordinate.
 * @private
 */
_ol_render_canvas_ReplayGroup_.fillCircleArrayRowToMiddle_ = function (array, x, y) {
  var i;
  var radius = Math.floor(array.length / 2);
  if (x >= radius) {
    for (i = radius; i < x; i++) {
      array[i][y] = true;
    }
  } else if (x < radius) {
    for (i = x + 1; i < radius; i++) {
      array[i][y] = true;
    }
  }
};

/**
 * This methods creates a circle inside a fitting array. Points inside the
 * circle are marked by true, points on the outside are undefined.
 * It uses the midpoint circle algorithm.
 * A cache is used to increase performance.
 * @param {number} radius Radius.
 * @returns {Array.<Array.<(boolean|undefined)>>} An array with marked circle points.
 * @private
 */
_ol_render_canvas_ReplayGroup_.getCircleArray_ = function (radius) {
  if (_ol_render_canvas_ReplayGroup_.circleArrayCache_[radius] !== undefined) {
    return _ol_render_canvas_ReplayGroup_.circleArrayCache_[radius];
  }

  var arraySize = radius * 2 + 1;
  var arr = new Array(arraySize);
  for (var i = 0; i < arraySize; i++) {
    arr[i] = new Array(arraySize);
  }

  var x = radius;
  var y = 0;
  var error = 0;

  while (x >= y) {
    _ol_render_canvas_ReplayGroup_.fillCircleArrayRowToMiddle_(arr, radius + x, radius + y);
    _ol_render_canvas_ReplayGroup_.fillCircleArrayRowToMiddle_(arr, radius + y, radius + x);
    _ol_render_canvas_ReplayGroup_.fillCircleArrayRowToMiddle_(arr, radius - y, radius + x);
    _ol_render_canvas_ReplayGroup_.fillCircleArrayRowToMiddle_(arr, radius - x, radius + y);
    _ol_render_canvas_ReplayGroup_.fillCircleArrayRowToMiddle_(arr, radius - x, radius - y);
    _ol_render_canvas_ReplayGroup_.fillCircleArrayRowToMiddle_(arr, radius - y, radius - x);
    _ol_render_canvas_ReplayGroup_.fillCircleArrayRowToMiddle_(arr, radius + y, radius - x);
    _ol_render_canvas_ReplayGroup_.fillCircleArrayRowToMiddle_(arr, radius + x, radius - y);

    y++;
    error += 1 + 2 * y;
    if (2 * (error - x) + 1 > 0) {
      x -= 1;
      error += 1 - 2 * x;
    }
  }

  _ol_render_canvas_ReplayGroup_.circleArrayCache_[radius] = arr;
  return arr;
};

/**
 * @param {!Object.<string, Array.<*>>} declutterReplays Declutter replays.
 * @param {CanvasRenderingContext2D} context Context.
 * @param {number} rotation Rotation.
 */
_ol_render_canvas_ReplayGroup_.replayDeclutter = function (declutterReplays, context, rotation) {
  var zs = Object.keys(declutterReplays).map(Number).sort(_array2.default.numberSafeCompareFunction);
  var skippedFeatureUids = {};
  for (var z = 0, zz = zs.length; z < zz; ++z) {
    var replayData = declutterReplays[zs[z].toString()];
    for (var i = 0, ii = replayData.length; i < ii;) {
      var replay = replayData[i++];
      var transform = replayData[i++];
      replay.replay(context, transform, rotation, skippedFeatureUids);
    }
  }
};

/**
 * @param {boolean} group Group with previous replay.
 * @return {ol.DeclutterGroup} Declutter instruction group.
 */
_ol_render_canvas_ReplayGroup_.prototype.addDeclutter = function (group) {
  var declutter = null;
  if (this.declutterTree_) {
    if (group) {
      declutter = this.declutterGroup_;
      /** @type {number} */declutter[4]++;
    } else {
      declutter = this.declutterGroup_ = _extent2.default.createEmpty();
      declutter.push(1);
    }
  }
  return declutter;
};

/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {ol.Transform} transform Transform.
 */
_ol_render_canvas_ReplayGroup_.prototype.clip = function (context, transform) {
  var flatClipCoords = this.getClipCoords(transform);
  context.beginPath();
  context.moveTo(flatClipCoords[0], flatClipCoords[1]);
  context.lineTo(flatClipCoords[2], flatClipCoords[3]);
  context.lineTo(flatClipCoords[4], flatClipCoords[5]);
  context.lineTo(flatClipCoords[6], flatClipCoords[7]);
  context.clip();
};

/**
 * @param {Array.<ol.render.ReplayType>} replays Replays.
 * @return {boolean} Has replays of the provided types.
 */
_ol_render_canvas_ReplayGroup_.prototype.hasReplays = function (replays) {
  for (var zIndex in this.replaysByZIndex_) {
    var candidates = this.replaysByZIndex_[zIndex];
    for (var i = 0, ii = replays.length; i < ii; ++i) {
      if (replays[i] in candidates) {
        return true;
      }
    }
  }
  return false;
};

/**
 * FIXME empty description for jsdoc
 */
_ol_render_canvas_ReplayGroup_.prototype.finish = function () {
  var zKey;
  for (zKey in this.replaysByZIndex_) {
    var replays = this.replaysByZIndex_[zKey];
    var replayKey;
    for (replayKey in replays) {
      replays[replayKey].finish();
    }
  }
};

/**
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {number} resolution Resolution.
 * @param {number} rotation Rotation.
 * @param {number} hitTolerance Hit tolerance in pixels.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *     to skip.
 * @param {function((ol.Feature|ol.render.Feature)): T} callback Feature
 *     callback.
 * @param {Object.<string, ol.DeclutterGroup>} declutterReplays Declutter
 *     replays.
 * @return {T|undefined} Callback result.
 * @template T
 */
_ol_render_canvas_ReplayGroup_.prototype.forEachFeatureAtCoordinate = function (coordinate, resolution, rotation, hitTolerance, skippedFeaturesHash, callback, declutterReplays) {

  hitTolerance = Math.round(hitTolerance);
  var contextSize = hitTolerance * 2 + 1;
  var transform = _transform4.default.compose(this.hitDetectionTransform_, hitTolerance + 0.5, hitTolerance + 0.5, 1 / resolution, -1 / resolution, -rotation, -coordinate[0], -coordinate[1]);
  var context = this.hitDetectionContext_;

  if (context.canvas.width !== contextSize || context.canvas.height !== contextSize) {
    context.canvas.width = contextSize;
    context.canvas.height = contextSize;
  } else {
    context.clearRect(0, 0, contextSize, contextSize);
  }

  /**
   * @type {ol.Extent}
   */
  var hitExtent;
  if (this.renderBuffer_ !== undefined) {
    hitExtent = _extent2.default.createEmpty();
    _extent2.default.extendCoordinate(hitExtent, coordinate);
    _extent2.default.buffer(hitExtent, resolution * (this.renderBuffer_ + hitTolerance), hitExtent);
  }

  var mask = _ol_render_canvas_ReplayGroup_.getCircleArray_(hitTolerance);
  var declutteredFeatures;
  if (this.declutterTree_) {
    declutteredFeatures = this.declutterTree_.all().map(function (entry) {
      return entry.value;
    });
  }

  /**
   * @param {ol.Feature|ol.render.Feature} feature Feature.
   * @return {?} Callback result.
   */
  function hitDetectionCallback(feature) {
    var imageData = context.getImageData(0, 0, contextSize, contextSize).data;
    for (var i = 0; i < contextSize; i++) {
      for (var j = 0; j < contextSize; j++) {
        if (mask[i][j]) {
          if (imageData[(j * contextSize + i) * 4 + 3] > 0) {
            var result;
            if (!declutteredFeatures || declutteredFeatures.indexOf(feature) !== -1) {
              result = callback(feature);
            }
            if (result) {
              return result;
            } else {
              context.clearRect(0, 0, contextSize, contextSize);
              return undefined;
            }
          }
        }
      }
    }
  }

  return this.replayHitDetection_(context, transform, rotation, skippedFeaturesHash, hitDetectionCallback, hitExtent, declutterReplays);
};

/**
 * @param {ol.Transform} transform Transform.
 * @return {Array.<number>} Clip coordinates.
 */
_ol_render_canvas_ReplayGroup_.prototype.getClipCoords = function (transform) {
  var maxExtent = this.maxExtent_;
  var minX = maxExtent[0];
  var minY = maxExtent[1];
  var maxX = maxExtent[2];
  var maxY = maxExtent[3];
  var flatClipCoords = [minX, minY, minX, maxY, maxX, maxY, maxX, minY];
  _transform2.default.transform2D(flatClipCoords, 0, 8, 2, transform, flatClipCoords);
  return flatClipCoords;
};

/**
 * @inheritDoc
 */
_ol_render_canvas_ReplayGroup_.prototype.getReplay = function (zIndex, replayType) {
  var zIndexKey = zIndex !== undefined ? zIndex.toString() : '0';
  var replays = this.replaysByZIndex_[zIndexKey];
  if (replays === undefined) {
    replays = {};
    this.replaysByZIndex_[zIndexKey] = replays;
  }
  var replay = replays[replayType];
  if (replay === undefined) {
    var Constructor = _ol_render_canvas_ReplayGroup_.BATCH_CONSTRUCTORS_[replayType];
    replay = new Constructor(this.tolerance_, this.maxExtent_, this.resolution_, this.pixelRatio_, this.overlaps_, this.declutterTree_);
    replays[replayType] = replay;
  }
  return replay;
};

/**
 * @return {Object.<string, Object.<ol.render.ReplayType, ol.render.canvas.Replay>>} Replays.
 */
_ol_render_canvas_ReplayGroup_.prototype.getReplays = function () {
  return this.replaysByZIndex_;
};

/**
 * @inheritDoc
 */
_ol_render_canvas_ReplayGroup_.prototype.isEmpty = function () {
  return _obj2.default.isEmpty(this.replaysByZIndex_);
};

/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {ol.Transform} transform Transform.
 * @param {number} viewRotation View rotation.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *     to skip.
 * @param {Array.<ol.render.ReplayType>=} opt_replayTypes Ordered replay types
 *     to replay. Default is {@link ol.render.replay.ORDER}
 * @param {Object.<string, ol.DeclutterGroup>=} opt_declutterReplays Declutter
 *     replays.
 */
_ol_render_canvas_ReplayGroup_.prototype.replay = function (context, transform, viewRotation, skippedFeaturesHash, opt_replayTypes, opt_declutterReplays) {

  /** @type {Array.<number>} */
  var zs = Object.keys(this.replaysByZIndex_).map(Number);
  zs.sort(_array2.default.numberSafeCompareFunction);

  // setup clipping so that the parts of over-simplified geometries are not
  // visible outside the current extent when panning
  context.save();
  this.clip(context, transform);

  var replayTypes = opt_replayTypes ? opt_replayTypes : _replay4.default.ORDER;
  var i, ii, j, jj, replays, replay;
  for (i = 0, ii = zs.length; i < ii; ++i) {
    var zIndexKey = zs[i].toString();
    replays = this.replaysByZIndex_[zIndexKey];
    for (j = 0, jj = replayTypes.length; j < jj; ++j) {
      var replayType = replayTypes[j];
      replay = replays[replayType];
      if (replay !== undefined) {
        if (opt_declutterReplays && (replayType == _replaytype2.default.IMAGE || replayType == _replaytype2.default.TEXT)) {
          var declutter = opt_declutterReplays[zIndexKey];
          if (!declutter) {
            opt_declutterReplays[zIndexKey] = [replay, transform.slice(0)];
          } else {
            declutter.push(replay, transform.slice(0));
          }
        } else {
          replay.replay(context, transform, viewRotation, skippedFeaturesHash);
        }
      }
    }
  }

  context.restore();
};

/**
 * @private
 * @param {CanvasRenderingContext2D} context Context.
 * @param {ol.Transform} transform Transform.
 * @param {number} viewRotation View rotation.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *     to skip.
 * @param {function((ol.Feature|ol.render.Feature)): T} featureCallback
 *     Feature callback.
 * @param {ol.Extent=} opt_hitExtent Only check features that intersect this
 *     extent.
 * @param {Object.<string, ol.DeclutterGroup>=} opt_declutterReplays Declutter
 *     replays.
 * @return {T|undefined} Callback result.
 * @template T
 */
_ol_render_canvas_ReplayGroup_.prototype.replayHitDetection_ = function (context, transform, viewRotation, skippedFeaturesHash, featureCallback, opt_hitExtent, opt_declutterReplays) {
  /** @type {Array.<number>} */
  var zs = Object.keys(this.replaysByZIndex_).map(Number);
  zs.sort(_array2.default.numberSafeCompareFunction);

  var i, j, replays, replay, result;
  for (i = zs.length - 1; i >= 0; --i) {
    var zIndexKey = zs[i].toString();
    replays = this.replaysByZIndex_[zIndexKey];
    for (j = _replay4.default.ORDER.length - 1; j >= 0; --j) {
      var replayType = _replay4.default.ORDER[j];
      replay = replays[replayType];
      if (replay !== undefined) {
        if (opt_declutterReplays && (replayType == _replaytype2.default.IMAGE || replayType == _replaytype2.default.TEXT)) {
          var declutter = opt_declutterReplays[zIndexKey];
          if (!declutter) {
            opt_declutterReplays[zIndexKey] = [replay, transform.slice(0)];
          } else {
            declutter.push(replay, transform.slice(0));
          }
        } else {
          result = replay.replayHitDetection(context, transform, viewRotation, skippedFeaturesHash, featureCallback, opt_hitExtent);
          if (result) {
            return result;
          }
        }
      }
    }
  }
  return undefined;
};

/**
 * @const
 * @private
 * @type {Object.<ol.render.ReplayType,
 *                function(new: ol.render.canvas.Replay, number, ol.Extent,
 *                number, number, boolean, Array.<ol.DeclutterGroup>)>}
 */
_ol_render_canvas_ReplayGroup_.BATCH_CONSTRUCTORS_ = {
  'Circle': _polygonreplay2.default,
  'Default': _replay2.default,
  'Image': _imagereplay2.default,
  'LineString': _linestringreplay2.default,
  'Polygon': _polygonreplay2.default,
  'Text': _textreplay2.default
};
exports.default = _ol_render_canvas_ReplayGroup_;
module.exports = exports['default'];