var util = require('util');
var Socket = require('net').Socket;
var auth = require('./auth');
var constants = require('./constants');
var Parser = require('./parser');
var OutgoingPacket = require('./outgoing_packet');
var Query = require('./query');
var EventEmitter = require('events').EventEmitter;

function Client() {
  if (!(this instanceof Client) || arguments.length) {
    throw new Error('deprecated: use mysql.createClient() instead');
  }

  EventEmitter.call(this);

  this.host = 'localhost';
  this.port = 3306;
  this.user = 'root';
  this.password = null;
  this.database = '';

  this.typeCast = true;
  this.flags = Client.defaultFlags;
  this.maxPacketSize = 0x01000000;
  this.charsetNumber = constants.UTF8_UNICODE_CI;
  this.debug = false;
  this.ending = false;
  this.connected = false;

  this._greeting = null;
  this._queue = [];
  this._socket = null;
  this._parser = null;
};
util.inherits(Client, EventEmitter);
module.exports = Client;

Client.prototype.connect = function() {
  throw new Error('deprecated: connect() is now done automatically.');
};

Client.prototype._connect = function() {
  this.destroy();

  var socket = this._socket = new Socket();
  var parser = this._parser = new Parser();
  var self = this;

  socket
    .on('error', this._connectionErrorHandler())
    .on('data', parser.write.bind(parser))
    .on('end', function() {
      if (self.ending) {
        // @todo destroy()?
        self.connected = false;
        self.ending = false;

        if (self._queue.length) {
          self._connect();
        }

        return;
      }

      if (!self.connected) {
        this.emit('error', new Error('reconnection attempt failed before connection was fully set up'));
        return;
      }

      self._connect();
    })
    .connect(this.port, this.host);

  parser.on('packet', this._handlePacket.bind(this));
};

Client.prototype.query = function(sql, params, cb) {
  if (Array.isArray(params)) {
    sql = this.format(sql, params);
  } else {
    cb = arguments[1];
  }

  var query = new Query({
    typeCast: this.typeCast,
    sql: sql
  });

  var self = this;
  if (cb) {
    var rows = [], fields = {};
    query
      .on('error', function(err) {
        cb(err);
        self._dequeue();
      })
      .on('field', function(field) {
        fields[field.name] = field;
      })
      .on('row', function(row) {
        rows.push(row);
      })
      .on('end', function(result) {
        if (result) {
          cb(null, result);
        } else {
          cb(null, rows, fields);
        }

        self._dequeue();
      });
  } else {
    query
      .on('error', function(err) {
        if (query.listeners('error').length <= 1) {
          self.emit('error', err);
        }
        self._dequeue();
      })
      .on('end', function(result) {
        self._dequeue();
      });
  }

  this._enqueue(function query() {
    var packet = new OutgoingPacket(1 + Buffer.byteLength(sql, 'utf-8'));

    packet.writeNumber(1, constants.COM_QUERY);
    packet.write(sql, 'utf-8');
    self.write(packet);
  }, query);

  return query;
};

Client.prototype.write = function(packet) {
  if (this.debug) {
    console.log('-> %s', packet.buffer.inspect());
  }

  this._socket.write(packet.buffer);
};

Client.prototype.format = function(sql, params) {
  var escape = this.escape;
  params = params.concat();

  sql = sql.replace(/\?/g, function() {
    if (params.length == 0) {
      throw new Error('too few parameters given');
    }

    return escape(params.shift());
  });

  if (params.length) {
    throw new Error('too many parameters given');
  }

  return sql;
};

Client.prototype.escape = function(val) {
  var escape = this.escape;

  if (val === undefined || val === null) {
    return 'NULL';
  }

  switch (typeof val) {
    case 'boolean': return (val) ? 'true' : 'false';
    case 'number': return val+'';
  }

  if (Array.isArray(val)) {
    var sanitized = val.map( function( v ) { return escape( v ); } );
    return "'" + sanitized.join( "','" ) + "'";
  }

  if (typeof val === 'object') {
    val = (typeof val.toISOString === 'function')
      ? val.toISOString()
      : val.toString();
  }

  val = val.replace(/[\0\n\r\b\t\\\'\"\x1a]/g, function(s) {
    switch(s) {
      case "\0": return "\\0";
      case "\n": return "\\n";
      case "\r": return "\\r";
      case "\b": return "\\b";
      case "\t": return "\\t";
      case "\x1a": return "\\Z";
      default: return "\\"+s;
    }
  });
  return "'"+val+"'";
};

Client.prototype.ping = function(cb) {
  var self = this;
  this._enqueue(function ping() {
    var packet = new OutgoingPacket(1);
    packet.writeNumber(1, constants.COM_PING);
    self.write(packet);
  }, cb);
};

Client.prototype.statistics = function(cb) {
  var self = this;
  this._enqueue(function statistics() {
    var packet = new OutgoingPacket(1);
    packet.writeNumber(1, constants.COM_STATISTICS);
    self.write(packet);
  }, cb);
};

Client.prototype.useDatabase = function(database, cb) {
  var self = this;
  this._enqueue(function useDatabase() {
    var packet = new OutgoingPacket(1 + Buffer.byteLength(database, 'utf-8'));
    packet.writeNumber(1, constants.COM_INIT_DB);
    packet.write(database, 'utf-8');
    self.write(packet);
  }, cb);
};

Client.prototype.destroy = function() {
  if (this._socket) {
    this._socket.destroy();
  }

  this._socket = null;
  this._parser = null;
  this.connected = false;
}

