(function() {
  var Checkpoint, History, MarkerLayer, Patch, SerializationVersion, Transaction;

  Patch = require('superstring').Patch;

  MarkerLayer = require('./marker-layer');

  SerializationVersion = 6;

  Checkpoint = (function() {
    function Checkpoint(id1, snapshot1, isBoundary1) {
      var ref;
      this.id = id1;
      this.snapshot = snapshot1;
      this.isBoundary = isBoundary1;
      if (this.snapshot == null) {
        if ((ref = global.atom) != null) {
          ref.assert(false, "Checkpoint created without snapshot");
        }
        this.snapshot = {};
      }
    }

    return Checkpoint;

  })();

  Transaction = (function() {
    function Transaction(markerSnapshotBefore1, patch1, markerSnapshotAfter1, groupingInterval1) {
      this.markerSnapshotBefore = markerSnapshotBefore1;
      this.patch = patch1;
      this.markerSnapshotAfter = markerSnapshotAfter1;
      this.groupingInterval = groupingInterval1 != null ? groupingInterval1 : 0;
      this.timestamp = Date.now();
    }

    Transaction.prototype.shouldGroupWith = function(previousTransaction) {
      var timeBetweenTransactions;
      timeBetweenTransactions = this.timestamp - previousTransaction.timestamp;
      return timeBetweenTransactions < Math.min(this.groupingInterval, previousTransaction.groupingInterval);
    };

    Transaction.prototype.groupWith = function(previousTransaction) {
      return new Transaction(previousTransaction.markerSnapshotBefore, Patch.compose([previousTransaction.patch, this.patch]), this.markerSnapshotAfter, this.groupingInterval);
    };

    return Transaction;

  })();

  module.exports = History = (function() {
    History.deserialize = function(state, buffer) {
      var history;
      history = new History(buffer);
      history.deserialize(state);
      return history;
    };

    function History(buffer1, maxUndoEntries) {
      this.buffer = buffer1;
      this.maxUndoEntries = maxUndoEntries;
      this.nextCheckpointId = 0;
      this.undoStack = [];
      this.redoStack = [];
    }

    History.prototype.createCheckpoint = function(snapshot, isBoundary) {
      var checkpoint;
      checkpoint = new Checkpoint(this.nextCheckpointId++, snapshot, isBoundary);
      this.undoStack.push(checkpoint);
      return checkpoint.id;
    };

    History.prototype.groupChangesSinceCheckpoint = function(checkpointId, markerSnapshotAfter, deleteCheckpoint) {
      var checkpointIndex, composedPatches, entry, i, j, markerSnapshotBefore, patchesSinceCheckpoint, ref;
      if (deleteCheckpoint == null) {
        deleteCheckpoint = false;
      }
      checkpointIndex = null;
      markerSnapshotBefore = null;
      patchesSinceCheckpoint = [];
      ref = this.undoStack;
      for (i = j = ref.length - 1; j >= 0; i = j += -1) {
        entry = ref[i];
        if (checkpointIndex != null) {
          break;
        }
        switch (entry.constructor) {
          case Checkpoint:
            if (entry.id === checkpointId) {
              checkpointIndex = i;
              markerSnapshotBefore = entry.snapshot;
            } else if (entry.isBoundary) {
              return false;
            }
            break;
          case Transaction:
            patchesSinceCheckpoint.unshift(entry.patch);
            break;
          case Patch:
            patchesSinceCheckpoint.unshift(entry);
            break;
          default:
            throw new Error("Unexpected undo stack entry type: " + entry.constructor.name);
        }
      }
      if (checkpointIndex != null) {
        composedPatches = Patch.compose(patchesSinceCheckpoint);
        if (patchesSinceCheckpoint.length > 0) {
          this.undoStack.splice(checkpointIndex + 1);
          this.undoStack.push(new Transaction(markerSnapshotBefore, composedPatches, markerSnapshotAfter));
        }
        if (deleteCheckpoint) {
          this.undoStack.splice(checkpointIndex, 1);
        }
        return composedPatches;
      } else {
        return false;
      }
    };

    History.prototype.getChangesSinceCheckpoint = function(checkpointId) {
      var checkpointIndex, entry, i, j, patchesSinceCheckpoint, ref;
      checkpointIndex = null;
      patchesSinceCheckpoint = [];
      ref = this.undoStack;
      for (i = j = ref.length - 1; j >= 0; i = j += -1) {
        entry = ref[i];
        if (checkpointIndex != null) {
          break;
        }
        switch (entry.constructor) {
          case Checkpoint:
            if (entry.id === checkpointId) {
              checkpointIndex = i;
            }
            break;
          case Transaction:
            patchesSinceCheckpoint.unshift(entry.patch);
            break;
          case Patch:
            patchesSinceCheckpoint.unshift(entry);
            break;
          default:
            throw new Error("Unexpected undo stack entry type: " + entry.constructor.name);
        }
      }
      if (checkpointIndex != null) {
        return Patch.compose(patchesSinceCheckpoint);
      } else {
        return null;
      }
    };

    History.prototype.enforceUndoStackSizeLimit = function() {
      if (this.undoStack.length > this.maxUndoEntries) {
        return this.undoStack.splice(0, this.undoStack.length - this.maxUndoEntries);
      }
    };

    History.prototype.applyGroupingInterval = function(groupingInterval) {
      var previousEntry, topEntry;
      topEntry = this.undoStack[this.undoStack.length - 1];
      previousEntry = this.undoStack[this.undoStack.length - 2];
      if (topEntry instanceof Transaction) {
        topEntry.groupingInterval = groupingInterval;
      } else {
        return;
      }
      if (groupingInterval === 0) {
        return;
      }
      if (previousEntry instanceof Transaction && topEntry.shouldGroupWith(previousEntry)) {
        return this.undoStack.splice(this.undoStack.length - 2, 2, topEntry.groupWith(previousEntry));
      }
    };

    History.prototype.pushChange = function(arg) {
      var newExtent, newStart, newText, oldExtent, oldText, patch;
      newStart = arg.newStart, oldExtent = arg.oldExtent, newExtent = arg.newExtent, oldText = arg.oldText, newText = arg.newText;
      patch = new Patch;
      patch.splice(newStart, oldExtent, newExtent, oldText, newText);
      this.undoStack.push(patch);
      return this.clearRedoStack();
    };

    History.prototype.popUndoStack = function() {
      var entry, i, j, patch, ref, ref1, snapshotBelow, spliceIndex;
      snapshotBelow = null;
      patch = null;
      spliceIndex = null;
      ref = this.undoStack;
      for (i = j = ref.length - 1; j >= 0; i = j += -1) {
        entry = ref[i];
        if (spliceIndex != null) {
          break;
        }
        switch (entry.constructor) {
          case Checkpoint:
            if (entry.isBoundary) {
              return false;
            }
            break;
          case Transaction:
            snapshotBelow = entry.markerSnapshotBefore;
            patch = entry.patch.invert();
            spliceIndex = i;
            break;
          case Patch:
            patch = entry.invert();
            spliceIndex = i;
            break;
          default:
            throw new Error("Unexpected entry type when popping undoStack: " + entry.constructor.name);
        }
      }
      if (spliceIndex != null) {
        (ref1 = this.redoStack).push.apply(ref1, this.undoStack.splice(spliceIndex).reverse());
        return {
          snapshot: snapshotBelow,
          patch: patch
        };
      } else {
        return false;
      }
    };

    History.prototype.popRedoStack = function() {
      var entry, i, j, patch, ref, ref1, snapshotBelow, spliceIndex;
      snapshotBelow = null;
      patch = null;
      spliceIndex = null;
      ref = this.redoStack;
      for (i = j = ref.length - 1; j >= 0; i = j += -1) {
        entry = ref[i];
        if (spliceIndex != null) {
          break;
        }
        switch (entry.constructor) {
          case Checkpoint:
            if (entry.isBoundary) {
              throw new Error("Invalid redo stack state");
            }
            break;
          case Transaction:
            snapshotBelow = entry.markerSnapshotAfter;
            patch = entry.patch;
            spliceIndex = i;
            break;
          case Patch:
            patch = entry;
            spliceIndex = i;
            break;
          default:
            throw new Error("Unexpected entry type when popping redoStack: " + entry.constructor.name);
        }
      }
      while (this.redoStack[spliceIndex - 1] instanceof Checkpoint) {
        spliceIndex--;
      }
      if (spliceIndex != null) {
        (ref1 = this.undoStack).push.apply(ref1, this.redoStack.splice(spliceIndex).reverse());
        return {
          snapshot: snapshotBelow,
          patch: patch
        };
      } else {
        return false;
      }
    };

    History.prototype.truncateUndoStack = function(checkpointId) {
      var entry, i, j, patchesSinceCheckpoint, ref, snapshotBelow, spliceIndex;
      snapshotBelow = null;
      spliceIndex = null;
      patchesSinceCheckpoint = [];
      ref = this.undoStack;
      for (i = j = ref.length - 1; j >= 0; i = j += -1) {
        entry = ref[i];
        if (spliceIndex != null) {
          break;
        }
        switch (entry.constructor) {
          case Checkpoint:
            if (entry.id === checkpointId) {
              snapshotBelow = entry.snapshot;
              spliceIndex = i;
            } else if (entry.isBoundary) {
              return false;
            }
            break;
          case Transaction:
            patchesSinceCheckpoint.push(entry.patch.invert());
            break;
          default:
            patchesSinceCheckpoint.push(entry.invert());
        }
      }
      if (spliceIndex != null) {
        this.undoStack.splice(spliceIndex);
        return {
          snapshot: snapshotBelow,
          patch: Patch.compose(patchesSinceCheckpoint)
        };
      } else {
        return false;
      }
    };

    History.prototype.clear = function() {
      this.clearUndoStack();
      return this.clearRedoStack();
    };

    History.prototype.clearUndoStack = function() {
      return this.undoStack.length = 0;
    };

    History.prototype.clearRedoStack = function() {
      return this.redoStack.length = 0;
    };

    History.prototype.toString = function() {
      var entry, j, len, output, ref;
      output = '';
      ref = this.undoStack;
      for (j = 0, len = ref.length; j < len; j++) {
        entry = ref[j];
        switch (entry.constructor) {
          case Checkpoint:
            output += "Checkpoint, ";
            break;
          case Transaction:
            output += "Transaction, ";
            break;
          case Patch:
            output += "Patch, ";
            break;
          default:
            output += "Unknown {" + (JSON.stringify(entry)) + "}, ";
        }
      }
      return '[' + output.slice(0, -2) + ']';
    };

    History.prototype.serialize = function(options) {
      return {
        version: SerializationVersion,
        nextCheckpointId: this.nextCheckpointId,
        undoStack: this.serializeStack(this.undoStack, options),
        redoStack: this.serializeStack(this.redoStack, options),
        maxUndoEntries: this.maxUndoEntries
      };
    };

    History.prototype.deserialize = function(state) {
      if (state.version !== SerializationVersion) {
        return;
      }
      this.nextCheckpointId = state.nextCheckpointId;
      this.maxUndoEntries = state.maxUndoEntries;
      this.undoStack = this.deserializeStack(state.undoStack);
      return this.redoStack = this.deserializeStack(state.redoStack);
    };


    /*
    Section: Private
     */

    History.prototype.getCheckpointIndex = function(checkpointId) {
      var entry, i, j, ref;
      ref = this.undoStack;
      for (i = j = ref.length - 1; j >= 0; i = j += -1) {
        entry = ref[i];
        if (entry instanceof Checkpoint && entry.id === checkpointId) {
          return i;
        }
      }
      return null;
    };

    History.prototype.serializeStack = function(stack, options) {
      var entry, j, len, results;
      results = [];
      for (j = 0, len = stack.length; j < len; j++) {
        entry = stack[j];
        switch (entry.constructor) {
          case Checkpoint:
            results.push({
              type: 'checkpoint',
              id: entry.id,
              snapshot: this.serializeSnapshot(entry.snapshot, options),
              isBoundary: entry.isBoundary
            });
            break;
          case Transaction:
            results.push({
              type: 'transaction',
              markerSnapshotBefore: this.serializeSnapshot(entry.markerSnapshotBefore, options),
              markerSnapshotAfter: this.serializeSnapshot(entry.markerSnapshotAfter, options),
              patch: entry.patch.serialize().toString('base64')
            });
            break;
          case Patch:
            results.push({
              type: 'patch',
              data: entry.serialize().toString('base64')
            });
            break;
          default:
            throw new Error("Unexpected undoStack entry type during serialization: " + entry.constructor.name);
        }
      }
      return results;
    };

    History.prototype.deserializeStack = function(stack) {
      var entry, j, len, results;
      results = [];
      for (j = 0, len = stack.length; j < len; j++) {
        entry = stack[j];
        switch (entry.type) {
          case 'checkpoint':
            results.push(new Checkpoint(entry.id, MarkerLayer.deserializeSnapshot(entry.snapshot), entry.isBoundary));
            break;
          case 'transaction':
            results.push(new Transaction(MarkerLayer.deserializeSnapshot(entry.markerSnapshotBefore), Patch.deserialize(Buffer.from(entry.patch, 'base64')), MarkerLayer.deserializeSnapshot(entry.markerSnapshotAfter)));
            break;
          case 'patch':
            results.push(Patch.deserialize(Buffer.from(entry.data, 'base64')));
            break;
          default:
            throw new Error("Unexpected undoStack entry type during deserialization: " + entry.type);
        }
      }
      return results;
    };

    History.prototype.serializeSnapshot = function(snapshot, options) {
      var id, layers, ref;
      if (!options.markerLayers) {
        return;
      }
      layers = {};
      for (id in snapshot) {
        snapshot = snapshot[id];
        if ((ref = this.buffer.getMarkerLayer(id)) != null ? ref.persistent : void 0) {
          layers[id] = snapshot;
        }
      }
      return layers;
    };

    return History;

  })();

}).call(this);
