contract HegicStakingPool is Ownable, ERC20{
    using SafeMath for uint;
    using SafeERC20 for IERC20;

    // Tokens
    IERC20 public immutable HEGIC;

    mapping(Asset => IHegicStaking) public staking; 

    uint public constant STAKING_LOT_PRICE = 888_000e18;
    uint public constant ACCURACY = 1e32;

    address payable public FALLBACK_RECIPIENT;
    address payable public FEE_RECIPIENT;

    address public constant OLD_HEGIC_STAKING_POOL = address(0xf4128B00AFdA933428056d0F0D1d7652aF7e2B35);
    address public constant Z_HEGIC = address(0x837010619aeb2AE24141605aFC8f66577f6fb2e7);

    uint public performanceFee = 5000;
    uint public discountedPerformanceFee = 4000;
    bool public depositsAllowed = true;
    uint public lockUpPeriod = 15 minutes;
    bool private migrating;

    uint public totalBalance;
    uint public lockedBalance;
    uint public totalNumberOfStakingLots;

    mapping(Asset => uint) public numberOfStakingLots;
    mapping(Asset => uint) public totalProfitPerToken;
    mapping(Asset => IERC20) public token;
    enum Asset {WBTC, ETH, USDC}

    mapping(address => uint) public ownerPerformanceFee;
    mapping(address => bool) public isNotFirstTime;
    mapping(address => uint) public lastDepositTime;
    mapping(address => mapping(Asset => uint)) lastProfit;
    mapping(address => mapping(Asset => uint)) savedProfit;

    event Deposit(address account, uint amount);
    event Withdraw(address account, uint amount);
    event BuyLot(Asset asset, address account);
    event SellLot(Asset asset, address account);
    event ClaimedProfit(address account, Asset asset, uint netProfit, uint fee);

    constructor(IERC20 _HEGIC, IERC20 _WBTC, IERC20 _WETH, IERC20 _USDC, IHegicStaking _stakingWBTC, IHegicStaking _stakingETH, IHegicStaking _stakingUSDC) public ERC20("Staked HEGIC", "sHEGIC"){
        HEGIC = _HEGIC;
        staking[Asset.WBTC] = _stakingWBTC;
        staking[Asset.ETH] = _stakingETH;
        staking[Asset.USDC] = _stakingUSDC;
        token[Asset.WBTC] = _WBTC;
        token[Asset.ETH] = _WETH;
        token[Asset.USDC] = _USDC;
        FEE_RECIPIENT = msg.sender;
        FALLBACK_RECIPIENT = msg.sender;

        // Approve Staking Lot Contract
        _HEGIC.approve(address(staking[Asset.WBTC]), type(uint256).max);
        _HEGIC.approve(address(staking[Asset.ETH]), type(uint256).max);
        _HEGIC.approve(address(staking[Asset.USDC]), type(uint256).max);
    }

    function approveContracts() external {
        require(depositsAllowed);
        HEGIC.approve(address(staking[Asset.WBTC]), type(uint256).max);
        HEGIC.approve(address(staking[Asset.ETH]), type(uint256).max);
        HEGIC.approve(address(staking[Asset.USDC]), type(uint256).max);
    }

    /**
     * @notice Stops the ability to add new deposits
     * @param _allow If set to false, new deposits will be rejected
     */
    function allowDeposits(bool _allow) external onlyOwner {
        depositsAllowed = _allow;
    }

    /**
     * @notice Changes Fee paid to creator (only paid when taking profits)
     * @param _fee New fee
     */
    function changePerformanceFee(uint _fee) external onlyOwner {
        require(_fee >= 0, "Fee too low");
        require(_fee <= 10000, "Fee too high");
        
        performanceFee = _fee;
    }

    /**
     * @notice Changes discounted Fee paid to creator (only paid when taking profits)
     * @param _fee New fee
     */
    function changeDiscountedPerformanceFee(uint _fee) external onlyOwner {
        require(_fee >= 0, "Fee too low");
        require(_fee <= 10000, "Fee too high");

        discountedPerformanceFee = _fee;
    }

    /**
     * @notice Changes Fee Recipient address
     * @param _recipient New address
     */
    function changeFeeRecipient(address _recipient) external onlyOwner {
        FEE_RECIPIENT = payable(_recipient);
    }

    /**
     * @notice Changes Fallback Recipient address. This is only used in case of unexpected behavior
     * @param _recipient New address
     */
    function changeFallbackRecipient(address _recipient) external onlyOwner {
        FALLBACK_RECIPIENT = payable(_recipient);
    }

    /**
     * @notice Toggles effect of lockup period by setting lockUpPeriod to 0 (disabled) or to 15 minutes(enabled)
     * @param _unlock Boolean: if true, unlocks funds
     */
    function unlockAllFunds(bool _unlock) external onlyOwner {
        if(_unlock) lockUpPeriod = 0;
        else lockUpPeriod = 15 minutes;
    }

    /**
     * @notice Migrates HEGIC from old staking pools (supports HegicStakingPool + ZLOT)
     * @param oldStakingPool staking pool from which we are migrating
     * @return HEGIC migrated
     */
    function migrateFromOldStakingPool(IOldStakingPool oldStakingPool) external returns (uint) {
        IERC20 sToken;
        // to avoid reseting fee during deposit
        isNotFirstTime[msg.sender] = true;
        if(address(oldStakingPool) == address(OLD_HEGIC_STAKING_POOL)) {
            sToken = IERC20(address(oldStakingPool));
            // take ownerPerformanceFee from old pool
            uint oldPerformanceFee = oldStakingPool.ownerPerformanceFee(msg.sender);
            uint dFee = discountedPerformanceFee;
            if(oldPerformanceFee > dFee) {
                ownerPerformanceFee[msg.sender] = dFee;
            } else {
                ownerPerformanceFee[msg.sender] = oldStakingPool.ownerPerformanceFee(msg.sender);
            }
        } else {
            // migrating from zLOT
            sToken = IERC20(Z_HEGIC);
            ownerPerformanceFee[msg.sender] = discountedPerformanceFee;
        }
        require(sToken.balanceOf(msg.sender) > 0, "Not enough balance / not supported");
        // requires approval
        uint256 oldBalance = sToken.balanceOf(msg.sender);
        sToken.safeTransferFrom(msg.sender, address(this), oldBalance);
        if(address(oldStakingPool) == address(OLD_HEGIC_STAKING_POOL)) {
            // migrating from HegicStakingPool
            oldStakingPool.withdraw(oldBalance);
        } else {
            // migrating from zLOT
            oldBalance = IOldPool(address(oldStakingPool)).withdraw(oldBalance);
        }
        migrating = true;
        deposit(oldBalance);
        return oldBalance;
    }

    /**
     * @notice Deposits _amount HEGIC in the contract. 
     * 
     * @param _amount Number of HEGIC to deposit in the contract // number of sHEGIC that will be minted
     */
    function deposit(uint _amount) public {
        require(_amount > 0, "Amount too low");
        require(depositsAllowed, "Deposits are not allowed at the moment");
        // set fee for that staking lot owner - this effectively sets the maximum FEE an owner can have
        // each time user deposits, this checks if current fee is higher or lower than previous fees
        // and updates it if it is lower
        if(ownerPerformanceFee[msg.sender] > performanceFee || !isNotFirstTime[msg.sender]) {
            ownerPerformanceFee[msg.sender] = performanceFee;
            isNotFirstTime[msg.sender] = true;
        }
        lastDepositTime[msg.sender] = block.timestamp;
        // receive deposit
        depositHegic(_amount);

        while(totalBalance.sub(lockedBalance) >= STAKING_LOT_PRICE){
            buyStakingLot();
        }
    }

    /**
     * @notice Withdraws _amount HEGIC from the contract. 
     * 
     * @param _amount Number of HEGIC to withdraw from contract // number of sHEGIC that will be burnt
     */
    function withdraw(uint _amount) public {
        require(_amount <= balanceOf(msg.sender), "Not enough balance");
        require(canWithdraw(msg.sender), "You deposited less than 15 mins ago. Your funds are locked");

        while(totalBalance.sub(lockedBalance) < _amount){
            sellStakingLot();
        }

        withdrawHegic(_amount);
    }

    /**
     * @notice Withdraws _amount HEGIC from the contract and claims all profit pending in contract
     * 
     */
    function claimProfitAndWithdraw() external {
        claimAllProfit();
        withdraw(balanceOf(msg.sender));
    }

    /**
     * @notice Claims profit for both assets. Profit will be paid to msg.sender
     * This is the most gas-efficient way to claim profits (instead of separately)
     * 
     */
    function claimAllProfit() public {
        claimProfit(Asset.WBTC);
        claimProfit(Asset.ETH);
        claimProfit(Asset.USDC);
    }

    /**
     * @notice Claims profit for specific _asset. Profit will be paid to msg.sender
     * 
     * @param _asset Asset (ETH or WBTC)
     */
    function claimProfit(Asset _asset) public {
        uint profit = saveProfit(msg.sender, _asset);
        savedProfit[msg.sender][_asset] = 0;
        
        _transferProfit(profit, _asset, msg.sender, ownerPerformanceFee[msg.sender]);
    }

    /**
     * @notice Returns profit to be paid when claimed
     * 
     * @param _account Account to get profit for
     * @param _asset Asset (ETH or WBTC)
     */
    function profitOf(address _account, Asset _asset) public view returns (uint profit) {
        return savedProfit[_account][_asset].add(getUnsaved(_account, _asset));
    }

    /**
     * @notice Returns address of Hegic's ETH Staking Lot contract
     */
    function getHegicStakingETH() public view returns (IHegicStaking HegicStakingETH){
        return staking[Asset.ETH];
    }

    /**
     * @notice Returns address of Hegic's WBTC Staking Lot contract
     */
    function getHegicStakingWBTC() public view returns (IHegicStaking HegicStakingWBTC){
        return staking[Asset.WBTC];
    }

    /**
     * @notice Returns address of Hegic's USDC Staking Lot contract
     */
    function getHegicStakingUSDC() public view returns (IHegicStaking HegicStakingWBTC){
        return staking[Asset.USDC];
    }

    /**
     * @notice Support function. Gets profit that has not been saved (either in Staking Lot contracts)
     * or in this contract
     * 
     * @param _account Account to get unsaved profit for
     * @param _asset Asset (ETH or WBTC)
     */
    function getUnsaved(address _account, Asset _asset) public view returns (uint profit) {
        profit = totalProfitPerToken[_asset].sub(lastProfit[_account][_asset]).add(getUnreceivedProfitPerToken(_asset)).mul(balanceOf(_account)).div(ACCURACY);
    }

    /**
     * @notice Internal function. Update profit per token for _asset
     * 
     * @param _asset Underlying asset (ETH or WBTC)
     */
    function updateProfit(Asset _asset) internal {
        uint profit = staking[_asset].profitOf(address(this));
        if(profit > 0) {
            profit = staking[_asset].claimProfits(address(this));
        }

        if(totalBalance <= 0) {
            IERC20 assetToken = token[_asset];
            assetToken.safeTransfer(FALLBACK_RECIPIENT, profit);
        } else {
            totalProfitPerToken[_asset] = totalProfitPerToken[_asset].add(profit.mul(ACCURACY).div(totalBalance));
        }
    }

    /**
     * @notice Internal function. Transfers net profit to the owner of the sHEGIC. 
     * 
     * @param _amount Amount of Asset (ETH or WBTC) to be sent
     * @param _asset Asset to be sent (ETH or WBTC)
     * @param _account Receiver of the net profit
     * @param _fee Fee % to be applied to the profit (100% = 100000)
     */
    function _transferProfit(uint _amount, Asset _asset, address _account, uint _fee) internal {
        uint netProfit = _amount.mul(uint(100000).sub(_fee)).div(100000);
        uint fee = _amount.sub(netProfit);

        IERC20 assetToken = token[_asset]; 
        assetToken.safeTransfer(_account, netProfit);
        assetToken.safeTransfer(FEE_RECIPIENT, fee);
        emit ClaimedProfit(_account, _asset, netProfit, fee);
    }

    /**
     * @notice Internal function to transfer deposited HEGIC to the contract and mint sHEGIC (Staked HEGIC)
     * @param _amount Amount of HEGIC to deposit // Amount of sHEGIC that will be minted
     */
    function depositHegic(uint _amount) internal {
        totalBalance = totalBalance.add(_amount); 
        // if we are during migration, we don't need to take HEGIC from the user
        if(!migrating) {
            HEGIC.safeTransferFrom(msg.sender, address(this), _amount);
        } else {
            migrating = false;
            require(totalBalance == HEGIC.balanceOf(address(this)).add(lockedBalance), "!");
        }

        _mint(msg.sender, _amount);
    }

    /**
     * @notice Internal function. Moves _amount HEGIC from contract to user
     * also burns staked HEGIC (sHEGIC) tokens
     * @param _amount Amount of HEGIC to withdraw // Amount of sHEGIC that will be burned
     */
    function withdrawHegic(uint _amount) internal {
        _burn(msg.sender, _amount);
        HEGIC.safeTransfer(msg.sender, _amount);
        totalBalance = totalBalance.sub(_amount);
        emit Withdraw(msg.sender, _amount);
    }

    /**
     * @notice Internal function. Chooses which lot to buy (ETH or WBTC) and buys it
     *
     */
    function buyStakingLot() internal {
        // we buy 1 ETH staking lot, then 1 WBTC staking lot, then 1 USDC staking lot, ...
        Asset asset = Asset.USDC;
        if(numberOfStakingLots[Asset.USDC] >= numberOfStakingLots[Asset.WBTC]){
            if(numberOfStakingLots[Asset.WBTC] >= numberOfStakingLots[Asset.ETH]){
                asset = Asset.ETH;
            } else {
                asset = Asset.WBTC;
            }
        }

        lockedBalance = lockedBalance.add(STAKING_LOT_PRICE);
        staking[asset].buyStakingLot(1);
        totalNumberOfStakingLots++;
        numberOfStakingLots[asset]++;
        emit BuyLot(asset, msg.sender);
    }

    /**
     * @notice Internal function. Chooses which lot to sell (ETH or WBTC or USDC) and sells it
     *
     */
    function sellStakingLot() internal {
        Asset asset = Asset.ETH;
        if(numberOfStakingLots[Asset.ETH] <= numberOfStakingLots[Asset.WBTC] || 
            staking[Asset.ETH].lastBoughtTimestamp(address(this))
                    .add(staking[Asset.ETH].classicLockupPeriod()) > block.timestamp || 
            staking[Asset.ETH].balanceOf(address(this)) == 0)
        {
            if(numberOfStakingLots[Asset.WBTC] <= numberOfStakingLots[Asset.USDC] && 
                staking[Asset.USDC].lastBoughtTimestamp(address(this))
                    .add(staking[Asset.USDC].classicLockupPeriod()) <= block.timestamp && 
                staking[Asset.USDC].balanceOf(address(this)) > 0){
                asset = Asset.USDC;
            } else if (staking[Asset.WBTC].lastBoughtTimestamp(address(this))
                    .add(staking[Asset.WBTC].classicLockupPeriod()) <= block.timestamp && 
                staking[Asset.WBTC].balanceOf(address(this)) > 0){
                asset = Asset.WBTC;
            } else {
                asset = Asset.ETH;
                // this require only applies here. otherwise conditions have been already checked
                require(
                    staking[asset].lastBoughtTimestamp(address(this))
                        .add(staking[asset].classicLockupPeriod()) <= block.timestamp &&
                        staking[asset].balanceOf(address(this)) > 0,
                    "Lot sale is locked by Hegic. Funds should be available in less than 24h"
                );
            }
        }

        lockedBalance = lockedBalance.sub(STAKING_LOT_PRICE);
        staking[asset].sellStakingLot(1);
        totalNumberOfStakingLots--;
        numberOfStakingLots[asset]--;
        emit SellLot(asset, msg.sender);
    }

    /**
     * @notice Support function. Calculates how much profit would receive each token if the contract claimed
     * profit accumulated in Hegic's Staking Lot contracts
     * 
     * @param _asset Asset (WBTC or ETH)
     */
    function getUnreceivedProfitPerToken(Asset _asset) public view returns (uint unreceivedProfitPerToken){
        uint profit = staking[_asset].profitOf(address(this));
        
        unreceivedProfitPerToken = profit.mul(ACCURACY).div(totalBalance);
    }

    /**
     * @notice Saves profit for a certain _account. This profit is absolute in value
     * this function is called before every token transfer to keep the state of profits correctly
     * 
     * @param _account account to save profit to
     */
    function saveProfit(address _account) internal {
        saveProfit(_account, Asset.WBTC);
        saveProfit(_account, Asset.ETH);
        saveProfit(_account, Asset.USDC);
    }

    /**
     * @notice Internal function that saves unpaid profit to keep accounting.
     * 
     * @param _account Account to save profit to
     * @param _asset Asset (WBTC or ETH)     
     */
    function saveProfit(address _account, Asset _asset) internal returns (uint profit) {
        updateProfit(_asset);
        uint unsaved = getUnsaved(_account, _asset);
        lastProfit[_account][_asset] = totalProfitPerToken[_asset];
        profit = savedProfit[_account][_asset].add(unsaved);
        savedProfit[_account][_asset] = profit;
    }

    /**
     * @notice Support function. Relevant to the profit system. It will save state of profit before each 
     * token transfer (either deposit or withdrawal)
     * 
     * @param from Account sending tokens 
     * @param to Account receiving tokens
     */
    function _beforeTokenTransfer(address from, address to, uint256) internal override {
        require(canWithdraw(from), "!locked funds");
        if (from != address(0)) saveProfit(from);
        if (to != address(0)) saveProfit(to);
    }

    /**
     * @notice Returns a boolean indicating if that specific _account can withdraw or not
     * (due to lockupperiod reasons)
     * @param _account Account to check withdrawal status 
     */
    function canWithdraw(address _account) public view returns (bool) {
        return (lastDepositTime[_account].add(lockUpPeriod) <= block.timestamp);
    }
}
function getHegicStakingUSDC() public view returns (IHegicStaking HegicStakingWBTC){
    return staking[Asset.USDC];
}

/**
 * @notice Returns address of Hegic's USDC Staking Lot contract
 */