// Contract: BnsdLaunchPool
// Target function: function withdrawLPTokens(uint256 _pid, uint256 _amount, IERC20 _lpAdd) external
e4 before using it anywhere => 80.00%

    event Deposit(address indexed user, uint256 indexed pid, uint256 amount);
    event Stake(address indexed user, address indexed lptoken, uint256 indexed pid, uint256 amount);
    event UnStake(address indexed user, address indexed lptoken, uint256 indexed pid, uint256 amount);
    event MoveStake(address indexed user, address indexed lptoken, uint256 pid, uint256 indexed pidnew, uint256 amount);
    event Withdraw(address indexed user, uint256 indexed pid, uint256 amount);
    event WithdrawAirdrop(address indexed user, address indexed token, uint256 amount);
    event ClaimAirdrop(address indexed user, address indexed token, uint256 amount);
    event AirdropDeposit(address indexed user, address indexed token, uint256 indexed pid, uint256 amount);
    event AirdropExtraWithdraw(address indexed user, address indexed token, uint256 indexed pid, uint256 amount);
    event Voted(address indexed user, uint256 indexed pid);

    constructor() public {
        devaddr = _msgSender();
    }

    modifier onlyAdmin() {
        require(devaddr == _msgSender(), "ND");
        _;
    }

    modifier onlyAdminOrTimeLock() {
        require((devaddr == _msgSender() || timeLock == _msgSender()), "ND");
        _;
    }

    function setTimeLockAdd(address _add) public onlyAdmin {
        timeLock = _add;
    }

    function poolLength() external view returns (uint256) {
        return poolInfo.length;
    }

    function getListOfSale(address _token) external view returns (uint256[] memory) {
        return listSaleTokens[_token];
    }
    
    function getUserDistPercent(uint256 _pid) external view returns (uint256[] memory) {
        return userDistInfo[_pid].percentArray;
    }
    
    function getUserDistDays(uint256 _pid) external view returns (uint256[] memory) {
        return userDistInfo[_pid].daysArray;
    }

    function getReserveUnlockPercent(uint256 _pid, address _reserveAdd) external view returns (uint256[] memory) {
        return useCaseInfo[_pid][_reserveAdd].unlock_perArray;
    }
    
    function getReserveUnlockDays(uint256 _pid, address _reserveAdd) external view returns (uint256[] memory) {
        return useCaseInfo[_pid][_reserveAdd].unlock_daysArray;
    }

    function getUserDistBlocks(uint256 _pid) external view returns (uint256[] memory) {
        uint256[] memory daysArray =  userDistInfo[_pid].daysArray;

        uint256 endPool = poolInfo[_pid].endBlock;
        for(uint256 i=0; i<daysArray.length; i++){
            daysArray[i] = (daysArray[i].mul(BLOCKS_PER_DAY)).add(endPool);
        }
        return daysArray;
    }

    function getOwnerDistPercent(uint256 _pid) external view returns (uint256[] memory) {
        return ownerDistInfo[_pid].percentArray;
    }
    
    function getOwnerDistDays(uint256 _pid) external view returns (uint256[] memory) {
        return ownerDistInfo[_pid].daysArray;
    }

    // Add a new token sale to the pool. Can only be called by the person having the invite code.
    
    function addNewPool(uint256 totalTokens, uint256 maxPerPerson, uint256 startBlock, uint256 endBlock, string memory namePool, IERC20 tokenAddress, uint256 _inviteCode) external returns (uint256) {
        require(endBlock > startBlock, "ESC"); // END START COMPARISON FAILED
        require(startBlock > block.number, "TLS"); // TIME LIMIT START SALE
        require(maxPerPerson !=0 && totalTokens!=0, "IIP"); // INVALID INDIVIDUAL PER PERSON
        require(inviteCodeList[_inviteCode][address(tokenAddress)][_msgSender()]==true,"IIC"); // INVALID INVITE CODE
        poolInfo.push(RaisePoolInfo({
            raiseToken: tokenAddress,
            maxTokensPerPerson: maxPerPerson,
            totalTokensOnSale: totalTokens,
            startBlock: startBlock,
            endBlock: endBlock,
            poolName: namePool,
            updateLocked: false,
            owner: _msgSender(),
            totalTokensSold: 0,
            balanceAdded: false,
            tokensDeposited: 0,
            paymentMethodAdded: false,
            votes: 0
        }));
        uint256 poolId = (poolInfo.length - 1);
        listSaleTokens[address(tokenAddress)].push(poolId);
        // This makes the invite code claimed
        inviteCodeList[_inviteCode][address(tokenAddress)][_msgSender()] = false;
        return poolId;
    }

    function _checkSumArray(uint256[] memory _percentArray) internal pure returns (bool) {
        uint256 _sum;
        for (uint256 i = 0; i < _percentArray.length; i++) {
            _sum = _sum.add(_percentArray[i]);
        }
        return (_sum==10000);
    }

    function _checkValidDaysArray(uint256[] memory _daysArray) internal pure returns (bool) {
        uint256 _lastDay = _daysArray[0];
        for (uint256 i = 1; i < _daysArray.length; i++) {
            if(_lastDay < _daysArray[i]){
                _lastDay = _daysArray[i];
            }
            else {
                return false;
            }
        }
        return true;
    }

    function _checkUpdateAllowed(uint256 _pid) internal view{
        RaisePoolInfo storage pool = poolInfo[_pid];
        require(pool.updateLocked == false, "CT2"); // CRITICAL TERMINATION 2
        require(pool.owner==_msgSender(), "OAU"); // OWNER AUTHORIZATION FAILED
        require(pool.startBlock > block.number, "CT"); // CRITICAL TERMINATION
    }

    // Add rule for funds locking after sale
    function updateUserDistributionRule(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
        userDistInfo[_pid] = DistributionInfo({
            percentArray: _percentArray,
            daysArray: _daysArray
        });
    }


    // Add rule for funds unlocking of the fund raiser after 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);
    }