require('classtool');

function spec(b) {
  var config = b.config || require('./config');
  var log = b.log || require('./util/log');

  var Opcode = require('./Opcode').class();

  // Make opcodes available as pseudo-constants
  for (var i in Opcode.map) {
    eval(i + " = " + Opcode.map[i] + ";");
  }

  var util = b.util || require('./util/util');
  var Parser = b.Parser || require('./util/BinaryParser').class();
  var Put = b.Put || require('bufferput');

  var TX_UNKNOWN = 0;
  var TX_PUBKEY = 1;
  var TX_PUBKEYHASH = 2;
  var TX_MULTISIG = 3;
  var TX_SCRIPTHASH = 4;

  var TX_TYPES = [
    'unknown',
    'pubkey',
    'pubkeyhash',
    'multisig',
    'scripthash'
  ];

  function Script(buffer) {
    if(buffer) {
      this.buffer = buffer;
    } else {
      this.buffer = util.EMPTY_BUFFER;
    }
    this.chunks = [];
    this.parse();
  };
  this.class = Script;

  Script.prototype.parse = function () {
    this.chunks = [];

    var parser = new Parser(this.buffer);
    while (!parser.eof()) {
      var opcode = parser.word8();

      var len;
      if (opcode > 0 && opcode < OP_PUSHDATA1) {
        // Read some bytes of data, opcode value is the length of data
        this.chunks.push(parser.buffer(opcode));
      } else if (opcode == OP_PUSHDATA1) {
        len = parser.word8();
        this.chunks.push(parser.buffer(len));
      } else if (opcode == OP_PUSHDATA2) {
        len = parser.word16le();
        this.chunks.push(parser.buffer(len));
      } else if (opcode == OP_PUSHDATA4) {
        len = parser.word32le();
        this.chunks.push(parser.buffer(len));
      } else {
        this.chunks.push(opcode);
      }
    }
  };

  Script.prototype.isPushOnly = function ()
  {
    for (var i = 0; i < this.chunks.length; i++)
      if (!Buffer.isBuffer(this.chunks[i]))
        return false;

    return true;
  };

  Script.prototype.isP2SH = function ()
  {
    return (this.chunks.length == 3 &&
	    this.chunks[0] == OP_HASH160 &&
	    Buffer.isBuffer(this.chunks[1]) &&
	    this.chunks[1].length == 20 &&
	    this.chunks[2] == OP_EQUAL);
  };

  Script.prototype.isPubkey = function ()
  {
    return (this.chunks.length == 2 &&
    	    Buffer.isBuffer(this.chunks[0]) &&
    	    this.chunks[1] == OP_CHECKSIG);
  };

  Script.prototype.isPubkeyHash = function ()
  {
    return (this.chunks.length == 5 &&
      	    this.chunks[0] == OP_DUP &&
      	    this.chunks[1] == OP_HASH160 &&
	    Buffer.isBuffer(this.chunks[2]) &&
	    this.chunks[2].length == 20 &&
      	    this.chunks[3] == OP_EQUALVERIFY &&
      	    this.chunks[4] == OP_CHECKSIG);
  };

  function isSmallIntOp(opcode)
  {
    return ((opcode == OP_0) ||
    	    ((opcode >= OP_1) && (opcode <= OP_16)));
  };

  Script.prototype.isMultiSig = function ()
  {
    return (this.chunks.length > 3 &&
    	    isSmallIntOp(this.chunks[0]) &&
    	    isSmallIntOp(this.chunks[-2]) &&
	    this.chunks[-1] == OP_CHECKMULTISIG);
  };

  // is this a script form we know?
  Script.prototype.classify = function ()
  {
  	if (this.isPubkeyHash())
		return TX_PUBKEYHASH;
	if (this.isP2SH())
		return TX_SCRIPTHASH;
	if (this.isMultiSig())
		return TX_MULTISIG;
	if (this.isPubkey())
		return TX_PUBKEY;
	return TX_UNKNOWN;
  };

  // extract useful data items from known scripts
  Script.prototype.capture = function ()
  {
  	var txType = this.classify();
	var res = [];
	switch (txType) {
	case TX_PUBKEY:
		res.push(this.chunks[0]);
		break;
	case TX_PUBKEYHASH:
		res.push(this.chunks[2]);
		break;
	case TX_MULTISIG:
		for (var i = 1; i < (this.chunks.length - 2); i++)
			res.push(this.chunks[i]);
		break;
	case TX_SCRIPTHASH:
		res.push(this.chunks[1]);
		break;

	case TX_UNKNOWN:
	default:
		// do nothing
		break;
	}

	return res;
  };

  // return first extracted data item from script
  Script.prototype.captureOne = function ()
  {
  	var arr = this.capture();
	return arr[0];
  };

  Script.prototype.getOutType = function ()
  {
    var txType = this.classify();
    switch (txType) {
      case TX_PUBKEY:		return 'Pubkey';
      case TX_PUBKEYHASH:	return 'Address';
      default:		return 'Strange';
    }
  };

  Script.prototype.getRawOutType = function() {
    return TX_TYPES[this.classify()];
  };

  Script.prototype.simpleOutHash = function ()
  {
    switch (this.getOutType()) {
    case 'Address':
      return this.chunks[2];
    case 'Pubkey':
      return util.sha256ripe160(this.chunks[0]);
    default:
      log.debug("Encountered non-standard scriptPubKey");
      log.debug("Strange script was: " + this.toString());
      return null;
    }
  };

  Script.prototype.getInType = function ()
  {
    if (this.chunks.length == 1) {
      // Direct IP to IP transactions only have the public key in their scriptSig.
      return 'Pubkey';
    } else if (this.chunks.length == 2 &&
               Buffer.isBuffer(this.chunks[0]) &&
               Buffer.isBuffer(this.chunks[1])) {
      return 'Address';
    } else {
      return 'Strange';
    }
  };

  Script.prototype.simpleInPubKey = function ()
  {
    switch (this.getInType()) {
    case 'Address':
      return this.chunks[1];
    case 'Pubkey':
      return null;
    default:
      log.debug("Encountered non-standard scriptSig");
      log.debug("Strange script was: " + this.toString());
      return null;
    }
  };

  Script.prototype.getBuffer = function ()
  {
    return this.buffer;
  };

  Script.prototype.getStringContent = function (truncate, maxEl)
  {
    if (truncate === null) {
      truncate = true;
    }

    if ("undefined" === typeof maxEl) {
      maxEl = 15;
    }

    var script = '';
    for (var i = 0, l = this.chunks.length; i < l; i++) {
      var chunk = this.chunks[i];

      if (i > 0) {
        script += " ";
      }

      if (Buffer.isBuffer(chunk)) {
        script += "0x"+util.formatBuffer(chunk, truncate ? null : 0);
      } else {
        script += Opcode.reverseMap[chunk];
      }

      if (maxEl && i > maxEl) {
        script += " ...";
        break;
      }
    }
    return script;
  };

  Script.prototype.toString = function (truncate, maxEl)
  {
    var script = "<Script ";
    script += this.getStringContent(truncate, maxEl);
    script += ">";
    return script;
  };


  Script.prototype.writeOp = function (opcode)
  {
    var buf = Buffer(this.buffer.length + 1);
    this.buffer.copy(buf);
    buf.writeUInt8(opcode, this.buffer.length);

    this.buffer = buf;

    this.chunks.push(opcode);
  };

  Script.prototype.writeN = function (n)
  {
    if (n < 0 || n > 16)
      throw new Error("writeN: out of range value " + n);

    if (n == 0)
      this.writeOp(OP_0);
    else
      this.writeOp(OP_1 + n - 1);
  };

  function prefixSize(data_length)
  {
    if (data_length < OP_PUSHDATA1) {
      return 1;
    } else if (data_length <= 0xff) {
      return 1 + 1;
    } else if (data_length <= 0xffff) {
      return 1 + 2;
    } else {
      return 1 + 4;
    }
  };

  function encodeLen(data_length) {
    var buf = undefined;
    if (data_length < OP_PUSHDATA1) {
      buf = new Buffer(1);
      buf.writeUInt8(data_length, 0);
    }
    
    else if (data_length <= 0xff) {
      buf = new Buffer(1 + 1);
      buf.writeUInt8(OP_PUSHDATA1, 0);
      buf.writeUInt8(data_length, 1);
    }
    
    else if (data_length <= 0xffff) {
      buf = new Buffer(1 + 2);
      buf.writeUInt8(OP_PUSHDATA2, 0);
      buf.writeUInt16LE(data_length, 1);
    }
    
    else {
      buf = new Buffer(1 + 4);
      buf.writeUInt8(OP_PUSHDATA4, 0);
      buf.writeUInt32LE(data_length, 1);
    }

    return buf;
  };

  Script.prototype.writeBytes = function (data)
  {
    var newSize = this.buffer.length + prefixSize(data.length) + data.length;
    this.buffer = Buffer.concat([this.buffer, encodeLen(data.length), data]);
    this.chunks.push(data);
  };

  Script.prototype.updateBuffer = function ()
  {
    this.buffer = Script.chunksToBuffer(this.chunks);
  };

  Script.prototype.findAndDelete = function (chunk)
  {
    var dirty = false;
    if (Buffer.isBuffer(chunk)) {
      for (var i = 0, l = this.chunks.length; i < l; i++) {
        if (Buffer.isBuffer(this.chunks[i]) &&
            this.chunks[i].compare(chunk) == 0) {
          this.chunks.splice(i, 1);
          dirty = true;
        }
      }
    } else if ("number" === typeof chunk) {
      for (var i = 0, l = this.chunks.length; i < l; i++) {
        if (this.chunks[i] === chunk) {
          this.chunks.splice(i, 1);
          dirty = true;
        }
      }
    } else {
      throw new Error("Invalid chunk datatype.");
    }
    if (dirty) {
      this.updateBuffer();
    }
  };

  /**
   * Creates a simple OP_CHECKSIG with pubkey output script.
   *
   * These are used for coinbase transactions and at some point were used for
   * IP-based transactions as well.
   */
  Script.createPubKeyOut = function (pubkey) {
    var script = new Script();
    script.writeBytes(pubkey);
    script.writeOp(OP_CHECKSIG);
    return script;
  };

  /**
   * Creates a standard txout script.
   */
  Script.createPubKeyHashOut = function (pubKeyHash) {
    var script = new Script();
    script.writeOp(OP_DUP);
    script.writeOp(OP_HASH160);
    script.writeBytes(pubKeyHash);
    script.writeOp(OP_EQUALVERIFY);
    script.writeOp(OP_CHECKSIG);
    return script;
  };

  Script.createMultisig = function(n_required, keys) {
    var script = new Script();
    script.writeN(n_required);
    keys.forEach(function(key) {
    	script.writeBytes(key);
    });
    script.writeN(keys.length);
    script.writeOp(OP_CHECKMULTISIG);
    return script;
  };

  Script.createP2SH = function(scriptHash) {
    var script = new Script();
    script.writeOp(OP_HASH160);
    script.writeBytes(scriptHash);
    script.writeOp(OP_EQUAL);
    return script;
  };

  Script.fromTestData = function (testData) {
    testData = testData.map(function (chunk) {
      if ("string" === typeof chunk) {
        return new Buffer(chunk, 'hex');
      } else {
        return chunk;
      }
    });

    var script = new Script();
    script.chunks = testData;
    script.updateBuffer();
    return script;
  };

  Script.fromChunks = function (chunks) {
    var script = new Script();
    script.chunks = chunks;
    script.updateBuffer();
    return script;
  };

  Script.chunksToBuffer = function (chunks) {
    var buf = Put();
    for (var i = 0, l = chunks.length; i < l; i++) {
      var data = chunks[i];
      if (Buffer.isBuffer(data)) {
        if (data.length < OP_PUSHDATA1) {
          buf.word8(data.length);
        } else if (data.length <= 0xff) {
          buf.word8(OP_PUSHDATA1);
          buf.word8(data.length);
        } else if (data.length <= 0xffff) {
          buf.word8(OP_PUSHDATA2);
          buf.word16le(data.length);
        } else {
          buf.word8(OP_PUSHDATA4);
          buf.word32le(data.length);
        }
        buf.put(data);
      } else if ("number" === typeof data) {
        buf.word8(data);
      } else {
        throw new Error("Script.chunksToBuffer(): Invalid chunk datatype");
      }
    }
    return buf.buffer();
  };

  return Script;
};
module.defineClass(spec);