const BN = require('bn.js');
const numberToBN = require('number-to-bn');
const keccak256 = require('./hash').keccak256;
const Buffer = require('safe-buffer').Buffer;
const _ = require('underscore');
const utf8 = require('utf8');
const Hash = require('./hash');

/**
 * Returns true if object is BN, otherwise false
 *
 * @method isBN
 * @param {Object} object
 * @return {Boolean}
 */
var isBN = function (object) {
    return BN.isBN(object);
};

/**
 * Returns true if object is BigNumber, otherwise false
 *
 * @method isBigNumber
 * @param {Object} object
 * @return {Boolean}
 */
var isBigNumber = function (object) {
    return object && object.constructor && object.constructor.name === 'BigNumber';
};

/**
 * Takes an input and transforms it into an BN
 *
 * @method toBN
 * @param {Number|String|BN} number, string, HEX string or BN
 * @return {BN} BN
 */
var toBN = function(number){
    try {
        return numberToBN.apply(null, arguments);
    } catch(e) {
        throw new Error(e + ' Given value: "'+ number +'"');
    }
};


const stringToHex = (str) => {
    var val = "0x";
    for (var i = 0; i < str.length; i++) {
        if (val === "") { val = str.charCodeAt(i).toString(16); } else { val += str.charCodeAt(i).toString(16); }
    }
    return val;
}

const hexToString = (str) => {
    let s = str.indexOf("0x") == 0 ? str.slice(2) : str;
    let val = '';
    for(let i = 0; i < s.length; i += 2) {
        val += String.fromCharCode(+parseInt(s.substr(i, 2), 16).toString(10))
    }
    return val;
}

function stripZeros(aInput) {
    var a = aInput; // eslint-disable-line
    var first = a[0]; // eslint-disable-line
    while (a.length > 0 && first.toString() === '0') {
        a = a.slice(1);
        first = a[0];
    }
    return a;
}

function bnToBuffer(bnInput) {
    var bn = bnInput; // eslint-disable-line
    var hex = bn.toString(16); // eslint-disable-line
    if (hex.length % 2) { hex = `0${hex}`; }
    return stripZeros(Buffer.from(hex, 'hex'));
}

function isHexString(value, length) {
    if (typeof(value) !== 'string' || !value.match(/^0x[0-9A-Fa-f]*$/)) {
        return false;
    }
    if (length && value.length !== 2 + 2 * length) { return false; }
    return true;
}

function hexOrBuffer(valueInput, name) {
    var value = valueInput; // eslint-disable-line
    if (!Buffer.isBuffer(value)) {
        if (!isHexString(value)) {
            const error = new Error(name ? (`[intjs-abi] invalid ${name}`) : '[intjs-abi] invalid hex or buffer, must be a prefixed alphanumeric even length hex string');
            error.reason = '[intjs-abi] invalid hex string, hex must be prefixed and alphanumeric (e.g. 0x023..)';
            error.value = value;
            throw error;
        }

        value = value.substring(2);
        if (value.length % 2) { value = `0${value}`; }
        value = Buffer.from(value, 'hex');
    }

    return value;
}

function hexlify(value) {
    if (typeof(value) === 'number') {
        return `0x${bnToBuffer(new BN(value)).toString('hex')}`;
    } else if (value.mod || value.modulo) {
        return `0x${bnToBuffer(value).toString('hex')}`;
    } else { // eslint-disable-line
        return `0x${hexOrBuffer(value).toString('hex')}`;
    }
}

// getKeys([{a: 1, b: 2}, {a: 3, b: 4}], 'a') => [1, 3]
function getKeys(params, key, allowEmpty) {
    var result = []; // eslint-disable-line

    if (!Array.isArray(params)) { throw new Error(`[intjs-abi] while getting keys, invalid params value ${JSON.stringify(params)}`); }

    for (var i = 0; i < params.length; i++) { // eslint-disable-line
        var value = params[i][key];  // eslint-disable-line
        if (allowEmpty && !value) {
            value = '';
        } else if (typeof(value) !== 'string') {
            throw new Error('[intjs-abi] while getKeys found invalid ABI data structure, type value not string');
        }
        result.push(value);
    }

    return result;
}

