contract ReefBasket is ReentrancyGuard, Ownable {
    using SafeMath for uint256;
    using Address for address;
    using SafeERC20 for IERC20;

    bool private stopped = false;
    uint16 public protocolTokenDisinvestPercentage;
    address public protocolTokenAddress;
    uint256 public minimalInvestment = 1 ether;

    // Limit how much funds we can handle
    uint256 public maxInvestedFunds = 100 ether;
    uint256 public currentInvestedFunds;

    // Define baskets
    struct UniswapV2Pool {
        uint8 weight;
        address uniswapToken0;
        address uniswapToken1;
    }

    struct Token {
        uint8 weight;
        address tokenAddress;
    }

    struct Pool {
        uint8 weight;
        address poolAddress;
    }

    struct Basket {
        string name;
        address referrer;
        UniswapV2Pool[] uniswapPools;
        Token[] tokens;
        Pool[] balancerPools;
        Pool[] mooniswapPools;
    }

    struct BasketBalance {
        uint256 investedAmount;
        mapping(uint256 => uint256) uniswapPools;
        mapping(uint256 => uint256) balancerPools;
        mapping(uint256 => uint256) mooniswapPools;
        mapping(uint256 => uint256) tokens;
    }

    struct UserBalance {
        mapping(uint256 => BasketBalance) basketBalances;
    }

    event Invest(
        address indexed user,
        uint256 indexed basketId,
        uint256 investedAmount
    );

    event Disinvest(
        address indexed user,
        uint256 indexed basketId,
        uint256 disinvestedAmount
    );

    event BasketCreated(uint256 indexed basketId, address indexed user);

    uint256 public availableBasketsSize;
    mapping(uint256 => Basket) public availableBaskets;

    mapping(address => UserBalance) private userBalance;

    address wethTokenAddress = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    constructor(
        uint16 _protocolTokenDisinvestPercentage,
        address _protocolTokenAddress
    ) public {
        protocolTokenDisinvestPercentage = _protocolTokenDisinvestPercentage;
        protocolTokenAddress = _protocolTokenAddress;
    }

    function balanceOfUniswapPools(address _owner, uint256 _basketIndex)
        public
        view
        returns (uint256[] memory)
    {
        Basket storage basket = availableBaskets[_basketIndex];

        uint256[] memory uniswapBalances = new uint256[](
            basket.uniswapPools.length
        );
        for (uint256 i = 0; i < basket.uniswapPools.length; i++) {
            uniswapBalances[i] = userBalance[_owner]
                .basketBalances[_basketIndex]
                .uniswapPools[i];
        }

        return uniswapBalances;
    }

    function balanceOfTokens(address _owner, uint256 _basketIndex)
        public
        view
        returns (uint256[] memory)
    {
        Basket storage basket = availableBaskets[_basketIndex];

        uint256[] memory tokenBalances = new uint256[](basket.tokens.length);
        for (uint256 i = 0; i < basket.tokens.length; i++) {
            tokenBalances[i] = userBalance[_owner].basketBalances[_basketIndex]
                .tokens[i];
        }

        return tokenBalances;
    }

    function balanceOfBalancerPools(address _owner, uint256 _basketIndex)
        public
        view
        returns (uint256[] memory)
    {
        Basket storage basket = availableBaskets[_basketIndex];

        uint256[] memory balancerBalances = new uint256[](
            basket.balancerPools.length
        );
        for (uint256 i = 0; i < basket.balancerPools.length; i++) {
            balancerBalances[i] = userBalance[_owner]
                .basketBalances[_basketIndex]
                .balancerPools[i];
        }

        return balancerBalances;
    }

    function balanceOfMooniswapPools(address _owner, uint256 _basketIndex)
        public
        view
        returns (uint256[] memory)
    {
        Basket storage basket = availableBaskets[_basketIndex];

        uint256[] memory mooniswapBalances = new uint256[](
            basket.mooniswapPools.length
        );
        for (uint256 i = 0; i < basket.mooniswapPools.length; i++) {
            mooniswapBalances[i] = userBalance[_owner]
                .basketBalances[_basketIndex]
                .mooniswapPools[i];
        }

        return mooniswapBalances;
    }

    function investedAmountInBasket(address _owner, uint256 _basketIndex)
        public
        view
        returns (uint256)
    {
        return userBalance[_owner].basketBalances[_basketIndex].investedAmount;
    }

    function getAvailableBasketUniswapPools(uint256 _basketIndex)
        public
        view
        returns (address[2][] memory, uint8[] memory)
    {
        Basket storage basket = availableBaskets[_basketIndex];

        address[2][] memory uniswapPools = new address[2][](
            basket.uniswapPools.length
        );
        uint8[] memory uniswapWeights = new uint8[](basket.uniswapPools.length);

        for (uint256 i = 0; i < basket.uniswapPools.length; i++) {
            uniswapPools[i][0] = basket.uniswapPools[i].uniswapToken0;
            uniswapPools[i][1] = basket.uniswapPools[i].uniswapToken1;

            uniswapWeights[i] = basket.uniswapPools[i].weight;
        }

        return (uniswapPools, uniswapWeights);
    }

    function getAvailableBasketTokens(uint8 _basketIndex)
        public
        view
        returns (address[] memory, uint8[] memory)
    {
        Basket storage basket = availableBaskets[_basketIndex];

        address[] memory tokens = new address[](basket.tokens.length);
        uint8[] memory tokensWeights = new uint8[](basket.tokens.length);
        for (uint256 i = 0; i < basket.tokens.length; i++) {
            tokens[i] = basket.tokens[i].tokenAddress;

            tokensWeights[i] = basket.tokens[i].weight;
        }

        return (tokens, tokensWeights);
    }

    function getAvailableBasketBalancerPools(uint256 _basketIndex)
        public
        view
        returns (address[] memory, uint8[] memory)
    {
        Basket storage basket = availableBaskets[_basketIndex];

        address[] memory balancerPools = new address[](
            basket.balancerPools.length
        );
        uint8[] memory balancerWeights = new uint8[](
            basket.balancerPools.length
        );
        for (uint256 i = 0; i < basket.balancerPools.length; i++) {
            balancerPools[i] = basket.balancerPools[i].poolAddress;

            balancerWeights[i] = basket.balancerPools[i].weight;
        }

        return (balancerPools, balancerWeights);
    }

    function getAvailableBasketMooniswapPools(uint256 _basketIndex)
        public
        view
        returns (address[] memory, uint8[] memory)
    {
        Basket storage basket = availableBaskets[_basketIndex];

        address[] memory mooniswapPools = new address[](
            basket.mooniswapPools.length
        );
        uint8[] memory mooniswapWeights = new uint8[](
            basket.mooniswapPools.length
        );
        for (uint256 i = 0; i < basket.mooniswapPools.length; i++) {
            mooniswapPools[i] = basket.mooniswapPools[i].poolAddress;

            mooniswapWeights[i] = basket.mooniswapPools[i].weight;
        }

        return (mooniswapPools, mooniswapWeights);
    }

    function createBasket(
        string memory _name,
        address[2][] memory _uniswapPools,
        uint8[] memory _uniswapPoolsWeights,
        address[] memory _tokens,
        uint8[] memory _tokensWeights,
        address[] memory _balancerPools,
        uint8[] memory _balancerPoolsWeights,
        address[] memory _mooniswapPools,
        uint8[] memory _mooniswapPoolsWeights
    ) public payable nonReentrant stopInEmergency returns (uint256) {
        require(
            _uniswapPoolsWeights.length > 0 ||
                _tokensWeights.length > 0 ||
                _balancerPoolsWeights.length > 0 ||
                _mooniswapPoolsWeights.length > 0,
            "0 assets given"
        );
        require(_uniswapPools.length == _uniswapPoolsWeights.length);
        require(_tokens.length == _tokensWeights.length);
        require(_balancerPools.length == _balancerPoolsWeights.length);
        require(_mooniswapPools.length == _mooniswapPoolsWeights.length);

        uint256 totalWeights;
        Basket storage basket = availableBaskets[availableBasketsSize];
        availableBasketsSize++;

        basket.name = _name;
        basket.referrer = msg.sender;

        for (uint256 i = 0; i < _uniswapPoolsWeights.length; i++) {
            totalWeights = (totalWeights).add(_uniswapPoolsWeights[i]);
        }
        for (uint256 i = 0; i < _tokensWeights.length; i++) {
            totalWeights = (totalWeights).add(_tokensWeights[i]);
        }
        for (uint256 i = 0; i < _balancerPoolsWeights.length; i++) {
            totalWeights = (totalWeights).add(_balancerPoolsWeights[i]);
        }
        for (uint256 i = 0; i < _mooniswapPoolsWeights.length; i++) {
            totalWeights = (totalWeights).add(_mooniswapPoolsWeights[i]);
        }

        require(totalWeights == 100, "Basket weights have to sum up to 100.");

        for (uint256 i = 0; i < _uniswapPools.length; i++) {
            UniswapV2Pool memory pool = UniswapV2Pool(
                _uniswapPoolsWeights[i],
                _uniswapPools[i][0],
                _uniswapPools[i][1]
            );

            basket.uniswapPools.push(pool);
        }

        for (uint256 i = 0; i < _tokensWeights.length; i++) {
            Token memory token = Token(_tokensWeights[i], _tokens[i]);

            basket.tokens.push(token);
        }

        for (uint256 i = 0; i < _balancerPools.length; i++) {
            Pool memory balancerPool = Pool(
                _balancerPoolsWeights[i],
                _balancerPools[i]
            );

            basket.balancerPools.push(balancerPool);
        }

        for (uint256 i = 0; i < _mooniswapPools.length; i++) {
            Pool memory mooniswapPool = Pool(
                _mooniswapPoolsWeights[i],
                _mooniswapPools[i]
            );

            basket.mooniswapPools.push(mooniswapPool);
        }

        emit BasketCreated(availableBasketsSize - 1, msg.sender);

        uint256[] memory baskets = new uint256[](1);
        uint256[] memory weights = new uint256[](1);
        baskets[0] = availableBasketsSize - 1;
        weights[0] = 100;

        return _multiInvest(baskets, weights, 1);
    }

    /**
    @notice This function is used to invest in given Uniswap V2 pair through ETH/ERC20 Tokens
    @param _basketIndexes basket indexes to invest into
    @param _weights corresponding basket weights (percentage) how much to invest
    @param _minPoolTokens Reverts if less tokens received than this
    @return Amount of LP bought
     */
    function invest(
        uint256[] memory _basketIndexes,
        uint256[] memory _weights,
        uint256 _minPoolTokens
    ) public payable nonReentrant stopInEmergency returns (uint256) {
        return _multiInvest(_basketIndexes, _weights, _minPoolTokens);
    }

    function _multiInvest(
        uint256[] memory _basketIndexes,
        uint256[] memory _weights,
        uint256 _minPoolTokens
    ) internal returns (uint256) {
        require(msg.value > 0, "Error: ETH not sent");

        // Check weights
        require(_basketIndexes.length == _weights.length);
        uint256 totalWeights;
        for (uint256 i = 0; i < _weights.length; i++) {
            totalWeights = (totalWeights).add(_weights[i]);
        }

        require(totalWeights == 100, "Basket _weights have to sum up to 100.");

        for (uint256 i = 0; i < _weights.length; i++) {
            uint256 basketInvestAmount = (msg.value).mul(_weights[i]).div(100);
            require(
                basketInvestAmount >= minimalInvestment,
                "Too low invest amount."
            );

            _invest(_basketIndexes[i], basketInvestAmount, _minPoolTokens);
        }

        // Return change
        (bool success, ) = msg.sender.call{value: address(this).balance}("");
    }

    function _invest(
        uint256 _basketIndex,
        uint256 _amount,
        uint256 _minPoolTokens
    ) internal returns (uint256) {
        require(
            _basketIndex < availableBasketsSize,
            "Error: basket index out of bounds"
        );
        uint256 startBalance = address(this).balance;

        // Invest into pools
        for (
            uint256 i = 0;
            i < availableBaskets[_basketIndex].uniswapPools.length;
            i++
        ) {
            UniswapV2Pool memory pool = availableBaskets[_basketIndex]
                .uniswapPools[i];

            uint256 investAmount = (_amount).mul(pool.weight).div(100);

            uint256 LPBought = ReefUniswap._investIntoUniswapPool(
                address(0),
                pool.uniswapToken0,
                pool.uniswapToken1,
                address(this),
                investAmount
            );

            require(LPBought >= _minPoolTokens, "ERR: High Slippage");

            userBalance[msg.sender].basketBalances[_basketIndex]
                .uniswapPools[i] = userBalance[msg.sender]
                .basketBalances[_basketIndex]
                .uniswapPools[i]
                .add(LPBought);
        }

        // Invest into tokens
        for (
            uint256 i = 0;
            i < availableBaskets[_basketIndex].tokens.length;
            i++
        ) {
            Token memory token = availableBaskets[_basketIndex].tokens[i];
            uint256 investAmount = (_amount).mul(token.weight).div(100);

            uint256 tokenBought = ReefUniswap.swapFromV2(
                address(0),
                token.tokenAddress,
                investAmount
            );

            userBalance[msg.sender].basketBalances[_basketIndex]
                .tokens[i] = userBalance[msg.sender]
                .basketBalances[_basketIndex]
                .tokens[i]
                .add(tokenBought);
        }

        // Invest into balancer pool
        for (
            uint256 i = 0;
            i < availableBaskets[_basketIndex].balancerPools.length;
            i++
        ) {
            Pool memory balancerPool = availableBaskets[_basketIndex]
                .balancerPools[i];
            uint256 investAmount = (_amount).mul(balancerPool.weight).div(100);

            uint256 balancerTokens = ReefBalancer.investIntoBalancerPool(
                address(this),
                address(0),
                balancerPool.poolAddress,
                investAmount,
                _minPoolTokens
            );

            userBalance[msg.sender].basketBalances[_basketIndex]
                .balancerPools[i] = userBalance[msg.sender]
                .basketBalances[_basketIndex]
                .balancerPools[i]
                .add(balancerTokens);
        }

        // Invest into Mooniswap pool
        for (
            uint256 i = 0;
            i < availableBaskets[_basketIndex].mooniswapPools.length;
            i++
        ) {
            Pool memory mooniswapPool = availableBaskets[_basketIndex]
                .mooniswapPools[i];
            uint256 investAmount = (_amount).mul(mooniswapPool.weight).div(100);

            uint256 fairSupply = ReefMooniswapV1.investIntoMooniswapPool(
                address(0),
                mooniswapPool.poolAddress,
                investAmount
            );

            userBalance[msg.sender].basketBalances[_basketIndex]
                .mooniswapPools[i] = userBalance[msg.sender]
                .basketBalances[_basketIndex]
                .mooniswapPools[i]
                .add(fairSupply);
        }

        // Update user balance
        uint256 diffBalance = startBalance.sub(address(this).balance);

        userBalance[msg.sender].basketBalances[_basketIndex]
            .investedAmount = userBalance[msg.sender]
            .basketBalances[_basketIndex]
            .investedAmount
            .add(diffBalance);

        // Update current funds
        currentInvestedFunds = currentInvestedFunds.add(diffBalance);
        require(
            currentInvestedFunds <= maxInvestedFunds,
            "Max invested funds exceeded"
        );

        emit Invest(
            msg.sender,
            _basketIndex,
            userBalance[msg.sender].basketBalances[_basketIndex].investedAmount
        );

        return
            userBalance[msg.sender].basketBalances[_basketIndex].investedAmount;
    }

    function disinvest(
        uint256 _basketIndex,
        uint256 _percent,
        uint256 _protocolYieldRatio,
        bool shouldRestake
    ) public payable nonReentrant stopInEmergency returns (uint256) {
        require(
            _basketIndex < availableBasketsSize,
            "Basket index out of bounds"
        );

        require(
            _percent > 0 && _percent <= 100,
            "Percent has to in interval (0, 100]"
        );
        require(
            _protocolYieldRatio <= 100,
            "Protocol yield ratio not in interval (0, 100]"
        );

        // Disinvest uniswap pools
        for (
            uint256 p = 0;
            p < availableBaskets[_basketIndex].uniswapPools.length;
            p++
        ) {
            uint256 currentBalance = userBalance[msg.sender]
                .basketBalances[_basketIndex]
                .uniswapPools[p];

            require(currentBalance > 0, "balance must be positive");

            UniswapV2Pool memory pool = availableBaskets[_basketIndex]
                .uniswapPools[p];
            uint256 disinvestAmount = (currentBalance).mul(_percent).div(100);

            (uint256 amountA, uint256 amountB) = ReefUniswap
                ._disinvestFromUniswapPool(
                address(0),
                pool.uniswapToken0,
                pool.uniswapToken1,
                disinvestAmount
            );

            userBalance[msg.sender].basketBalances[_basketIndex]
                .uniswapPools[p] = userBalance[msg.sender]
                .basketBalances[_basketIndex]
                .uniswapPools[p]
                .sub(disinvestAmount);
        }

        // Disinvest tokens
        for (
            uint256 t = 0;
            t < availableBaskets[_basketIndex].tokens.length;
            t++
        ) {
            uint256 currentBalance = userBalance[msg.sender]
                .basketBalances[_basketIndex]
                .tokens[t];

            require(currentBalance > 0, "balance must be positive");

            Token memory token = availableBaskets[_basketIndex].tokens[t];
            uint256 disinvestAmount = (currentBalance).mul(_percent).div(100);

            if (ReefUniswap.canSwapFromV2(token.tokenAddress, address(0))) {
                uint256 tokenBought = ReefUniswap.swapFromV2(
                    token.tokenAddress,
                    address(0),
                    disinvestAmount
                );

                TransferHelper.safeTransfer(
                    address(0),
                    msg.sender,
                    tokenBought
                );
            }

            userBalance[msg.sender].basketBalances[_basketIndex]
                .tokens[t] = userBalance[msg.sender]
                .basketBalances[_basketIndex]
                .tokens[t]
                .sub(disinvestAmount);
        }

        // Disinvest Balancer pools
        for (
            uint256 b = 0;
            b < availableBaskets[_basketIndex].balancerPools.length;
            b++
        ) {
            require(
                userBalance[msg.sender].basketBalances[_basketIndex]
                    .balancerPools[b] > 0,
                "balance must be positive"
            );

            uint256 disinvestAmount = (
                userBalance[msg.sender].basketBalances[_basketIndex]
                    .balancerPools[b]
            )
                .mul(_percent)
                .div(100);

            IERC20(availableBaskets[_basketIndex].balancerPools[b].poolAddress)
                .approve(address(ReefBalancer), disinvestAmount);

            // TODO: figure out slippage
            uint256 balancerTokens = ReefBalancer.disinvestFromBalancerPool(
                payable(address(this)),
                address(0),
                availableBaskets[_basketIndex].balancerPools[b].poolAddress,
                disinvestAmount,
                1
            );

            userBalance[msg.sender].basketBalances[_basketIndex]
                .balancerPools[b] = userBalance[msg.sender]
                .basketBalances[_basketIndex]
                .balancerPools[b]
                .sub(disinvestAmount);
        }

        // Disinvest Mooniswap pools
        for (
            uint256 b = 0;
            b < availableBaskets[_basketIndex].mooniswapPools.length;
            b++
        ) {
            require(
                userBalance[msg.sender].basketBalances[_basketIndex]
                    .mooniswapPools[b] > 0,
                "balance must be positive"
            );

            uint256 disinvestAmount = (
                userBalance[msg.sender].basketBalances[_basketIndex]
                    .mooniswapPools[b]
            )
                .mul(_percent)
                .div(100);

            // TODO: figure out slippage
            ReefMooniswapV1.disinvestFromMooniswapPool(
                address(0),
                availableBaskets[_basketIndex].mooniswapPools[b].poolAddress,
                disinvestAmount
            );

            userBalance[msg.sender].basketBalances[_basketIndex]
                .mooniswapPools[b] = userBalance[msg.sender]
                .basketBalances[_basketIndex]
                .mooniswapPools[b]
                .sub(disinvestAmount);
        }

        // Update user balance
        uint256 basketDisinvestAmount = (
            userBalance[msg.sender].basketBalances[_basketIndex].investedAmount
        )
            .mul(_percent)
            .div(100);

        userBalance[msg.sender].basketBalances[_basketIndex]
            .investedAmount = userBalance[msg.sender]
            .basketBalances[_basketIndex]
            .investedAmount
            .sub(basketDisinvestAmount);

        emit Disinvest(msg.sender, _basketIndex, basketDisinvestAmount);

        // Update current funds
        currentInvestedFunds = currentInvestedFunds.sub(basketDisinvestAmount);

        // Stake the profit into REEF tokens
        if (address(this).balance > basketDisinvestAmount) {
            uint256 profit = address(this).balance - basketDisinvestAmount;

            // Return the liquidation
            uint256 yieldRatio = protocolTokenDisinvestPercentage >
                _protocolYieldRatio
                ? protocolTokenDisinvestPercentage
                : _protocolYieldRatio;

            if (yieldRatio > 0) {
                // Check if we restake into the ETH/protocolToken pool
                if (shouldRestake) {
                    ReefUniswap._investIntoUniswapPool(
                        address(0),
                        wethTokenAddress,
                        protocolTokenAddress,
                        msg.sender,
                        profit.mul(yieldRatio).div(100)
                    );
                } else {
                    uint256 protocolTokenAmount = ReefUniswap.swapFromV2(
                        address(0),
                        protocolTokenAddress,
                        profit.mul(yieldRatio).div(100)
                    );

                    if (protocolTokenAmount > 0) {
                        TransferHelper.safeTransfer(
                            protocolTokenAddress,
                            msg.sender,
                            protocolTokenAmount
                        );
                    }
                }
            }
        }

        // Return the remaining ETH
        (bool success, ) = msg.sender.call{value: address(this).balance}("");
    }

    function setProtocolTokenDisinvestPercentage(uint16 _newPercentage)
        public
        onlyOwner
    {
        require(
            _newPercentage >= 0 && _newPercentage < 100,
            "_newPercentage must be between 0 and 100."
        );
        protocolTokenDisinvestPercentage = _newPercentage;
    }

    function setProtocolTokenAddress(address _newProtocolTokenAddress)
        public
        onlyOwner
    {
        protocolTokenAddress = _newProtocolTokenAddress;
    }

    function setMinimalInvestment(uint256 _minimalInvestment) public onlyOwner {
        minimalInvestment = _minimalInvestment;
    }

    function setMaxInvestedFunds(uint256 _maxInvestedFunds) public onlyOwner {
        require(
            _maxInvestedFunds >= currentInvestedFunds,
            "Max funds lower than current funds."
        );
        maxInvestedFunds = _maxInvestedFunds;
    }

    // circuit breaker modifiers
    modifier stopInEmergency {
        if (stopped) {
            revert("Temporarily Paused");
        } else {
            _;
        }
    }

    function inCaseTokengetsStuck(IERC20 _TokenAddress) public onlyOwner {
        uint256 qty = _TokenAddress.balanceOf(address(this));
        TransferHelper.safeTransfer(address(_TokenAddress), owner(), qty);
    }

    // to Pause the contract
    function toggleContractActive() public onlyOwner {
        stopped = !stopped;
    }

    // to withdraw any ETH balance sitting in the contract
    function withdraw() public onlyOwner {
        uint256 contractBalance = address(this).balance;
        address payable _to = payable(owner());
        _to.transfer(contractBalance);
    }

    receive() external payable {}
}
function withdraw() public onlyOwner {
    uint256 contractBalance = address(this).balance;
    address payable _to = payable(owner());
    _to.transfer(contractBalance);
}

// to withdraw any ETH balance sitting in the contract