// File: contracts/qntmStaking.sol



//SPDX-License-Identifier: MIT

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

pragma solidity ^0.8.17;

contract DSMath {
    function add(uint x, uint y) internal pure returns (uint z) {
        require((z = x + y) >= x, "ds-math-add-overflow");
    }

    function sub(uint x, uint y) internal pure returns (uint z) {
        require((z = x - y) <= x, "ds-math-sub-underflow");
    }

    function mul(uint x, uint y) internal pure returns (uint z) {
        require(y == 0 || (z = x * y) / y == x, "ds-math-mul-overflow");
    }

    function min(uint x, uint y) internal pure returns (uint z) {
        return x <= y ? x : y;
    }

    function max(uint x, uint y) internal pure returns (uint z) {
        return x >= y ? x : y;
    }

    function imin(int x, int y) internal pure returns (int z) {
        return x <= y ? x : y;
    }

    function imax(int x, int y) internal pure returns (int z) {
        return x >= y ? x : y;
    }

    uint constant WAD = 10 ** 18;
    uint constant RAY = 10 ** 27;

    function wmul(uint x, uint y) internal pure returns (uint z) {
        z = add(mul(x, y), WAD / 2) / WAD;
    }

    function rmul(uint x, uint y) internal pure returns (uint z) {
        z = add(mul(x, y), RAY / 2) / RAY;
    }

    function wdiv(uint x, uint y) internal pure returns (uint z) {
        z = add(mul(x, WAD), y / 2) / y;
    }

    function rdiv(uint x, uint y) internal pure returns (uint z) {
        z = add(mul(x, RAY), y / 2) / y;
    }

    // This famous algorithm is called "exponentiation by squaring"
    // and calculates x^n with x as fixed-point and n as regular unsigned.
    //
    // It's O(log n), instead of O(n) for naive repeated multiplication.
    //
    // These facts are why it works:
    //
    //  If n is even, then x^n = (x^2)^(n/2).
    //  If n is odd,  then x^n = x * x^(n-1),
    //   and applying the equation for even x gives
    //    x^n = x * (x^2)^((n-1) / 2).
    //
    //  Also, EVM division is flooring and
    //    floor[(n-1) / 2] = floor[n / 2].
    //
    function rpow(uint x, uint n) internal pure returns (uint z) {
        z = n % 2 != 0 ? x : RAY;

        for (n /= 2; n != 0; n /= 2) {
            x = rmul(x, x);

            if (n % 2 != 0) {
                z = rmul(z, x);
            }
        }
    }
}