function coderNumber(size, signed) {
    return {
        encode: function encodeNumber(valueInput) {
            var value = valueInput; // eslint-disable-line

            if (typeof value === 'object'
                && value.toString
                && (value.toTwos || value.dividedToIntegerBy)) {
                value = (value.toString(10)).split('.')[0];
            }

            if (typeof value === 'string' || typeof value === 'number') {
                value = String(value).split('.')[0];
            }

            value = numberToBN(value);
            value = value.toTwos(size * 8).maskn(size * 8);
            if (signed) {
                value = value.fromTwos(size * 8).toTwos(256);
            }
            return value.toArrayLike(Buffer, 'be', 32);
        },
        decode: function decodeNumber(data, offset) {
            var junkLength = 32 - size; // eslint-disable-line
            var value = new BN(data.slice(offset + junkLength, offset + 32)); // eslint-disable-line
            if (signed) {
                value = value.fromTwos(size * 8);
            } else {
                value = value.maskn(size * 8);
            }
            return {
                consumed: 32,
                value: new BN(value.toString(10)),
            };
        },
    };
}
const uint256Coder = coderNumber(32, false);

const coderBoolean = {
    encode: function encodeBoolean(value) {
        return uint256Coder.encode(value ? 1 : 0);
    },
    decode: function decodeBoolean(data, offset) {
        var result = uint256Coder.decode(data, offset); // eslint-disable-line
        return {
            consumed: result.consumed,
            value: !result.value.isZero(),
        };
    },
};

function coderFixedBytes(length) {
    return {
        encode: function encodeFixedBytes(valueInput) {
            var value = valueInput; // eslint-disable-line
            value = hexOrBuffer(value);

            if (value.length === 32) { return value; }

            var result = Buffer.alloc(32); // eslint-disable-line
            result.fill(0);
            value.copy(result);
            return result;
        },
        decode: function decodeFixedBytes(data, offset) {
            if (data.length !== 0 && data.length < offset + 32) { throw new Error(`[intjs-abi] while decoding fixed bytes, invalid bytes data length: ${length}`); }

            return {
                consumed: 32,
                value: `0x${data.slice(offset, offset + length).toString('hex')}`,
            };
        },
    };
}

const coderAddress = {
    encode: function encodeAddress(valueInput) {
        var value = valueInput; // eslint-disable-line
        var result = Buffer.alloc(32); // eslint-disable-line
        if (!isHexString(value, 20)) { throw new Error('[intjs-abi] while encoding address, invalid address value, not alphanumeric 20 byte hex string'); }
        value = hexOrBuffer(value);
        result.fill(0);
        value.copy(result, 12);
        return result;
    },
    decode: function decodeAddress(data, offset) {
        if (data.length === 0) {
            return {
                consumed: 32,
                value: '0x',
            };
        }
        if (data.length !== 0 && data.length < offset + 32) { throw new Error(`[intjs-abi] while decoding address data, invalid address data, invalid byte length ${data.length}`); }
        return {
            consumed: 32,
            value: `0x${data.slice(offset + 12, offset + 32).toString('hex')}`,
        };
    },
};

function encodeDynamicBytesHelper(value) {
    var dataLength = parseInt(32 * Math.ceil(value.length / 32)); // eslint-disable-line
    var padding = Buffer.alloc(dataLength - value.length); // eslint-disable-line
    padding.fill(0);

    return Buffer.concat([
        uint256Coder.encode(value.length),
        value,
        padding,
    ]);
}

function decodeDynamicBytesHelper(data, offset) {
    if (data.length !== 0 && data.length < offset + 32) { throw new Error(`[intjs-abi] while decoding dynamic bytes data, invalid bytes length: ${data.length} should be less than ${offset + 32}`); }

    var length = uint256Coder.decode(data, offset).value; // eslint-disable-line
    length = length.toNumber();
    if (data.length !== 0 && data.length < offset + 32 + length) { throw new Error(`[intjs-abi] while decoding dynamic bytes data, invalid bytes length: ${data.length} should be less than ${offset + 32 + length}`); }

    return {
        consumed: parseInt(32 + 32 * Math.ceil(length / 32), 10),
        value: data.slice(offset + 32, offset + 32 + length),
    };
}

