contract DecentralizedOTC is UseOracle, UseBondMaker, UseSafeMath, Time, CallOptionCalculator {
    uint256 internal constant MIN_EXCHANGE_RATE_E8 = 0.000001 * 10**8;
    uint256 internal constant MAX_EXCHANGE_RATE_E8 = 1000000 * 10**8;

    int256 internal constant MAX_SPREAD_E8 = 0.15 * 10**8; // 15%

    mapping(bytes32 => address) public deployer;

    /**
     * @notice ERC20pool is the amount of ERC20 deposit of a deployer.
     * @param ERC20Address is the target ERC20 token address.
     * @param spread is the fee base of the bid-ask spread.
     * @param isLBTSellPool is whether this pool is for the LBT sale or not.
     */
    struct PoolInfo {
        ERC20 ERC20Address;
        int16 spread;
        bool isLBTSellPool;
    }
    mapping(bytes32 => PoolInfo) public poolMap;

    mapping(bytes32 => PriceOracleInterface) internal _erc20OracleMap;

    event LogERC20TokenLBTSwap(
        bytes32 indexed poolID,
        address indexed sender,
        uint256 indexed bondGroupID,
        uint256 volume, // decimal: 8
        uint256 LBTAmount, // decimal: BondToken.decimals()
        uint256 ERC20Amount // decimal: ERC20.decimals()
    );

    event LogLBTERC20TokenSwap(
        bytes32 indexed poolID,
        address indexed sender,
        uint256 indexed bondGroupID,
        uint256 volume, // decimal: 8
        uint256 LBTAmount, // decimal: BondToken.decimals()
        uint256 ERC20Amount // decimal: ERC20.decimals()
    );

    event LogCreateERC20Pool(
        address indexed deployer,
        address indexed ERC20Address,
        bytes32 indexed poolID,
        int16 spread,
        bool isLBTSellPool
    );

    event LogDeleteERC20Pool(bytes32 indexed poolID);

    modifier isExistentPool(bytes32 erc20PoolID) {
        require(deployer[erc20PoolID] != address(0), "the pool does not exist");
        _;
    }

    constructor(address bondMakerAddress, address oracleAddress)
        public
        UseOracle(oracleAddress)
        UseBondMaker(bondMakerAddress)
    {
        require(
            _bondMakerContract.decimalsOfOraclePrice() == 8,
            "the decimals of oracle price must be 8"
        );
        require(_bondMakerContract.decimalsOfBond() == 8, "the decimals of bond token must be 8");
        require(
            oracleAddress == _bondMakerContract.oracleAddress(),
            "the oracle address is differ"
        );
    }

    /**
     * @notice providers set a pool and deposit to a pool.
     * If there is vesting(lockUp) setting, users of their pool transfer LBT to grants of the vesting ERC20 contract.
     */
    function setPoolMap(
        address ERC20Address,
        int16 spread,
        bool isLBTSellPool
    ) external returns (bytes32 erc20PoolID) {
        erc20PoolID = keccak256(abi.encode(msg.sender, ERC20Address, spread, isLBTSellPool));
        require(deployer[erc20PoolID] == address(0), "already registered");
        require(msg.sender != address(0), "deployer must be non-zero address");
        require(spread > -1000 && spread < 1000, "the range of fee base must be -999~999");
        require(ERC20Address != address(0), "ERC20 address is 0x0");

        poolMap[erc20PoolID] = PoolInfo(ERC20(ERC20Address), spread, isLBTSellPool);
        deployer[erc20PoolID] = msg.sender;
        emit LogCreateERC20Pool(msg.sender, ERC20Address, erc20PoolID, spread, isLBTSellPool);
    }

    /**
     * @notice providers must provide LBT price caluculator and ERC20 price oracle.
     */
    function setProvider(
        bytes32 erc20PoolID,
        address oracleAddress,
        address
    ) external isExistentPool(erc20PoolID) {
        require(msg.sender == deployer[erc20PoolID], "only deployer is allowed to execute");
        _erc20OracleMap[erc20PoolID] = PriceOracleInterface(oracleAddress);
    }

    /**
     * @dev (deprecated) Use getPoolInfo(poolID).
     */
    function oracleInfo(bytes32 poolID)
        external
        view
        returns (address oracleAddress, address calculatorAddress)
    {
        oracleAddress = address(_erc20OracleMap[poolID]);
        calculatorAddress = address(0);
    }

    function getPoolInfo(bytes32 poolID)
        external
        view
        isExistentPool(poolID)
        returns (
            address deployerAddress,
            address erc20Address,
            int16 feeBase,
            bool isLBTSellPool,
            address erc20OracleAddress
        )
    {
        deployerAddress = deployer[poolID];
        PoolInfo memory poolInfo = poolMap[poolID];
        erc20Address = address(poolInfo.ERC20Address);
        feeBase = poolInfo.spread;
        isLBTSellPool = poolInfo.isLBTSellPool;
        erc20OracleAddress = address(_erc20OracleMap[poolID]);
    }

    function getOraclePrice(bytes32 erc20PoolID) external returns (uint256 priceE8) {
        return _getOraclePrice(erc20PoolID);
    }

    function _getOraclePrice(bytes32 erc20PoolID)
        internal
        isExistentPool(erc20PoolID)
        returns (uint256 priceE8)
    {
        PriceOracleInterface oracleContract = _erc20OracleMap[erc20PoolID];
        require(address(oracleContract) != address(0), "invalid ERC20 price oracle");
        return oracleContract.latestPrice();
    }

    /**
     * @notice Returns the exchange rate included spread.
     */
    function calcRateLBT2ERC20(
        bytes32 sbtID,
        bytes32 erc20PoolID,
        uint256 maturity
    ) external returns (uint256 rateLBT2ERC20E8) {
        (rateLBT2ERC20E8, , , ) = _calcRateLBT2ERC20(sbtID, erc20PoolID, maturity);
    }

    function _calcRateLBT2ERC20(
        bytes32 sbtID,
        bytes32 erc20PoolID,
        uint256 maturity
    )
        internal
        returns (
            uint256 rateLBT2ERC20E8,
            uint256 lbtPriceE8,
            uint256 erc20PriceE8,
            int256 spreadE8
        )
    {
        PoolInfo memory pool = poolMap[erc20PoolID];
        (uint256 etherPriceE8, uint256 ethVolatilityE8) = _getOracleData();
        (, , uint256 strikePriceE8, ) = _bondMakerContract.getBond(sbtID);
        erc20PriceE8 = _getOraclePrice(erc20PoolID);

        (lbtPriceE8, spreadE8) = _calcLbtPriceAndSpread(
            strikePriceE8,
            etherPriceE8,
            ethVolatilityE8,
            maturity,
            pool.spread * 10 // feeBaseE4
        );
        uint256 rateE8 = lbtPriceE8.mul(10**8).div(
            erc20PriceE8,
            "ERC20 oracle price must be non-zero"
        );
        require(rateE8 > MIN_EXCHANGE_RATE_E8, "exchange rate is too small");
        // require(rateE8 < MAX_EXCHANGE_RATE_E8, "exchange rate is too large");

        if (pool.isLBTSellPool) {
            rateLBT2ERC20E8 = rateE8.mul(uint256(10**8 + spreadE8)) / (10**8);
        } else {
            rateLBT2ERC20E8 = rateE8.mul(uint256(10**8 - spreadE8)) / (10**8);
        }
    }

    /**
     * @dev Gets LBT data, and outputs the spread and the exchange rate excluding spread.
     */
    function _calcLbtPriceAndSpread(
        uint256 strikePriceE8,
        uint256 etherPriceE8,
        uint256 ethVolatilityE8,
        uint256 maturity,
        int256 feeBaseE4
    ) internal view returns (uint256 lbtPriceE8, int256 spreadE8) {
        uint256 untilMaturity = maturity.sub(
            _getBlockTimestampSec(),
            "LBT should not have expired"
        );

        uint8 decimalsOfPrice = 0; // mutable
        {
            uint256 threshould = 1000000 * 10**8;
            while (threshould >= 10) {
                if (strikePriceE8 >= threshould) {
                    break;
                }
                decimalsOfPrice++;
                threshould /= 10;
            }
        }
        // assert(decimalsOfPrice >= 0 && decimalsOfPrice <= 14);

        uint256 lbtLeverageE4;
        {
            uint256 etherPrice = _applyDecimalGap(etherPriceE8, 8, decimalsOfPrice);
            uint256 strikePrice = _applyDecimalGap(strikePriceE8, 8, decimalsOfPrice);
            uint256 lbtPrice;
            (lbtPrice, lbtLeverageE4, ) = calcLbtPriceAndLeverage(
                etherPrice.toInt256(),
                strikePrice.toInt256(),
                ethVolatilityE8.toInt256(),
                untilMaturity.toInt256()
            );

            lbtPriceE8 = _applyDecimalGap(lbtPrice, decimalsOfPrice, 8);
        }

        uint256 volE8 = ethVolatilityE8 < 10**8 ? 10**8 : ethVolatilityE8 > 2 * 10**8
            ? 2 * 10**8
            : ethVolatilityE8;
        uint256 volTimesLevE4 = (volE8 * lbtLeverageE4) / 10**8;
        spreadE8 =
            feeBaseE4 *
            (feeBaseE4 < 0 || volTimesLevE4 < 10**4 ? 10**4 : volTimesLevE4).toInt256();
        spreadE8 = spreadE8 > MAX_SPREAD_E8 ? MAX_SPREAD_E8 : spreadE8;
    }

    /**
     * @notice removes a decimal gap from rate.
     */
    function _applyDecimalGap(
        uint256 baseAmount,
        ERC20 baseToken,
        ERC20 quoteToken
    ) private view returns (uint256) {
        uint8 decimalsOfBase = baseToken.decimals();
        uint8 decimalsOfQuote = quoteToken.decimals();
        return _applyDecimalGap(baseAmount, decimalsOfBase, decimalsOfQuote);
    }

    function _applyDecimalGap(
        uint256 amount,
        uint8 decimalsOfBase,
        uint8 decimalsOfQuote
    ) private pure returns (uint256) {
        uint256 n;
        uint256 d;

        if (decimalsOfBase > decimalsOfQuote) {
            d = decimalsOfBase - decimalsOfQuote;
        } else if (decimalsOfBase < decimalsOfQuote) {
            n = decimalsOfQuote - decimalsOfBase;
        }

        // The consequent multiplication would overflow under extreme and non-blocking circumstances.
        require(n < 19 && d < 19, "decimal gap needs to be lower than 19");
        return amount.mul(10**n).div(10**d);
    }

    /**
     * @notice Before this function, approve is needed to be excuted.
     * Main function of this contract. Users exchange ERC20 tokens (like USDC Token) to LBT
     */
    function exchangeERC20ToLBT(
        uint256 bondGroupID,
        bytes32 erc20PoolID,
        uint256 ERC20Amount,
        uint256 expectedAmount,
        uint256 range
    ) public returns (uint256 LBTAmount) {
        LBTAmount = _exchangeERC20ToLBT(bondGroupID, erc20PoolID, ERC20Amount);
        if (expectedAmount != 0) {
            require(LBTAmount.mul(1000 + range).div(1000) >= expectedAmount, "out of price range");
        }
    }

    function _exchangeERC20ToLBT(
        uint256 bondGroupID,
        bytes32 erc20PoolID,
        uint256 ERC20Amount
    ) internal isExistentPool(erc20PoolID) returns (uint256 LBTAmount) {
        bytes32 sbtID;
        bytes32 lbtID;
        {
            (bytes32[] memory bonds, ) = _bondMakerContract.getBondGroup(bondGroupID);
            require(bonds.length == 2, "the bond group must include only 2 types of bond.");
            lbtID = bonds[1];
            sbtID = bonds[0];
        }

        ERC20 bondToken;
        uint256 maturity;
        {
            address contractAddress;
            (contractAddress, maturity, , ) = _bondMakerContract.getBond(lbtID);
            require(contractAddress != address(0), "the bond is not registered");
            bondToken = ERC20(contractAddress);
        }

        ERC20 token;
        {
            PoolInfo memory pool = poolMap[erc20PoolID];
            require(pool.isLBTSellPool, "This pool is for buying LBT");
            token = pool.ERC20Address;
        }

        uint256 volumeE8;
        {
            (uint256 rateE8, , uint256 erc20PriceE8, ) = _calcRateLBT2ERC20(
                sbtID,
                erc20PoolID,
                maturity
            );
            require(rateE8 != 0, "exchange rate included spread must be non-zero");
            LBTAmount = _applyDecimalGap(ERC20Amount.mul(10**8), token, bondToken).div(rateE8);
            require(LBTAmount != 0, "must transfer non-zero LBT amount");
            volumeE8 = erc20PriceE8.mul(ERC20Amount).div(10**uint256(token.decimals()));
            volumeE8 = _calcUsdVolume(volumeE8);
        }

        require(
            token.transferFrom(msg.sender, deployer[erc20PoolID], ERC20Amount),
            "fail to transfer ERC20 token"
        );
        require(
            bondToken.transferFrom(deployer[erc20PoolID], msg.sender, LBTAmount),
            "fail to transfer LBT"
        );

        emit LogERC20TokenLBTSwap(
            erc20PoolID,
            msg.sender,
            bondGroupID,
            volumeE8,
            LBTAmount,
            ERC20Amount
        );
    }

    /**
     * @notice Before this function, approve is needed to be excuted.
     * Main function of this contract. Users exchange LBT to ERC20 tokens (like USDC Token)
     */
    function exchangeLBT2ERC20(
        uint256 bondGroupID,
        bytes32 erc20PoolID,
        uint256 LBTAmount,
        uint256 expectedAmount,
        uint256 range
    ) public returns (uint256 ERC20Amount) {
        ERC20Amount = _exchangeLBT2ERC20(bondGroupID, erc20PoolID, LBTAmount);
        if (expectedAmount != 0) {
            require(
                ERC20Amount.mul(1000 + range).div(1000) >= expectedAmount,
                "out of price range"
            );
        }
    }

    function _exchangeLBT2ERC20(
        uint256 bondGroupID,
        bytes32 erc20PoolID,
        uint256 LBTAmount
    ) internal isExistentPool(erc20PoolID) returns (uint256 ERC20Amount) {
        bytes32 lbtID;
        bytes32 sbtID;
        {
            (bytes32[] memory bonds, ) = _bondMakerContract.getBondGroup(bondGroupID);
            require(bonds.length == 2, "the bond group must include only 2 types of bond.");
            lbtID = bonds[1];
            sbtID = bonds[0];
        }

        ERC20 bondToken;
        uint256 maturity;
        {
            address contractAddress;
            (contractAddress, maturity, , ) = _bondMakerContract.getBond(lbtID);
            require(contractAddress != address(0), "the bond is not registered");
            bondToken = ERC20(contractAddress);
        }

        ERC20 token;
        {
            PoolInfo memory pool = poolMap[erc20PoolID];
            require(!pool.isLBTSellPool, "This pool is not for buying LBT");
            token = pool.ERC20Address;
        }

        uint256 volumeE8;
        {
            (uint256 rateE8, uint256 lbtPriceE8, , ) = _calcRateLBT2ERC20(
                sbtID,
                erc20PoolID,
                maturity
            );
            require(rateE8 != 0, "exchange rate included spread must be non-zero");
            ERC20Amount = _applyDecimalGap(LBTAmount.mul(rateE8), bondToken, token).div(10**8);
            require(ERC20Amount != 0, "must transfer non-zero token amount");
            volumeE8 = lbtPriceE8.mul(LBTAmount).div(10**uint256(bondToken.decimals()));
            volumeE8 = _calcUsdVolume(volumeE8);
        }

        require(
            token.transferFrom(deployer[erc20PoolID], msg.sender, ERC20Amount),
            "fail to transfer ERC20 token"
        );
        require(
            bondToken.transferFrom(msg.sender, deployer[erc20PoolID], LBTAmount),
            "fail to transfer LBT"
        );

        emit LogLBTERC20TokenSwap(
            erc20PoolID,
            msg.sender,
            bondGroupID,
            volumeE8,
            LBTAmount,
            ERC20Amount
        );
    }

    function deletePoolAndProvider(bytes32 erc20PoolID) public isExistentPool(erc20PoolID) {
        require(deployer[erc20PoolID] == msg.sender, "this pool is not owned");
        delete deployer[erc20PoolID];
        delete poolMap[erc20PoolID];
        delete _erc20OracleMap[erc20PoolID];

        emit LogDeleteERC20Pool(erc20PoolID);
    }

    function bondMakerAddress() external view returns (address) {
        return address(_bondMakerContract);
    }

    /**
     * @dev Converts the unit of the strike price to USD.
     * Considering oracle, this function is non-payable.
     */
    function _calcUsdVolume(uint256 volume) internal virtual returns (uint256) {
        return volume;
    }
}
function _applyDecimalGap(
    uint256 baseAmount,
    ERC20 baseToken,
    ERC20 quoteToken
) private view returns (uint256) {
    uint8 decimalsOfBase = baseToken.decimals();
    uint8 decimalsOfQuote = quoteToken.decimals();
    return _applyDecimalGap(baseAmount, decimalsOfBase, decimalsOfQuote);
}

/**
 * @notice removes a decimal gap from rate.
 */