contract DMC_DMCPool is LPTokenWrapper, IRewardDistributionRecipient {
    IERC20 public yfi;
    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 totalRewardsUnfactored = 0;
    uint256 public totalRewards = 0;
    address public deployer;

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

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

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

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

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

        super.initialize(_y);
        yfi = IERC20(_yfi);

        duration = _duration;
        starttime = _starttime;
        notifyRewardAmount(_initreward.mul(50).div(100));
    }

    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)
                    .div(totalSupply())
            );
    }

    function earnedUnfactored(address account) public view returns (uint256) {
        return
            balanceOf(account)
                .mul(rewardPerToken().sub(userRewardPerTokenPaid[account]))
                .div(1e18)
                .add(rewardsUnfactored[account]);
    }

    // stake visibility is public as overriding LPTokenWrapper's stake() function
    function stake(uint256 amount) public updateReward(msg.sender) checkhalve checkStart{
        require(amount > 0, "Cannot stake 0");
        super.stake(amount);
        emit Staked(msg.sender, amount);

        updateAvgStakeStartTime(msg.sender, balanceOf(msg.sender), amount, false);
        updateAvgStakeStartTime(address(this), totalSupply(), amount, false);
    }

    function withdraw(uint256 amount) public updateReward(msg.sender) checkhalve checkStart{
        require(amount > 0, "Cannot withdraw 0");
        super.withdraw(amount);
        emit Withdrawn(msg.sender, amount);

        avgStakeStartTime[msg.sender] = block.timestamp;
        updateAvgStakeStartTime(address(this), totalSupply(), amount, true);
    }

    function exit() external {
        getReward();
        withdraw(balanceOf(msg.sender));
    }

    function getReward() public updateReward(msg.sender) checkhalve checkStart{
        uint256 rewardUnfactored = earnedUnfactored(msg.sender);
        if (rewardUnfactored > 0) {
            uint256 reward = rewardUnfactored.mul(relativeTimeFactor(msg.sender)).div(1e18);
            rewardsUnfactored[msg.sender] = 0;
            rewards[msg.sender] = rewards[msg.sender].add(reward);
            yfi.safeTransfer(msg.sender, reward);
            emit RewardPaid(msg.sender, reward);
            totalRewardsUnfactored = totalRewardsUnfactored.add(rewardUnfactored);
            totalRewards = totalRewards.add(reward);
            avgStakeStartTime[msg.sender] = block.timestamp;
        }
    }

    modifier checkhalve(){
        if (block.timestamp >= periodFinish) {
            initreward = initreward.mul(50).div(100);

            rewardRate = initreward.div(duration);
            periodFinish = block.timestamp.add(duration);
            emit RewardAdded(initreward);
        }
        _;
    }

    modifier checkStart(){
        require(block.timestamp > starttime,"not start");
        _;
    }

    function notifyRewardAmount(uint256 reward)
        internal
        updateReward(address(0))
    {
        if (block.timestamp >= periodFinish) {
            rewardRate = reward.div(duration);
        } else {
            uint256 remaining = periodFinish.sub(block.timestamp);
            uint256 leftover = remaining.mul(rewardRate);
            rewardRate = reward.add(leftover).div(duration);
        }
        initreward = reward;
        lastUpdateTime = block.timestamp;
        periodFinish = block.timestamp.add(duration);
        emit RewardAdded(reward);
    }

    function updateAvgStakeStartTime(address account, uint256 newBalance, uint256 amount, bool withdrawing) internal {
        uint256 accountAvgStakeStartTime = getAvgStakeStartTime(account);
        uint256 timeDiff = block.timestamp.sub(accountAvgStakeStartTime);
        if (withdrawing) {
            uint256 oldBalance = newBalance.add(amount);
            avgStakeStartTime[account] = accountAvgStakeStartTime.add(
                amount.mul(timeDiff).div(oldBalance)
            );
        } else {
            avgStakeStartTime[account] = accountAvgStakeStartTime.add(
                amount.mul(timeDiff).div(newBalance)
            );
        }
    }

    function getAvgStakeStartTime(address account) internal view returns (uint256) {
        return Math.max(starttime, avgStakeStartTime[account]);
    }

    /**
     * 0.03 / hr for first 48 hrs
     * 0.01 / hr between 48 - 168 hrs
     * 0.05 / hr after 168 hrs
     */
    function timeFactor(address account) public view returns (uint256) {
        uint256 avgStakeTime = block.timestamp.sub(getAvgStakeStartTime(account));
        uint256 hoursPassedE3 = avgStakeTime.mul(1e3).div(1 hours);
        if (hoursPassedE3 <= 0) {
            return 1e18;
        }
        if (hoursPassedE3 <= 48_000) {
            return 1e18 + avgStakeTime.mul(3e16).div(1 hours);
        }
        if (hoursPassedE3 <= 168_000) {
            return 244e16 /* 1 + 48 * 0.03 */ + avgStakeTime.sub(48 hours).mul(1e16).div(1 hours);
        }
        return 364e16 /* 1 + 48 * 0.03 + 120 * 0.01 */ + avgStakeTime.sub(168 hours).mul(5e16).div(1 hours);
    }

    function relativeTimeFactor(address account) public view returns (uint256) {
        return timeFactor(account).mul(1e18).div(timeFactor(address(this)));
    }

    function earned(address account) public view returns (uint256) {
        return earnedUnfactored(account).mul(relativeTimeFactor(account)).div(1e18);
    }
}
function stake(uint256 amount) public updateReward(msg.sender) checkhalve checkStart{
    require(amount > 0, "Cannot stake 0");
    super.stake(amount);
    emit Staked(msg.sender, amount);

    updateAvgStakeStartTime(msg.sender, balanceOf(msg.sender), amount, false);
    updateAvgStakeStartTime(address(this), totalSupply(), amount, false);
}

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