const coderDynamicBytes = {
    encode: function encodeDynamicBytes(value) {
        return encodeDynamicBytesHelper(hexOrBuffer(value));
    },
    decode: function decodeDynamicBytes(data, offset) {
        var result = decodeDynamicBytesHelper(data, offset); // eslint-disable-line
        result.value = `0x${result.value.toString('hex')}`;
        return result;
    },
    dynamic: true,
};

const coderString = {
    encode: function encodeString(value) {
        return encodeDynamicBytesHelper(Buffer.from(value, 'utf8'));
    },
    decode: function decodeString(data, offset) {
        var result = decodeDynamicBytesHelper(data, offset); // eslint-disable-line
        result.value = result.value.toString('utf8');
        return result;
    },
    dynamic: true,
};

function coderArray(coder, lengthInput) {
    return {
        encode: function encodeArray(value) {
            var result = Buffer.alloc(0); // eslint-disable-line
            var length = lengthInput; // eslint-disable-line

            if (!Array.isArray(value)) { throw new Error('[intjs-abi] while encoding array, invalid array data, not type Object (Array)'); }

            if (length === -1) {
                length = value.length;
                result = uint256Coder.encode(length);
            }

            if (length !== value.length) { throw new Error(`[intjs-abi] while encoding array, size mismatch array length ${length} does not equal ${value.length}`); }

            value.forEach((resultValue) => {
                result = Buffer.concat([
                    result,
                    coder.encode(resultValue),
                ]);
            });

            return result;
        },
        decode: function decodeArray(data, offsetInput) {
            var length = lengthInput; // eslint-disable-line
            var offset = offsetInput; // eslint-disable-line
            // @TODO:
            // if (data.length < offset + length * 32) { throw new Error('invalid array'); }

            var consumed = 0; // eslint-disable-line
            var decodeResult; // eslint-disable-line

            if (length === -1) {
                decodeResult = uint256Coder.decode(data, offset);
                length = decodeResult.value.toNumber();
                consumed += decodeResult.consumed;
                offset += decodeResult.consumed;
            }

            var value = []; // eslint-disable-line

            for (var i = 0; i < length; i++) { // eslint-disable-line
                const loopResult = coder.decode(data, offset);
                consumed += loopResult.consumed;
                offset += loopResult.consumed;
                value.push(loopResult.value);
            }

            return {
                consumed,
                value,
            };
        },
        dynamic: (lengthInput === -1),
    };
}

// Break the type up into [staticType][staticArray]*[dynamicArray]? | [dynamicType] and
// build the coder up from its parts
const paramTypePart = new RegExp(/^((u?int|bytes)([0-9]*)|(address|bool|string)|(\[([0-9]*)\]))/);

function getParamCoder(typeInput) {
    var type = typeInput; // eslint-disable-line
    var coder = null; // eslint-disable-line
    const invalidTypeErrorMessage = `[intjs-abi] while getting param coder (getParamCoder) type value ${JSON.stringify(type)} is either invalid or unsupported by intjs-abi.`;

    while (type) {
        var part = type.match(paramTypePart); // eslint-disable-line
        if (!part) { throw new Error(invalidTypeErrorMessage); }
        type = type.substring(part[0].length);

        var prefix = (part[2] || part[4] || part[5]); // eslint-disable-line
        switch (prefix) {
            case 'int': case 'uint':
                if (coder) { throw new Error(invalidTypeErrorMessage); }
                var intSize = parseInt(part[3] || 256); // eslint-disable-line
                if (intSize === 0 || intSize > 256 || (intSize % 8) !== 0) {
                    throw new Error(`[intjs-abi] while getting param coder for type ${type}, invalid ${prefix}<N> width: ${type}`);
                }

                coder = coderNumber(intSize / 8, (prefix === 'int'));
                break;

            case 'bool':
                if (coder) { throw new Error(invalidTypeErrorMessage); }
                coder = coderBoolean;
                break;

            case 'string':
                if (coder) { throw new Error(invalidTypeErrorMessage); }
                coder = coderString;
                break;

            case 'bytes':
                if (coder) { throw new Error(invalidTypeErrorMessage); }
                if (part[3]) {
                    var size = parseInt(part[3]); // eslint-disable-line
                    if (size === 0 || size > 32) {
                        throw new Error(`[intjs-abi] while getting param coder for prefix bytes, invalid type ${type}, size ${size} should be 0 or greater than 32`);
                    }
                    coder = coderFixedBytes(size);
                } else {
                    coder = coderDynamicBytes;
                }
                break;

            case 'address':
                if (coder) { throw new Error(invalidTypeErrorMessage); }
                coder = coderAddress;
                break;

            case '[]':
                if (!coder || coder.dynamic) { throw new Error(invalidTypeErrorMessage); }
                coder = coderArray(coder, -1);
                break;

            // "[0-9+]"
            default:
                if (!coder || coder.dynamic) { throw new Error(invalidTypeErrorMessage); }
                var defaultSize = parseInt(part[6]); // eslint-disable-line
                coder = coderArray(coder, defaultSize);
        }
    }

    if (!coder) { throw new Error(invalidTypeErrorMessage); }
    return coder;
}



