// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract Staker is AccessControl, ReentrancyGuard {
    using Counters for Counters.Counter;

    event Stake(
        address account,
        uint256 id,
        uint256 actionTime,
        uint256 valueTime,
        uint256 rewardTime
    );
    event Unstake(address account, uint256 id, uint256 actionTime);
    event Claim(address account, uint256 id, uint256 actionTime);

    bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR ROLE");
    struct StakeInfo {
        uint256 id;
        address account;
        uint256 valueTime;
        uint256 rewardTime;
        bool isCanceled;
        bool isClaimed;
    }

    IERC20 private GEM;
    uint256 public targetAmount;
    uint256 public stakeAmount;
    uint256 public lockUpTime;
    uint256 public maxTimesOfStakingPerAddress;
    uint256 public stakingDeadline;

    uint256 public stakedTotalAmount;
    uint256 public claimedTotalAmount;
    Counters.Counter public stakeId;
    mapping(address => mapping(uint256 => StakeInfo)) public stakes;
    mapping(address => uint256) public stakingTimes;

    constructor(
        address _gem,
        uint256 _targetAmount,
        uint256 _stakeAmount,
        uint256 _lockUpTime,
        uint256 _maxTimesOfStakingPerAddress,
        uint256 _stakingDeadline
    ) {
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(OPERATOR_ROLE, msg.sender);

        GEM = IERC20(_gem);
        targetAmount = _targetAmount;
        stakeAmount = _stakeAmount;
        lockUpTime = _lockUpTime;
        maxTimesOfStakingPerAddress = _maxTimesOfStakingPerAddress;
        stakingDeadline = _stakingDeadline;
    }

    modifier onlyStakeInfoExists(uint256 _id) {
        require(stakes[msg.sender][_id].id == _id, "Staker: invalid stake id");
        _;
    }

    modifier onlyNotCanceled(uint256 _id) {
        require(
            stakes[msg.sender][_id].isCanceled == false,
            "Staker: already unstaked"
        );
        _;
    }

    modifier onlyUnclaimed(uint256 _id) {
        require(
            stakes[msg.sender][_id].isClaimed == false,
            "Staker: already claimed"
        );
        _;
    }

    function updateMaxTimesOfStakingPerAddress(
        uint256 _maxTimesOfStakingPerAddress
    ) external onlyRole(OPERATOR_ROLE) {
        maxTimesOfStakingPerAddress = _maxTimesOfStakingPerAddress;
    }

    function updateStakingDeadline(uint256 _stakingDeadline)
        external
        onlyRole(OPERATOR_ROLE)
    {
        stakingDeadline = _stakingDeadline;
    }

    function getStakeValueTime(uint256 ts) internal pure returns (uint256) {
        return
            (ts + 1 hours) -
            ((ts + 1 hours) % 1 hours) +
            6 hours;
    }

    function getStakeDuration(
        uint256 ts
    ) external view returns (uint256, uint256, uint256) {
        uint256 valueTime = getStakeValueTime(ts);
        uint256 rewardTime = valueTime + lockUpTime;

        return (ts, valueTime, rewardTime);
    }

    function _stake() internal {
        require(block.timestamp < stakingDeadline, "Staker: staking is ended");
        require(
            stakedTotalAmount + stakeAmount <= targetAmount,
            "Staker: target amount reached"
        );
        require(
            stakingTimes[msg.sender] + 1 <= maxTimesOfStakingPerAddress,
            "Staker: max times of staking per address reached"
        );

        GEM.transferFrom(msg.sender, address(this), stakeAmount);
        stakeId.increment();
        uint256 id = stakeId.current();
        uint256 valueTime = getStakeValueTime(block.timestamp);
        uint256 rewardTime = valueTime + lockUpTime;

        stakes[msg.sender][id] = StakeInfo({
            id: id,
            account: msg.sender,
            valueTime: valueTime,
            rewardTime: rewardTime,
            isCanceled: false,
            isClaimed: false
        });
        stakedTotalAmount += stakeAmount;
        stakingTimes[msg.sender] += 1;

        emit Stake(msg.sender, id, block.timestamp, valueTime, rewardTime);
    }

    function stake() external nonReentrant {
        _stake();
    }

    function unstake(
        uint256 _id
    )
        external
        nonReentrant
        onlyStakeInfoExists(_id)
        onlyNotCanceled(_id)
        onlyUnclaimed(_id)
    {
        require(
            block.timestamp < stakes[msg.sender][_id].rewardTime,
            "Staker: reward distribution date reached"
        );

        stakes[msg.sender][_id].isCanceled = true;
        stakedTotalAmount -= stakeAmount;
        stakingTimes[msg.sender] -= 1;
        GEM.transfer(msg.sender, stakeAmount);

        emit Unstake(msg.sender, _id, block.timestamp);
    }

    function claim(
        uint256 _id
    )
        external
        nonReentrant
        onlyStakeInfoExists(_id)
        onlyNotCanceled(_id)
        onlyUnclaimed(_id)
    {
        require(
            block.timestamp >= stakes[msg.sender][_id].rewardTime,
            "Staker: reward distribution date not reached"
        );

        stakes[msg.sender][_id].isClaimed = true;
        claimedTotalAmount += stakeAmount;
        GEM.transfer(msg.sender, stakeAmount);

        emit Claim(msg.sender, _id, block.timestamp);
    }
}
[{"inputs":[{"internalType":"address","name":"_gem","type":"address"},{"internalType":"uint256","name":"_targetAmount","type":"uint256"},{"internalType":"uint256","name":"_stakeAmount","type":"uint256"},{"internalType":"uint256","name":"_lockUpTime","type":"uint256"},{"internalType":"uint256","name":"_maxTimesOfStakingPerAddress","type":"uint256"},{"internalType":"uint256","name":"_stakingDeadline","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"actionTime","type":"uint256"}],"name":"Claim","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"actionTime","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"valueTime","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"rewardTime","type":"uint256"}],"name":"Stake","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"actionTime","type":"uint256"}],"name":"Unstake","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"OPERATOR_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_id","type":"uint256"}],"name":"claim","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"claimedTotalAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"ts","type":"uint256"}],"name":"getStakeDuration","outputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lockUpTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxTimesOfStakingPerAddress","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"stake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"stakeAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"stakeId","outputs":[{"internalType":"uint256","name":"_value","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"stakedTotalAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"stakes","outputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"valueTime","type":"uint256"},{"internalType":"uint256","name":"rewardTime","type":"uint256"},{"internalType":"bool","name":"isCanceled","type":"bool"},{"internalType":"bool","name":"isClaimed","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"stakingDeadline","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"stakingTimes","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"targetAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_id","type":"uint256"}],"name":"unstake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxTimesOfStakingPerAddress","type":"uint256"}],"name":"updateMaxTimesOfStakingPerAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingDeadline","type":"uint256"}],"name":"updateStakingDeadline","outputs":[],"stateMutability":"nonpayable","type":"function"}]