contract RewardDistributor is IRewardDistributor, ReentrancyGuard {
    using SafeMath for uint256;
    using Address for address;

    // Referral Rewards
    struct ReferralShare {
        uint256 amount;
        uint256 numberOfTimesClaimed;
    }

    mapping (address => ReferralShare) public referralShares;

    uint256 public totalReferralShares;
    
    // Token Address
    address public rewardToken;

    // Pancakeswap Router
    address public router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    
    // Owner of contract
    address public tokenOwner;
    
    modifier onlyToken() {
        require(msg.sender == rewardToken, 'Invalid Token!'); _;
    }
    
    modifier onlyOwner() {
        require(msg.sender == tokenOwner, 'Invalid Token Owner'); _;
    }

    // EVENTS
    event SetRewardTokenAddress(address indexed rewardToken);
    event TransferedTokenOwnership(address indexed newOwner);
    event UpgradeDistributor(address indexed newDistributor);
    event AddRewardHolderShare(address indexed rewardRecipient, uint256 indexed amount);
    event RemoveRewardholder(address indexed rewardRecipient);
    event GiftReward(address indexed rewardRecipient, address indexed giftRecipient, uint256 indexed amount);
    event ClaimReward(address indexed rewardRecipient);
    event ClaimRewardInDesiredToken(address indexed rewardRecipient, address indexed desiredToken);
    event ClaimRewardToDesiredWallet(address indexed rewardRecipient, address indexed desiredWallet);
    event ClaimRewardInDesiredTokenToDesiredWallet(address indexed rewardRecipient, address indexed desiredWallet, address indexed desiredToken);
    event UpdateRouterAddress(address indexed router);

    constructor () {
        tokenOwner = msg.sender;
    }

    receive() external payable {}

    function setRewardTokenAddress(address _rewardToken) external onlyOwner {
        require(rewardToken != _rewardToken && _rewardToken != address(0), 'Invalid Reward Token!');

        if (rewardToken != address(0)) {
            uint256 balance = IBEP20(rewardToken).balanceOf(address(this));
            if (balance > 0) {
                require(IBEP20(rewardToken).transfer(tokenOwner, balance), 'Transfer Failed!');
            }
        }
        
        rewardToken = _rewardToken;
        emit SetRewardTokenAddress(_rewardToken);
    }
    
    function transferTokenOwnership(address newOwner) external onlyOwner {
        require(newOwner != address(0), 'Invalid New Owner');
        tokenOwner = newOwner;
        emit TransferedTokenOwnership(newOwner);
    }
    
    function upgradeDistributor(address newDistributor) external onlyOwner {
        require(newDistributor != address(this) && newDistributor != address(0), 'Invalid Distributor!');
        uint256 balance = IBEP20(rewardToken).balanceOf(address(this));
        if (balance > 0) require(IBEP20(rewardToken).transfer(newDistributor, balance), 'Transfer Failed!');
        emit UpgradeDistributor(newDistributor);
        selfdestruct(payable(tokenOwner));
    }

    function addRewardHolderShare(address rewardRecipient, uint256 amount) external override onlyToken {
        referralShares[rewardRecipient].amount = referralShares[rewardRecipient].amount.add(amount);
        totalReferralShares = totalReferralShares.add(amount);
        emit AddRewardHolderShare(rewardRecipient, amount);
    }

    function updateRouterAddress(address _router) external onlyOwner {
        require(_router != address(0), 'Router Address Invalid!');
        require(router != _router, 'Router Address already exists!');
        router = _router;
        emit UpdateRouterAddress(_router);
    }

    // Back-Up withdraw, in case BNB gets sent in here
    // NOTE: This function is to be called if and only if BNB gets sent into this contract. 
    // On no other occurence should this function be called. 
    function emergencyWithdrawEthInWei(address payable recipient, uint256 amount) external onlyOwner {
        require(recipient != address(0), 'Invalid Recipient!');
        require(amount > 0, 'Invalid Amount!');
        recipient.transfer(amount);
    }

    // Withdraw BEP20 tokens sent to this contract
    // NOTE: This function is to be called if and only if BEP20 tokens gets sent into this contract. 
    // On no other occurence should this function be called. 
    function emergencyWithdrawTokens(address token) external onlyOwner {
        require(token != address(0), 'Invalid Token!');

        uint256 balance = IBEP20(token).balanceOf(address(this));
        if (balance > 0) {
            require(IBEP20(token).transfer(tokenOwner, balance), 'Transfer Failed!');
        }
    }

    function giftReward(address giftRecipient, uint256 amount) external nonReentrant {
        _giftReward(msg.sender, giftRecipient, amount);
        emit GiftReward(msg.sender, giftRecipient, amount);
    }

    function claimReward() external nonReentrant {
        _claimRewardInBNB(msg.sender);
        emit ClaimReward(msg.sender);
    }

    function claimRewardInDesiredToken(address desiredToken) external nonReentrant {
        _claimRewardInDesiredToken(msg.sender, desiredToken);
        emit ClaimRewardInDesiredToken(msg.sender, desiredToken);
    }

    function claimRewardToDesiredWallet(address desiredWallet) external nonReentrant {
        _claimRewardInBNBToDesiredWallet(msg.sender, desiredWallet);
        emit ClaimRewardToDesiredWallet(msg.sender, desiredWallet);
    }

    function claimRewardInDesiredTokenToDesiredWallet(address desiredToken, address desiredWallet) external nonReentrant {
        require(desiredWallet != address(0), "Invalid Destination Wallet!");
        _claimRewardInDesiredTokenToDesiredWallet(msg.sender, desiredWallet, desiredToken);
        emit ClaimRewardInDesiredTokenToDesiredWallet(msg.sender, desiredWallet, desiredToken);
    }

    function _giftReward(address rewardRecipient, address giftRecipient, uint256 amount) private {
        require(rewardRecipient != address(0), 'Invalid Reward Recipient!');
        require(giftRecipient != address(0), 'Invalid Gift Recipient!');
        require(referralShares[rewardRecipient].amount > 0, 'Insufficient Balance!');
        require(amount > 0, 'Invalid Amount!');
        require(amount <= referralShares[rewardRecipient].amount, 'Insufficient Balance!');

        if (referralShares[rewardRecipient].amount <= IBEP20(rewardToken).balanceOf(address(this))) {
            require(IBEP20(rewardToken).transfer(giftRecipient, amount), 'Transfer Failed!');
            
            referralShares[rewardRecipient].amount = referralShares[rewardRecipient].amount.sub(amount);
        }
    }
    
    function _claimRewardInBNB(address rewardRecipient) private {
        require(rewardRecipient != address(0), 'Invalid Reward Recipient!');
        require(referralShares[rewardRecipient].amount > 0, 'Insufficient Balance!');

        if (referralShares[rewardRecipient].amount <= IBEP20(rewardToken).balanceOf(address(this))) {
            // Swap token and send to the reward recipient
            _swapAndSendBNB(rewardRecipient, referralShares[rewardRecipient].amount);

            // Set amount to 0, set number of times claimed
            referralShares[rewardRecipient].amount = 0;
            referralShares[rewardRecipient].numberOfTimesClaimed = referralShares[rewardRecipient].numberOfTimesClaimed.add(1);
        }
    }

    function _claimRewardInDesiredToken(address rewardRecipient, address desiredToken) private {
        require(rewardRecipient != address(0), 'Invalid Reward Recipient!');
        require(desiredToken != address(0), 'Invalid Desired Token!');
        require(referralShares[rewardRecipient].amount > 0, 'Insufficient Balance!');

        if (referralShares[rewardRecipient].amount <= IBEP20(rewardToken).balanceOf(address(this))) {
            // Swap token and send to the reward recipient
            _swapAndSendToken(rewardRecipient, referralShares[rewardRecipient].amount, desiredToken);

            // Set amount to 0, set number of times claimed
            referralShares[rewardRecipient].amount = 0;
            referralShares[rewardRecipient].numberOfTimesClaimed = referralShares[rewardRecipient].numberOfTimesClaimed.add(1);
        }
    }

    function _claimRewardInBNBToDesiredWallet(address rewardRecipient, address desiredWallet) private {
        require(rewardRecipient != address(0), 'Invalid Reward Recipient!');
        require(desiredWallet != address(0), 'Zero Address!');
        require(referralShares[rewardRecipient].amount > 0, 'Insufficient Balance!');

        if (referralShares[rewardRecipient].amount <= IBEP20(rewardToken).balanceOf(address(this))) {
            // Swap token and send to the reward recipient
            _swapAndSendBNB(desiredWallet, referralShares[rewardRecipient].amount);

            // Set amount to 0, set number of times claimed
            referralShares[rewardRecipient].amount = 0;
            referralShares[rewardRecipient].numberOfTimesClaimed = referralShares[rewardRecipient].numberOfTimesClaimed.add(1);
        }
    }

    function _claimRewardInDesiredTokenToDesiredWallet(address rewardRecipient, address desiredWallet, address desiredToken) private {
        require(rewardRecipient != address(0), 'Invalid Reward Recipient!');
        require(desiredWallet != address(0), 'Zero Address!');
        require(desiredToken != address(0), 'Invalid Desired Token!');
        require(referralShares[rewardRecipient].amount > 0, 'Insufficient Balance!');

        if (referralShares[rewardRecipient].amount <= IBEP20(rewardToken).balanceOf(address(this))) {
            // Swap token and send to the reward recipient
            _swapAndSendToken(desiredWallet, referralShares[rewardRecipient].amount, desiredToken);

            // Set amount to 0, set number of times claimed
            referralShares[rewardRecipient].amount = 0;
            referralShares[rewardRecipient].numberOfTimesClaimed = referralShares[rewardRecipient].numberOfTimesClaimed.add(1);
        }
    }

    function _swapAndSendBNB(address recipient, uint256 amount) private {
        IUniswapV2Router02 pcsV2Router = IUniswapV2Router02(router);

        address[] memory path = new address[](2);
        path[0] = rewardToken;
        path[1] = pcsV2Router.WETH();

        IBEP20(rewardToken).approve(address(pcsV2Router), amount);

        pcsV2Router.swapExactTokensForETHSupportingFeeOnTransferTokens(
            amount,
            0,
            path,
            recipient,
            block.timestamp.add(30)
        );
    }

    function _swapAndSendToken(address recipient, uint256 amount, address token) private {
        IUniswapV2Router02 pcsV2Router = IUniswapV2Router02(router);

        address[] memory path = new address[](3);
        path[0] = rewardToken;
        path[1] = pcsV2Router.WETH();
        path[2] = token;

        IBEP20(rewardToken).approve(address(pcsV2Router), amount);

        pcsV2Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
            amount,
            0,
            path,
            recipient,
            block.timestamp.add(30)
        );
    }
}
function emergencyWithdrawTokens(address token) external onlyOwner {
    require(token != address(0), 'Invalid Token!');

    uint256 balance = IBEP20(token).balanceOf(address(this));
    if (balance > 0) {
        require(IBEP20(token).transfer(tokenOwner, balance), 'Transfer Failed!');
    }
}

// Withdraw BEP20 tokens sent to this contract
// NOTE: This function is to be called if and only if BEP20 tokens gets sent into this contract. 
// On no other occurence should this function be called. 