contract Staking is Ownable, DSMath {
    struct Pool {
        uint256 Id;
        uint256 APY;
        uint256 MinTime;
    }

    struct StakeProfile {
        uint256 totalStaked;
        uint256 stakeEnd;
        uint256 stakeId;
        uint256 paidRewards;
        uint256 stakeStart;
        uint256 lastClaimtime;
    }

    Pool private Pool1 = Pool(0, 10, 0);
    Pool private Pool2 = Pool(0, 30, 7 days);
    Pool private Pool3 = Pool(0, 100, 30 days);

    mapping(uint256 => Pool) Pools;

    //Staking Settings
    IERC20 public stakingToken;
    address public stakingVault;

    //Stakers
    uint256 public totalStaked;
    uint256 public totalLocked;
    mapping(uint256 => mapping(address => StakeProfile)) stakers;

    constructor() {
        Pools[0] = Pool1;
        Pools[1] = Pool2;
        Pools[2] = Pool3;
    }

    function stake(uint256 _stakeNumber, uint256 poolId) external {
        //Validating...
        require(_stakeNumber > 0, "can not stake 0 tokens!");
        require(poolId < 3, "Invalid Pool!");
        //Getting corresponding stake pool
        Pool memory targetPool = Pools[poolId];
        //updating staker profile:
        StakeProfile memory profile = stakers[poolId][msg.sender];
        //Adding tokens to staker profile
        profile.totalStaked += _stakeNumber;
        //Setting stake end time but first making sure that we are not overwriting it!
        if (profile.stakeEnd == 0) {
            profile.stakeEnd = block.timestamp + targetPool.MinTime;
        }
        //Setting stake start time if not already set
        if (profile.stakeStart == 0) {
            profile.stakeStart = block.timestamp;
            profile.lastClaimtime = block.timestamp;
        }
        //setting stake Id
        profile.stakeId = poolId;
        stakers[poolId][msg.sender] = profile;
        //Transfering tokens and increasing total staked amount
        if (poolId != 0) {
            totalLocked += _stakeNumber;
        }
        totalStaked += _stakeNumber;
        stakingToken.transferFrom(msg.sender, address(this), _stakeNumber);
    }

    function unstake(uint256 _unstakeNumber, uint256 poolId) external {
        //getting corresponding stake profile
        StakeProfile memory profile = stakers[poolId][msg.sender];
        //Validating
        require(
            profile.totalStaked >= _unstakeNumber,
            "Can't unstake more than balance."
        );
        require(
            block.timestamp >= profile.stakeEnd,
            "Can't unstake before end time!"
        );
        require(_unstakeNumber > 0, "specify a number more than 0");
        require(poolId < 3, "Invalid Pool!");
        //calculating rewards if there is any
        uint256 totalRewards = getRewards(msg.sender, poolId);
        //Updating staker profile
        //- Reducing unstake amount
        profile.totalStaked -= _unstakeNumber;
        if (totalRewards > 0) {
            profile.paidRewards += totalRewards;
        }
        profile.lastClaimtime = block.timestamp;
        //- Reseting time if unstaked all
        if (profile.totalStaked == 0) {
            profile.stakeEnd = 0;
            profile.stakeStart = 0;
            profile.paidRewards = 0;
            profile.lastClaimtime = 0;
        }
        //- Writing updated profile to storage
        stakers[poolId][msg.sender] = profile;
        //- Reducing from total staked
        if (poolId != 0) {
            totalLocked -= _unstakeNumber;
        }
        totalStaked -= _unstakeNumber;
        //sending tokens to staker
        if (
            stakingToken.balanceOf(stakingVault) >= totalRewards &&
            totalRewards > 0
        ) {
            stakingToken.transferFrom(stakingVault, msg.sender, totalRewards);
        }
        stakingToken.transfer(msg.sender, _unstakeNumber);
    }

    //Setters
    function setStakingVault(address newVault) external onlyOwner {
        stakingVault = newVault;
    }

    function setStakingToken(address newStakingToken) external onlyOwner {
        stakingToken = IERC20(newStakingToken);
    }

    //Getters
    function getRewards(
        address _staker,
        uint256 poolId
    ) public view returns (uint256) {
        //getting corresponding stake profile
        StakeProfile memory profile = stakers[poolId][_staker];
        if (profile.totalStaked == 0) {
            return 0;
        }
        uint256 elapsedTime = block.timestamp - profile.lastClaimtime;
        uint256 apy = Pools[poolId].APY;
        uint256 Interest = calculateInteresetInSeconds(
            profile.totalStaked,
            apy,
            elapsedTime
        );
        return Interest - profile.totalStaked;
    }

    //put apy in %, example : 100% == 100, 160% = 160
    function calculateInteresetInSeconds(
        uint256 principal,
        uint256 apy,
        uint256 _seconds
    ) public pure returns (uint256) {
        //Calculating the ratio per second
        //ratio per seconds
        uint256 _ratio = ratio(apy);
        //Interest after _seconds
        return accrueInterest(principal, _ratio, _seconds);
    }

    function ratio(uint256 n) internal pure returns (uint256) {
        uint256 numerator = n * 10 ** 25;
        uint256 denominator = 365 * 86400;
        uint256 result = uint256(10 ** 27) + uint256(numerator / denominator);
        return result;
    }

    function accrueInterest(
        uint _principal,
        uint _rate,
        uint _age
    ) internal pure returns (uint) {
        return rmul(_principal, rpow(_rate, _age));
    }

    function getStakerProfile(
        address _staker,
        uint256 _poolId
    ) public view returns (StakeProfile memory) {
        return stakers[_poolId][_staker];
    }

    function getPoolInfo(uint256 poolId) public view returns (Pool memory) {
        return Pools[poolId];
    }

    function getStakedInPool(
        address account,
        uint256 poolId
    ) public view returns (uint256) {
        return stakers[poolId][account].totalStaked;
    }

    function getCanUnstake(
        address account,
        uint256 poolId
    ) public view returns (bool) {
        StakeProfile memory profile = stakers[poolId][account];
        if (profile.stakeEnd == 0 || profile.totalStaked == 0) {
            return false;
        }
        if (poolId == 0) {
            return false;
        }
        return profile.stakeEnd < block.timestamp;
    }
}

// File: @openzeppelin/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: @openzeppelin/contracts/token/ERC20/IERC20.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `from` to `to` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) external returns (bool);
}

// File: @openzeppelin/contracts/utils/Context.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}