/*
    This file is part of web3.js.

    web3.js is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    web3.js is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with web3.js.  If not, see <http://www.gnu.org/licenses/>.
*/
/** @file ipcprovider.js
 * @authors:
 *   Fabian Vogelsteller <fabian@ethdev.com>
 * @date 2015
 */

'use strict';

var _ = require('underscore');
var errors = {
  InvalidConnection: function(host) {
    return new Error(
      "CONNECTION ERROR: Couldn't connect to node " + host + '.'
    );
  },
  InvalidResponse: function(result) {
    var message =
      !!result && !!result.error && !!result.error.message
        ? result.error.message
        : 'Invalid JSON RPC response: ' + JSON.stringify(result);
    return new Error(message);
  }
};

var IpcProvider = function(path, net) {
  var _this = this;
  this.responseCallbacks = {};
  this.path = path;

  this.connection = net.connect({ path: this.path });

  this.connection.on('error', function(e) {
    console.error('IPC Connection Error', e);
    _this._timeout();
  });

  this.connection.on('end', function() {
    _this._timeout();
  });

  // LISTEN FOR CONNECTION RESPONSES
  this.connection.on('data', function(data) {
    /*jshint maxcomplexity: 6 */

    _this._parseResponse(data.toString()).forEach(function(result) {
      var id = null;

      // get the id which matches the returned id
      if (_.isArray(result)) {
        result.forEach(function(load) {
          if (_this.responseCallbacks[load.id]) id = load.id;
        });
      } else {
        id = result.id;
      }

      // fire the callback
      if (_this.responseCallbacks[id]) {
        _this.responseCallbacks[id](null, result);
        delete _this.responseCallbacks[id];
      }
    });
  });
};

/**
Will parse the response and make an array out of it.

@method _parseResponse
@param {String} data
*/
IpcProvider.prototype._parseResponse = function(data) {
  var _this = this,
    returnValues = [];

  // DE-CHUNKER
  var dechunkedData = data
    .replace(/\}[\n\r]?\{/g, '}|--|{') // }{
    .replace(/\}\][\n\r]?\[\{/g, '}]|--|[{') // }][{
    .replace(/\}[\n\r]?\[\{/g, '}|--|[{') // }[{
    .replace(/\}\][\n\r]?\{/g, '}]|--|{') // }]{
    .split('|--|');

  dechunkedData.forEach(function(data) {
    // prepend the last chunk
    if (_this.lastChunk) data = _this.lastChunk + data;

    var result = null;

    try {
      result = JSON.parse(data);
    } catch (e) {
      _this.lastChunk = data;

      // start timeout to cancel all requests
      clearTimeout(_this.lastChunkTimeout);
      _this.lastChunkTimeout = setTimeout(function() {
        _this._timeout();
        throw errors.InvalidResponse(data);
      }, 1000 * 15);

      return;
    }

    // cancel timeout and set chunk to null
    clearTimeout(_this.lastChunkTimeout);
    _this.lastChunk = null;

    if (result) returnValues.push(result);
  });

  return returnValues;
};

/**
Get the adds a callback to the responseCallbacks object,
which will be called if a response matching the response Id will arrive.

@method _addResponseCallback
*/
IpcProvider.prototype._addResponseCallback = function(payload, callback) {
  var id = payload.id || payload[0].id;
  var method = payload.method || payload[0].method;

  this.responseCallbacks[id] = callback;
  this.responseCallbacks[id].method = method;
};

/**
Timeout all requests when the end/error event is fired

@method _timeout
*/
IpcProvider.prototype._timeout = function() {
  for (var key in this.responseCallbacks) {
    if (this.responseCallbacks.hasOwnProperty(key)) {
      this.responseCallbacks[key](errors.InvalidConnection('on IPC'));
      delete this.responseCallbacks[key];
    }
  }
};

/**
Check if the current connection is still valid.

@method isConnected
*/
IpcProvider.prototype.isConnected = function() {
  var _this = this;

  // try reconnect, when connection is gone
  if (!_this.connection.writable)
    _this.connection.connect({ path: _this.path });

  return !!this.connection.writable;
};

IpcProvider.prototype.send = function(payload) {
  if (this.connection.writeSync) {
    var result;

    // try reconnect, when connection is gone
    if (!this.connection.writable) this.connection.connect({ path: this.path });

    var data = this.connection.writeSync(JSON.stringify(payload));

    try {
      result = JSON.parse(data);
    } catch (e) {
      throw errors.InvalidResponse(data);
    }

    return result;
  } else {
    throw new Error(
      'You tried to send "' +
        payload.method +
        '" synchronously. Synchronous requests are not supported by the IPC provider.'
    );
  }
};

IpcProvider.prototype.sendAsync = function(payload, callback) {
  // try reconnect, when connection is gone
  if (!this.connection.writable) this.connection.connect({ path: this.path });

  this.connection.write(JSON.stringify(payload));
  this._addResponseCallback(payload, callback);
};

module.exports = IpcProvider;
