// File: src/Staking.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.12;

import "./interfaces/IERC20.sol";
import "./libraries/SafeERC20.sol";
import "./libraries/EnumerableSet.sol";

contract Staking {
    using SafeERC20 for IERC20;
    using EnumerableSet for EnumerableSet.UintSet;
    mapping (address => EnumerableSet.UintSet) orderList;
    
    uint256 idx;
    bool private entered;
    address public owner;

    IERC20 public L;

    struct Order {
        address owner;
        uint256 amount;
        uint256 shares;
        uint256 debt;
        uint256 multiplier;
        uint256 stakeTime;
        uint256 unstakeTime;
    }
    mapping (uint256 => Order) public orders;
    mapping (uint256 => uint256) public multipliers;

    uint256 public totalAmountStaked;
    uint256 public totalShares;
    uint256 public rewardPerSecond;
    uint256 public accRewardPerShare;
    uint256 public totalReward;
    uint256 public lastRewardTime;
    uint256 public deadline;

    // ============ Events ============

    event MultiplierEnabled(uint256 indexed day, uint256 indexed multiplier);
    event OwnershipTransferred(address indexed user, address indexed newOwner);
    event RewardPerSecondChanged(uint256 indexed rewardPerSecond, uint256 startTime, uint256 endTime);
    event Staked(address indexed account, uint256 indexed amount);
    event Unstaked(address indexed account, uint256 indexed amount, uint256 reward);

    constructor(address token) {
        L = IERC20(token);

        multipliers[0] = 1 ether;
        emit MultiplierEnabled(0, 1 ether);

        multipliers[30] = 1.2 ether;
        emit MultiplierEnabled(30, 1.2 ether);

        multipliers[60] = 1.5 ether;
        emit MultiplierEnabled(60, 1.5 ether);

        multipliers[90] = 2 ether;
        emit MultiplierEnabled(90, 2 ether);

        owner = msg.sender;
        emit OwnershipTransferred(address(0), msg.sender);
    }

    // ============ Modifiers ============

    modifier nonReentrant() {
        require(!entered, "reentrant");
        entered = true;
        _;
        entered = false;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "unauthorized");

        _;
    }

    // ============ Routine Functions ============

    function stake(uint256 amount, uint256 day) external nonReentrant {
        require(amount > 0, "amount must be greater than 0");
        require(L.balanceOf(msg.sender) >= amount, "insufficient balance");
        require(L.allowance(msg.sender, address(this)) >= amount, "insufficient allowance");

        update();

        uint256 multiplier = multipliers[day];
        require(multiplier != 0, "multiplier does not exist");

        L.safeTransferFrom(msg.sender, address(this), amount);
        idx++;
        Order storage order = orders[idx];
        order.owner = msg.sender;
        order.amount += amount;
        order.shares = amount * multiplier / 10**18;
        order.debt = order.shares * accRewardPerShare / 10**18;
        order.multiplier = multiplier;
        order.stakeTime = block.timestamp;
        order.unstakeTime = block.timestamp + day * 86400;

        addOrder(msg.sender, idx);

        totalAmountStaked += amount;
        totalShares += order.shares;

        emit Staked(msg.sender, amount);
    }

    function unstake(uint256 id) external nonReentrant {
        Order storage order = orders[id];

        uint256 amount = order.amount;
        require(amount > 0, "not exist");

        require(order.owner == msg.sender, "only owner");
        require(order.unstakeTime < block.timestamp, "not due");

        update();

        uint256 reward = order.shares * accRewardPerShare / 10**18 - order.debt;

        totalAmountStaked -= amount;
        totalShares -= order.shares;

        delOrder(msg.sender, id);
        delete orders[id];

        L.safeTransfer(msg.sender, amount + reward);

        emit Unstaked(msg.sender, amount, reward);
    }

    function transferOwnership(address newOwner) external onlyOwner {
        owner = newOwner;
        emit OwnershipTransferred(msg.sender, newOwner);
    }

    function changeRewardPerSecond(uint256 newRewardPerSecond, uint256 newDeadline) external onlyOwner {
        update();

        require(newDeadline > lastRewardTime, "forbid");

        uint256 balance = (newDeadline - lastRewardTime) * newRewardPerSecond;
        if (totalReward < balance) {
            balance -= totalReward;
            totalReward += balance;
            L.safeTransferFrom(msg.sender, address(this), balance);
        }
        
        rewardPerSecond = newRewardPerSecond;
        deadline = newDeadline;

        emit RewardPerSecondChanged(rewardPerSecond, lastRewardTime, deadline);
    }

    function enabledMultiplier(uint256 day, uint256 multiplier) external onlyOwner {
        multipliers[day] = multiplier;
        emit MultiplierEnabled(day, multiplier);
    }

    // ============ View Functions ============

    function earned(uint256 id) external view returns (uint256) {
        Order memory order = orders[id];

        if (order.amount == 0) {
            return 0;
        }

        uint256 ts;
        if (block.timestamp >= deadline) {
            if (deadline > lastRewardTime) {
                ts = deadline - lastRewardTime;
            }
        } else {
            ts = block.timestamp - lastRewardTime;
        }

        uint256 reward = rewardPerSecond * ts;
        if (totalReward < reward) {
            reward = totalReward;
        }
        uint256 _accRewardPerShare = accRewardPerShare + reward * 10**18 / totalShares;
        return order.shares * _accRewardPerShare / 10**18 - order.debt;
    }

    function getOrderLength(address account) public view returns (uint256) {
        return EnumerableSet.length(orderList[account]);
    }

    function getOrderList(address account) public view returns (uint256[] memory) {
        return EnumerableSet.values(orderList[account]);
    }

    function isOrderExist(address account, uint256 id) public view returns (bool) {
        return EnumerableSet.contains(orderList[account], id);
    }

    function getOrder(address account, uint256 index) public view returns (uint256) {
        require(index <= getOrderLength(account) - 1, "order index out of bounds");
        return EnumerableSet.at(orderList[account], index);
    }

    function getBlockTimestamp() external view returns (uint256) {
        return block.timestamp;
    }

    // ============ Internal Functions ============

    function addOrder(address account, uint256 id) internal returns (bool) {
        require(!isOrderExist(account, id), "order exist");
        return EnumerableSet.add(orderList[account], id);
    }

    function delOrder(address account, uint256 id) internal returns (bool) {
        require(isOrderExist(account, id), "order not exist");
        return EnumerableSet.remove(orderList[account], id);
    }

    function update() internal {
        if (block.timestamp <= lastRewardTime) {
            return;
        }

        if (totalShares == 0) {
            lastRewardTime = block.timestamp;
            return;
        }

        uint256 ts;
        if (block.timestamp >= deadline) {
            if (deadline > lastRewardTime) {
                ts = deadline - lastRewardTime;
            }
        } else {
            ts = block.timestamp - lastRewardTime;
        }

        if (ts > 0) {
            uint256 reward = rewardPerSecond * ts;
            if (totalReward >= reward) {
                totalReward -= reward;
            } else {
                reward = totalReward;
                totalReward = 0;
            }
            accRewardPerShare += reward * 10**18 / totalShares;
            lastRewardTime = block.timestamp;
        }
    }
}
// File: src/interfaces/IERC20.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.12;


