'use strict';

var util = require('../utils/util');
var crypto = require('../crypto/crypto');
var BufferWriter = require('../utils/writer');
var assert = require('assert');
var EventEmitter = require('events').EventEmitter;
var IOClient = require('socket.io-client');

function ProxySocket(uri) {
  if (!(this instanceof ProxySocket))
    return new ProxySocket(uri);

  EventEmitter.call(this);

  this.info = null;

  this.socket = new IOClient(uri, { reconnection: false });
  this.sendBuffer = [];
  this.recvBuffer = [];
  this.paused = false;
  this.snonce = null;
  this.bytesWritten = 0;
  this.bytesRead = 0;
  this.remoteAddress = null;
  this.remotePort = 0;

  this.closed = false;

  this._init();
}

util.inherits(ProxySocket, EventEmitter);

ProxySocket.prototype._init = function _init() {
  var self = this;

  this.socket.on('info', function(info) {
    if (self.closed)
      return;

    self.info = info;

    if (info.pow) {
      self.snonce = new Buffer(info.snonce, 'hex');
      self.target = new Buffer(info.target, 'hex');
    }

    self.emit('info', info);
  });

  this.socket.on('error', function(err) {
    console.error(err);
  });

  this.socket.on('tcp connect', function(addr, port) {
    if (self.closed)
      return;
    self.remoteAddress = addr;
    self.remotePort = port;
    self.emit('connect');
  });

  this.socket.on('tcp data', function(data) {
    data = new Buffer(data, 'hex');
    if (self.paused) {
      self.recvBuffer.push(data);
      return;
    }
    self.bytesRead += data.length;
    self.emit('data', data);
  });

  this.socket.on('tcp close', function(data) {
    if (self.closed)
      return;
    self.closed = true;
    self.emit('close');
  });

  this.socket.on('tcp error', function(e) {
    var err = new Error(e.message);
    err.code = e.code;
    self.emit('error', err);
  });

  this.socket.on('tcp timeout', function() {
    self.emit('timeout');
  });

  this.socket.on('disconnect', function() {
    if (self.closed)
      return;
    self.closed = true;
    self.emit('close');
  });
};

ProxySocket.prototype.resolve = function resolve(name, record, callback) {
  var e;
  this.socket.emit('dns resolve', name, record, function(err, results) {
    if (err) {
      e = new Error(err.message);
      e.code = err.code || null;
      callback(e);
      return;
    }
    callback(null, results);
  });
};

ProxySocket.prototype.connect = function connect(port, host) {
  var nonce = 0;
  var i, pow;

  this.remoteAddress = host;
  this.remotePort = port;

  if (this.closed) {
    this.sendBuffer.length = 0;
    return;
  }

  if (!this.info) {
    this.once('info', connect.bind(this, port, host));
    return;
  }

  if (this.info.pow) {
    util.log(
      'Solving proof of work to create socket (%d, %s) -- please wait.',
      port, host);

    pow = new BufferWriter();
    pow.writeU32(nonce);
    pow.writeBytes(this.snonce);
    pow.writeU32(port);
    pow.writeString(host, 'ascii');
    pow = pow.render();

    do {
      nonce++;
      assert(nonce <= 0xffffffff, 'Could not create socket.');
      pow.writeUInt32LE(nonce, 0, true);
    } while (util.cmp(crypto.hash256(pow), this.target) > 0);

    util.log('Solved proof of work: %d', nonce);
  }

  this.socket.emit('tcp connect', port, host, nonce);

  for (i = 0; i < this.sendBuffer.length; i++)
    this.write(this.sendBuffer[i]);

  this.sendBuffer.length = 0;
};

ProxySocket.prototype.setKeepAlive = function setKeepAlive(enable, delay) {
  this.socket.emit('tcp keep alive', enable, delay);
};

ProxySocket.prototype.setNoDelay = function setNoDelay(enable) {
  this.socket.emit('tcp no delay', enable);
};

ProxySocket.prototype.setTimeout = function setTimeout(timeout, callback) {
  this.socket.emit('tcp set timeout', timeout);
  if (callback)
    this.on('timeout', callback);
};

ProxySocket.prototype.write = function write(data, callback) {
  if (!this.info) {
    this.sendBuffer.push(data);

    if (callback)
      callback();

    return true;
  }

  this.bytesWritten += data.length;

  this.socket.emit('tcp data', data.toString('hex'));

  if (callback)
    callback();

  return true;
};

ProxySocket.prototype.pause = function pause() {
  this.paused = true;
};

ProxySocket.prototype.resume = function resume() {
  var recv = this.recvBuffer;
  var i, data;

  this.paused = false;
  this.recvBuffer = [];

  for (i = 0; i < recv.length; i++) {
    data = recv[i];
    this.bytesRead += data.length;
    this.emit('data', data);
  }
};

ProxySocket.prototype.destroy = function destroy() {
  if (this.closed)
    return;
  this.closed = true;
  this.socket.disconnect();
};

ProxySocket.connect = function connect(uri, port, host) {
  var socket = new ProxySocket(uri);
  socket.connect(port, host);
  return socket;
};

module.exports = ProxySocket;
