contract StakingRewardsFactory is Ownable {
    using SafeMath for uint;
    // immutables
    address public rewardsToken;
    address public govRewardAccount;
    address public devRewardAccount0;
    address public devRewardAccount1;
    uint public stakingRateGenesis=28620;
    uint public stakingRateTotal=1000000;//100%Percent * 10000
    uint public stakingRewardsGenesis;
    address public timelock;

    // the staking tokens for which the rewards contract has been deployed
    address[] public stakingTokens;

    // info about rewards for a particular staking token
    struct StakingRewardsInfo {
        address stakingRewards;
        uint16 rewardRate;
    }

    // rewards info by staking token
    mapping(address => StakingRewardsInfo) public stakingRewardsInfoByStakingToken;

    constructor(
        address _rewardsToken,
        address _govRewardAccount,
        address _devRewardAccount0,
        address _devRewardAccount1,
        uint _stakingRewardsGenesis
    ) Ownable() public {
        require(_stakingRewardsGenesis >= block.timestamp, 'StakingRewardsFactory::constructor: genesis too soon');

        rewardsToken = _rewardsToken;
        govRewardAccount = _govRewardAccount;
        devRewardAccount0 = _devRewardAccount0;
        devRewardAccount1 = _devRewardAccount1;
        stakingRewardsGenesis = _stakingRewardsGenesis;
        timelock = owner();
    }

    ///// permissioned functions

    // deploy a staking reward contract for the staking token, and store the reward amount
    // the reward will be distributed to the staking reward contract no sooner than the genesis
    function deploy(address[] memory _stakingTokens, uint16[] memory _rewardRates) public {
        require(msg.sender == timelock, "!timelock");
        require(_stakingTokens.length == _rewardRates.length, "stakingTokens and rewardRates lengths mismatch");

        for (uint i = 0; i < stakingTokens.length; i++) {
            StakingRewardsInfo storage  info = stakingRewardsInfoByStakingToken[stakingTokens[i]];
            info.rewardRate = 0;
        }
        uint16  totalRate = 0;


        for (uint i = 0; i < _rewardRates.length; i++) {
            require(_rewardRates[i] > 0, "rewardRate zero");
            require(_stakingTokens[i] != address(0), "StakingRewardsFactory::deploy: stakingToken empty");

            totalRate = totalRate + _rewardRates[i];

            StakingRewardsInfo storage  info = stakingRewardsInfoByStakingToken[_stakingTokens[i]];
            require(info.rewardRate==0, "StakingRewardsFactory::deploy: _stakingTokens is repeat");

            info.rewardRate = _rewardRates[i];
            if(info.stakingRewards == address(0)){
                info.stakingRewards = address(new StakingRewards(/*_rewardsDistribution=*/ address(this), rewardsToken, _stakingTokens[i]));
                stakingTokens.push(_stakingTokens[i]);
            }
        }
        require(totalRate == 10000, 'StakingRewardsFactory::deploy: totalRate not equal to 10000');
    }

    function rewardTotalToken() public view returns (uint256) {
        if (stakingRateGenesis >= stakingRateTotal) {
            return stakingRateTotal.mul(27).mul(1e18).div(100);
        }
        return stakingRateGenesis.mul(27).mul(1e18).div(100);
    }

    ///// permissionless functions

    // call notifyRewardAmount for all staking tokens.
    function notifyRewardAmounts() public {
        require(stakingTokens.length > 0, 'StakingRewardsFactory::notifyRewardAmounts: called before any deploys');
        require(block.timestamp >= stakingRewardsGenesis, 'StakingRewardsFactory::notifyRewardAmounts: reward not start');
        require(stakingRateTotal > 0, 'StakingRewardsFactory::notifyRewardAmounts: reward is over');

        if(stakingRateGenesis >= stakingRateTotal){
            stakingRateGenesis = stakingRateTotal;
        }
        uint _totalRewardAmount = rewardTotalToken();// equal 270000 * 28620 / 10000 / 100

        stakingRewardsGenesis = stakingRewardsGenesis + 7 days;
        stakingRateTotal = stakingRateTotal.sub(stakingRateGenesis);
        stakingRateGenesis = stakingRateGenesis.mul(9730).div(10000);//next reward rate equal stakingRateGenesis * (1-2.7%)

        _mint(_totalRewardAmount);

        uint _govFundAmount = _totalRewardAmount.div(27);// 1/27
        uint _devFundAmount = _totalRewardAmount.mul(5).div(27);// 5/27
        uint _devFundAmount0 = _devFundAmount.mul(80).div(100);//80%
        _reserveRewards(govRewardAccount,_govFundAmount);
        _reserveRewards(devRewardAccount0,_devFundAmount0);
        _reserveRewards(devRewardAccount1,_devFundAmount.sub(_devFundAmount0));//20%

        uint _poolRewardAmount = _totalRewardAmount.sub(_govFundAmount).sub(_devFundAmount); // 21/27
        _notifyPoolRewardAmounts(_poolRewardAmount);
    }

    function _notifyPoolRewardAmounts(uint _poolRewardAmount) private {
        uint _surplusRewardAmount = _poolRewardAmount;
        uint _rewardAmount = 0;
        address farmAddr;

        for (uint i = 0; i < stakingTokens.length; i++) {
            StakingRewardsInfo memory info = stakingRewardsInfoByStakingToken[stakingTokens[i]];
            if(info.rewardRate <= 0){
                continue;
            }
            if(stakingTokens[i] == rewardsToken){
                farmAddr = info.stakingRewards;
                continue;
            }
            _rewardAmount = _poolRewardAmount.mul(info.rewardRate).div(10000);
            if(_rewardAmount >= _surplusRewardAmount){
                _rewardAmount = _surplusRewardAmount;
            }
            _surplusRewardAmount = _surplusRewardAmount.sub(_rewardAmount);
            _notifyRewardAmount(info.stakingRewards,_rewardAmount);
        }
        _surplusRewardAmount = IERC20(rewardsToken).balanceOf(address(this));
        if(_surplusRewardAmount > 0 && farmAddr != address(0)){
            _notifyRewardAmount(farmAddr,_surplusRewardAmount);
        }
    }


    // notify reward amount for an individual staking token.
    // this is a fallback in case the notifyRewardAmounts costs too much gas to call for all contracts
    function _notifyRewardAmount(address _stakingToken,uint _rewardAmount) private {
        require(_stakingToken != address(0), 'StakingRewardsFactory::notifyRewardAmount: not deployed');

        if (_rewardAmount > 0) {
            require(
                IERC20(rewardsToken).transfer(_stakingToken, _rewardAmount),
                'StakingRewardsFactory::notifyRewardAmount: transfer failed'
            );
            StakingRewards(_stakingToken).notifyRewardAmount(_rewardAmount);
        }
    }

    function _reserveRewards(address _account,uint _rawRewardsAmount) private {
        require(_account != address(0), 'StakingRewardsFactory::_reserveRewards: not deployed');

        require(
            IERC20(rewardsToken).transfer(_account, _rawRewardsAmount),
            'StakingRewardsFactory::_reserveRewards: transfer failed'
        );
    }

    function _mint(uint _mintAmount) private {
        require(_mintAmount > 0, 'StakingRewardsFactory::_mint: mintAmount is zero');

        IMinterV2ERC20(rewardsToken).mint(address(this), _mintAmount);
    }

    function setTimeLock(address _timelock) public {
        require(msg.sender == timelock, "!timelock");
        timelock = _timelock;
    }
}
function _notifyRewardAmount(address _stakingToken,uint _rewardAmount) private {
    require(_stakingToken != address(0), 'StakingRewardsFactory::notifyRewardAmount: not deployed');

    if (_rewardAmount > 0) {
        require(
            IERC20(rewardsToken).transfer(_stakingToken, _rewardAmount),
            'StakingRewardsFactory::notifyRewardAmount: transfer failed'
        );
        StakingRewards(_stakingToken).notifyRewardAmount(_rewardAmount);
    }
}

// notify reward amount for an individual staking token.
// this is a fallback in case the notifyRewardAmounts costs too much gas to call for all contracts