(function() {
  var Delegator, Emitter, Grim, Marker, OptionKeys, Point, Range, extend, isEqual, omit, pick, ref, size;

  ref = require('underscore-plus'), extend = ref.extend, isEqual = ref.isEqual, omit = ref.omit, pick = ref.pick, size = ref.size;

  Emitter = require('event-kit').Emitter;

  Delegator = require('delegato');

  Point = require('./point');

  Range = require('./range');

  Grim = require('grim');

  OptionKeys = new Set(['reversed', 'tailed', 'invalidate', 'exclusive']);

  module.exports = Marker = (function() {
    Delegator.includeInto(Marker);

    Marker.extractParams = function(inputParams) {
      var containsCustomProperties, i, key, len, outputParams, ref1;
      outputParams = {};
      containsCustomProperties = false;
      if (inputParams != null) {
        ref1 = Object.keys(inputParams);
        for (i = 0, len = ref1.length; i < len; i++) {
          key = ref1[i];
          if (OptionKeys.has(key)) {
            outputParams[key] = inputParams[key];
          } else if (key === 'clipDirection' || key === 'skipSoftWrapIndentation') {

          } else {
            containsCustomProperties = true;
            if (outputParams.properties == null) {
              outputParams.properties = {};
            }
            outputParams.properties[key] = inputParams[key];
          }
        }
      }
      if (containsCustomProperties) {
        Grim.deprecate("Assigning custom properties to a marker when creating/copying it is\ndeprecated. Please, consider storing the custom properties you need in\nsome other object in your package, keyed by the marker's id property.");
      }
      return outputParams;
    };

    Marker.delegatesMethods('containsPoint', 'containsRange', 'intersectsRow', {
      toMethod: 'getRange'
    });

    function Marker(id, layer, range, params) {
      this.id = id;
      this.layer = layer;
      this.tailed = params.tailed, this.reversed = params.reversed, this.valid = params.valid, this.invalidate = params.invalidate, this.exclusive = params.exclusive, this.properties = params.properties;
      this.emitter = new Emitter;
      if (this.tailed == null) {
        this.tailed = true;
      }
      if (this.reversed == null) {
        this.reversed = false;
      }
      if (this.valid == null) {
        this.valid = true;
      }
      if (this.invalidate == null) {
        this.invalidate = 'overlap';
      }
      if (this.properties == null) {
        this.properties = {};
      }
      this.hasChangeObservers = false;
      this.rangeWhenDestroyed = null;
      Object.freeze(this.properties);
      this.layer.setMarkerIsExclusive(this.id, this.isExclusive());
    }


    /*
    Section: Event Subscription
     */

    Marker.prototype.onDidDestroy = function(callback) {
      this.layer.markersWithDestroyListeners.add(this);
      return this.emitter.on('did-destroy', callback);
    };

    Marker.prototype.onDidChange = function(callback) {
      if (!this.hasChangeObservers) {
        this.previousEventState = this.getSnapshot(this.getRange());
        this.hasChangeObservers = true;
        this.layer.markersWithChangeListeners.add(this);
      }
      return this.emitter.on('did-change', callback);
    };

    Marker.prototype.getRange = function() {
      var ref1;
      return (ref1 = this.rangeWhenDestroyed) != null ? ref1 : this.layer.getMarkerRange(this.id);
    };

    Marker.prototype.setRange = function(range, params) {
      if (params == null) {
        params = {};
      }
      return this.update(this.getRange(), {
        reversed: params.reversed,
        tailed: true,
        range: Range.fromObject(range, true),
        exclusive: params.exclusive
      });
    };

    Marker.prototype.getHeadPosition = function() {
      if (this.reversed) {
        return this.getStartPosition();
      } else {
        return this.getEndPosition();
      }
    };

    Marker.prototype.setHeadPosition = function(position) {
      var oldRange, params;
      position = Point.fromObject(position);
      oldRange = this.getRange();
      params = {};
      if (this.hasTail()) {
        if (this.isReversed()) {
          if (position.isLessThan(oldRange.end)) {
            params.range = new Range(position, oldRange.end);
          } else {
            params.reversed = false;
            params.range = new Range(oldRange.end, position);
          }
        } else {
          if (position.isLessThan(oldRange.start)) {
            params.reversed = true;
            params.range = new Range(position, oldRange.start);
          } else {
            params.range = new Range(oldRange.start, position);
          }
        }
      } else {
        params.range = new Range(position, position);
      }
      return this.update(oldRange, params);
    };

    Marker.prototype.getTailPosition = function() {
      if (this.reversed) {
        return this.getEndPosition();
      } else {
        return this.getStartPosition();
      }
    };

    Marker.prototype.setTailPosition = function(position) {
      var oldRange, params;
      position = Point.fromObject(position);
      oldRange = this.getRange();
      params = {
        tailed: true
      };
      if (this.reversed) {
        if (position.isLessThan(oldRange.start)) {
          params.reversed = false;
          params.range = new Range(position, oldRange.start);
        } else {
          params.range = new Range(oldRange.start, position);
        }
      } else {
        if (position.isLessThan(oldRange.end)) {
          params.range = new Range(position, oldRange.end);
        } else {
          params.reversed = true;
          params.range = new Range(oldRange.end, position);
        }
      }
      return this.update(oldRange, params);
    };

    Marker.prototype.getStartPosition = function() {
      var ref1, ref2;
      return (ref1 = (ref2 = this.rangeWhenDestroyed) != null ? ref2.start : void 0) != null ? ref1 : this.layer.getMarkerStartPosition(this.id);
    };

    Marker.prototype.getEndPosition = function() {
      var ref1, ref2;
      return (ref1 = (ref2 = this.rangeWhenDestroyed) != null ? ref2.end : void 0) != null ? ref1 : this.layer.getMarkerEndPosition(this.id);
    };

    Marker.prototype.clearTail = function() {
      var headPosition;
      headPosition = this.getHeadPosition();
      return this.update(this.getRange(), {
        tailed: false,
        reversed: false,
        range: Range(headPosition, headPosition)
      });
    };

    Marker.prototype.plantTail = function() {
      var headPosition;
      if (!this.hasTail()) {
        headPosition = this.getHeadPosition();
        return this.update(this.getRange(), {
          tailed: true,
          range: new Range(headPosition, headPosition)
        });
      }
    };

    Marker.prototype.isReversed = function() {
      return this.tailed && this.reversed;
    };

    Marker.prototype.hasTail = function() {
      return this.tailed;
    };

    Marker.prototype.isValid = function() {
      return !this.isDestroyed() && this.valid;
    };

    Marker.prototype.isDestroyed = function() {
      return this.layer.isDestroyed() || (this.rangeWhenDestroyed != null);
    };

    Marker.prototype.isExclusive = function() {
      if (this.exclusive != null) {
        return this.exclusive;
      } else {
        return this.getInvalidationStrategy() === 'inside' || !this.hasTail();
      }
    };

    Marker.prototype.isEqual = function(other) {
      return this.invalidate === other.invalidate && this.tailed === other.tailed && this.reversed === other.reversed && isEqual(this.properties, other.properties) && this.getRange().isEqual(other.getRange());
    };

    Marker.prototype.getInvalidationStrategy = function() {
      return this.invalidate;
    };

    Marker.prototype.getProperties = function() {
      return this.properties;
    };

    Marker.prototype.setProperties = function(properties) {
      return this.update(this.getRange(), {
        properties: extend({}, this.properties, properties)
      });
    };

    Marker.prototype.copy = function(options) {
      var snapshot;
      if (options == null) {
        options = {};
      }
      snapshot = this.getSnapshot(null);
      options = Marker.extractParams(options);
      return this.layer.createMarker(this.getRange(), extend({}, snapshot, options, {
        properties: extend({}, snapshot.properties, options.properties)
      }));
    };

    Marker.prototype.destroy = function() {
      if (this.rangeWhenDestroyed != null) {
        return;
      }
      this.layer.markersWithChangeListeners["delete"](this);
      this.layer.markersWithDestroyListeners["delete"](this);
      this.rangeWhenDestroyed = this.getRange();
      this.layer.destroyMarker(this.id);
      return this.emitter.emit('did-destroy');
    };

    Marker.prototype.compare = function(other) {
      if ((this.rangeWhenDestroyed != null) || (other.rangeWhenDestroyed != null)) {
        return this.getRange().compare(other.getRange());
      } else {
        return this.layer.compareMarkers(this.id, other.id);
      }
    };

    Marker.prototype.matchesParams = function(params) {
      var i, key, len, ref1;
      ref1 = Object.keys(params);
      for (i = 0, len = ref1.length; i < len; i++) {
        key = ref1[i];
        if (!this.matchesParam(key, params[key])) {
          return false;
        }
      }
      return true;
    };

    Marker.prototype.matchesParam = function(key, value) {
      switch (key) {
        case 'startPosition':
          return this.getStartPosition().isEqual(value);
        case 'endPosition':
          return this.getEndPosition().isEqual(value);
        case 'containsPoint':
        case 'containsPosition':
          return this.containsPoint(value);
        case 'containsRange':
          return this.containsRange(value);
        case 'startRow':
          return this.getStartPosition().row === value;
        case 'endRow':
          return this.getEndPosition().row === value;
        case 'intersectsRow':
          return this.intersectsRow(value);
        case 'invalidate':
        case 'reversed':
        case 'tailed':
          return isEqual(this[key], value);
        case 'valid':
          return this.isValid() === value;
        default:
          return isEqual(this.properties[key], value);
      }
    };

    Marker.prototype.update = function(oldRange, arg, textChanged) {
      var exclusive, properties, propertiesChanged, range, reversed, tailed, updated, valid, wasExclusive;
      range = arg.range, reversed = arg.reversed, tailed = arg.tailed, valid = arg.valid, exclusive = arg.exclusive, properties = arg.properties;
      if (textChanged == null) {
        textChanged = false;
      }
      if (this.isDestroyed()) {
        return;
      }
      wasExclusive = this.isExclusive();
      updated = propertiesChanged = false;
      if ((range != null) && !range.isEqual(oldRange)) {
        this.layer.setMarkerRange(this.id, range);
        updated = true;
      }
      if ((reversed != null) && reversed !== this.reversed) {
        this.reversed = reversed;
        updated = true;
      }
      if ((tailed != null) && tailed !== this.tailed) {
        this.tailed = tailed;
        updated = true;
      }
      if ((valid != null) && valid !== this.valid) {
        this.valid = valid;
        updated = true;
      }
      if ((exclusive != null) && exclusive !== this.exclusive) {
        this.exclusive = exclusive;
        updated = true;
      }
      if (wasExclusive !== this.isExclusive()) {
        this.layer.setMarkerIsExclusive(this.id, this.isExclusive());
        updated = true;
      }
      if ((properties != null) && !isEqual(properties, this.properties)) {
        this.properties = Object.freeze(properties);
        propertiesChanged = true;
        updated = true;
      }
      this.emitChangeEvent(range != null ? range : oldRange, textChanged, propertiesChanged);
      if (updated && !textChanged) {
        this.layer.markerUpdated();
      }
      return updated;
    };

    Marker.prototype.getSnapshot = function(range) {
      return Object.freeze({
        range: range,
        properties: this.properties,
        reversed: this.reversed,
        tailed: this.tailed,
        valid: this.valid,
        invalidate: this.invalidate,
        exclusive: this.exclusive
      });
    };

    Marker.prototype.toString = function() {
      return "[Marker " + this.id + ", " + (this.getRange()) + "]";
    };


    /*
    Section: Private
     */

    Marker.prototype.inspect = function() {
      return this.toString();
    };

    Marker.prototype.emitChangeEvent = function(currentRange, textChanged, propertiesChanged) {
      var newHeadPosition, newState, newTailPosition, oldHeadPosition, oldState, oldTailPosition;
      if (!this.hasChangeObservers) {
        return;
      }
      oldState = this.previousEventState;
      if (currentRange == null) {
        currentRange = this.getRange();
      }
      if (!(propertiesChanged || oldState.valid !== this.valid || oldState.tailed !== this.tailed || oldState.reversed !== this.reversed || oldState.range.compare(currentRange) !== 0)) {
        return false;
      }
      newState = this.previousEventState = this.getSnapshot(currentRange);
      if (oldState.reversed) {
        oldHeadPosition = oldState.range.start;
        oldTailPosition = oldState.range.end;
      } else {
        oldHeadPosition = oldState.range.end;
        oldTailPosition = oldState.range.start;
      }
      if (newState.reversed) {
        newHeadPosition = newState.range.start;
        newTailPosition = newState.range.end;
      } else {
        newHeadPosition = newState.range.end;
        newTailPosition = newState.range.start;
      }
      this.emitter.emit("did-change", {
        wasValid: oldState.valid,
        isValid: newState.valid,
        hadTail: oldState.tailed,
        hasTail: newState.tailed,
        oldProperties: oldState.properties,
        newProperties: newState.properties,
        oldHeadPosition: oldHeadPosition,
        newHeadPosition: newHeadPosition,
        oldTailPosition: oldTailPosition,
        newTailPosition: newTailPosition,
        textChanged: textChanged
      });
      return true;
    };

    return Marker;

  })();

}).call(this);
