contract DHPTSwap is Initializable, OwnableUpgradeSafe {
    using SafeMath for uint256;

    IAddressResolver public addressResolver;
    address public factory;
    address public oracle;

    bool public enableBuy;
    bool public enableSell;
    bool public enableOracleBuy;
    bool public enableOracleSell;
    
    uint8 public oracleBlockBias;

    mapping(address => uint256) public dhptWhitelist;

    bytes32 private constant _EXCHANGE_RATES_KEY = "ExchangeRates";
    bytes32 private constant _EXCHANGER_KEY = "Exchanger";
    bytes32 private constant _SYNTHETIX_KEY = "Synthetix";
    bytes32 private constant _SUSD_KEY = "sUSD";
    address public dao;
    mapping(bytes32 => bool) public dhptFromEnabled;
    mapping(address => uint8) public stableCoins;

    event SellDHPT(
        address fundAddress,
        address investor,
        uint256 susdAmount,
        uint256 dhptAmount,
        uint256 tokenPrice,
        uint256 time,
        bool oracleSwap
    );

    event BuyDHPT(
        address fundAddress,
        address investor,
        uint256 susdAmount,
        uint256 dhptAmount,
        uint256 tokenPrice,
        uint256 time,
        bool oracleSwap
    );

    event SwapDHPT(
        address fundAddressA,
        uint256 tokenPriceA,
        uint256 amountA,
        address fundAddressB,
        uint256 tokenPriceB,
        uint256 amountB,
        address investor,
        uint256 time,
        bool oracleSwap
    );

    function initialize(IAddressResolver _addressResolver, address _factory, address _oracle) public initializer {
        OwnableUpgradeSafe.__Ownable_init();

        enableBuy = true;
        enableSell = true;
        enableOracleBuy = false;
        enableOracleSell = false;

        addressResolver = _addressResolver;
        factory = _factory;
        oracle = _oracle;
        oracleBlockBias = 25;
    }

    function setStableCoin(address stableCoin, uint8 tokenPrecision) public onlyOwner {
        stableCoins[stableCoin] = tokenPrecision;
    }

    // BUY & SELL

    // user selling DHPT for sUSD
    function sellDHPT(address poolAddress, uint256 dhptAmount, address stableCoin) public {
        uint8 stableCoinPrecision = stableCoins[stableCoin];
        require(stableCoinPrecision > 0, "selected stable coin is disabled");

        require(enableSell, "sell disabled");
        require(_canSellDhpt(poolAddress, dhptAmount), "unable to sell tokens");
        require(dhptAmount > 10000, "amount too small");
       
        uint256 poolPrice = tokenPriceWithSettle(poolAddress);
        require(poolPrice > 0, "poolPrice is not valid value");

        require(
            IERC20(poolAddress).transferFrom(
                msg.sender,
                address(this),
                dhptAmount
            ),
            "token transfer failed"
        );

        uint256 stableCoinAmount = dhptAmount.mul(poolPrice).div(10**uint(stableCoinPrecision));

        require(
            IERC20(stableCoin).transfer(
                msg.sender, stableCoinAmount
            ),
            "stable coin transfer failed"
        );
        
        emit SellDHPT(
            poolAddress,
            msg.sender,
            stableCoinAmount,
            dhptAmount,
            poolPrice,
            block.timestamp,
            false
        );
    }

    // user buying DHPT for sUSD. consider DHPT 24h lockup
    function buyDHPT(address poolAddress, address stableCoin, uint256 stableCoinAmount) public {
        uint8 stableCoinPrecision = stableCoins[stableCoin];
        require(stableCoinPrecision > 0, "selected stable coin is disabled");
        
        require(enableBuy, "buy disabled");
        require(dhptWhitelist[poolAddress] > 0, "pool not whitelisted");
        require(stableCoinAmount > 10000, "amount too small");

        uint256 poolPrice = tokenPriceWithSettle(poolAddress);
        uint256 dhptAmount = stableCoinAmount.mul(10**uint(stableCoinPrecision)).div(poolPrice);
        IDHedge(poolAddress).setLastDeposit(msg.sender);
        
        require(
            IERC20(stableCoin).transferFrom(
                msg.sender,
                address(this),
                stableCoinAmount
            ),
            "stable coin transfer failed"
        );
      
        require(
            IERC20(poolAddress).transfer(msg.sender, dhptAmount),
            "pool-token transfer failed"
        );

        emit BuyDHPT(
            poolAddress,
            msg.sender,
            stableCoinAmount,
            dhptAmount,
            poolPrice,
            block.timestamp,
            false
        );
    }
    
    // user buying DHPT for sUSD. consider DHPT 24h lockup
    function swapDHPT(address poolAddressA, uint256 poolAmountA, address poolAddressB) public {
       require(enableBuy, "buy disabled");
       require(enableSell, "sell disabled");
       require(dhptWhitelist[poolAddressA] > 0, "from-token not whitelisted");
       require(poolAmountA > 10000, "amount too small");
       require(dhptWhitelist[poolAddressB] > 0, "to-token not whitelisted");

       uint256 poolPriceA = tokenPriceWithSettle(poolAddressA);
       uint256 sUsdAmount = poolAmountA.mul(poolPriceA).div(10**18);

       uint256 poolPriceB = tokenPriceWithSettle(poolAddressB);
       uint256 poolAmountB = sUsdAmount.mul(10**18).div(poolPriceB);
       IDHedge(poolAddressB).setLastDeposit(msg.sender);

        require(
            IERC20(poolAddressA).transferFrom(
                msg.sender,
                address(this),
                poolAmountA
            ),
            "from-token transfer failed"
        );
      
        require(
            IERC20(poolAddressB).transfer(msg.sender, poolAmountB),
            "to-token transfer failed"
        );

        emit SwapDHPT(
            poolAddressA,
            poolPriceA,
            poolAmountA,
            poolAddressB,
            poolPriceB,
            poolAmountB,
            msg.sender,
            block.timestamp,
            false
        );
    }
    

    // ORACLE FUNCTIONS
    
    function oracleBuyDHPT(address poolAddress, address stableCoin, uint256 stableCoinAmount, uint256 blockNumber, uint256 poolPrice, bytes memory signature)
       public
    {
        _requireOracle(enableOracleBuy, stableCoinAmount, blockNumber);
        uint8 stableCoinPrecision = stableCoins[stableCoin];
        require(stableCoinPrecision > 0, "selected stable coin is disabled");
        
        require(dhptWhitelist[poolAddress] > 0, "pool not whitelisted");
        require(_isOracleSigValid(msg.sender, blockNumber, poolAddress, poolPrice, stableCoinAmount, signature), "signature invalid");
        uint256 dhptAmount = stableCoinAmount.mul(10**uint(stableCoinPrecision)).div(poolPrice);

        IDHedge(poolAddress).setLastDeposit(msg.sender);
        
        require(
            IERC20(stableCoin).transferFrom(
                msg.sender,
                address(this),
                stableCoinAmount
            ),
            "stable coin transfer failed"
        );

        require(
            IERC20(poolAddress).transfer(msg.sender, dhptAmount),
            "token transfer failed"
        );

        emit BuyDHPT(
            poolAddress,
            msg.sender,
            stableCoinAmount,
            dhptAmount,
            poolPrice,
            block.timestamp,
            true
        );
        
    }
    
    function oracleSellDHPT(address poolAddress, address stableCoin, uint256 dhptAmount, uint256 blockNumber, uint256 poolPrice, bytes memory signature)
       public
    {
        _requireOracle(enableOracleSell, dhptAmount, blockNumber);
        uint8 stableCoinPrecision = stableCoins[stableCoin];
        require(stableCoinPrecision > 0, "selected stable coin is disabled");        
        require(_canSellDhpt(poolAddress, dhptAmount), "unable to sell tokens");
       
        uint256 stableCoinAmount = dhptAmount.mul(poolPrice).div(10**uint(stableCoinPrecision));
        require(_isOracleSigValid(msg.sender, blockNumber, poolAddress, poolPrice, stableCoinAmount, signature), "signature invalid");
        
        require(
            IERC20(poolAddress).transferFrom(
                msg.sender,
                address(this),
                dhptAmount
            ),
            "token transfer failed"
        );

       
        require(
            IERC20(stableCoin).transfer(
                msg.sender, stableCoinAmount
            ),
            "stable coin transfer failed"
        );

        emit SellDHPT(
            poolAddress,
            msg.sender,
            stableCoinAmount,
            dhptAmount,
            poolPrice,
            block.timestamp,
            true
        );
    }

    // oracle swap buy DHPT from external sources of DHPT liquidity
    function oracleBuyDHPTFrom(address poolAddress, address fromAddress, uint256 susdAmount, uint256 blockNumber, uint256 poolPrice, bytes memory signature) 
        public
    {
        _requireOracle(enableOracleBuy, susdAmount, blockNumber);
        require(_isOracleSigValid(msg.sender, blockNumber, poolAddress, poolPrice, susdAmount, signature), "signature invalid");
        require(dhptWhitelist[poolAddress] > 0, "pool not whitelisted");

        uint256 dhptAmount = susdAmount.mul(10**18).div(poolPrice);
        require(_dhptFromEnabled(poolAddress, fromAddress), "source liquidity disabled");
        IDHedge(poolAddress).setLastDeposit(msg.sender);

        require(
            IERC20(_getAssetProxy(_SUSD_KEY)).transferFrom(
                msg.sender,
                fromAddress,
                susdAmount
            ),
            "susd transfer failed"
        );

        require(
            IERC20(poolAddress).transferFrom(
                fromAddress,
                msg.sender,
                dhptAmount
            ),
            "token transfer failed"
        );

        emit BuyDHPT(
            poolAddress,
            msg.sender,
            susdAmount,
            dhptAmount,
            poolPrice,
            block.timestamp,
            true
        );
    }

    function oracleSwapDHPT(address poolAddressA, uint256 poolAmountA, uint256 poolPriceA, address poolAddressB,
                             uint256 poolPriceB, uint256 blockNumber, bytes memory signature)
        public
    {
        _requireOracle(enableOracleBuy && enableOracleSell, poolAmountA, blockNumber);
        require(_canSellDhpt(poolAddressA, poolAmountA), "unable to sell tokens");
        require(dhptWhitelist[poolAddressB] > 0, "pool not whitelisted");

       //swap logic starts here
       require(_isOracleSwapSigValid(msg.sender, blockNumber, poolAddressA, poolPriceA, poolAmountA, poolAddressB, poolPriceB, signature), "signature invalid");

        require(
            IERC20(poolAddressA).transferFrom(
                msg.sender,
                address(this),
                poolAmountA
            ),
            "from-token transfer failed"
        );

        uint256 poolAmountB = (poolAmountA.mul(poolPriceA)).div(poolPriceB);

        require(
            IERC20(poolAddressB).transfer(msg.sender, poolAmountB),
            "to-token transfer failed"
        );

        emit SwapDHPT(
            poolAddressA,
            poolPriceA,
            poolAmountA,
            poolAddressB,
            poolPriceB,
            poolAmountB,
            msg.sender,
            block.timestamp,
            true
        );
    }

    function _requireOracle(bool enableOracle, uint256 amount, uint256 blockNumber)
        internal
        view
    {
        require(blockNumber.add(oracleBlockBias) > block.number, "transaction timed out");
        require(enableOracle, "oracle disabled");
        require(amount > 10000, "amount too small");
        require(blockNumber <= block.number, "invalid block number");
    }
    
    function _isOracleSigValid(address sender, uint256 blockNumber, address poolAddress, uint256 poolPrice, uint256 amount, bytes memory signature)
        internal
        view
        returns (bool)
    {
        bytes32 hash = keccak256(abi.encodePacked(sender, blockNumber, poolAddress, poolPrice, amount));
        bytes32 ethHash = ECDSA.toEthSignedMessageHash(hash);
        
        if (ECDSA.recover(ethHash, signature) == oracle) {
            return true;
        } else {
            return false;
        }
    }

    function _isOracleSwapSigValid(address sender, uint256 blockNumber, address poolAddressA, uint256 poolPriceA, uint256 poolAmountA, address poolAddressB, uint256 poolPriceB, bytes memory signature)
        internal
        view
        returns (bool)
    {
        bytes32 hash = keccak256(abi.encodePacked(sender, blockNumber, poolAddressA, poolPriceA, poolAmountA, poolAddressB, poolPriceB ));
        bytes32 ethHash = ECDSA.toEthSignedMessageHash(hash);
        
        if (ECDSA.recover(ethHash, signature) == oracle) {
            return true;
        } else {
            return false;
        }
    }

    function dhptFromLiquidity(address poolAddress, address fromAddress)
    public
    view
    returns (uint256)
    {
        if (_dhptFromEnabled(poolAddress, fromAddress)) {
            return IERC20(poolAddress).allowance(fromAddress, address(this));
        } else {
            return 0;
        }
    }

    function enableLiquidity(address[] memory poolAddresses, bool[] memory enabled)
        public
    {
        require(poolAddresses.length == enabled.length, "invalid input lengths");

        for (uint256 i = 0; i < poolAddresses.length; i++) {
            bytes32 hash = keccak256(abi.encodePacked(poolAddresses[i], msg.sender));
            dhptFromEnabled[hash] = enabled[i];
        }
    }

    function _dhptFromEnabled(address poolAddress, address fromAddress)
        internal
        view
        returns (bool)
    {
        bytes32 hash = keccak256(abi.encodePacked(poolAddress, fromAddress));
        return dhptFromEnabled[hash];
    }


    // ADMIN

    // whitelist dHEDGE pools the contract will accept tokens from
    function whitelistDhpt(address[] memory addresses, uint256[] memory amounts)
        public
        onlyOwner
    {
        require(addresses.length == amounts.length, "invalid input lengths");

        for (uint256 i = 0; i < addresses.length; i++) {
            require(IPoolDirectory(factory).isPool(addresses[i]), "not a pool");

            dhptWhitelist[addresses[i]] = amounts[i];
        }
    }

    function setAddressResolver(IAddressResolver _addressResolver)
        public
        onlyOwner
    {
        addressResolver = _addressResolver;
    }
    
    function setFactory(address _factory)
        public
        onlyOwner
    {
        factory = _factory;
    }

    function setDao(address _dao)
        public
        onlyOwner
    {
        dao = _dao;
    }
    
    function setOracle(address _oracle)
        public
        onlyOwner
    {
        oracle = _oracle;
    }
    
    function setOracleBlockBias(uint8 _oracleBlockBias)
        public
        onlyOwner
    {
        oracleBlockBias = _oracleBlockBias;
    }

    function withdrawToken(address tokenAddress, uint256 amount)
        public
        onlyDao
    {
        require(
            IERC20(tokenAddress).transfer(
                dao,
                amount
            ),
            "token transfer failed"
        );
    }

    function withdrawTokenTo(address tokenAddress, uint256 amount, address toAddress)
        public
        onlyDao
    {
        require(
            IERC20(tokenAddress).transfer(
                toAddress,
                amount
                ),
            "token transfer failed"
            );
    }

    function enableBuySell(bool _enableBuy, bool _enableSell, bool _enableOracleBuy, bool _enableOracleSell)
        public
        onlyOwner
    {
        enableBuy = _enableBuy;
        enableSell = _enableSell;
        enableOracleBuy = _enableOracleBuy;
        enableOracleSell = _enableOracleSell;
    }
    

    // VIEWS

    function _canSellDhpt(address poolAddress, uint256 dhptAmount)
        internal
        view
        returns (bool)
    {
        uint256 dhptBalance = tokenBalanceOf(poolAddress);
        if (dhptWhitelist[poolAddress] >= dhptBalance.add(dhptAmount)) {
            return true;
        } else {
            return false;
        }
    }

    function tokenBalanceOf(address tokenAddress)
        public
        view
        returns (uint256)
    {
        return IERC20(tokenAddress).balanceOf(address(this));
    }

    function _availDhptToSell(address poolAddress)
        internal
        view
        returns (uint256)
    {
        uint256 dhptBalance = tokenBalanceOf(poolAddress);
        return dhptWhitelist[poolAddress].sub(dhptBalance);
    }

    // maximum DHPT that can be sold by the user taking into account contract sUSD balance
    function maxDhptToSell(address poolAddress) public view returns (uint256) {
        uint256 availDhpt = _availDhptToSell(poolAddress);
        uint256 susdBalance = IERC20(_getAssetProxy(_SUSD_KEY)).balanceOf(
            address(this)
        );
        uint256 poolPrice = tokenPriceWithSettle(poolAddress);
        require(poolPrice>0, 'error in maxDhptToSell: poolPrice should be non-zero');
        // how much DHPT the contract can buy with its sUSD balance
        uint256 susdForDhpt = susdBalance.mul(10**18).div(poolPrice);

        if (susdForDhpt > availDhpt) {
            return availDhpt;
        } else {
            return susdForDhpt;
        }
    }

    // maximum sUSD that can be sold by the user taking into account contract DHPT balance
    function maxSusdToSell(address poolAddress) public view returns (uint256) {
        uint256 dhptBalance = IERC20(poolAddress).balanceOf(address(this));
        uint256 poolPrice = tokenPriceWithSettle(poolAddress);
        // how much sUSD the contract can buy with its DHPT balance
        uint256 dhptForSusd = dhptBalance.mul(poolPrice).div(10**18);

        return dhptForSusd;
    }

    // returns token price with any pending Synthetix settlement amounts
    function tokenPriceWithSettle(address poolAddress)
        public
        view
        returns (uint256)
    {
        IDHedge dhpool = IDHedge(poolAddress);
        IExchanger ex = IExchanger(addressResolver.getAddress(_EXCHANGER_KEY));

        uint256 totalValue = 0;
        bytes32[] memory supportedAssets = dhpool.getSupportedAssets();
        uint256 totalSupply = dhpool.totalSupply();

        require(totalSupply > 0, "pool must not be empty");

        for (uint256 i = 0; i < supportedAssets.length; i++) {
            uint256 assetTotal = IERC20(_getAssetProxy(supportedAssets[i]))
                .balanceOf(poolAddress);

            if (assetTotal > 0) {
                uint256 waitingPeriod = ex.maxSecsLeftInWaitingPeriod(
                    poolAddress,
                    supportedAssets[i]
                );
                require(waitingPeriod == 0, "wait for settlement");

                (
                    uint256 reclaimAmount,
                    uint256 rebateAmount,
                    /*uint256 entries*/
                ) = ex.settlementOwing(poolAddress, supportedAssets[i]);

                if (rebateAmount > 0) {
                    assetTotal = assetTotal.add(rebateAmount);
                }
                if (reclaimAmount > 0) {
                    assetTotal = assetTotal.sub(reclaimAmount);
                }

                IExchangeRates exchangeRates = IExchangeRates(
                    addressResolver.getAddress(_EXCHANGE_RATES_KEY)
                );
                totalValue = totalValue.add(
                    exchangeRates
                        .rateForCurrency(supportedAssets[i])
                        .mul(assetTotal)
                        .div(10**18)
                );
            }
        }
        uint256 lastFeeMintPrice = dhpool.tokenPriceAtLastFeeMint();
        require(totalSupply>0, 'totalSupply should be non-zero');
        uint256 tokenPrice = totalValue.mul(10**18).div(totalSupply);

        if (lastFeeMintPrice.add(100) < tokenPrice) {
            return tokenPrice.sub(_getTokenPriceImpact(poolAddress, tokenPrice, lastFeeMintPrice));
        } else {
            return tokenPrice;
        }
    }
    
    // token price at which the manager's fee was last minted
    function getLastFeeMintPrice(address poolAddress) public view returns (uint256) {
        IDHedge dhpool = IDHedge(poolAddress);
        return dhpool.tokenPriceAtLastFeeMint();
    }

    // token price decrease from manager fee minting
    function _getTokenPriceImpact(address poolAddress, uint256 tokenPrice, uint256 lastFeeMintPrice) internal view returns (uint256) {
        uint256 managerFeeNumerator;
        uint256 managerFeeDenominator;
        (managerFeeNumerator, managerFeeDenominator) = IHasFeeInfo(factory).getPoolManagerFee(poolAddress);
        return tokenPrice.sub(lastFeeMintPrice).mul(managerFeeNumerator).div(managerFeeDenominator);
    }

    function _getAssetProxy(bytes32 key) internal view returns (address) {
        address synth = ISynthetix(addressResolver.getAddress(_SYNTHETIX_KEY))
            .synths(key);
        require(synth != address(0), "invalid key");
        address proxy = ISynth(synth).proxy();
        require(proxy != address(0), "invalid proxy");
        return proxy;
    }

    // MODIFIERS
    modifier onlyDao() {
        require(msg.sender == dao, "only dao");
        _;
    }

}
function _getTokenPriceImpact(address poolAddress, uint256 tokenPrice, uint256 lastFeeMintPrice) internal view returns (uint256) {
    uint256 managerFeeNumerator;
    uint256 managerFeeDenominator;
    (managerFeeNumerator, managerFeeDenominator) = IHasFeeInfo(factory).getPoolManagerFee(poolAddress);
    return tokenPrice.sub(lastFeeMintPrice).mul(managerFeeNumerator).div(managerFeeDenominator);
}

// token price decrease from manager fee minting