// Generated by CoffeeScript 1.3.3
(function() {

  var merge, merge_sort;

  merge = function(left, right, comparison) {
    var result;
    result = new Array();
    while ((left.length > 0) && (right.length > 0)) {
      if (comparison(left[0], right[0]) <= 0) {
        result.push(left.shift());
      } else {
        result.push(right.shift());
      }
    }
    while (left.length > 0) {
      result.push(left.shift());
    }
    while (right.length > 0) {
      result.push(right.shift());
    }
    return result;
  };

  merge_sort = function(array, comparison) {
    var middle;
    if (array.length < 2) {
      return array;
    }
    middle = Math.ceil(array.length / 2);
    return merge(merge_sort(array.slice(0, middle), comparison), merge_sort(array.slice(middle), comparison), comparison);
  };

  var crypto, int32Max, merge_sort;

  crypto = require("crypto");

  int32Max = Math.pow(2, 32);

  getTable = function(key) {
    var ah, al, decrypt_table, hash, i, md5sum, table;
    table = new Array(256);
    decrypt_table = new Array(256);
    md5sum = crypto.createHash("md5");
    md5sum.update(key);
    hash = new Buffer(md5sum.digest(), "binary");
    al = hash.readUInt32LE(0);
    ah = hash.readUInt32LE(4);
    i = 0;
    while (i < 256) {
      table[i] = i;
      i++;
    }
    i = 1;
    while (i < 1024) {
      table = merge_sort(table, function(x, y) {
        return ((ah % (x + i)) * int32Max + al) % (x + i) - ((ah % (y + i)) * int32Max + al) % (y + i);
      });
      i++;
    }
    i = 0;
    while (i < 256) {
      decrypt_table[table[i]] = i;
      ++i;
    }
    return [table, decrypt_table];
  };

  encrypt = function(table, buf) {
    var i;
    i = 0;
    while (i < buf.length) {
      buf[i] = table[buf[i]];
      i++;
    }
    return buf;
  };

  parseArgs = function() {
    var defination, lastKey, nextIsValue, oneArg, result, _, _ref;
    defination = {
      '-l': 'local_port',
      '-p': 'server_port',
      '-s': 'server',
      '-k': 'password'
    };
    result = {};
    nextIsValue = false;
    lastKey = null;
    _ref = process.argv;
    for (_ in _ref) {
      oneArg = _ref[_];
      if (nextIsValue) {
        result[lastKey] = oneArg;
        nextIsValue = false;
      } else if (oneArg in defination) {
        lastKey = defination[oneArg];
        nextIsValue = true;
      }
    }
    return result;
  };

  var KEY, PORT, REMOTE_PORT, SERVER, config, configContent, configFromArgs, decryptTable, encrypt, encryptTable, fs, getServer, inetAton, inetNtoa, k, net, path, server, tables, timeout, v;

  inetNtoa = function(buf) {
    return buf[0] + "." + buf[1] + "." + buf[2] + "." + buf[3];
  };

  inetAton = function(ipStr) {
    var buf, i, parts;
    parts = ipStr.split(".");
    if (parts.length !== 4) {
      return null;
    } else {
      buf = new Buffer(4);
      i = 0;
      while (i < 4) {
        buf[i] = +parts[i];
        i++;
      }
      return buf;
    }
  };

  fs = require("fs");

  path = require("path");

  configFromArgs = parseArgs();

  config = {
    "server": "127.0.0.1",
    "server_port": 8388,
    "local_prot": 1080,
    "password": "test",
    "timeout": 60
  }

  for (k in configFromArgs) {
    v = configFromArgs[k];
    config[k] = v;
  }

  SERVER = config.server;

  REMOTE_PORT = config.server_port;

  PORT = config.local_port;

  KEY = config.password;

  timeout = Math.floor(config.timeout * 1000);

  getServer = function() {
    if (SERVER instanceof Array) {
      return SERVER[Math.floor(Math.random() * SERVER.length)];
    } else {
      return SERVER;
    }
  };

  net = require("net");

  console.log("calculating ciphers");

  console.log = function() {};

  tables = getTable(KEY);

  encryptTable = tables[0];

  decryptTable = tables[1];

  server = net.createServer(function(connection) {
    var addrLen, addrToSend, cachedPieces, headerLength, remote, remoteAddr, remotePort, stage;
    console.log("server connected");
    console.log("concurrent connections: " + server.connections);
    stage = 0;
    headerLength = 0;
    remote = null;
    cachedPieces = [];
    addrLen = 0;
    remoteAddr = null;
    remotePort = null;
    addrToSend = "";
    connection.on("data", function(data) {
      var aServer, addrtype, buf, cmd, reply, tempBuf;
      if (stage === 5) {
        encrypt(encryptTable, data);
        // Android Patch
        if (data == null || !remote.write(data)) {
          connection.pause();
        }
        return;
      }
      if (stage === 0) {
        tempBuf = new Buffer(2);
        tempBuf.write("\u0005\u0000", 0);
        connection.write(tempBuf);
        stage = 1;
        return;
      }
      if (stage === 1) {
        try {
          cmd = data[1];
          addrtype = data[3];
          if (cmd !== 1) {
            console.warn("unsupported cmd: " + cmd);
            reply = new Buffer("\u0005\u0007\u0000\u0001", "binary");
            connection.end(reply);
            return;
          }
          if (addrtype === 3) {
            addrLen = data[4];
          } else if (addrtype !== 1) {
            console.warn("unsupported addrtype: " + addrtype);
            connection.end();
            return;
          }
          addrToSend = data.slice(3, 4).toString("binary");
          if (addrtype === 1) {
            remoteAddr = inetNtoa(data.slice(4, 8));
            addrToSend += data.slice(4, 10).toString("binary");
            remotePort = data.readUInt16BE(8);
            headerLength = 10;
          } else {
            remoteAddr = data.slice(5, 5 + addrLen).toString("binary");
            addrToSend += data.slice(4, 5 + addrLen + 2).toString("binary");
            remotePort = data.readUInt16BE(5 + addrLen);
            headerLength = 5 + addrLen + 2;
          }
          buf = new Buffer(10);
          buf.write("\u0005\u0000\u0000\u0001", 0, 4, "binary");
          buf.write("\u0000\u0000\u0000\u0000", 4, 4, "binary");
          buf.writeInt16BE(remotePort, 8);
          connection.write(buf);
          aServer = getServer();
          remote = net.connect(REMOTE_PORT, aServer, function() {
            var addrToSendBuf, i, piece;
            console.log("connecting " + remoteAddr + " via " + aServer);
            addrToSendBuf = new Buffer(addrToSend, "binary");
            encrypt(encryptTable, addrToSendBuf);
            remote.write(addrToSendBuf);
            i = 0;
            while (i < cachedPieces.length) {
              piece = cachedPieces[i];
              encrypt(encryptTable, piece);
              remote.write(piece);
              i++;
            }
            cachedPieces = null;
            return stage = 5;
          });
          remote.on("data", function(data) {
            encrypt(decryptTable, data);
            if (!connection.write(data)) {
              return remote.pause();
            }
          });
          remote.on("end", function() {
            console.log("remote disconnected");
            connection.end();
            return console.log("concurrent connections: " + server.connections);
          });
          remote.on("error", function() {
            if (stage === 4) {
              console.warn("remote connection refused");
              connection.destroy();
              return;
            }
            console.warn("remote error");
            connection.end();
            return console.log("concurrent connections: " + server.connections);
          });
          remote.on("drain", function() {
            return connection.resume();
          });
          remote.setTimeout(timeout, function() {
            connection.end();
            return remote.destroy();
          });
          if (data.length > headerLength) {
            buf = new Buffer(data.length - headerLength);
            data.copy(buf, 0, headerLength);
            cachedPieces.push(buf);
            buf = null;
          }
          return stage = 4;
        } catch (e) {
          console.warn(e);
          connection.destroy();
          if (remote) {
            return remote.destroy();
          }
        }
      } else {
        if (stage === 4) {
          return cachedPieces.push(data);
        }
      }
    });
    connection.on("end", function() {
      console.log("server disconnected");
      if (remote) {
        remote.destroy();
      }
      return console.log("concurrent connections: " + server.connections);
    });
    connection.on("error", function() {
      console.warn("server error");
      if (remote) {
        remote.destroy();
      }
      return console.log("concurrent connections: " + server.connections);
    });
    connection.on("drain", function() {
      if (remote && stage === 5) {
        return remote.resume();
      }
    });
    return connection.setTimeout(timeout, function() {
      if (remote) {
        remote.destroy();
      }
      return connection.destroy();
    });
  });

  var pid = ""+process.pid; // need to turn into a string
  fs.writeFile('/data/data/com.github.shadowsocks/shadowsocks.pid', pid, function (err) {
     // Ignore
  });

  server.listen(PORT, function() {
    return console.log("server listening at port " + PORT);
  });

  server.on("error", function(e) {
    if (e.code === "EADDRINUSE") {
      return console.warn("Address in use, aborting");
    }
  });

}).call(this);
