// Contract: BnsdLaunchPool
// Target function: function depositAirdropTokens(uint256 _pid, uint256 _amount, IERC20 _tokenAdd) external
 =>  uint256) public totalTokenClaimed;

    // To store total token raised by a project - POOL => TOKEN => AMT
    mapping (uint256 => mapping (address => uint256)) public fundsRaisedSoFar;

    mapping (uint256 => address) private tempAdmin;

    // To store total token claimed by a project
    mapping (uint256 => mapping (address => uint256)) public fundsClaimedSoFar;

    // To store addresses voted for a project - POOL => USER => BOOL
    mapping (uint256 => mapping (address => bool)) public userVotes;

    // No of blocks in a day  - 6700
    uint256 public constant BLOCKS_PER_DAY = 6700; // Changing to 5 for test cases

    // Info of each pool on blockchain.
    RaisePoolInfo[] public poolInfo;

    // Info of reserve pool of any project - POOL => RESERVE_ADD => USECASEINFO 
    mapping (uint256 => mapping (address => UseCasePoolInfo)) public useCaseInfo;

    // To store total token reserved 
    mapping (uint256 =>  uint256) public totalTokenReserved;

    // To store total reserved claimed 
    mapping (uint256 =>  uint256) public totalReservedTokenClaimed;

    // To store list of all sales associated with a token 
    mapping (address =>  uint256[]) public listSaleTokens;

    // To store list of all currencies allowed for a sale 
    mapping (uint256 =>  address[]) public listSupportedCurrencies;

    // To store list of all reserve addresses for a sale 
    mapping (uint256 =>  address[]) public listReserveAddresses;

    // To check if staking is enabled on a token 
    mapping (address =>  bool) public stakingEnabled;

    // To get staking weight of a token 
    mapping (address =>  uint256) public stakingWeight;

    // To store sum of weight of all staking tokens
    uint256 public totalStakeWeight;

    // To store list of staking addresses 
    address[] public stakingPools;

    // To store stats of staked tokens per sale  
    mapping (uint256 => mapping (address => uint256)) public stakedLPTokensInfo;

    // To store user staked amount for a sale -  POOL => USER => LP_TOKEN
    mapping (uint256 => mapping (address => mapping (address => uint256))) public userStakeInfo;

    // To store reward claimed by a user - POOL => USER => BOOL
    mapping (uint256 => mapping (address => bool)) public rewardClaimed;

    // To store airdrop claimed by a user - POOL => USER => BOOL
    mapping (uint256 => mapping (address => bool)) public airdropClaimed;

    // To store extra airdrop tokens withdrawn by fund raiser - POOL => BOOL
    mapping (uint256 =>  bool) public extraAirdropClaimed;

    // To store airdrop info for a sale  
    mapping (uint256 =>  AirdropPoolInfo) public airdropInfo;

    // To store airdrop tokens balance of a user , TOKEN => USER => BAL
    mapping (address => mapping (address => uint256)) public airdropBalances;

    uint256 public fee = 300; // To be divided by 1e4 before using it anywhere => 3.00%

    uint256 public constant rewardPer = 8000; // To be divided by 1e4 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);
    }