// Contract: BnsdLaunchPool
// Target function: function withdrawExtraICOTokens(uint256 _pid, uint256 _amount, IERC20 _tokenAdd) external
 sale
    function updateOwnerDistributionRule(uint256 _pid, uint256[] memory _percentArray, uint256[] memory _daysArray) external {
        require(_percentArray.length == _daysArray.length, "LM"); // LENGTH MISMATCH
        _checkUpdateAllowed(_pid);
        require(_checkSumArray(_percentArray), "SE"); // SUM OF PERCENT INVALID
        require(_checkValidDaysArray(_daysArray), "DMI"); // DAYS SHOULD BE MONOTONIICALLY INCREASING
        ownerDistInfo[_pid] = DistributionInfo({
            percentArray: _percentArray,
            daysArray: _daysArray
        });
    }


    // Lock sale detail changes in future
    function lockPool(uint256 _pid) external {
        require(poolInfo[_pid].paymentMethodAdded==true, "CP"); // CHECK PAYMENT METHOD FAILED
        _checkUpdateAllowed(_pid);
        poolInfo[_pid].updateLocked = true;
    }

    
    // Add supported currencies and their rate w.r.t token on sale
    // rateToken = price of one satoshi of the token in terms of token to be raised * 1e18
    // 1 BNSD =  0.00021 ETH => 1e18 BNSD Satoshi = 0.00021 * 1e18 ETH satoshi => 1 BNSD Satoshi = 0.00021 ETH satoshi => rateToken = 0.00021 * 1e18 = 21 * 1e13
    // rateToken for BNSD/ETH pair = 21 * 1e13;
    function addSupportedCurrencies(uint256 _pid, address _tokenRaise, uint256 rateToken) external {
        _checkUpdateAllowed(_pid);
        require(rateToken!=0, "IR"); // INVALID RATE
        require(_tokenRaise!=address(poolInfo[_pid].raiseToken), "IT"); // INVALIID PURCHASE TOKEN
        if(saleRateInfo[_pid][_tokenRaise] == 0){
            listSupportedCurrencies[_pid].push(_tokenRaise);
        }
        saleRateInfo[_pid][_tokenRaise] = rateToken;
        poolInfo[_pid].paymentMethodAdded = true; 
    }

    function getSupportedCurrencies(uint256 _pid) external view returns (address[] memory) {
        return listSupportedCurrencies[_pid];
    }

    function _checkUpdateReserveAllowed(uint256 _pid, address _resAdd) internal view returns (bool) {
        UseCasePoolInfo storage poolU = useCaseInfo[_pid][_resAdd];
        return (poolU.exists == false || poolU.tokensDeposited == false);
        // if(poolU.exists == false || poolU.tokensDeposited == false){
        //     return true;
        // }
        // return false;
    }

    function addReservePool(uint256 _pid, address _reserveAdd, string memory _nameReserve, uint256 _totalTokens, uint256[] memory _perArray, uint256[] memory _daysArray) external {
        _checkUpdateAllowed(_pid);
        require(_checkUpdateReserveAllowed(_pid, _reserveAdd) == true, "UB"); // UPDATE RESERVE FAILED
        require(_checkSumArray(_perArray), "SE"); // SUM OF PERCENT INVALID
        require(_checkValidDaysArray(_daysArray), "DMI"); // DAYS SHOULD BE MONOTONIICALLY INCREASING
        require(_perArray.length==_daysArray.length, "IAL"); // INVALID ARRAY LENGTH
        if(useCaseInfo[_pid][_reserveAdd].exists == false){
            listReserveAddresses[_pid].push(_reserveAdd);
        }
        useCaseInfo[_pid][_reserveAdd] = UseCasePoolInfo({
            reserveAdd: _reserveAdd,
            useName: _nameReserve,
            tokensAllocated: _totalTokens,
            unlock_perArray: _perArray,
            unlock_daysArray: _daysArray,
            tokensDeposited: false,
            tokensClaimed: 0,
            exists: true
        });
    }

    function getReserveAddresses(uint256 _pid) external view returns (address[] memory) {
        return listReserveAddresses[_pid];
    }

    function tokensPurchaseAmt(uint256 _pid, address _tokenAdd, uint256 amt) public view returns (uint256) {
        uint256 rateToken = saleRateInfo[_pid][_tokenAdd];
        require(rateToken!=0, "NAT"); // NOT AVAILABLE TOKEN
        return (amt.mul(1e18)).div(rateToken);
    }

    // Check if user can deposit specfic amount of funds to the pool 
    function _checkDepositAllowed(uint256 _pid, address _tokenAdd, uint256 _amt) internal view returns (uint256){
        RaisePoolInfo storage pool = poolInfo[_pid];
        uint256 userBought = userTokenAllocation[_pid][_msgSender()];
        uint256 purchasePossible = tokensPurchaseAmt(_pid, _tokenAdd, _amt);
        require(pool.balanceAdded == true, "NA"); // NOT AVAILABLE
        require(pool.startBlock <= block.number, "NT1"); // NOT AVAILABLE TIME 1
        require(pool.endBlock >= block.number, "NT2"); // NOT AVAILABLE TIME 2
        require(pool.totalTokensSold.add(purchasePossible) <= pool.totalTokensOnSale, "PLE"); // POOL LIMIT EXCEEDED
        require(userBought.add(purchasePossible) <= pool.maxTokensPerPerson, "ILE"); // INDIVIDUAL LIMIT EXCEEDED
        return purchasePossible;
    }


    // Check max a user can deposit right now
    function getMaxDepositAllowed(uint256 _pid, address _tokenAdd, address _user) external view returns (uint256){
        RaisePoolInfo storage pool = poolInfo[_pid];
        uint256 maxBuyPossible = (pool.maxTokensPerPerson).sub(userTokenAllocation[_pid][_user]);
        uint256 maxBuyPossiblePoolLimit = (pool.totalTokensOnSale).sub(pool.totalTokensSold);

        if(maxBuyPossiblePoolLimit < maxBuyPossible){
            maxBuyPossible = maxBuyPossiblePoolLimit;
        }

        if(block.number >= pool.startBlock && block.number <= pool.endBlock && pool.balanceAdded == true){
            uint256 rateToken = saleRateInfo[_pid][_tokenAdd];
            return (maxBuyPossible.mul(rateToken).div(1e18));
        }
        else {
            return 0;
        }
    }


    // Check if deposit is enabled for a pool
    function checkDepositEnabled(uint256 _pid) external view returns (bool){
        RaisePoolInfo storage pool = poolInfo[_pid];

        if(pool.balanceAdded == true && pool.startBlock <= block.number && pool.endBlock >= block.number && pool.totalTokensSold <= pool.totalTokensOnSale && pool.paymentMethodAdded==true){
            return true;
        }
        else {
            return false;
        }
    }


    // Deposit ICO tokens to start a pool for ICO.
    function depositICOTokens(uint256 _pid, uint256 _amount, IERC20 _tokenAdd) external {
        RaisePoolInfo storage pool = poolInfo[_pid];
        address msgSender = _msgSender();
        require(_tokenAdd == pool.raiseToken, "NOT"); // NOT VALID TOKEN
        require(msgSender == pool.owner, "NAU"); // NOT AUTHORISED USER
        require(block.number < pool.endBlock, "NT"); // No point adding tokens after sale has ended - Possible deadlock case
        _tokenAdd.safeTransferFrom(msgSender, address(this), _amount);
        pool.tokensDeposited = (pool.tokensDeposited).add(_amount);
        if(pool.tokensDeposited >= pool.totalTokensOnSale){
            pool.balanceAdded = true;
        }
        emit Deposit(msgSender, _pid, _amount);
    }

    // Deposit Airdrop tokens anytime before end of the sale.
    function depositAirdropTokens(uint256 _pid, uint256 _amount, IERC20 _tokenAdd) external {
        RaisePoolInfo storage pool = poolInfo[_pid];
        require(block.number < pool.endBlock, "NT"); // NOT VALID TIME
        AirdropPoolInfo storage airdrop = airdropInfo[_pid];
        require((_tokenAdd == airdrop.airdropToken || airdrop.airdropExists==false), "NOT"); // NOT VALID TOKEN
        require(_msgSender() == pool.owner || _msgSender() == devaddr , "NAU"); // NOT AUTHORISED USER
        _tokenAdd.safeTransferFrom(_msgSender(), address(this), _amount);
        airdrop.totalTokensAvailable = (airdrop.totalTokensAvailable).add(_amount);
        if(!airdrop.airdropExists){
            airdrop.airdropToken = _tokenAdd;
            airdrop.airdropExists = true;
        }
        emit AirdropDeposit(_msgSender(), address(_tokenAdd), _pid, _amount);
    }

    // Withdraw extra airdrop tokens - Possible only if no one added liquidity to one of the pools
    function withdrawExtraAirdropTokens(uint256 _pid) external {
        require(extraAirdropClaimed[_pid]==false, "NA"); // NOT AVAILABLE
        RaisePoolInfo storage pool = poolInfo[_pid];
        require(block.number > pool.endBlock, "NSE"); // SALE NOT ENDED
        address msgSender = _msgSender();
        require(msgSender == pool.owner, "NAU"); //  NOT AUTHORISED USER
        uint256 extraTokens = calculateExtraAirdropTokens(_pid);
        require(extraTokens!=0, "NAT"); // NOT AVAILABLE TOKEN
        extraAirdropClaimed[_pid] = true;
        airdropInfo[_pid].airdropToken.safeTransfer(msgSender, extraTokens);
        emit AirdropExtraWithdraw(msg.sender, address(airdropInfo[_pid].airdropToken), _pid, extraTokens);
    }

    function calculateExtraAirdropTokens(uint256 _pid) public view returns (uint256){
        if(extraAirdropClaimed[_pid] == true) return 0;
        uint256 _totalTokens;
        for (uint256 i=0; i<stakingPools.length; i++){
            uint256 stake = stakedLPTokensInfo[_pid][stakingPools[i]];
            if(stake == 0){
                _totalTokens = _totalTokens.add(((stakingWeight[stakingPools[i]]).mul(airdropInfo[_pid].totalTokensAvailable)).div(totalStakeWeight));
            }
        }
        return _totalTokens;
    }

    // Deposit LP tokens for a sale.
    function stakeLPTokens(uint256 _pid, uint256 _amount, IERC20 _lpAdd) external {
        require(stakingEnabled[address(_lpAdd)]==true, "NST"); // NOT STAKING TOKEN
        RaisePoolInfo storage pool = poolInfo[_pid];
        require(block.number < pool.startBlock, "NT"); // NOT VALID TIME
        address msgSender = _msgSender();
        _lpAdd.safeTransferFrom(msgSender, address(this), _amount);
        stakedLPTokensInfo[_pid][address(_lpAdd)] = (stakedLPTokensInfo[_pid][address(_lpAdd)]).add(_amount);
        userStakeInfo[_pid][msgSender][address(_lpAdd)] = (userStakeInfo[_pid][msgSender][address(_lpAdd)]).add(_amount);
        emit Stake(msg.sender, address(_lpAdd), _pid, _amount);
    }

    // Withdraw LP tokens from a sale after it's over => Automatically claims rewards and airdrops also
    function withdrawLPTokens(uint256 _pid, uint256 _amount, IERC20 _lpAdd) external {
        require(stakingEnabled[address(_lpAdd)]==true, "NAT"); // NOT AUTHORISED TOKEN
        RaisePoolInfo storage pool = poolInfo[_pid];
        require(block.number > pool.endBlock, "SE"); // SALE NOT ENDED
        address msgSender = _msgSender();
        claimRewardAndAirdrop(_pid);
        userStakeInfo[_pid][msgSender][address(_lpAdd)] = (userStakeInfo[_pid][msgSender][address(_lpAdd)]).sub(_amount);
        _lpAdd.safeTransfer(msgSender, _amount);
        emit UnStake(msg.sender, address(_lpAdd), _pid, _amount);
    }

    // Withdraw airdrop tokens accumulated over one or more than one sale.
    function withdrawAirdropTokens(IERC20 _token, uint256 _amount) external {
        address msgSender = _msgSender();
        airdropBalances[address(_token)][msgSender] = (airdropBalances[address(_token)][msgSender]).sub(_amount);
        _token.safeTransfer(msgSender, _amount);
        emit WithdrawAirdrop(msgSender, address(_token), _amount);
    }

    // Move LP tokens from one sale to another directly => Automatically claims rewards and airdrops also
    function moveLPTokens(uint256 _pid, uint256 _newpid, uint256 _amount, address _lpAdd) external {
        require(stakingEnabled[_lpAdd]==true, "NAT1"); // NOT AUTHORISED TOKEN 1
        RaisePoolInfo storage poolOld = poolInfo[_pid];
        RaisePoolInfo storage poolNew = poolInfo[_newpid];
        require(block.number > poolOld.endBlock, "NUA"); // OLD SALE NOT ENDED
        require(block.number < poolNew.startBlock, "NSA"); // SALE START CHECK FAILED
        address msgSender = _msgSender();
        claimRewardAndAirdrop(_pid);
        userStakeInfo[_pid][msgSender][_lpAdd] = (userStakeInfo[_pid][msgSender][_lpAdd]).sub(_amount);
        userStakeInfo[_newpid][msgSender][_lpAdd] = (userStakeInfo[_newpid][msgSender][_lpAdd]).add(_amount);
        emit MoveStake(msg.sender, _lpAdd, _pid, _newpid, _amount);
    }

    function claimRewardAndAirdrop(uint256 _pid) public {
        RaisePoolInfo storage pool = poolInfo[_pid];
        require(block.number > pool.endBlock, "SE"); // SUM INVALID
        _claimReward(_pid, _msgSender());
        _claimAirdrop(_pid, _msgSender());
    }

    function _claimReward(uint256 _pid, address _user) internal {
        if (rewardClaimed[_pid][_user]==false){
            rewardClaimed[_pid][_user] = true;
            for (uint256 i=0; i<stakingPools.length; i++){
                for(uint256 j=0; j<listSupportedCurrencies[_pid].length; j++){
                    uint256 _tokenAmt = getReward(_pid, _user, stakingPools[i], listSupportedCurrencies[_pid][j]);
                    _creditAirdrop(_user, listSupportedCurrencies[_pid][j], _tokenAmt);
                }
            }
        }
    }

    function _claimAirdrop(uint256 _pid, address _user) internal {
        if (airdropClaimed[_pid][_user]==false){
            airdropClaimed[_pid][_user] = true;
            address _airdropToken = address(airdropInfo[_pid].airdropToken);
            uint256 _tokenAmt = 0;
            for (uint256 i=0; i<stakingPools.length; i++){
                _tokenAmt = _tokenAmt.add(getAirdrop(_pid, _user, stakingPools[i]));
            }
            if(_tokenAmt !=0){
                _creditAirdrop(_user, _airdropToken, _tokenAmt);
            }
        }
    }

    function _creditAirdrop(address _user, address _token, uint256 _amt) internal {
        airdropBalances[_token][_user] = (airdropBalances[_token][_user]).add(_amt);
        emit ClaimAirdrop(_user, _token, _amt);
    }

    function getReward(uint256 _pid, address _user, address _lpAdd, address _token) public view returns (uint256) {
          uint256 stake = stakedLPTokensInfo[_pid][_lpAdd];
          if(stake==0) return 0;
          uint256 _multipliedData = (userStakeInfo[_pid][_user][_lpAdd]).mul(fundsRaisedSoFar[_pid][_token]);
          _multipliedData = (_multipliedData).mul(rewardPer).mul(fee).mul(stakingWeight[_lpAdd]);
          return (((_multipliedData).div(stake)).div(1e8)).div(totalStakeWeight);
    }

    function getAirdrop(uint256 _pid, address _user, address _lpAdd) public view returns (uint256) {
          uint256 _userStaked = userStakeInfo[_pid][_user][_lpAdd];
          uint256 _totalStaked = stakedLPTokensInfo[_pid][_lpAdd];
          if(_totalStaked==0) return 0;
          return ((((_userStaked).mul(airdropInfo[_pid].totalTokensAvailable).mul(stakingWeight[_lpAdd])).div(_totalStaked))).div(totalStakeWeight);
    }

    // Deposit ICO tokens for a use case as reserve.
    function depositReserveICOTokens(uint256 _pid, uint256 _amount, IERC20 _tokenAdd, address _resAdd) external {
        RaisePoolInfo storage pool = poolInfo[_pid];
        UseCasePoolInfo storage poolU = useCaseInfo[_pid][_resAdd];
        address msgSender = _msgSender();
        require(_tokenAdd == pool.raiseToken, "NOT"); // NOT AUTHORISED TOKEN
        require(msgSender == pool.owner, "NAU"); // NOT AUTHORISED USER
        require(poolU.tokensDeposited == false, "DR"); // TOKENS NOT DEPOSITED
        require(poolU.tokensAllocated == _amount && _amount!=0, "NA"); // NOT AVAILABLE
        require(block.number < pool.endBlock, "CRN"); // CANNOT_RESERVE_NOW to avoid deadlocks
        _tokenAdd.safeTransferFrom(msgSender, address(this), _amount);
        totalTokenReserved[_pid] = (totalTokenReserved[_pid]).add(_amount);
        poolU.tokensDeposited = true;
        emit Deposit(msg.sender, _pid, _amount);
    }