contract DecentralizedOTC is UseOracle, UseBondMaker, UseSafeMath, Time {
    constructor(address bondMakerAddress, address oracleAddress)
        public
        UseOracle(oracleAddress)
        UseBondMaker(bondMakerAddress)
    {}

    int256 internal constant SQRT_YEAR_E8 = 561569229926;
    int256 internal constant SQRT_2PI_E8 = 250662827;
    uint256 internal constant MIN_EXCHANG_RATE_E8 = 0.000001 * 10**8;
    uint256 internal constant MAX_EXCHANG_RATE_E8 = 1000000 * 10**8;
    int256 internal constant MIN_ND1 = 0.1 * 10**4;
    int256 internal constant MAX_ND1 = 0.9 * 10**4;
    int256 internal constant MAX_SPREAD_E7 = 0.15 * 10**7; // 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;

    struct OracleInfo {
        address oracleAddress;
        address calculatorAddress;
    }
    mapping(bytes32 => OracleInfo) public oracleInfo;

    event LogERC20TokenLBTSwap(
        bytes32 indexed poolID,
        address indexed sender,
        uint256 indexed bondGroupID,
        address ERC20Address,
        uint256 LBTAmount,
        uint256 ERC20Amount
    );

    event LogLBTERC20TokenSwap(
        bytes32 indexed poolID,
        address indexed sender,
        uint256 indexed bondGroupID,
        address ERC20Address,
        uint256 LBTAmount,
        uint256 ERC20Amount
    );

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

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

    /**
     * @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, "spread range 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 calculatorAddress
    ) external isExistentPool(erc20PoolID) {
        require(msg.sender == deployer[erc20PoolID], "only deployer is allowed to execute");
        oracleInfo[erc20PoolID] = OracleInfo(oracleAddress, calculatorAddress);
    }

    function getOraclePrice(bytes32 erc20PoolID)
        public
        view
        isExistentPool(erc20PoolID)
        returns (uint256 priceE4)
    {
        ERC20OracleInterface oracleContract = ERC20OracleInterface(
            oracleInfo[erc20PoolID].oracleAddress
        );
        require(address(oracleContract) != address(0), "invalid ERC20 price oracle");
        return oracleContract.getPrice();
    }

    function _getEtherOraclePrice()
        internal
        virtual
        returns (uint256 etherPriceE4, uint256 volatilityE8)
    {
        uint256 etherPriceE8;
        (etherPriceE8, volatilityE8) = _getOracleData();
        etherPriceE4 = etherPriceE8.div(10**4);
    }

    /**
     * @notice Gets LBT data and market Ether data, and outputs the theoretical price of the LBT.
     */
    function getLBTTheoreticalPrice(
        bytes32 erc20PoolID,
        uint256 etherPriceE4,
        uint256 strikePriceE4,
        uint256 ethVolatilityE8,
        uint256 maturity
    ) public view isExistentPool(erc20PoolID) returns (uint256) {
        require(
            _getBlockTimestampSec() < maturity && _getBlockTimestampSec() >= maturity - 12 weeks,
            "LBT should not have expired and the maturity should not be so distant"
        );
        uint256 untilMaturity = maturity.sub(_getBlockTimestampSec());
        PricingInterface pricerContract = PricingInterface(
            oracleInfo[erc20PoolID].calculatorAddress
        );
        require(address(pricerContract) != address(0), "pricer contract is invalid");
        return
            pricerContract.pricing(
                etherPriceE4.toInt256(),
                strikePriceE4.toInt256(),
                ethVolatilityE8.toInt256(),
                untilMaturity.toInt256()
            );
    }

    /**
     * @notice Returns the exchange rate included spread.
     */
    function calcRateLBT2ERC20(
        bytes32 sbtID,
        bytes32 erc20PoolID,
        uint256 maturity
    ) public returns (uint256 rateLBT2ERC20E8) {
        PoolInfo memory pool = poolMap[erc20PoolID];
        (uint256 etherPriceE4, uint256 ethVolatilityE8) = _getEtherOraclePrice();
        (uint256 rateE8, uint256 lbtLeverageE4) = _calcRateLBT2ERC20ExcludedSpread(
            sbtID,
            erc20PoolID,
            etherPriceE4,
            ethVolatilityE8,
            maturity
        );
        require(rateE8 > MIN_EXCHANG_RATE_E8, "exchange rate is too small");
        require(rateE8 < MAX_EXCHANG_RATE_E8, "exchange rate is too large");

        uint256 volE8 = ethVolatilityE8 < 10**8 ? 10**8 : ethVolatilityE8 > 2 * 10**8
            ? 2 * 10**8
            : ethVolatilityE8;
        uint256 volTimesLevE4 = (volE8 * lbtLeverageE4) / 10**8;
        int256 spreadE7 = pool.spread *
            (pool.spread < 0 || volTimesLevE4 < 10**4 ? 10**4 : volTimesLevE4).toInt256();
        spreadE7 = spreadE7 > MAX_SPREAD_E7 ? MAX_SPREAD_E7 : spreadE7;

        if (pool.isLBTSellPool) {
            return rateE8.mul(uint256(10000000 + spreadE7)).div(10000000);
        } else {
            return rateE8.mul(uint256(10000000 - spreadE7)).div(10000000);
        }
    }

    /**
     * @dev Gets LBT data, and outputs the exchange rate excluded spread.
     */
    function _calcRateLBT2ERC20ExcludedSpread(
        bytes32 sbtID,
        bytes32 erc20PoolID,
        uint256 etherPriceE4,
        uint256 ethVolatilityE8,
        uint256 maturity
    ) internal view returns (uint256 rateLBT2ERC20E8, uint256 lbtLeverageE4) {
        require(
            ethVolatilityE8 != 0,
            "system error: the volatility of ETH should be non-zero value"
        );
        (uint256 lowestPriceE4, uint256 strikePriceE4) = _getLowestPrice(sbtID, etherPriceE4);
        uint256 lbtPriceE4 = getLBTTheoreticalPrice(
            erc20PoolID,
            etherPriceE4,
            strikePriceE4,
            ethVolatilityE8,
            maturity
        );
        if (lowestPriceE4 > lbtPriceE4) {
            lbtPriceE4 = lowestPriceE4;
        }
        require(lbtPriceE4 <= etherPriceE4, "LBT price needs to be less than ether price");
        require(lbtPriceE4 != 0, "This LBT is not available : 0-value");

        lbtLeverageE4 = calcLbtLeverage(
            etherPriceE4,
            strikePriceE4,
            ethVolatilityE8,
            maturity.sub(_getBlockTimestampSec()),
            lbtPriceE4
        );

        rateLBT2ERC20E8 = lbtPriceE4.mul(1e8).div(
            getOraclePrice(erc20PoolID),
            "ERC20 oracle price must be non-zero"
        );

        return (rateLBT2ERC20E8, lbtLeverageE4);
    }

    /**
     * @notice removes a decimal gap from rate.
     */
    function _applyDecimalGap(
        uint256 amount,
        ERC20 bondToken,
        ERC20 token
    ) private view returns (uint256) {
        uint256 n;
        uint256 d;

        uint8 decimalsOfBondToken = bondToken.decimals();
        uint8 decimalsOfToken = token.decimals();
        if (decimalsOfBondToken > decimalsOfToken) {
            d = decimalsOfBondToken - decimalsOfToken;
        } else if (decimalsOfBondToken < decimalsOfToken) {
            n = decimalsOfToken - decimalsOfBondToken;
        }

        // 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 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];
        }

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

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

        ERC20 token = pool.ERC20Address;
        {
            uint256 rateE8 = calcRateLBT2ERC20(sbtID, erc20PoolID, maturity);
            LBTAmount = _applyDecimalGap(ERC20Amount.mul(1e8), token, bondToken).div(rateE8);
            require(LBTAmount != 0, "must transfer non-zero LBT amount");
        }

        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,
            address(token),
            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];
        }

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

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

        ERC20 token = pool.ERC20Address;
        {
            uint256 rateE8 = calcRateLBT2ERC20(sbtID, erc20PoolID, maturity);
            ERC20Amount = _applyDecimalGap(LBTAmount.mul(rateE8), bondToken, token).div(1e8);
            require(ERC20Amount != 0, "must transfer non-zero token amount");
        }

        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,
            address(token),
            LBTAmount,
            ERC20Amount
        );
    }

    /**
     * @notice this function is scam prevention. LBT price will not be lower than EtherPrice - StrikePrice.
     */
    function _getLowestPrice(bytes32 sbtID, uint256 etherPrice)
        internal
        view
        returns (uint256 lowestPrice, uint256 strikePrice)
    {
        (, , strikePrice, ) = _bondMakerContract.getBond(sbtID);
        require(strikePrice != 0, "Your LBT input is not recognized as LBT");
        if (etherPrice > strikePrice) {
            lowestPrice = etherPrice.sub(strikePrice);
        }
    }

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

    /**
     * @dev Calcurate an approximate value of the square root of x by Newton's method.
     */
    function _sqrt(int256 x) internal pure returns (int256 y) {
        require(x >= 0, "cannot calculate the square root of a negative number");
        int256 z = (x + 1) / 2;
        y = x;
        while (z < y) {
            y = z;
            z = (x / z + z) / 2;
        }
    }

    /**
     * @notice Calcurate the exponent of the int256 value.
     * WARNING: 10**8 is of uint256 type, but _pow(10, 8) is of int256 type.
     * @dev If y is zero, z is 1 whatever x is.
     */
    function _pow(int256 x, uint256 y) internal pure returns (int256 z) {
        z = 1;
        for (uint256 i = 0; i < y; i++) {
            z = z.mul(x);
        }
    }

    /**
     * @notice Calculate an approximate value of the logarithm of input value by
     * Taylor expansion.
     * @dev log(x + 1) = x - 1/2 x^2 + 1/3 x^3 - 1/4 x^4 + 1/5 x^5
     *                     - 1/6 x^6 + 1/7 x^7 - 1/8 x^8 + ...
     */
    function _logTaylor(int256 inputE4) internal pure returns (int256 outputE4) {
        outputE4 = 0;
        int256 sign;
        require(inputE4 < 2 * 10**4, "inputE4 < 20000 (2) is required");
        for (uint256 i = 1; i < 9; i++) {
            if (i % 2 == 0) {
                sign = -1;
            } else {
                sign = 1;
            }
            outputE4 = outputE4.add(
                _pow(inputE4, i).div(_pow(10, 4 * i - 4)).div(int256(i)).mul(sign)
            );
        }
    }

    /**
     * @notice Calculate the cumulative distribution function of standard normal
     * distribution by Taylor expansion.
     * @dev N(x)
     *    = exp(-(x^2)/2) / sqrt(2*PI)
     *    = 1/2 + (x - 1/6 x^3 + 1/40 x^5 - 1/330 x^7 + 3456 x^9 - ...)/sqrt(2*PI)
     */
    function _NTaylor(int256 inputE4) internal pure returns (int256 outputE4) {
        int256 t = inputE4
            .sub(_pow(inputE4, 3).div(6 * 10**8))
            .add(_pow(inputE4, 5).div(40 * 10**16))
            .sub(_pow(inputE4, 7).div(330 * 10**24))
            .add(_pow(inputE4, 9).div(3456 * 10**32));
        return t.mul(10**8).div(SQRT_2PI_E8).add(5 * 10**3);
    }

    function _calcNd1(
        int256 spotPerStrikeE4,
        int256 volatilityE8,
        int256 untilMaturity
    ) internal pure returns (int256 nd1E4) {
        int256 logSigE4 = _logTaylor(spotPerStrikeE4.sub(10**4));
        int256 sigE8 = volatilityE8.mul(_sqrt(untilMaturity)).mul(10**8).div(SQRT_YEAR_E8);
        int256 d1E4 = logSigE4.mul(10**8).div(sigE8).add(sigE8.div(2 * 10**4));
        return _NTaylor(d1E4);
    }

    function _calcLbtLeverage(
        uint256 etherPriceE4,
        uint256 lbtPriceE4,
        int256 nd1E4
    ) internal pure returns (uint256 lbtLeverageE4) {
        int256 modifiedNd1E4 = nd1E4 < MIN_ND1 ? MIN_ND1 : nd1E4 > MAX_ND1 ? MAX_ND1 : nd1E4;
        return
            lbtPriceE4 != 0
                ? modifiedNd1E4.toUint256().mul(etherPriceE4).div(lbtPriceE4)
                : 100 * 10**4;
    }

    function calcLbtLeverage(
        uint256 etherPriceE4,
        uint256 strikePriceE4,
        uint256 ethVolatilityE8,
        uint256 untilMaturity,
        uint256 lbtPriceE4
    ) public pure returns (uint256 lbtLeverageE4) {
        uint256 spotPerStrikeE4 = etherPriceE4.mul(10**4).div(
            strikePriceE4,
            "strike price must be non-zero"
        );

        /// @dev cannot calculate nd1 when pricePerStrikeE4 >= 20000
        int256 nd1E4 = MAX_ND1;
        if (spotPerStrikeE4 < 2 * 10**4) {
            nd1E4 = _calcNd1(
                spotPerStrikeE4.toInt256(),
                ethVolatilityE8.toInt256(),
                untilMaturity.toInt256()
            );
        }

        return lbtLeverageE4 = _calcLbtLeverage(etherPriceE4, lbtPriceE4, nd1E4);
    }
}
function getLBTTheoreticalPrice(
    bytes32 erc20PoolID,
    uint256 etherPriceE4,
    uint256 strikePriceE4,
    uint256 ethVolatilityE8,
    uint256 maturity
) public view isExistentPool(erc20PoolID) returns (uint256) {
    require(
        _getBlockTimestampSec() < maturity && _getBlockTimestampSec() >= maturity - 12 weeks,
        "LBT should not have expired and the maturity should not be so distant"
    );
    uint256 untilMaturity = maturity.sub(_getBlockTimestampSec());
    PricingInterface pricerContract = PricingInterface(
        oracleInfo[erc20PoolID].calculatorAddress
    );
    require(address(pricerContract) != address(0), "pricer contract is invalid");
    return
        pricerContract.pricing(
            etherPriceE4.toInt256(),
            strikePriceE4.toInt256(),
            ethVolatilityE8.toInt256(),
            untilMaturity.toInt256()
        );
}

/**
 * @notice Gets LBT data and market Ether data, and outputs the theoretical price of the LBT.
 */