// translation of https://github.com/djspiewak/cccp/blob/master/agent/src/main/scala/com/codecommit/cccp/agent/state.scala

if (typeof ot === 'undefined') {
  var ot = {};
}

ot.Client = (function (global) {
  'use strict';

  // Client constructor
  function Client (revision) {
    this.revision = revision; // the next expected revision number
    this.setState(synchronized_); // start state
  }

  Client.prototype.setState = function (state) {
    this.state = state;
  };

  // Call this method when the user changes the document.
  Client.prototype.applyClient = function (operation) {
    this.setState(this.state.applyClient(this, operation));
  };

  // Call this method with a new operation from the server
  Client.prototype.applyServer = function (revision, operation) {
    this.setState(this.state.applyServer(this, revision, operation));
  };

  Client.prototype.applyOperations = function (head, operations) {
    this.setState(this.state.applyOperations(this, head, operations));
  };

  Client.prototype.serverAck = function (revision) {
    this.setState(this.state.serverAck(this, revision));
  };

  Client.prototype.serverReconnect = function () {
    if (typeof this.state.resend === 'function') { this.state.resend(this); }
  };

  // Transforms a selection from the latest known server state to the current
  // client state. For example, if we get from the server the information that
  // another user's cursor is at position 3, but the server hasn't yet received
  // our newest operation, an insertion of 5 characters at the beginning of the
  // document, the correct position of the other user's cursor in our current
  // document is 8.
  Client.prototype.transformSelection = function (selection) {
    return this.state.transformSelection(selection);
  };

  // Override this method.
  Client.prototype.sendOperation = function (revision, operation) {
    throw new Error("sendOperation must be defined in child class");
  };

  // Override this method.
  Client.prototype.applyOperation = function (operation) {
    throw new Error("applyOperation must be defined in child class");
  };


  // In the 'Synchronized' state, there is no pending operation that the client
  // has sent to the server.
  function Synchronized () {}
  Client.Synchronized = Synchronized;

  Synchronized.prototype.applyClient = function (client, operation) {
    // When the user makes an edit, send the operation to the server and
    // switch to the 'AwaitingConfirm' state
    client.sendOperation(client.revision, operation);
    return new AwaitingConfirm(operation);
  };

  Synchronized.prototype.applyServer = function (client, revision, operation) {
    if (revision - client.revision > 1) {
      throw new Error("Invalid revision.");
    }
    client.revision = revision;
    // When we receive a new operation from the server, the operation can be
    // simply applied to the current document
    client.applyOperation(operation);
    return this;
  };

  Synchronized.prototype.serverAck = function (client, revision) {
    throw new Error("There is no pending operation.");
  };

  // Nothing to do because the latest server state and client state are the same.
  Synchronized.prototype.transformSelection = function (x) { return x; };

  // Singleton
  var synchronized_ = new Synchronized();


  // In the 'AwaitingConfirm' state, there's one operation the client has sent
  // to the server and is still waiting for an acknowledgement.
  function AwaitingConfirm (outstanding) {
    // Save the pending operation
    this.outstanding = outstanding;
  }
  Client.AwaitingConfirm = AwaitingConfirm;

  AwaitingConfirm.prototype.applyClient = function (client, operation) {
    // When the user makes an edit, don't send the operation immediately,
    // instead switch to 'AwaitingWithBuffer' state
    return new AwaitingWithBuffer(this.outstanding, operation);
  };

  AwaitingConfirm.prototype.applyServer = function (client, revision, operation) {
    if (revision - client.revision > 1) {
      throw new Error("Invalid revision.");
    }
    client.revision = revision;
    // This is another client's operation. Visualization:
    //
    //                   /\
    // this.outstanding /  \ operation
    //                 /    \
    //                 \    /
    //  pair[1]         \  / pair[0] (new outstanding)
    //  (can be applied  \/
    //  to the client's
    //  current document)
    var pair = operation.constructor.transform(this.outstanding, operation);
    client.applyOperation(pair[1]);
    return new AwaitingConfirm(pair[0]);
  };

  AwaitingConfirm.prototype.serverAck = function (client, revision) {
    if (revision - client.revision > 1) {
      return new Stale(this.outstanding, client, revision).getOperations();
    }
    client.revision = revision;
    // The client's operation has been acknowledged
    // => switch to synchronized state
    return synchronized_;
  };

  AwaitingConfirm.prototype.transformSelection = function (selection) {
    return selection.transform(this.outstanding);
  };

  AwaitingConfirm.prototype.resend = function (client) {
    // The confirm didn't come because the client was disconnected.
    // Now that it has reconnected, we resend the outstanding operation.
    client.sendOperation(client.revision, this.outstanding);
  };


  // In the 'AwaitingWithBuffer' state, the client is waiting for an operation
  // to be acknowledged by the server while buffering the edits the user makes
  function AwaitingWithBuffer (outstanding, buffer) {
    // Save the pending operation and the user's edits since then
    this.outstanding = outstanding;
    this.buffer = buffer;
  }
  Client.AwaitingWithBuffer = AwaitingWithBuffer;

  AwaitingWithBuffer.prototype.applyClient = function (client, operation) {
    // Compose the user's changes onto the buffer
    var newBuffer = this.buffer.compose(operation);
    return new AwaitingWithBuffer(this.outstanding, newBuffer);
  };

  AwaitingWithBuffer.prototype.applyServer = function (client, revision, operation) {
    if (revision - client.revision > 1) {
      throw new Error("Invalid revision.");
    }
    client.revision = revision;
    // Operation comes from another client
    //
    //                       /\
    //     this.outstanding /  \ operation
    //                     /    \
    //                    /\    /
    //       this.buffer /  \* / pair1[0] (new outstanding)
    //                  /    \/
    //                  \    /
    //          pair2[1] \  / pair2[0] (new buffer)
    // the transformed    \/
    // operation -- can
    // be applied to the
    // client's current
    // document
    //
    // * pair1[1]
    var transform = operation.constructor.transform;
    var pair1 = transform(this.outstanding, operation);
    var pair2 = transform(this.buffer, pair1[1]);
    client.applyOperation(pair2[1]);
    return new AwaitingWithBuffer(pair1[0], pair2[0]);
  };

  AwaitingWithBuffer.prototype.serverAck = function (client, revision) {
    if (revision - client.revision > 1) {
      return new StaleWithBuffer(this.outstanding, this.buffer, client, revision).getOperations();
    }
    client.revision = revision;
    // The pending operation has been acknowledged
    // => send buffer
    client.sendOperation(client.revision, this.buffer);
    return new AwaitingConfirm(this.buffer);
  };

  AwaitingWithBuffer.prototype.transformSelection = function (selection) {
    return selection.transform(this.outstanding).transform(this.buffer);
  };

  AwaitingWithBuffer.prototype.resend = function (client) {
    // The confirm didn't come because the client was disconnected.
    // Now that it has reconnected, we resend the outstanding operation.
    client.sendOperation(client.revision, this.outstanding);
  };


  function Stale(acknowlaged, client, revision) {
    this.acknowlaged = acknowlaged;
    this.client = client;
    this.revision = revision;
  }
  Client.Stale = Stale;

  Stale.prototype.applyClient = function (client, operation) {
    return new StaleWithBuffer(this.acknowlaged, operation, client, this.revision);
  };

  Stale.prototype.applyServer = function (client, revision, operation) {
    throw new Error("Ignored server-side change.");
  };

  Stale.prototype.applyOperations = function (client, head, operations) {
    var transform = this.acknowlaged.constructor.transform;
    for (var i = 0; i < operations.length; i++) {
      var op = ot.TextOperation.fromJSON(operations[i]);
      var pair = transform(this.acknowlaged, op);
      client.applyOperation(pair[1]);
      this.acknowlaged = pair[0];
    }
    client.revision = this.revision;
    return synchronized_;
  };

  Stale.prototype.serverAck = function (client, revision) {
    throw new Error("There is no pending operation.");
  };

  Stale.prototype.transformSelection = function (selection) {
    return selection;
  };

  Stale.prototype.getOperations = function () {
    this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
    return this;
  };


  function StaleWithBuffer(acknowlaged, buffer, client, revision) {
    this.acknowlaged = acknowlaged;
    this.buffer = buffer;
    this.client = client;
    this.revision = revision;
  }
  Client.StaleWithBuffer = StaleWithBuffer;

  StaleWithBuffer.prototype.applyClient = function (client, operation) {
    var buffer = this.buffer.compose(operation);
    return new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision);
  };

  StaleWithBuffer.prototype.applyServer = function (client, revision, operation) {
    throw new Error("Ignored server-side change.");
  };

  StaleWithBuffer.prototype.applyOperations = function (client, head, operations) {
    var transform = this.acknowlaged.constructor.transform;
    for (var i = 0; i < operations.length; i++) {
      var op = ot.TextOperation.fromJSON(operations[i]);
      var pair1 = transform(this.acknowlaged, op);
      var pair2 = transform(this.buffer, pair1[1]);
      client.applyOperation(pair2[1]);
      this.acknowlaged = pair1[0];
      this.buffer = pair2[0];
    }
    client.revision = this.revision;
    client.sendOperation(client.revision, this.buffer);
    return new AwaitingConfirm(this.buffer);
  };

  StaleWithBuffer.prototype.serverAck = function (client, revision) {
    throw new Error("There is no pending operation.");
  };

  StaleWithBuffer.prototype.transformSelection = function (selection) {
    return selection;
  };

  StaleWithBuffer.prototype.getOperations = function () {
    this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
    return this;
  };


  return Client;

}(this));

if (typeof module === 'object') {
  module.exports = ot.Client;
}