contract StakingRewards is LPTokenWrapper, Auth, ReentrancyGuard {
    // --- Auth ---

    address public gov;
    uint256 public duration;

    uint256 public initreward;
    uint256 public starttime;
    uint256 public periodFinish = 0;
    uint256 public rewardRate = 0;
    uint256 public lastUpdateTime;
    uint256 public rewardPerTokenStored;
    uint256 public totalRewards = 0;
    bool public fairDistribution = false;
    uint256 public fairDistributionMaxValue = 0;
    uint256 public fairDistributionTime = 0;

    mapping(address => uint256) public userRewardPerTokenPaid;
    mapping(address => uint256) public rewards;

    event RewardAdded(uint256 reward);
    event StopRewarding();
    event Staked(address indexed user, address indexed gem, uint256 amount);
    event Withdrawn(address indexed user, address indexed gem, uint256 amount);
    event RewardPaid(address indexed user, uint256 reward);

    modifier updateReward(address account) {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = lastTimeRewardApplicable();
        if (account != address(0)) {
            rewards[account] = earned(account);
            userRewardPerTokenPaid[account] = rewardPerTokenStored;
        }
        _;
    }

    constructor() public {
        deployer = msg.sender;
    }

    function initialize(
        address _gov,
        uint256 _duration,
        uint256 _initreward,
        uint256 _starttime
    ) public initializer {
        // only deployer can initialize
        require(deployer == msg.sender);

        require(_starttime >= block.timestamp);

        gov = _gov;

        duration = _duration;
        starttime = _starttime;
        initRewardAmount(_initreward);
    }

    function setupGemForRewardChecker(address a) public {
        require(deployer == msg.sender);
        gemForRewardChecker = IGemForRewardChecker(a);
    }

    function setupFairDistribution(
        uint256 _fairDistributionMaxValue,
        uint256 _fairDistributionTime
    ) public {
        // only deployer can initialize
        require(deployer == msg.sender);
        require(fairDistribution == false);

        fairDistribution = true;
        fairDistributionMaxValue = _fairDistributionMaxValue * (10**decimals);
        fairDistributionTime = _fairDistributionTime;
    }

    function registerPairDesc(
        address gem,
        address adapter,
        uint256 factor,
        address staker
    ) public auth nonReentrant {
        require(gem != address(0x0), "gem is null");
        require(adapter != address(0x0), "adapter is null");

        require(checkGem(gem), "bad gem");

        registerGem(gem);

        pairDescs[gem] = PairDesc({
            gem: gem,
            adapter: adapter,
            factor: factor,
            staker: staker,
            name: "dummy"
        });
    }

    function resetDeployer() public {
        // only deployer can do it
        require(deployer == msg.sender);
        deployer = address(0);
    }

    function lastTimeRewardApplicable() public view returns (uint256) {
        return Math.min(block.timestamp, periodFinish);
    }

    function rewardPerToken() public view returns (uint256) {
        if (totalSupply() == 0) {
            return rewardPerTokenStored;
        }
        return
            rewardPerTokenStored.add(
                lastTimeRewardApplicable()
                    .sub(lastUpdateTime)
                    .mul(rewardRate)
                    .mul(1e18 * (10**decimals))
                    .div(totalSupply())
            );
    }

    function earned(address account) public view returns (uint256) {
        return
            balanceOf(account)
                .mul(rewardPerToken().sub(userRewardPerTokenPaid[account]))
                .div(1e18 * (10**decimals))
                .add(rewards[account]);
    }

    function testFairDistribution(
        address usr,
        address gem,
        uint256 amount
    ) public view returns (bool) {
        return testFairDistributionByValue(usr, calcCheckValue(amount, gem));
    }

    function testFairDistributionByValue(address usr, uint256 value) public view returns (bool) {
        if (fairDistribution) {
            return
                balanceOf(usr).add(value) <= fairDistributionMaxValue ||
                block.timestamp >= starttime.add(fairDistributionTime);
        }
        return true;
    }

    function checkFairDistribution(address usr) public view checkStart {
        if (fairDistribution) {
            require(
                balanceOf(usr) <= fairDistributionMaxValue ||
                    block.timestamp >= starttime.add(fairDistributionTime),
                "Fair-distribution-limit"
            );
        }
    }

    // stake visibility is public as overriding LPTokenWrapper's stake() function
    function stake(
        uint256 amount,
        address gem,
        address usr
    ) public nonReentrant updateReward(usr) checkFinish checkStart {
        require(amount > 0, "Cannot stake 0");
        require(pairDescs[gem].staker == msg.sender, "Stake from join only allowed");

        stakeLp(amount, gem, usr);
        emit Staked(usr, gem, amount);
    }

    function withdraw(
        uint256 amount,
        address gem,
        address usr
    ) public nonReentrant updateReward(usr) checkFinish checkStart {
        require(amount > 0, "Cannot withdraw 0");
        require(pairDescs[gem].staker == msg.sender, "Stake from join only allowed");

        withdrawLp(amount, gem, usr);
        emit Withdrawn(usr, gem, amount);
    }

    function getReward()
        public
        nonReentrant
        updateReward(msg.sender)
        checkFinish
        checkStart
        returns (uint256)
    {
        uint256 reward = earned(msg.sender);
        if (reward > 0) {
            rewards[msg.sender] = 0;
            IERC20(gov).safeTransfer(msg.sender, reward);
            emit RewardPaid(msg.sender, reward);
            totalRewards = totalRewards.add(reward);
            return reward;
        }
        return 0;
    }

    modifier checkFinish() {
        if (block.timestamp > periodFinish) {
            initreward = 0;
            rewardRate = 0;
            periodFinish = uint256(-1);
            emit StopRewarding();
        }
        _;
    }

    modifier checkStart() {
        require(allowToStart(), "not start");
        _;
    }

    function allowToStart() public view returns (bool) {
        return block.timestamp > starttime;
    }

    function initRewardAmount(uint256 reward) internal updateReward(address(0)) {
        require(starttime >= block.timestamp);
        rewardRate = reward.div(duration);
        initreward = reward;
        lastUpdateTime = starttime;
        periodFinish = starttime.add(duration);
        emit RewardAdded(reward);
    }
}
/* import "./ReentrancyGuard.sol"; */
function stake(
    uint256 amount,
    address gem,
    address usr
) public nonReentrant updateReward(usr) checkFinish checkStart {
    require(amount > 0, "Cannot stake 0");
    require(pairDescs[gem].staker == msg.sender, "Stake from join only allowed");

    stakeLp(amount, gem, usr);
    emit Staked(usr, gem, amount);
}

// stake visibility is public as overriding LPTokenWrapper's stake() function