contract DfTokenizedStrategy is DSMath, Ownable {

    using UniversalERC20 for IToken;

    struct TokenizedStrategy {
        // bytes32 (== uint256) slot
        uint80 initialEth;      // in eth – max more 1.2 mln eth
        uint80 entryEthPrice;   // in usd – max more 1.2 mln USD for 1 eth
        uint8 profitPercent;    // min profit percent
        bool onlyWithProfit;    // strategy can be closed only with profitPercent profit
        bool isStrategyClosed;  // strategy is closed
    }

    // address public constant DF_FINANCE_OPEN = address(0xBA3EEeb0cf1584eE565F34fCaBa74d3e73268c0b);   // TODO: DfFinanceOpenCompound address
    address public constant DF_FINANCE_OPEN = address(0x7eF7eBf6c5DA51A95109f31063B74ECf269b22bE);   // TODO: DfFinanceOpenCompound v2 address

    address public constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
    address public constant USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

    address public profitToken;
    address public dfFinanceClose;

    // deposited eth in strategy by owner (with depositEth count)
    uint256 public ethInDeposit;

    TokenizedStrategy public strategy;

    // ** EVENTS **

    event ProfitTokenCreated(
        address indexed profitToken
    );

    event DepositWithdrawn(
        address indexed user,
        uint ethToWithdraw,
        uint usdToWithdraw
    );

    event ProfitWithdrawn(
        address indexed user,
        uint ethToWithdraw,
        uint usdToWithdraw
    );

    // ** MODIFIERS **

    modifier onlyDfClose {
        require(msg.sender == dfFinanceClose, "Permission denied");
        _;
    }

    modifier afterStrategyClosed {
        require(strategy.isStrategyClosed, "Strategy is not closed");
        _;
    }

    // ** CONSTRUCTOR **

    constructor(
        string memory _tokenName,
        string memory _tokenSymbol,
        address _issuer,
        uint _extraCoef,
        uint _profitPercent,
        bytes memory _exchangeData,
        uint _usdcToBuyEth,
        uint _ethType,
        bool _onlyWithProfit
    ) public payable {
        require(_profitPercent > 0, "Profit percent can not be zero");

        uint curDeposit = address(this).balance;

        uint extraEth = mul(curDeposit, sub(_extraCoef, 100)) / 100;
        uint curEthPrice = wdiv(_usdcToBuyEth * 1e12, extraEth);

        // _profitPercent in percent (10 == 10%)
        uint tokensPerEth = wmul(mul(_profitPercent, WAD) / 100, curEthPrice);  // number of tokens for profit distribution per 1 eth

        // create token contract and mint tokens
        address tokenAddr = address(
            new DfProfitToken(
                _tokenName,
                _tokenSymbol,
                _issuer,
                wmul(curDeposit, tokensPerEth)
            )
        );
        profitToken = tokenAddr;

        // open strategy
        IDfFinanceOpen(DF_FINANCE_OPEN)
            .deal
            .value(curDeposit)
            (
                address(this),
                _extraCoef,
                _profitPercent,
                _exchangeData,
                _usdcToBuyEth,
                _ethType
            );

        // UPD states after open strategy
        ethInDeposit = curDeposit;
        strategy = TokenizedStrategy({
            initialEth: uint80(curDeposit),
            entryEthPrice: uint80(curEthPrice),
            profitPercent: uint8(_profitPercent),
            onlyWithProfit: _onlyWithProfit,
            isStrategyClosed: false
        });
        dfFinanceClose = IDfFinanceOpen(DF_FINANCE_OPEN).dfFinanceClose();

        emit ProfitTokenCreated(tokenAddr);
    }

    // ** PUBLIC VIEW functions **

    function calculateProfit(address _userAddr) public view returns(
        uint ethToWithdraw,
        uint usdToWithdraw
    ) {
        // return zero if the strategy is not closed
        if (!strategy.isStrategyClosed) {
            return (0, 0);
        }

        uint ethBalance = IToken(ETH_ADDRESS).universalBalanceOf(address(this));
        uint usdBalance = IToken(USDC_ADDRESS).universalBalanceOf(address(this));

        uint tokenTotalSupply = IERC20(profitToken).totalSupply();

        if (ethBalance == 0 && usdBalance == 0 || tokenTotalSupply == 0) {
            return (0, 0);
        }

        uint userTokenBalance = IERC20(profitToken).balanceOf(_userAddr);
        uint userShare = wdiv(userTokenBalance, tokenTotalSupply);

        ethToWithdraw = wmul(ethBalance, userShare);
        usdToWithdraw = wmul(usdBalance * 1e12, userShare) / 1e12;
    }

    // ** PUBLIC functions **

    function withdrawProfit() public afterStrategyClosed {
        _withdrawProfitHelper(msg.sender);
    }

    function withdrawProfit(address[] memory _accounts) public afterStrategyClosed {
        for (uint i = 0; i < _accounts.length; i++) {
           _withdrawProfitHelper(_accounts[i]);
        }
    }

    // ** ONLY_OWNER functions – calls DfFinanceClose **

    function collectAndCloseByUser(
        address _dfWallet,
        uint256 _ethForRedeem,
        uint256 _minAmountUsd,
        bool _onlyProfitInUsd,
        bytes memory _exData
    ) public payable onlyOwner {

        IDfFinanceClose(dfFinanceClose)
            .collectAndCloseByUser
            .value(msg.value)
            (
                _dfWallet,
                _ethForRedeem,
                _minAmountUsd,
                _onlyProfitInUsd,
                _exData
            );

    }

    function depositEth(address _dfWallet) public payable onlyOwner {
        (address strategyOwner,,,,,,,,) = IDfFinanceClose(dfFinanceClose).getStrategy(_dfWallet);
        require(address(this) == strategyOwner, "Incorrect dfWallet address");

        uint ethAmount = msg.value;

        IDfFinanceClose(dfFinanceClose)
            .depositEth
            .value(ethAmount)
            (
                _dfWallet
            );

        // UPD ethInDeposit state
        ethInDeposit = add(ethInDeposit, ethAmount);
    }

    function migrateStrategies(address[] memory _dfWallets) public onlyOwner {
        IDfFinanceClose(dfFinanceClose).migrateStrategies(_dfWallets);
    }

    function exitAfterLiquidation(
        address _dfWallet,
        uint256 _ethForRedeem,
        uint256 _minAmountUsd,
        bytes memory _exData
    ) public payable onlyOwner {

        IDfFinanceClose(dfFinanceClose)
            .exitAfterLiquidation
            .value(msg.value)
            (
                _dfWallet,
                _ethForRedeem,
                _minAmountUsd,
                _exData
            );

    }

    function externalCall(address payable _to, bytes memory _data) public payable onlyOwner {
        uint ethAmount = msg.value;
        bytes32 response;

        assembly {
            let succeeded := call(sub(gas, 5000), _to, ethAmount, add(_data, 0x20), mload(_data), 0, 32)
            response := mload(0)
            switch iszero(succeeded)
            case 1 {
                revert(0, 0)
            }
        }
    }

    // ** CALLBACK function **

    // closing strategy callback handler
    function __callback() external onlyDfClose {
        strategy.isStrategyClosed = true;

        // withdraw owner's deposit
        _withdrawDeposit();

        require(!(strategy.onlyWithProfit) ||
                _isProfitable(), "Strategy is not profitable enough");
    }

    // ** INTERNAL VIEW functions **

    function _isProfitable() internal view returns(bool) {

        uint ethBalance = IToken(ETH_ADDRESS).universalBalanceOf(address(this));
        uint usdBalance = IToken(USDC_ADDRESS).universalBalanceOf(address(this));

        // profitPercent in percent (10 == 10%)
        uint targetProfitEth = wmul(strategy.initialEth, WAD * strategy.profitPercent / 100);
        uint targetProfitUsd = IERC20(profitToken).totalSupply() / 1e12;  // 1 profit token == 1 USD

        // strategy is profitable enough for closing
        if (ethBalance >= targetProfitEth || usdBalance >= targetProfitUsd) {
            return true;
        }

        return false;
    }

    function _calculateWithdrawalOnDeposit() internal view returns(
        uint ethToWithdraw,
        uint usdToWithdraw,
        uint depositEth     // rest deposit in eth after this withdrawal
    ) {
        depositEth = ethInDeposit;
        if (depositEth == 0) {
            return (0, 0, 0);
        }

        uint ethBalance = IToken(ETH_ADDRESS).universalBalanceOf(address(this));
        uint usdBalance = IToken(USDC_ADDRESS).universalBalanceOf(address(this));

        // ethToWithdraw calculate
        if (ethBalance >= depositEth) {
            ethToWithdraw = depositEth;
        } else if (ethBalance > 0) {
            ethToWithdraw = ethBalance;
        }

        // update depositEth counter
        if (ethToWithdraw > 0) {
            depositEth = sub(depositEth, ethToWithdraw);
        }

        // calculate usdToWithdraw if there is not enough ETH
        if (depositEth > 0) {
            uint ethPrice = strategy.entryEthPrice;
            uint depositUsd = wmul(depositEth, ethPrice) / 1e12;  // rest deposit in USDC

            // usdToWithdraw calculate
            if (usdBalance >= depositUsd) {
                usdToWithdraw = depositUsd;
            } else if (usdBalance > 0) {
                usdToWithdraw = usdBalance;
            }

            // update depositEth counter
            if (usdToWithdraw > 0) {
                depositUsd = sub(depositUsd, usdToWithdraw);
                depositEth = wdiv(depositUsd * 1e12, ethPrice);
            }
        }
    }

    // ** INTERNAL functions **

    function _withdrawDeposit() internal {
        // calculate withdrawal on deposit
        (uint ethToWithdraw, uint usdToWithdraw, uint restDepositEth) = _calculateWithdrawalOnDeposit();

        // UPD ethInDeposit state
        ethInDeposit = restDepositEth;

        // withdraw deposit
        address userAddr = msg.sender;
        _withdrawHelper(userAddr, ethToWithdraw, usdToWithdraw);

        emit DepositWithdrawn(userAddr, ethToWithdraw, usdToWithdraw);
    }

    function _withdrawProfitHelper(address _userAddr) internal {
        uint tokenBalance = IERC20(profitToken).balanceOf(_userAddr);

        if (tokenBalance == 0) {
            return;  // User has no tokens to burn
        }

        // calculate user's profit
        (uint ethToWithdraw, uint usdToWithdraw) = calculateProfit(_userAddr);

        // burn all user's tokens
        _burnTokensHelper(_userAddr, tokenBalance);

        // withdraw user's profit
        _withdrawHelper(_userAddr, ethToWithdraw, usdToWithdraw);

        emit ProfitWithdrawn(_userAddr, ethToWithdraw, usdToWithdraw);
    }

    function _burnTokensHelper(address _userAddr, uint _amountToBurn) internal {
        IERC20Burnable(profitToken).burnFrom(_userAddr, _amountToBurn);
    }

    function _withdrawHelper(
        address _user, uint _ethToWithdraw, uint _usdToWithdraw
    ) internal {
        // withdraw ETH to user
        if (_ethToWithdraw > 0) {
            IToken(ETH_ADDRESS).universalTransfer(_user, _ethToWithdraw, true);
        }

        // withdraw USDC to user
        if (_usdToWithdraw > 0) {
            IToken(USDC_ADDRESS).universalTransfer(_user, _usdToWithdraw);
        }
    }

    // **FALLBACK function**
    function() external payable {}

}
function _isProfitable() internal view returns(bool) {

    uint ethBalance = IToken(ETH_ADDRESS).universalBalanceOf(address(this));
    uint usdBalance = IToken(USDC_ADDRESS).universalBalanceOf(address(this));

    // profitPercent in percent (10 == 10%)
    uint targetProfitEth = wmul(strategy.initialEth, WAD * strategy.profitPercent / 100);
    uint targetProfitUsd = IERC20(profitToken).totalSupply() / 1e12;  // 1 profit token == 1 USD

    // strategy is profitable enough for closing
    if (ethBalance >= targetProfitEth || usdBalance >= targetProfitUsd) {
        return true;
    }

    return false;
}

// ** INTERNAL VIEW functions **