contract ConversionRates is
    ConversionRatesInterface,
    VolumeImbalanceRecorder,
    Utils
{
    // bps - basic rate steps. one step is 1 / 10000 of the rate.
    struct StepFunction {
        int256[] x; // quantity for each step. Quantity of each step includes previous steps.
        int256[] y; // rate change per quantity step  in bps.
    }

    struct TokenData {
        bool listed; // was added to reserve
        bool enabled; // whether trade is enabled
        // position in the compact data
        uint256 compactDataArrayIndex;
        uint256 compactDataFieldIndex;
        // rate data. base and changes according to quantity and reserve balance.
        // generally speaking. Sell rate is 1 / buy rate i.e. the buy in the other direction.
        uint256 baseBuyRate; // in PRECISION units. see KyberConstants
        uint256 baseSellRate; // PRECISION units. without (sell / buy) spread it is 1 / baseBuyRate
        StepFunction buyRateQtyStepFunction; // in bps. higher quantity - bigger the rate.
        StepFunction sellRateQtyStepFunction; // in bps. higher the qua
        StepFunction buyRateImbalanceStepFunction; // in BPS. higher reserve imbalance - bigger the rate.
        StepFunction sellRateImbalanceStepFunction;
    }

    /*
    this is the data for tokenRatesCompactData
    but solidity compiler optimizer is sub-optimal, and cannot write this structure in a single storage write
    so we represent it as bytes32 and do the byte tricks ourselves.
    struct TokenRatesCompactData {
        bytes14 buy;  // change buy rate of token from baseBuyRate in 10 bps
        bytes14 sell; // change sell rate of token from baseSellRate in 10 bps

        uint32 blockNumber;
    } */
    uint256 public validRateDurationInBlocks = 10; // rates are valid for this amount of blocks
    ERC20[] internal listedTokens;
    mapping(address => TokenData) internal tokenData;
    bytes32[] internal tokenRatesCompactData;
    uint256 public numTokensInCurrentCompactData = 0;
    address public reserveContract;
    uint256 internal constant NUM_TOKENS_IN_COMPACT_DATA = 16;
    uint256 internal constant BYTES_14_OFFSET = (2 **
        (8 * NUM_TOKENS_IN_COMPACT_DATA));
    uint256 internal constant MAX_STEPS_IN_FUNCTION = 10;
    int256 internal constant MAX_BPS_ADJUSTMENT = 10**11; // 1B %
    int256 internal constant MIN_BPS_ADJUSTMENT = -100 * 100; // cannot go down by more than 100%

    function ConversionRates(address _admin)
        public
        VolumeImbalanceRecorder(_admin)
    {} // solhint-disable-line no-empty-blocks

    function addToken(ERC20 token) public onlyAdmin {
        require(!tokenData[token].listed);
        tokenData[token].listed = true;
        listedTokens.push(token);

        if (numTokensInCurrentCompactData == 0) {
            tokenRatesCompactData.length++; // add new structure
        }

        tokenData[token].compactDataArrayIndex =
            tokenRatesCompactData.length -
            1;
        tokenData[token].compactDataFieldIndex = numTokensInCurrentCompactData;

        numTokensInCurrentCompactData =
            (numTokensInCurrentCompactData + 1) %
            NUM_TOKENS_IN_COMPACT_DATA;

        setGarbageToVolumeRecorder(token);

        setDecimals(token);
    }

    function setCompactData(
        bytes14[] buy,
        bytes14[] sell,
        uint256 blockNumber,
        uint256[] indices
    ) public onlyOperator {
        require(buy.length == sell.length);
        require(indices.length == buy.length);
        require(blockNumber <= 0xFFFFFFFF);

        uint256 bytes14Offset = BYTES_14_OFFSET;

        for (uint256 i = 0; i < indices.length; i++) {
            require(indices[i] < tokenRatesCompactData.length);
            uint256 data =
                uint256(buy[i]) |
                    (uint256(sell[i]) * bytes14Offset) |
                    (blockNumber * (bytes14Offset * bytes14Offset));
            tokenRatesCompactData[indices[i]] = bytes32(data);
        }
    }

    function setBaseRate(
        ERC20[] tokens,
        uint256[] baseBuy,
        uint256[] baseSell,
        bytes14[] buy,
        bytes14[] sell,
        uint256 blockNumber,
        uint256[] indices
    ) public onlyOperator {
        require(tokens.length == baseBuy.length);
        require(tokens.length == baseSell.length);
        require(sell.length == buy.length);
        require(sell.length == indices.length);

        for (uint256 ind = 0; ind < tokens.length; ind++) {
            require(tokenData[tokens[ind]].listed);
            tokenData[tokens[ind]].baseBuyRate = baseBuy[ind];
            tokenData[tokens[ind]].baseSellRate = baseSell[ind];
        }

        setCompactData(buy, sell, blockNumber, indices);
    }

    function setQtyStepFunction(
        ERC20 token,
        int256[] xBuy,
        int256[] yBuy,
        int256[] xSell,
        int256[] ySell
    ) public onlyOperator {
        require(xBuy.length == yBuy.length);
        require(xSell.length == ySell.length);
        require(xBuy.length <= MAX_STEPS_IN_FUNCTION);
        require(xSell.length <= MAX_STEPS_IN_FUNCTION);
        require(tokenData[token].listed);

        tokenData[token].buyRateQtyStepFunction = StepFunction(xBuy, yBuy);
        tokenData[token].sellRateQtyStepFunction = StepFunction(xSell, ySell);
    }

    function setImbalanceStepFunction(
        ERC20 token,
        int256[] xBuy,
        int256[] yBuy,
        int256[] xSell,
        int256[] ySell
    ) public onlyOperator {
        require(xBuy.length == yBuy.length);
        require(xSell.length == ySell.length);
        require(xBuy.length <= MAX_STEPS_IN_FUNCTION);
        require(xSell.length <= MAX_STEPS_IN_FUNCTION);
        require(tokenData[token].listed);

        tokenData[token].buyRateImbalanceStepFunction = StepFunction(
            xBuy,
            yBuy
        );
        tokenData[token].sellRateImbalanceStepFunction = StepFunction(
            xSell,
            ySell
        );
    }

    function setValidRateDurationInBlocks(uint256 duration) public onlyAdmin {
        validRateDurationInBlocks = duration;
    }

    function enableTokenTrade(ERC20 token) public onlyAdmin {
        require(tokenData[token].listed);
        require(tokenControlInfo[token].minimalRecordResolution != 0);
        tokenData[token].enabled = true;
    }

    function disableTokenTrade(ERC20 token) public onlyAlerter {
        require(tokenData[token].listed);
        tokenData[token].enabled = false;
    }

    function setReserveAddress(address reserve) public onlyAdmin {
        reserveContract = reserve;
    }

    function recordImbalance(
        ERC20 token,
        int256 buyAmount,
        uint256 rateUpdateBlock,
        uint256 currentBlock
    ) public {
        require(msg.sender == reserveContract);

        if (rateUpdateBlock == 0) rateUpdateBlock = getRateUpdateBlock(token);

        return addImbalance(token, buyAmount, rateUpdateBlock, currentBlock);
    }

    /* solhint-disable function-max-lines */
    function getRate(
        ERC20 token,
        uint256 currentBlockNumber,
        bool buy,
        uint256 qty
    ) public view returns (uint256) {
        // check if trade is enabled
        if (!tokenData[token].enabled) return 0;
        if (tokenControlInfo[token].minimalRecordResolution == 0) return 0; // token control info not set

        // get rate update block
        bytes32 compactData =
            tokenRatesCompactData[tokenData[token].compactDataArrayIndex];

        uint256 updateRateBlock = getLast4Bytes(compactData);
        if (currentBlockNumber >= updateRateBlock + validRateDurationInBlocks)
            return 0; // rate is expired
        // check imbalance
        int256 totalImbalance;
        int256 blockImbalance;
        (totalImbalance, blockImbalance) = getImbalance(
            token,
            updateRateBlock,
            currentBlockNumber
        );

        // calculate actual rate
        int256 imbalanceQty;
        int256 extraBps;
        int8 rateUpdate;
        uint256 rate;

        if (buy) {
            // start with base rate
            rate = tokenData[token].baseBuyRate;

            // add rate update
            rateUpdate = getRateByteFromCompactData(compactData, token, true);
            extraBps = int256(rateUpdate) * 10;
            rate = addBps(rate, extraBps);

            // compute token qty
            qty = getTokenQty(token, rate, qty);
            imbalanceQty = int256(qty);
            totalImbalance += imbalanceQty;

            // add qty overhead
            extraBps = executeStepFunction(
                tokenData[token].buyRateQtyStepFunction,
                int256(qty)
            );
            rate = addBps(rate, extraBps);

            // add imbalance overhead
            extraBps = executeStepFunction(
                tokenData[token].buyRateImbalanceStepFunction,
                totalImbalance
            );
            rate = addBps(rate, extraBps);
        } else {
            // start with base rate
            rate = tokenData[token].baseSellRate;

            // add rate update
            rateUpdate = getRateByteFromCompactData(compactData, token, false);
            extraBps = int256(rateUpdate) * 10;
            rate = addBps(rate, extraBps);

            // compute token qty
            imbalanceQty = -1 * int256(qty);
            totalImbalance += imbalanceQty;

            // add qty overhead
            extraBps = executeStepFunction(
                tokenData[token].sellRateQtyStepFunction,
                int256(qty)
            );
            rate = addBps(rate, extraBps);

            // add imbalance overhead
            extraBps = executeStepFunction(
                tokenData[token].sellRateImbalanceStepFunction,
                totalImbalance
            );
            rate = addBps(rate, extraBps);
        }

        if (abs(totalImbalance) >= getMaxTotalImbalance(token)) return 0;
        if (
            abs(blockImbalance + imbalanceQty) >= getMaxPerBlockImbalance(token)
        ) return 0;

        return rate;
    }

    /* solhint-enable function-max-lines */

    function getBasicRate(ERC20 token, bool buy) public view returns (uint256) {
        if (buy) return tokenData[token].baseBuyRate;
        else return tokenData[token].baseSellRate;
    }

    function getCompactData(ERC20 token)
        public
        view
        returns (
            uint256,
            uint256,
            bytes1,
            bytes1
        )
    {
        require(tokenData[token].listed);

        uint256 arrayIndex = tokenData[token].compactDataArrayIndex;
        uint256 fieldOffset = tokenData[token].compactDataFieldIndex;

        return (
            arrayIndex,
            fieldOffset,
            bytes1(
                getRateByteFromCompactData(
                    tokenRatesCompactData[arrayIndex],
                    token,
                    true
                )
            ),
            bytes1(
                getRateByteFromCompactData(
                    tokenRatesCompactData[arrayIndex],
                    token,
                    false
                )
            )
        );
    }

    function getTokenBasicData(ERC20 token) public view returns (bool, bool) {
        return (tokenData[token].listed, tokenData[token].enabled);
    }

    /* solhint-disable code-complexity */
    function getStepFunctionData(
        ERC20 token,
        uint256 command,
        uint256 param
    ) public view returns (int256) {
        if (command == 0)
            return int256(tokenData[token].buyRateQtyStepFunction.x.length);
        if (command == 1)
            return tokenData[token].buyRateQtyStepFunction.x[param];
        if (command == 2)
            return int256(tokenData[token].buyRateQtyStepFunction.y.length);
        if (command == 3)
            return tokenData[token].buyRateQtyStepFunction.y[param];

        if (command == 4)
            return int256(tokenData[token].sellRateQtyStepFunction.x.length);
        if (command == 5)
            return tokenData[token].sellRateQtyStepFunction.x[param];
        if (command == 6)
            return int256(tokenData[token].sellRateQtyStepFunction.y.length);
        if (command == 7)
            return tokenData[token].sellRateQtyStepFunction.y[param];

        if (command == 8)
            return
                int256(tokenData[token].buyRateImbalanceStepFunction.x.length);
        if (command == 9)
            return tokenData[token].buyRateImbalanceStepFunction.x[param];
        if (command == 10)
            return
                int256(tokenData[token].buyRateImbalanceStepFunction.y.length);
        if (command == 11)
            return tokenData[token].buyRateImbalanceStepFunction.y[param];

        if (command == 12)
            return
                int256(tokenData[token].sellRateImbalanceStepFunction.x.length);
        if (command == 13)
            return tokenData[token].sellRateImbalanceStepFunction.x[param];
        if (command == 14)
            return
                int256(tokenData[token].sellRateImbalanceStepFunction.y.length);
        if (command == 15)
            return tokenData[token].sellRateImbalanceStepFunction.y[param];

        revert();
    }

    /* solhint-enable code-complexity */

    function getRateUpdateBlock(ERC20 token) public view returns (uint256) {
        bytes32 compactData =
            tokenRatesCompactData[tokenData[token].compactDataArrayIndex];
        return getLast4Bytes(compactData);
    }

    function getListedTokens() public view returns (ERC20[]) {
        return listedTokens;
    }

    function getTokenQty(
        ERC20 token,
        uint256 ethQty,
        uint256 rate
    ) internal view returns (uint256) {
        uint256 dstDecimals = getDecimals(token);
        uint256 srcDecimals = ETH_DECIMALS;

        return calcDstQty(ethQty, srcDecimals, dstDecimals, rate);
    }

    function getLast4Bytes(bytes32 b) internal pure returns (uint256) {
        // cannot trust compiler with not turning bit operations into EXP opcode
        return uint256(b) / (BYTES_14_OFFSET * BYTES_14_OFFSET);
    }

    function getRateByteFromCompactData(
        bytes32 data,
        ERC20 token,
        bool buy
    ) internal view returns (int8) {
        uint256 fieldOffset = tokenData[token].compactDataFieldIndex;
        uint256 byteOffset;
        if (buy) byteOffset = 32 - NUM_TOKENS_IN_COMPACT_DATA + fieldOffset;
        else byteOffset = 4 + fieldOffset;

        return int8(data[byteOffset]);
    }

    function executeStepFunction(StepFunction f, int256 x)
        internal
        pure
        returns (int256)
    {
        uint256 len = f.y.length;
        for (uint256 ind = 0; ind < len; ind++) {
            if (x <= f.x[ind]) return f.y[ind];
        }

        return f.y[len - 1];
    }

    function addBps(uint256 rate, int256 bps) internal pure returns (uint256) {
        require(rate <= MAX_RATE);
        require(bps >= MIN_BPS_ADJUSTMENT);
        require(bps <= MAX_BPS_ADJUSTMENT);

        uint256 maxBps = 100 * 100;
        return (rate * uint256(int256(maxBps) + bps)) / maxBps;
    }

    function abs(int256 x) internal pure returns (uint256) {
        if (x < 0) return uint256(-1 * x);
        else return uint256(x);
    }
}
function getRateUpdateBlock(ERC20 token) public view returns (uint256) {
    bytes32 compactData =
        tokenRatesCompactData[tokenData[token].compactDataArrayIndex];
    return getLast4Bytes(compactData);
}

/* solhint-enable code-complexity */