// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

(function() {
  var internal = mojo.internal;

  /**
   * The state of |endpoint|. If both the endpoint and its peer have been
   * closed, removes it from |endpoints_|.
   * @enum {string}
   */
  var EndpointStateUpdateType = {
    ENDPOINT_CLOSED: 'endpoint_closed',
    PEER_ENDPOINT_CLOSED: 'peer_endpoint_closed'
  };

  function check(condition, output) {
    if (!condition) {
      // testharness.js does not rethrow errors so the error stack needs to be
      // included as a string in the error we throw for debugging layout tests.
      throw new Error((new Error()).stack);
    }
  }

  function InterfaceEndpoint(router, interfaceId) {
    this.router_ = router;
    this.id = interfaceId;
    this.closed = false;
    this.peerClosed = false;
    this.handleCreated = false;
    this.disconnectReason = null;
    this.client = null;
  }

  InterfaceEndpoint.prototype.sendMessage = function(message) {
    message.setInterfaceId(this.id);
    return this.router_.connector_.accept(message);
  };

  function Router(handle, setInterfaceIdNamespaceBit) {
    if (!(handle instanceof MojoHandle)) {
      throw new Error("Router constructor: Not a handle");
    }
    if (setInterfaceIdNamespaceBit === undefined) {
      setInterfaceIdNamespaceBit = false;
    }

    this.connector_ = new internal.Connector(handle);

    this.connector_.setIncomingReceiver({
        accept: this.accept.bind(this),
    });
    this.connector_.setErrorHandler({
        onError: this.onPipeConnectionError.bind(this),
    });

    this.setInterfaceIdNamespaceBit_ = setInterfaceIdNamespaceBit;
    // |cachedMessageData| caches infomation about a message, so it can be
    // processed later if a client is not yet attached to the target endpoint.
    this.cachedMessageData = null;
    this.controlMessageHandler_ = new internal.PipeControlMessageHandler(this);
    this.controlMessageProxy_ =
        new internal.PipeControlMessageProxy(this.connector_);
    this.nextInterfaceIdValue_ = 1;
    this.encounteredError_ = false;
    this.endpoints_ = new Map();
  }

  Router.prototype.associateInterface = function(handleToSend) {
    if (!handleToSend.pendingAssociation()) {
      return internal.kInvalidInterfaceId;
    }

    var id = 0;
    do {
      if (this.nextInterfaceIdValue_ >= internal.kInterfaceIdNamespaceMask) {
        this.nextInterfaceIdValue_ = 1;
      }
      id = this.nextInterfaceIdValue_++;
      if (this.setInterfaceIdNamespaceBit_) {
        id += internal.kInterfaceIdNamespaceMask;
      }
    } while (this.endpoints_.has(id));

    var endpoint = new InterfaceEndpoint(this, id);
    this.endpoints_.set(id, endpoint);
    if (this.encounteredError_) {
      this.updateEndpointStateMayRemove(endpoint,
          EndpointStateUpdateType.PEER_ENDPOINT_CLOSED);
    }
    endpoint.handleCreated = true;

    if (!handleToSend.notifyAssociation(id, this)) {
      // The peer handle of |handleToSend|, which is supposed to join this
      // associated group, has been closed.
      this.updateEndpointStateMayRemove(endpoint,
          EndpointStateUpdateType.ENDPOINT_CLOSED);

      pipeControlMessageproxy.notifyPeerEndpointClosed(id,
          handleToSend.disconnectReason());
    }

    return id;
  };

  Router.prototype.attachEndpointClient = function(
      interfaceEndpointHandle, interfaceEndpointClient) {
    check(internal.isValidInterfaceId(interfaceEndpointHandle.id()));
    check(interfaceEndpointClient);

    var endpoint = this.endpoints_.get(interfaceEndpointHandle.id());
    check(endpoint);
    check(!endpoint.client);
    check(!endpoint.closed);
    endpoint.client = interfaceEndpointClient;

    if (endpoint.peerClosed) {
      setTimeout(endpoint.client.notifyError.bind(endpoint.client), 0);
    }

    if (this.cachedMessageData && interfaceEndpointHandle.id() ===
        this.cachedMessageData.message.getInterfaceId()) {
      setTimeout((function() {
        if (!this.cachedMessageData) {
          return;
        }

        var targetEndpoint = this.endpoints_.get(
            this.cachedMessageData.message.getInterfaceId());
        // Check that the target endpoint's client still exists.
        if (targetEndpoint && targetEndpoint.client) {
          var message = this.cachedMessageData.message;
          var messageValidator = this.cachedMessageData.messageValidator;
          this.cachedMessageData = null;
          this.connector_.resumeIncomingMethodCallProcessing();
          var ok = endpoint.client.handleIncomingMessage(message,
              messageValidator);

          // Handle invalid cached incoming message.
          if (!internal.isTestingMode() && !ok) {
            this.connector_.handleError(true, true);
          }
        }
      }).bind(this), 0);
    }

    return endpoint;
  };

  Router.prototype.detachEndpointClient = function(
      interfaceEndpointHandle) {
    check(internal.isValidInterfaceId(interfaceEndpointHandle.id()));
    var endpoint = this.endpoints_.get(interfaceEndpointHandle.id());
    check(endpoint);
    check(endpoint.client);
    check(!endpoint.closed);

    endpoint.client = null;
  };

  Router.prototype.createLocalEndpointHandle = function(
      interfaceId) {
    if (!internal.isValidInterfaceId(interfaceId)) {
      return new internal.InterfaceEndpointHandle();
    }

    // Unless it is the primary ID, |interfaceId| is from the remote side and
    // therefore its namespace bit is supposed to be different than the value
    // that this router would use.
    if (!internal.isPrimaryInterfaceId(interfaceId) &&
        this.setInterfaceIdNamespaceBit_ ===
            internal.hasInterfaceIdNamespaceBitSet(interfaceId)) {
      return new internal.InterfaceEndpointHandle();
    }

    var endpoint = this.endpoints_.get(interfaceId);

    if (!endpoint) {
      endpoint = new InterfaceEndpoint(this, interfaceId);
      this.endpoints_.set(interfaceId, endpoint);

      check(!endpoint.handleCreated);

      if (this.encounteredError_) {
        this.updateEndpointStateMayRemove(endpoint,
            EndpointStateUpdateType.PEER_ENDPOINT_CLOSED);
      }
    } else {
      // If the endpoint already exist, it is because we have received a
      // notification that the peer endpoint has closed.
      check(!endpoint.closed);
      check(endpoint.peerClosed);

      if (endpoint.handleCreated) {
        return new internal.InterfaceEndpointHandle();
      }
    }

    endpoint.handleCreated = true;
    return new internal.InterfaceEndpointHandle(interfaceId, this);
  };

  Router.prototype.accept = function(message) {
    var messageValidator = new internal.Validator(message);
    var err = messageValidator.validateMessageHeader();

    var ok = false;
    if (err !== internal.validationError.NONE) {
      internal.reportValidationError(err);
    } else if (message.deserializeAssociatedEndpointHandles(this)) {
      if (internal.isPipeControlMessage(message)) {
        ok = this.controlMessageHandler_.accept(message);
      } else {
        var interfaceId = message.getInterfaceId();
        var endpoint = this.endpoints_.get(interfaceId);
        if (!endpoint || endpoint.closed) {
          return true;
        }

        if (!endpoint.client) {
          // We need to wait until a client is attached in order to dispatch
          // further messages.
          this.cachedMessageData = {message: message,
              messageValidator: messageValidator};
          this.connector_.pauseIncomingMethodCallProcessing();
          return true;
        }
        ok = endpoint.client.handleIncomingMessage(message, messageValidator);
      }
    }
    return ok;
  };

  Router.prototype.close = function() {
    this.connector_.close();
    // Closing the message pipe won't trigger connection error handler.
    // Explicitly call onPipeConnectionError() so that associated endpoints
    // will get notified.
    this.onPipeConnectionError();
  };

  Router.prototype.onPeerAssociatedEndpointClosed = function(interfaceId,
      reason) {
    var endpoint = this.endpoints_.get(interfaceId);
    if (!endpoint) {
      endpoint = new InterfaceEndpoint(this, interfaceId);
      this.endpoints_.set(interfaceId, endpoint);
    }

    if (reason) {
      endpoint.disconnectReason = reason;
    }

    if (!endpoint.peerClosed) {
      if (endpoint.client) {
        setTimeout(endpoint.client.notifyError.bind(endpoint.client, reason),
                   0);
      }
      this.updateEndpointStateMayRemove(endpoint,
          EndpointStateUpdateType.PEER_ENDPOINT_CLOSED);
    }
    return true;
  };

  Router.prototype.onPipeConnectionError = function() {
    this.encounteredError_ = true;

    for (var endpoint of this.endpoints_.values()) {
      if (endpoint.client) {
        setTimeout(
            endpoint.client.notifyError.bind(
                endpoint.client, endpoint.disconnectReason),
            0);
      }
      this.updateEndpointStateMayRemove(endpoint,
          EndpointStateUpdateType.PEER_ENDPOINT_CLOSED);
    }
  };

  Router.prototype.closeEndpointHandle = function(interfaceId, reason) {
    if (!internal.isValidInterfaceId(interfaceId)) {
      return;
    }
    var endpoint = this.endpoints_.get(interfaceId);
    check(endpoint);
    check(!endpoint.client);
    check(!endpoint.closed);

    this.updateEndpointStateMayRemove(endpoint,
        EndpointStateUpdateType.ENDPOINT_CLOSED);

    if (!internal.isPrimaryInterfaceId(interfaceId) || reason) {
      this.controlMessageProxy_.notifyPeerEndpointClosed(interfaceId, reason);
    }

    if (this.cachedMessageData && interfaceId ===
        this.cachedMessageData.message.getInterfaceId()) {
      this.cachedMessageData = null;
      this.connector_.resumeIncomingMethodCallProcessing();
    }
  };

  Router.prototype.updateEndpointStateMayRemove = function(endpoint,
      endpointStateUpdateType) {
    if (endpointStateUpdateType === EndpointStateUpdateType.ENDPOINT_CLOSED) {
      endpoint.closed = true;
    } else {
      endpoint.peerClosed = true;
    }
    if (endpoint.closed && endpoint.peerClosed) {
      this.endpoints_.delete(endpoint.id);
    }
  };

  internal.Router = Router;
})();