interface IERC20 {
    function name() external view returns (string memory);

    function symbol() external view returns (string memory);

    function decimals() external view returns (uint8);

    function factory() external view returns (address);

    function totalSupply() external view returns (uint256);
    
    function balanceOf(address account) external view returns (uint256);

    function allowance(address owner, address spender) external view returns (uint256);

    function nonces(address account) external view returns (uint256);

    function mint(address to, uint256 amount) external;

    function burn(uint256 amount) external;

    function approve(address spender, uint256 amount) external returns (bool);

    function transfer(address recipient, uint256 amount) external returns (bool);
    
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;

    event Transfer(address indexed from, address indexed to, uint256 value);
    
    event Approval(address indexed owner, address indexed spender, uint256 value);
}
// File: src/libraries/SafeERC20.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.12;

import "../interfaces/IERC20.sol";
import "./Address.sol";


library SafeERC20 {
    using Address for address;

    function safeTransfer(
        IERC20 token,
        address to,
        uint256 value
    ) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
    }

    function safeTransferFrom(
        IERC20 token,
        address from,
        address to,
        uint256 value
    ) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
    }

    function safeApprove(
        IERC20 token,
        address spender,
        uint256 value
    ) internal {

        require(
            (value == 0) || (token.allowance(address(this), spender) == 0),
            "SafeERC20: approve from non-zero to non-zero allowance"
        );
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
    }

    function safeIncreaseAllowance(
        IERC20 token,
        address spender,
        uint256 value
    ) internal {
        uint256 newAllowance = token.allowance(address(this), spender) + value;
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
    }

    function safeDecreaseAllowance(
        IERC20 token,
        address spender,
        uint256 value
    ) internal {
        unchecked {
            uint256 oldAllowance = token.allowance(address(this), spender);
            require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
            uint256 newAllowance = oldAllowance - value;
            _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
        }
    }

    function safePermit(
        IERC20 token,
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal {
        uint256 nonceBefore = token.nonces(owner);
        token.permit(owner, spender, value, deadline, v, r, s);
        uint256 nonceAfter = token.nonces(owner);
        require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed");
    }

    function _callOptionalReturn(IERC20 token, bytes memory data) private {
        bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
        if (returndata.length > 0) {
            require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
        }
    }
}

// File: src/libraries/EnumerableSet.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.12;

