// 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.

define('serial_service', [
    'content/public/renderer/service_provider',
    'data_receiver',
    'data_sender',
    'device/serial/serial.mojom',
    'mojo/public/js/bindings/core',
    'mojo/public/js/bindings/router',
], function(serviceProvider,
            dataReceiver,
            dataSender,
            serialMojom,
            core,
            routerModule) {
  /**
   * A Javascript client for the serial service and connection Mojo services.
   *
   * This provides a thick client around the Mojo services, exposing a JS-style
   * interface to serial connections and information about serial devices. This
   * converts parameters and result between the Apps serial API types and the
   * Mojo types.
   */

  var service = new serialMojom.SerialServiceProxy(new routerModule.Router(
      serviceProvider.connectToService(serialMojom.SerialServiceProxy.NAME_)));

  function getDevices() {
    return service.getDevices().then(function(response) {
      return $Array.map(response.devices, function(device) {
        var result = {path: device.path};
        if (device.has_vendor_id)
          result.vendorId = device.vendor_id;
        if (device.has_product_id)
          result.productId = device.product_id;
        if (device.display_name)
          result.displayName = device.display_name;
        return result;
      });
    });
  }

  var DEFAULT_CLIENT_OPTIONS = {
    persistent: false,
    name: '',
    receiveTimeout: 0,
    sendTimeout: 0,
    bufferSize: 4096,
  };

  var DATA_BITS_TO_MOJO = {
    undefined: serialMojom.DataBits.NONE,
    'seven': serialMojom.DataBits.SEVEN,
    'eight': serialMojom.DataBits.EIGHT,
  };
  var STOP_BITS_TO_MOJO = {
    undefined: serialMojom.StopBits.NONE,
    'one': serialMojom.StopBits.ONE,
    'two': serialMojom.StopBits.TWO,
  };
  var PARITY_BIT_TO_MOJO = {
    undefined: serialMojom.ParityBit.NONE,
    'no': serialMojom.ParityBit.NO,
    'odd': serialMojom.ParityBit.ODD,
    'even': serialMojom.ParityBit.EVEN,
  };
  var SEND_ERROR_TO_MOJO = {
    undefined: serialMojom.SendError.NONE,
    'disconnected': serialMojom.SendError.DISCONNECTED,
    'pending': serialMojom.SendError.PENDING,
    'timeout': serialMojom.SendError.TIMEOUT,
    'system_error': serialMojom.SendError.SYSTEM_ERROR,
  };
  var RECEIVE_ERROR_TO_MOJO = {
    undefined: serialMojom.ReceiveError.NONE,
    'disconnected': serialMojom.ReceiveError.DISCONNECTED,
    'device_lost': serialMojom.ReceiveError.DEVICE_LOST,
    'timeout': serialMojom.ReceiveError.TIMEOUT,
    'system_error': serialMojom.ReceiveError.SYSTEM_ERROR,
  };

  function invertMap(input) {
    var output = {};
    for (var key in input) {
      if (key == 'undefined')
        output[input[key]] = undefined;
      else
        output[input[key]] = key;
    }
    return output;
  }
  var DATA_BITS_FROM_MOJO = invertMap(DATA_BITS_TO_MOJO);
  var STOP_BITS_FROM_MOJO = invertMap(STOP_BITS_TO_MOJO);
  var PARITY_BIT_FROM_MOJO = invertMap(PARITY_BIT_TO_MOJO);
  var SEND_ERROR_FROM_MOJO = invertMap(SEND_ERROR_TO_MOJO);
  var RECEIVE_ERROR_FROM_MOJO = invertMap(RECEIVE_ERROR_TO_MOJO);

  function getServiceOptions(options) {
    var out = {};
    if (options.dataBits)
      out.data_bits = DATA_BITS_TO_MOJO[options.dataBits];
    if (options.stopBits)
      out.stop_bits = STOP_BITS_TO_MOJO[options.stopBits];
    if (options.parityBit)
      out.parity_bit = PARITY_BIT_TO_MOJO[options.parityBit];
    if ('ctsFlowControl' in options) {
      out.has_cts_flow_control = true;
      out.cts_flow_control = options.ctsFlowControl;
    }
    if ('bitrate' in options)
      out.bitrate = options.bitrate;
    return out;
  }

  function convertServiceInfo(result) {
    if (!result.info)
      throw new Error('Failed to get ConnectionInfo.');
    return {
      ctsFlowControl: !!result.info.cts_flow_control,
      bitrate: result.info.bitrate || undefined,
      dataBits: DATA_BITS_FROM_MOJO[result.info.data_bits],
      stopBits: STOP_BITS_FROM_MOJO[result.info.stop_bits],
      parityBit: PARITY_BIT_FROM_MOJO[result.info.parity_bit],
    };
  }

  function Connection(
      remoteConnection, router, receivePipe, sendPipe, id, options) {
    this.remoteConnection_ = remoteConnection;
    this.router_ = router;
    this.options_ = {};
    for (var key in DEFAULT_CLIENT_OPTIONS) {
      this.options_[key] = DEFAULT_CLIENT_OPTIONS[key];
    }
    this.setClientOptions_(options);
    this.receivePipe_ =
        new dataReceiver.DataReceiver(receivePipe,
                                      this.options_.bufferSize,
                                      serialMojom.ReceiveError.DISCONNECTED);
    this.sendPipe_ = new dataSender.DataSender(
        sendPipe, this.options_.bufferSize, serialMojom.SendError.DISCONNECTED);
    this.id_ = id;
    getConnections().then(function(connections) {
      connections[this.id_] = this;
    }.bind(this));
    this.paused_ = false;
    this.sendInProgress_ = false;

    // queuedReceiveData_ or queuedReceiveError will store the receive result or
    // error, respectively, if a receive completes or fails while this
    // connection is paused. At most one of the the two may be non-null: a
    // receive completed while paused will only set one of them, no further
    // receives will be performed while paused and a queued result is dispatched
    // before any further receives are initiated when unpausing.
    this.queuedReceiveData_ = null;
    this.queuedReceiveError = null;

    this.startReceive_();
  }

  Connection.create = function(path, options) {
    options = options || {};
    var serviceOptions = getServiceOptions(options);
    var pipe = core.createMessagePipe();
    var sendPipe = core.createMessagePipe();
    var receivePipe = core.createMessagePipe();
    service.connect(path,
                    serviceOptions,
                    pipe.handle0,
                    sendPipe.handle0,
                    receivePipe.handle0);
    var router = new routerModule.Router(pipe.handle1);
    var connection = new serialMojom.ConnectionProxy(router);
    return connection.getInfo().then(convertServiceInfo).then(function(info) {
      return Promise.all([info, allocateConnectionId()]);
    }).catch(function(e) {
      router.close();
      core.close(sendPipe.handle1);
      core.close(receivePipe.handle1);
      throw e;
    }).then(function(results) {
      var info = results[0];
      var id = results[1];
      var serialConnectionClient = new Connection(connection,
                                                  router,
                                                  receivePipe.handle1,
                                                  sendPipe.handle1,
                                                  id,
                                                  options);
      var clientInfo = serialConnectionClient.getClientInfo_();
      for (var key in clientInfo) {
        info[key] = clientInfo[key];
      }
      return {
        connection: serialConnectionClient,
        info: info,
      };
    });
  };

  Connection.prototype.close = function() {
    this.router_.close();
    this.receivePipe_.close();
    this.sendPipe_.close();
    clearTimeout(this.receiveTimeoutId_);
    clearTimeout(this.sendTimeoutId_);
    return getConnections().then(function(connections) {
      delete connections[this.id_];
      return true;
    }.bind(this));
  };

  Connection.prototype.getClientInfo_ = function() {
    var info = {
      connectionId: this.id_,
      paused: this.paused_,
    };
    for (var key in this.options_) {
      info[key] = this.options_[key];
    }
    return info;
  };

  Connection.prototype.getInfo = function() {
    var info = this.getClientInfo_();
    return this.remoteConnection_.getInfo().then(convertServiceInfo).then(
        function(result) {
      for (var key in result) {
        info[key] = result[key];
      }
      return info;
    }).catch(function() {
      return info;
    });
  };

  Connection.prototype.setClientOptions_ = function(options) {
    if ('name' in options)
      this.options_.name = options.name;
    if ('receiveTimeout' in options)
      this.options_.receiveTimeout = options.receiveTimeout;
    if ('sendTimeout' in options)
      this.options_.sendTimeout = options.sendTimeout;
    if ('bufferSize' in options)
      this.options_.bufferSize = options.bufferSize;
  };

  Connection.prototype.setOptions = function(options) {
    this.setClientOptions_(options);
    var serviceOptions = getServiceOptions(options);
    if ($Object.keys(serviceOptions).length == 0)
      return true;
    return this.remoteConnection_.setOptions(serviceOptions).then(
        function(result) {
      return !!result.success;
    }).catch(function() {
      return false;
    });
  };

  Connection.prototype.getControlSignals = function() {
    return this.remoteConnection_.getControlSignals().then(function(result) {
      if (!result.signals)
        throw new Error('Failed to get control signals.');
      var signals = result.signals;
      return {
        dcd: !!signals.dcd,
        cts: !!signals.cts,
        ri: !!signals.ri,
        dsr: !!signals.dsr,
      };
    });
  };

  Connection.prototype.setControlSignals = function(signals) {
    var controlSignals = {};
    if ('dtr' in signals) {
      controlSignals.has_dtr = true;
      controlSignals.dtr = signals.dtr;
    }
    if ('rts' in signals) {
      controlSignals.has_rts = true;
      controlSignals.rts = signals.rts;
    }
    return this.remoteConnection_.setControlSignals(controlSignals).then(
        function(result) {
      return !!result.success;
    });
  };

  Connection.prototype.flush = function() {
    return this.remoteConnection_.flush().then(function(result) {
      return !!result.success;
    });
  };

  Connection.prototype.setPaused = function(paused) {
    this.paused_ = paused;
    if (paused) {
      clearTimeout(this.receiveTimeoutId_);
      this.receiveTimeoutId_ = null;
    } else if (!this.receiveInProgress_) {
      this.startReceive_();
    }
  };

  Connection.prototype.send = function(data) {
    if (this.sendInProgress_)
      return Promise.resolve({bytesSent: 0, error: 'pending'});

    if (this.options_.sendTimeout) {
      this.sendTimeoutId_ = setTimeout(function() {
        this.sendPipe_.cancel(serialMojom.SendError.TIMEOUT);
      }.bind(this), this.options_.sendTimeout);
    }
    this.sendInProgress_ = true;
    return this.sendPipe_.send(data).then(function(bytesSent) {
      return {bytesSent: bytesSent};
    }).catch(function(e) {
      return {
        bytesSent: e.bytesSent,
        error: SEND_ERROR_FROM_MOJO[e.error],
      };
    }).then(function(result) {
      if (this.sendTimeoutId_)
        clearTimeout(this.sendTimeoutId_);
      this.sendTimeoutId_ = null;
      this.sendInProgress_ = false;
      return result;
    }.bind(this));
  };

  Connection.prototype.startReceive_ = function() {
    this.receiveInProgress_ = true;
    var receivePromise = null;
    // If we have a queued receive result, dispatch it immediately instead of
    // starting a new receive.
    if (this.queuedReceiveData_) {
      receivePromise = Promise.resolve(this.queuedReceiveData_);
      this.queuedReceiveData_ = null;
    } else if (this.queuedReceiveError) {
      receivePromise = Promise.reject(this.queuedReceiveError);
      this.queuedReceiveError = null;
    } else {
      receivePromise = this.receivePipe_.receive();
    }
    receivePromise.then(this.onDataReceived_.bind(this)).catch(
        this.onReceiveError_.bind(this));
    this.startReceiveTimeoutTimer_();
  };

  Connection.prototype.onDataReceived_ = function(data) {
    this.startReceiveTimeoutTimer_();
    this.receiveInProgress_ = false;
    if (this.paused_) {
      this.queuedReceiveData_ = data;
      return;
    }
    if (this.onData) {
      this.onData(data);
    }
    if (!this.paused_) {
      this.startReceive_();
    }
  };

  Connection.prototype.onReceiveError_ = function(e) {
    clearTimeout(this.receiveTimeoutId_);
    this.receiveInProgress_ = false;
    if (this.paused_) {
      this.queuedReceiveError = e;
      return;
    }
    var error = e.error;
    this.paused_ = true;
    if (this.onError)
      this.onError(RECEIVE_ERROR_FROM_MOJO[error]);
  };

  Connection.prototype.startReceiveTimeoutTimer_ = function() {
    clearTimeout(this.receiveTimeoutId_);
    if (this.options_.receiveTimeout && !this.paused_) {
      this.receiveTimeoutId_ = setTimeout(this.onReceiveTimeout_.bind(this),
                                          this.options_.receiveTimeout);
    }
  };

  Connection.prototype.onReceiveTimeout_ = function() {
    if (this.onError)
      this.onError('timeout');
    this.startReceiveTimeoutTimer_();
  };

  var connections_ = {};
  var nextConnectionId_ = 0;

  // Wrap all access to |connections_| through getConnections to avoid adding
  // any synchronous dependencies on it. This will likely be important when
  // supporting persistent connections by stashing them.
  function getConnections() {
    return Promise.resolve(connections_);
  }

  function getConnection(id) {
    return getConnections().then(function(connections) {
      if (!connections[id])
        throw new Error('Serial connection not found.');
      return connections[id];
    });
  }

  function allocateConnectionId() {
    return Promise.resolve(nextConnectionId_++);
  }

  return {
    getDevices: getDevices,
    createConnection: Connection.create,
    getConnection: getConnection,
    getConnections: getConnections,
    // For testing.
    Connection: Connection,
  };
});