const zero = new BN(0);
const negative1 = new BN(-1);

const unitMap = {
    'wei':          '1', // eslint-disable-line
    'int':        '1000000000000000000', // eslint-disable-line
};

/**
 * Returns value of unit in Wei
 *
 * @method getValueOfUnit
 * @param {String} unit the unit to convert to, default int
 * @returns {BigNumber} value of the unit (in Wei)
 * @throws error if the unit is not correct:w
 */
function getValueOfUnit(unitInput) {
    const unit = unitInput ? unitInput.toLowerCase() : 'int';
    var unitValue = unitMap[unit]; // eslint-disable-line

    if (typeof unitValue !== 'string') {
        throw new Error(`[intjs-unit] the unit provided ${unitInput} doesn't exists, please use the one of the following units ${JSON.stringify(unitMap, null, 2)}`);
    }

    return new BN(unitValue, 10);
}

function numberToString(arg) {
    if (typeof arg === 'string') {
        if (!arg.match(/^-?[0-9.]+$/)) {
            throw new Error(`while converting number to string, invalid number value '${arg}', should be a number matching (^-?[0-9.]+).`);
        }
        return arg;
    } else if (typeof arg === 'number') {
        return String(arg);
    } else if (typeof arg === 'object' && arg.toString && (arg.toTwos || arg.dividedToIntegerBy)) {
        if (arg.toPrecision) {
            return String(arg.toPrecision());
        } else { // eslint-disable-line
            return arg.toString(10);
        }
    }
    throw new Error(`while converting number to string, invalid number value '${arg}' type ${typeof arg}.`);
}

function fromWei(weiInput, unit, optionsInput) {
    var wei = numberToBN(weiInput); // eslint-disable-line
    var negative = wei.lt(zero); // eslint-disable-line
    const base = getValueOfUnit(unit);
    const baseLength = unitMap[unit].length - 1 || 1;
    const options = optionsInput || {};

    if (negative) {
        wei = wei.mul(negative1);
    }

    var fraction = wei.mod(base).toString(10); // eslint-disable-line

    while (fraction.length < baseLength) {
        fraction = `0${fraction}`;
    }

    if (!options.pad) {
        fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1];
    }

    var whole = wei.div(base).toString(10); // eslint-disable-line

    if (options.commify) {
        whole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
    }

    var value = `${whole}${fraction == '0' ? '' : `.${fraction}`}`; // eslint-disable-line

    if (negative) {
        value = `-${value}`;
    }

    return value;
}