library EnumerableSet {
    struct Set {
        bytes32[] _values;
        mapping(bytes32 => uint256) _indexes;
    }

    function _add(Set storage set, bytes32 value) private returns (bool) {
        if (!_contains(set, value)) {
            set._values.push(value);
            set._indexes[value] = set._values.length;
            return true;
        } else {
            return false;
        }
    }

    function _remove(Set storage set, bytes32 value) private returns (bool) {
        uint256 valueIndex = set._indexes[value];

        if (valueIndex != 0) {

            uint256 toDeleteIndex = valueIndex - 1;
            uint256 lastIndex = set._values.length - 1;

            if (lastIndex != toDeleteIndex) {
                bytes32 lastValue = set._values[lastIndex];

                set._values[toDeleteIndex] = lastValue;
                set._indexes[lastValue] = valueIndex;
            }

            set._values.pop();

            delete set._indexes[value];

            return true;
        } else {
            return false;
        }
    }

    function _contains(Set storage set, bytes32 value) private view returns (bool) {
        return set._indexes[value] != 0;
    }

    function _length(Set storage set) private view returns (uint256) {
        return set._values.length;
    }

    function _at(Set storage set, uint256 index) private view returns (bytes32) {
        return set._values[index];
    }

    function _values(Set storage set) private view returns (bytes32[] memory) {
        return set._values;
    }

    struct Bytes32Set {
        Set _inner;
    }

    function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {
        return _add(set._inner, value);
    }

    function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {
        return _remove(set._inner, value);
    }

    function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {
        return _contains(set._inner, value);
    }

    function length(Bytes32Set storage set) internal view returns (uint256) {
        return _length(set._inner);
    }

    function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {
        return _at(set._inner, index);
    }

    function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
        return _values(set._inner);
    }

    struct AddressSet {
        Set _inner;
    }

    function add(AddressSet storage set, address value) internal returns (bool) {
        return _add(set._inner, bytes32(uint256(uint160(value))));
    }

    function remove(AddressSet storage set, address value) internal returns (bool) {
        return _remove(set._inner, bytes32(uint256(uint160(value))));
    }

    function contains(AddressSet storage set, address value) internal view returns (bool) {
        return _contains(set._inner, bytes32(uint256(uint160(value))));
    }

    function length(AddressSet storage set) internal view returns (uint256) {
        return _length(set._inner);
    }

    function at(AddressSet storage set, uint256 index) internal view returns (address) {
        return address(uint160(uint256(_at(set._inner, index))));
    }

    function values(AddressSet storage set) internal view returns (address[] memory) {
        bytes32[] memory store = _values(set._inner);
        address[] memory result;

        assembly {
            result := store
        }

        return result;
    }

    struct UintSet {
        Set _inner;
    }

    function add(UintSet storage set, uint256 value) internal returns (bool) {
        return _add(set._inner, bytes32(value));
    }

    function remove(UintSet storage set, uint256 value) internal returns (bool) {
        return _remove(set._inner, bytes32(value));
    }

    function contains(UintSet storage set, uint256 value) internal view returns (bool) {
        return _contains(set._inner, bytes32(value));
    }

    function length(UintSet storage set) internal view returns (uint256) {
        return _length(set._inner);
    }

    function at(UintSet storage set, uint256 index) internal view returns (uint256) {
        return uint256(_at(set._inner, index));
    }

    function values(UintSet storage set) internal view returns (uint256[] memory) {
        bytes32[] memory store = _values(set._inner);
        uint256[] memory result;

        assembly {
            result := store
        }

        return result;
    }
}

// File: src/libraries/Address.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.12;


library Address {
    function isContract(address account) internal view returns (bool) {
        return account.code.length > 0;
    }

    function sendValue(address payable recipient, uint256 amount) internal {
        require(address(this).balance >= amount, "Address: insufficient balance");

        (bool success, ) = recipient.call{value: amount}("");
        require(success, "Address: unable to send value, recipient may have reverted");
    }

    function functionCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionCall(target, data, "Address: low-level call failed");
    }

    function functionCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, errorMessage);
    }

    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value
    ) internal returns (bytes memory) {
        return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
    }

    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value,
        string memory errorMessage
    ) internal returns (bytes memory) {
        require(address(this).balance >= value, "Address: insufficient balance for call");
        require(isContract(target), "Address: call to non-contract");

        (bool success, bytes memory returndata) = target.call{value: value}(data);
        return verifyCallResult(success, returndata, errorMessage);
    }

    function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
        return functionStaticCall(target, data, "Address: low-level static call failed");
    }

    function functionStaticCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        require(isContract(target), "Address: static call to non-contract");

        (bool success, bytes memory returndata) = target.staticcall(data);
        return verifyCallResult(success, returndata, errorMessage);
    }

    function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionDelegateCall(target, data, "Address: low-level delegate call failed");
    }

    function functionDelegateCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        require(isContract(target), "Address: delegate call to non-contract");

        (bool success, bytes memory returndata) = target.delegatecall(data);
        return verifyCallResult(success, returndata, errorMessage);
    }

    function verifyCallResult(
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal pure returns (bytes memory) {
        if (success) {
            return returndata;
        } else {
            if (returndata.length > 0) {
                assembly {
                    let returndata_size := mload(returndata)
                    revert(add(32, returndata), returndata_size)
                }
            } else {
                revert(errorMessage);
            }
        }
    }
}