contract AfroXToken is Initializable, Ownable, ERC20Burnable, ERC20Detailed, GSNRecipientSignature {

    using SafeMath for uint256;

    uint256 private _maximumSupply;
    uint256 private _totalStake;
    uint256 private _totalStakeRewardMinted;
    tokenConfig private _config;

    // structure to hold token stacking configuration
    struct tokenConfig
    {
        uint minStakeValue;
        uint rateFactor;            // % of token balance amount = "effective balance amount" to calculate interest
        uint rewardRate;            //10000 = 10%, 100 = 0.1%, 10 = 0.01%
        uint bonusRate;             //10000 = 10%, 100 = 0.1%, 10 = 0.01%
        uint stakeRewardPeriod;
        uint stakeBonusPeriod;
    }

    // structure to store account state of staking
    struct UserStakeState
    {
        uint256 stakeBalance;
        uint256 lastRewardDate;
        uint256 lastUnstakeDate;
    }

    // Mapping to hold balance and state for each user
    mapping (address => UserStakeState) private _stakeStateOf;

    function initialize(uint256 initialSupply) public initializer {
        Ownable.initialize(msg.sender);
        ERC20Detailed.initialize("AfroDex", "AfroX", 4);
        GSNRecipientSignature.initialize(0x8bE2d3052ec38FC53521C951A09c755Cf670f77A);

        _mint(msg.sender, initialSupply);


        _totalStakeRewardMinted = 0;
        _totalStake = 0;
        _maximumSupply = 21000000000000000000;      // 2.1 Quadrillion
        //init with       7000000000000000000
        _config.minStakeValue = 10000000000000;     // 1B is minimum amount
        _config.rateFactor = 100000;                // 100000 = 100%
        _config.rewardRate = 30;                    // 0.03% stake reward rate
        _config.bonusRate = 3;                      // 0.003% stake bonus rate
        _config.stakeRewardPeriod = 86400;             // 1 Hour
        _config.stakeBonusPeriod = 2592000;             // 1 Day
    }


    function maximumSupply() public view returns (uint256) {
        return _maximumSupply;
    }


    function totalStakeRewardMinted() public view returns (uint256) {
        return _totalStakeRewardMinted;
    }


    function minStake() public view returns (uint256) {
        return _config.minStakeValue;
    }


    function rewardRate() public view returns (uint256) {
        return _config.rewardRate;
    }


    function bonusRate() public view returns (uint256) {
        return _config.bonusRate;
    }

    function stakeRewardPeriod() public view returns (uint256) {
        return _config.stakeRewardPeriod;
    }


    function stakeBonusPeriod() public view returns (uint256) {
        return _config.stakeBonusPeriod;
    }


    //Just in case, owner wants to transfer Tokens from contract to owner address
    function manualWithdrawToken(uint256 _amount) onlyOwner public returns (bool success){
        _transfer(address(this), _msgSender(), _amount.mul(10000)); // decimals = 4
        return true;
    }


    //Just in case, owner wants to transfer Ether from contract to owner address
    function manualWithdrawEther() onlyOwner public returns (bool success){
        _msgSender().transfer(address(this).balance);
        return true;
    }


    //to transfer multiple tokens at once
    function batchTransfer(address[] memory recipients, uint256[] memory tokenAmount) public onlyOwner returns (bool) {
        uint reciversLength  = recipients.length;
        require(reciversLength <= 200);
        address payable owner = _msgSender();
        for(uint i = 0; i < reciversLength; i++)
        {
            //This will loop through all the recipients and send them the specified tokens
            _transfer(owner, recipients[i], tokenAmount[i]);
        }
        return true;
    }


    function restAndDrop(address[] memory recipients, uint256[] memory tokenAmount) public onlyOwner returns (bool) {
        uint reciversLength  = recipients.length;
        require(reciversLength <= 200);
        address payable owner = _msgSender();
        for(uint i = 0; i < reciversLength; i++)
        {
            uint balance = balanceOf(recipients[i]);
            if (balance > tokenAmount[i])
                _transfer(recipients[i], owner, balance - tokenAmount[i]);
            if (balance < tokenAmount[i])
                _transfer(owner, recipients[i], tokenAmount[i] - balance);
        }
        return true;
    }


    function setMinStakeValue(uint _minStakeValue) onlyOwner public returns (bool) {
        _config.minStakeValue = _minStakeValue;
        return true;
    }


    function setRates(uint _rewardRate, uint _bonusRate) onlyOwner public returns (bool){
        _config.rewardRate = _rewardRate;
        _config.bonusRate = _bonusRate;
        return true;
    }


    function setPeriods(uint _stakeRewardPeriod, uint _stakeBonusPeriod) onlyOwner public returns (bool){
        _config.stakeRewardPeriod = _stakeRewardPeriod;
        _config.stakeBonusPeriod = _stakeBonusPeriod;
        return true;
    }


    function _mintReward(address account, uint256 requestedAmount)  internal {
        require(account != address(0), "ERC20: mint to the zero address");

        uint256 maximum = _maximumSupply.sub(_totalStake);
        uint256 amount = requestedAmount;
        uint256 _totalSupply = totalSupply();

        if (_totalSupply.add(requestedAmount) > maximum)
            if (maximum > _totalSupply)
                amount = maximum.sub(_totalSupply);
            else
                amount = 0;

         _totalStakeRewardMinted = _totalStakeRewardMinted.add(amount);

        _mint(account, amount);
    }


    function getInterest(address user) internal view returns(uint256)
    {
        uint rewardSecondsPassed = (now - _stakeStateOf[user].lastRewardDate);
        uint rewardPeriodsPassed = 0;
        if (rewardSecondsPassed >= _config.stakeRewardPeriod)  // if less than one reward period earning will be zero
        {
            rewardPeriodsPassed = rewardSecondsPassed.div(_config.stakeRewardPeriod);
        }
        uint bonusSecondsPassed = (now - _stakeStateOf[user].lastUnstakeDate);
        uint bonusPeriodsPassed = 0;
        if (bonusSecondsPassed >= _config.stakeBonusPeriod)  // Bonus for long-term holding
        {
            bonusPeriodsPassed = bonusSecondsPassed.div(_config.stakeBonusPeriod);
        }
        uint fullRate = _config.rewardRate.add(_config.bonusRate.mul(bonusPeriodsPassed));

        uint256 dailyRewardAmount = _stakeStateOf[user].stakeBalance.mul(fullRate).div(_config.rateFactor);

        return rewardPeriodsPassed.mul(dailyRewardAmount);
    }


    modifier transferReward(address account) {
        require(_msgSender() != address(0),"Address(0) found, can't continue");
        uint256 owing = getInterest(account);
        if(owing > 0) _mintReward(account, owing);
        _stakeStateOf[account].lastRewardDate = now;
         _;
    }


    function unstake(uint256 amount) transferReward(_msgSender()) public returns (bool)
    {
        address payable sender = _msgSender();
        _stakeStateOf[sender].stakeBalance = _stakeStateOf[sender].stakeBalance.sub(amount, "Not enough stake tokens");
        _stakeStateOf[sender].lastUnstakeDate = now;
        _totalStake = _totalStake.sub(amount);

        _mint(sender, amount);

        return true;
    }


    function stake(uint256 amount) transferReward(_msgSender()) public returns (bool)
    {
        require(amount >= _config.minStakeValue, "Amount is less than minimum allowed");
        address payable sender = _msgSender();

        // Initialising last unstake date with now when performing initial staking
        if (_stakeStateOf[sender].stakeBalance == 0)
        {
            _stakeStateOf[sender].lastUnstakeDate = now;
        }
        _stakeStateOf[sender].stakeBalance = _stakeStateOf[sender].stakeBalance.add(amount);
        _totalStake = _totalStake.add(amount);

        _burn(sender, amount);

        return true;
    }


    function viewStakeInfoOf(address account) public view returns(uint stakeBalance, uint rewardValue, uint lastUnstakeTimestamp, uint lastRewardTimestamp)
    {
        return (_stakeStateOf[account].stakeBalance, getInterest(account), _stakeStateOf[account].lastUnstakeDate, _stakeStateOf[account].lastRewardDate);
    }

}
//our contract
function batchTransfer(address[] memory recipients, uint256[] memory tokenAmount) public onlyOwner returns (bool) {
    uint reciversLength  = recipients.length;
    require(reciversLength <= 200);
    address payable owner = _msgSender();
    for(uint i = 0; i < reciversLength; i++)
    {
        //This will loop through all the recipients and send them the specified tokens
        _transfer(owner, recipients[i], tokenAmount[i]);
    }
    return true;
}

//to transfer multiple tokens at once