function toWei(intInput, unit) {
    var int = numberToString(intInput); // eslint-disable-line
    const base = getValueOfUnit(unit);
    const baseLength = unitMap[unit].length - 1 || 1;

    // Is it negative?
    var negative = (int.substring(0, 1) === '-'); // eslint-disable-line
    if (negative) {
        int = int.substring(1);
    }

    if (int === '.') { throw new Error(`[intjs-unit] while converting number ${intInput} to wei, invalid value`); }

    // Split it into a whole and fractional part
    var comps = int.split('.'); // eslint-disable-line
    if (comps.length > 2) { throw new Error(`[intjs-unit] while converting number ${intInput} to wei,  too many decimal points`); }

    var whole = comps[0], fraction = comps[1]; // eslint-disable-line

    if (!whole) { whole = '0'; }
    if (!fraction) { fraction = '0'; }
    if (fraction.length > baseLength) { throw new Error(`[intjs-unit] while converting number ${intInput} to wei, too many decimal places`); }

    while (fraction.length < baseLength) {
        fraction += '0';
    }

    whole = new BN(whole);
    fraction = new BN(fraction);
    var wei = (whole.mul(base)).add(fraction); // eslint-disable-line

    if (negative) {
        wei = wei.mul(negative1);
    }

    return new BN(wei.toString(10), 10);
}

/**
 *
 * @method fromWei
 * @param {Number|String} number can be a number, number string or a HEX of a decimal
 * @return {String|Object} When given a BN object it returns one as well, otherwise a number
 */
var fromINT = function(number) {
    if(!isBN(number) && !_.isString(number)) {
        throw new Error('Please pass numbers as strings or BN objects to avoid precision errors.');
    }

    return isBN(number) ? toWei(number, "int") : toWei(number, "int").toString(10);
};

/**
 *
 * @method toINT
 * @param {Number|String|BN} number can be a number, number string or a HEX of a decimal
 * @return {String|Object} When given a BN object it returns one as well, otherwise a number
 */
var toINT = function(number) {
    if(!isBN(number) && !_.isString(number)) {
        throw new Error('Please pass numbers as strings or BN objects to avoid precision errors.');
    }

    return isBN(number) ? fromWei(number, 'int') : fromWei(number, 'int').toString(10);
};

/**
 * Checks if the given string is an address
 *
 * @method isAddress
 * @param {String} address the given HEX address
 * @return {Boolean}
 */
var isAddress = function (address) {
    // check if it has the basic requirements of an address
    if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) {
        return false;
        // If it's ALL lowercase or ALL upppercase
    } else if (/^(0x|0X)?[0-9a-f]{40}$/.test(address) || /^(0x|0X)?[0-9A-F]{40}$/.test(address)) {
        return true;
        // Otherwise check each case
    } else {
        return checkAddressChecksum(address);
    }
};



/**
 * Checks if the given string is a checksummed address
 *
 * @method checkAddressChecksum
 * @param {String} address the given HEX address
 * @return {Boolean}
 */
var checkAddressChecksum = function (address) {
    // Check each case
    address = address.replace(/^0x/i,'');
    var addressHash = sha3(address.toLowerCase()).replace(/^0x/i,'');

    for (var i = 0; i < 40; i++ ) {
        // the nth letter should be uppercase if the nth digit of casemap is 1
        if ((parseInt(addressHash[i], 16) > 7 && address[i].toUpperCase() !== address[i]) || (parseInt(addressHash[i], 16) <= 7 && address[i].toLowerCase() !== address[i])) {
            return false;
        }
    }
    return true;
};

/**
 * Converts to a checksum address
 *
 * @method toChecksumAddress
 * @param {String} address the given HEX address
 * @return {String}
 */
var toChecksumAddress = function (address) {
    if (typeof address === 'undefined') return '';

    if(!/^(0x)?[0-9a-f]{40}$/i.test(address))
        throw new Error('Given address "'+ address +'" is not a valid intchain address.');



    address = address.toLowerCase().replace(/^0x/i,'');
    var addressHash = utils.sha3(address).replace(/^0x/i,'');
    var checksumAddress = '0x';

    for (var i = 0; i < address.length; i++ ) {
        // If ith character is 9 to f then make it uppercase
        if (parseInt(addressHash[i], 16) > 7) {
            checksumAddress += address[i].toUpperCase();
        } else {
            checksumAddress += address[i];
        }
    }
    return checksumAddress;
};

/**
 * Check if string is HEX, requires a 0x in front
 *
 * @method isHexStrict
 * @param {String} hex to be checked
 * @returns {Boolean}
 */