Client.prototype.end = function(cb) {
  var self = this;

  this.ending = true;

  this._enqueue(function end() {
    var packet = new OutgoingPacket(1);
    packet.writeNumber(1, constants.COM_QUIT);
    self.write(packet);

    // @todo handle clean shut down properly
    if (cb) {
      self._socket.on('end', cb);
    }

    self._dequeue();
  }, cb);
};

Client.prototype._enqueue = function(fn, delegate) {
  if (!this._socket) {
    this._connect();
  }

  this._queue.push({fn: fn, delegate: delegate});
  if (this._queue.length === 1 && this.connected) {
    fn();
  }
};

Client.prototype._dequeue = function() {
  this._queue.shift();

  if (!this._queue.length) {
    return;
  }

  if (!this.connected) {
    this._connect();
    return;
  }

  this._queue[0].fn();
};

Client.prototype._handlePacket = function(packet) {
  if (this.debug) {
    this._debugPacket(packet);
  }

  if (packet.type == Parser.GREETING_PACKET) {
    this._sendAuth(packet);
    return;
  }

  if (packet.type == Parser.USE_OLD_PASSWORD_PROTOCOL_PACKET) {
    this._sendOldAuth(this._greeting);
    return;
  }

  if (!this.connected) {
    if (packet.type != Parser.ERROR_PACKET) {
      this.connected = true;

      if (this._queue.length) this._queue[0].fn();
      return;
    }

    this._connectionErrorHandler()(Client._packetToUserObject(packet));
    return;
  }

  // @TODO Simplify the code below and above as well
  var type = packet.type;
  var task = this._queue[0];
  var delegate = (task)
        ? task.delegate
        : null;

  if (delegate instanceof Query) {
    delegate._handlePacket(packet);
    return;
  }

  if (type != Parser.ERROR_PACKET) {
    this.connected = true;
    if (delegate) {
      delegate(null, Client._packetToUserObject(packet));
    }
  } else {
    packet = Client._packetToUserObject(packet);
    if (delegate) {
      delegate(packet);
    } else {
      this.emit('error', packet);
    }
  }
  this._dequeue();
};

Client.prototype._connectionErrorHandler = function() {
  return function(err) {
    this.destroy();

    var task = this._queue[0];
    var delegate = (task)
      ? task.delegate
      : null;

    if (delegate instanceof Query) {
      delegate.emit('error', err);
      return;
    }

    if (!delegate) {
      this.emit('error', err);
    } else {
      delegate(err);
      this._queue.shift();
    }

    if (this._queue.length) {
      this._connect();
    }
  }.bind(this);
};

Client.prototype._sendAuth = function(greeting) {
  var token = auth.token(this.password, greeting.scrambleBuffer);
  var packetSize = (
    4 + 4 + 1 + 23 +
    this.user.length + 1 +
    token.length + 1 +
    this.database.length + 1
  );
  var packet = new OutgoingPacket(packetSize, greeting.number+1);

  packet.writeNumber(4, this.flags);
  packet.writeNumber(4, this.maxPacketSize);
  packet.writeNumber(1, this.charsetNumber);
  packet.writeFiller(23);
  packet.writeNullTerminated(this.user);
  packet.writeLengthCoded(token);
  packet.writeNullTerminated(this.database);

  this.write(packet);

  // Keep a reference to the greeting packet. We might receive a
  // USE_OLD_PASSWORD_PROTOCOL_PACKET as a response, in which case we will need
  // the greeting packet again. See _sendOldAuth()
  this._greeting = greeting;
};

Client._packetToUserObject = function(packet) {
  var userObject = (packet.type == Parser.ERROR_PACKET)
    ? new Error()
    : {};

  for (var key in packet) {
    var newKey = key;
    if (key == 'type' || key == 'number' || key == 'length' || key == 'received') {
      continue;
    }

    if (key == 'errorMessage') {
      newKey = 'message';
    } else if (key == 'errorNumber') {
      newKey = 'number';
    }

    userObject[newKey] = packet[key];
  }

  return userObject;
};

Client.prototype._debugPacket = function(packet) {
  var packetName = null;
  for (var key in Parser) {
    if (!key.match(/_PACKET$/)) {
      continue;
    }

    if (Parser[key] == packet.type) {
      packetName = key;
      break;
    }
  }
  console.log('<- %s: %j', packetName, packet);
};

Client.prototype._sendOldAuth = function(greeting) {
  var token = auth.scramble323(greeting.scrambleBuffer, this.password);
  var packetSize = (
    token.length + 1
  );
  var packet = new OutgoingPacket(packetSize, greeting.number+3);

  // I could not find any official documentation for this, but from sniffing
  // the mysql command line client, I think this is the right way to send the
  // scrambled token after receiving the USE_OLD_PASSWORD_PROTOCOL_PACKET.
  packet.write(token);
  packet.writeFiller(1);

  this.write(packet);
};

Client.defaultFlags =
    constants.CLIENT_LONG_PASSWORD
  | constants.CLIENT_FOUND_ROWS
  | constants.CLIENT_LONG_FLAG
  | constants.CLIENT_CONNECT_WITH_DB
  | constants.CLIENT_ODBC
  | constants.CLIENT_LOCAL_FILES
  | constants.CLIENT_IGNORE_SPACE
  | constants.CLIENT_PROTOCOL_41
  | constants.CLIENT_INTERACTIVE
  | constants.CLIENT_IGNORE_SIGPIPE
  | constants.CLIENT_TRANSACTIONS
  | constants.CLIENT_RESERVED
  | constants.CLIENT_SECURE_CONNECTION
  | constants.CLIENT_MULTI_STATEMENTS
  | constants.CLIENT_MULTI_RESULTS;