var isHexStrict = function (hex) {
    return ((_.isString(hex) || _.isNumber(hex)) && /^(-)?0x[0-9a-f]*$/i.test(hex));
};

/**
 * Convert a byte array to a hex string
 *
 * Note: Implementation from crypto-js
 *
 * @method bytesToHex
 * @param {Array} bytes
 * @return {String} the hex string
 */
var bytesToHex = function(bytes) {
    for (var hex = [], i = 0; i < bytes.length; i++) {
        /* jshint ignore:start */
        hex.push((bytes[i] >>> 4).toString(16));
        hex.push((bytes[i] & 0xF).toString(16));
        /* jshint ignore:end */
    }
    return '0x'+ hex.join("");
};

/**
 * Convert a hex string to a byte array
 *
 * Note: Implementation from crypto-js
 *
 * @method hexToBytes
 * @param {string} hex
 * @return {Array} the byte array
 */
var hexToBytes = function(hex) {
    hex = hex.toString(16);

    if (!isHexStrict(hex)) {
        throw new Error('Given value "'+ hex +'" is not a valid hex string.');
    }

    hex = hex.replace(/^0x/i,'');

    for (var bytes = [], c = 0; c < hex.length; c += 2)
        bytes.push(parseInt(hex.substr(c, 2), 16));
    return bytes;
};

/**
 * Should be called to get hex representation (prefixed by 0x) of utf8 string
 *
 * @method utf8ToHex
 * @param {String} str
 * @returns {String} hex representation of input string
 */
var utf8ToHex = function(str) {
    str = utf8.encode(str);
    var hex = "";

    // remove \u0000 padding from either side
    str = str.replace(/^(?:\u0000)*/,'');
    str = str.split("").reverse().join("");
    str = str.replace(/^(?:\u0000)*/,'');
    str = str.split("").reverse().join("");

    for(var i = 0; i < str.length; i++) {
        var code = str.charCodeAt(i);
        // if (code !== 0) {
        var n = code.toString(16);
        hex += n.length < 2 ? '0' + n : n;
        // }
    }

    return "0x" + hex;
};

/**
 * Should be called to get utf8 from it's hex representation
 *
 * @method hexToUtf8
 * @param {String} hex
 * @returns {String} ascii string representation of hex value
 */
var hexToUtf8 = function(hex) {
    if (!isHexStrict(hex))
        throw new Error('The parameter "'+ hex +'" must be a valid HEX string.');

    var str = "";
    var code = 0;
    hex = hex.replace(/^0x/i,'');

    // remove 00 padding from either side
    hex = hex.replace(/^(?:00)*/,'');
    hex = hex.split("").reverse().join("");
    hex = hex.replace(/^(?:00)*/,'');
    hex = hex.split("").reverse().join("");

    var l = hex.length;

    for (var i=0; i < l; i+=2) {
        code = parseInt(hex.substr(i, 2), 16);
        // if (code !== 0) {
        str += String.fromCharCode(code);
        // }
    }

    return utf8.decode(str);
};


/**
 * Converts value to it's number representation
 *
 * @method hexToNumber
 * @param {String|Number|BN} value
 * @return {String}
 */
var hexToNumber = function (value) {
    if (!value) {
        return value;
    }

    if (typeof value === 'string' && !isHexStrict(value)) {
        throw new Error('Given value "'+value+'" is not a valid hex string.');
    }

    return toBN(value).toNumber();
};

/**
 * Converts value to it's decimal representation in string
 *
 * @method hexToNumberString
 * @param {String|Number|BN} value
 * @return {String}
 */
var hexToNumberString = function (value) {
    if (!value) return value;

    if (typeof value === 'string' && !isHexStrict(value)) {
        throw new Error('Given value "'+value+'" is not a valid hex string.');
    }

    return toBN(value).toString(10);
};


/**
 * Converts value to it's hex representation
 *
 * @method numberToHex
 * @param {String|Number|BN} value
 * @return {String}
 */
var numberToHex = function (value) {
    if ((value === null || value === undefined)) {
        return value;
    }

    if (!isFinite(value) && !isHexStrict(value)) {
        throw new Error('Given input "'+value+'" is not a number.');
    }

    var number = toBN(value);
    var result = number.toString(16);

    return number.lt(new BN(0)) ? '-0x' + result.substr(1) : '0x' + result;
};

/**
 * Auto converts any given value into it's hex representation.
 *
 * And even stringifys objects before.
 *
 * @method toHex
 * @param {String|Number|BN|Object|Buffer} value
 * @param {Boolean} returnType
 * @return {String}
 */
var toHex = function (value, returnType) {
    /*jshint maxcomplexity: false */

    if (isAddress(value)) {
        return returnType ? 'address' : '0x'+ value.toLowerCase().replace(/^0x/i,'');
    }

    if (typeof value === 'boolean' ) {
        return returnType ? 'bool' : value ? '0x01' : '0x00';
    }

    if (Buffer.isBuffer(value)) {
        return '0x' + value.toString('hex');
    }

    if (typeof value === 'object' && !!value && !isBigNumber(value) && !isBN(value)) {
        return returnType ? 'string' : utf8ToHex(JSON.stringify(value));
    }

    // if its a negative number, pass it through numberToHex
    if (typeof value === 'string') {
        if (value.indexOf('-0x') === 0 || value.indexOf('-0X') === 0) {
            return returnType ? 'int256' : numberToHex(value);
        } else if(value.indexOf('0x') === 0 || value.indexOf('0X') === 0) {
            return returnType ? 'bytes' : value;
        } else if (!isFinite(value)) {
            return returnType ? 'string' : utf8ToHex(value);
        }
    }

    return returnType ? (value < 0 ? 'int256' : 'uint256') : numberToHex(value);
};

/**
 * Check if string is HEX
 *
 * @method isHex
 * @param {String} hex to be checked
 * @returns {Boolean}
 */
var isHex = function (hex) {
    return ((typeof hex === 'string' || typeof hex === 'number') && /^(-0x|0x)?[0-9a-f]*$/i.test(hex));
};

/**
 * Remove 0x prefix from string
 *
 * @method stripHexPrefix
 * @param {String} str to be checked
 * @returns {String}
 */
var stripHexPrefix = function (str) {
    if (str !== 0 && isHex(str))
        return str.replace(/^(-)?0x/i, '$1')
    return str;
};


/**
 * Hashes values to a sha3 hash using keccak 256
 *
 * To hash a HEX string the hex must have 0x in front.
 *
 * @method sha3
 * @return {String} the sha3 string
 */
var SHA3_NULL_S = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470';

var sha3 = function (value) {
    if (isBN(value)) {
        value = value.toString();
    }

    if (isHexStrict(value) && /^0x/i.test((value).toString())) {
        value = hexToBytes(value);
    }

    var returnValue = Hash.keccak256(value); // jshint ignore:line

    if(returnValue === SHA3_NULL_S) {
        return null;
    } else {
        return returnValue;
    }
};
// expose the under the hood keccak256
sha3._Hash = Hash;

/**
 * @method sha3Raw
 *
 * @param value
 *
 * @returns {string}
 */
var sha3Raw = function(value) {
    value = sha3(value);

    if (value === null) {
        return SHA3_NULL_S;
    }

    return value;
};


module.exports = {
    BN,
    isBN,
    fromINT,
    toINT,
    stringToHex,
    hexToString,
    bnToBuffer,
    isHexString,
    isAddress,
    hexOrBuffer,
    hexlify,
    stripZeros,

    isBigNumber,
    toBN,
    // isTopic,
    checkAddressChecksum,
    toChecksumAddress,
    utf8ToHex,
    hexToUtf8,
    toUtf8: hexToUtf8,
    hexToNumber,
    hexToNumberString,
    numberToHex,
    toHex,
    isHex,
    bytesToHex,
    hexToBytes,
    isHexStrict,
    stripHexPrefix,
    sha3,
    sha3Raw,

    keccak256,

    getKeys,
    numberToBN,
    coderNumber,
    uint256Coder,
    coderBoolean,
    coderFixedBytes,
    coderAddress,
    coderDynamicBytes,
    coderString,
    coderArray,
    paramTypePart,
    getParamCoder,
};
