// Contract: PRINT
// Target function: function contractURI() public view returns (string memory)
 the zero address.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     */
    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _beforeTokenTransfer(address(0), account, amount);

        _totalSupply += amount;
        _balances[account] += amount;
        emit Transfer(address(0), account, amount);

        _afterTokenTransfer(address(0), account, amount);
    }

    /**
     * @dev Destroys `amount` tokens from `account`, reducing the
     * total supply.
     *
     * Emits a {Transfer} event with `to` set to the zero address.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     * - `account` must have at least `amount` tokens.
     */
    function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");

        _beforeTokenTransfer(account, address(0), amount);

        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        unchecked {
            _balances[account] = accountBalance - amount;
        }
        _totalSupply -= amount;

        emit Transfer(account, address(0), amount);

        _afterTokenTransfer(account, address(0), amount);
    }

    /**
     * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
     *
     * This internal function is equivalent to `approve`, and can be used to
     * e.g. set automatic allowances for certain subsystems, etc.
     *
     * Emits an {Approval} event.
     *
     * Requirements:
     *
     * - `owner` cannot be the zero address.
     * - `spender` cannot be the zero address.
     */
    function _approve(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

    /**
     * @dev Updates `owner` s allowance for `spender` based on spent `amount`.
     *
     * Does not update the allowance amount in case of infinite allowance.
     * Revert if not enough allowance is available.
     *
     * Might emit an {Approval} event.
     */
    function _spendAllowance(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            unchecked {
                _approve(owner, spender, currentAllowance - amount);
            }
        }
    }

    /**
     * @dev Hook that is called before any transfer of tokens. This includes
     * minting and burning.
     *
     * Calling conditions:
     *
     * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * will be transferred to `to`.
     * - when `from` is zero, `amount` tokens will be minted for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens will be burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}

    /**
     * @dev Hook that is called after any transfer of tokens. This includes
     * minting and burning.
     *
     * Calling conditions:
     *
     * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * has been transferred to `to`.
     * - when `from` is zero, `amount` tokens have been minted for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens have been burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _afterTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}
}

/**
 * @dev PRINT token interface
 */

interface IP is IERC20 {
  function addToBuyTracker(address _user, uint256 _amount) external;

  function burn(uint256 _amount) external;

  function stakeMintToVester(uint256 _amount) external;
}

contract PVester is Ownable {
  IP private _p;

  uint256 public fullyVestedPeriod = 90 days;
  uint256 public withdrawsPerPeriod = 10;

  struct TokenVest {
    uint256 start;
    uint256 end;
    uint256 totalWithdraws;
    uint256 withdrawsCompleted;
    uint256 amount;
  }
  mapping(address => TokenVest[]) public vests;
  address[] public stakeContracts;

  event CreateVest(address indexed user, uint256 amount);
  event Withdraw(address indexed user, uint256 index, uint256 amountWithdrawn);

  modifier onlyStake() {
    bool isStake;
    for (uint256 i = 0; i < stakeContracts.length; i++) {
      if (msg.sender == stakeContracts[i]) {
        isStake = true;
        break;
      }
    }
    require(isStake, 'not a staking contract');
    _;
  }

  constructor(address _token) {
    _p = IP(_token);
  }

  // we expect the staking contract (re: the owner) to transfer tokens to
  // this contract, so no need to transferFrom anywhere
  function createVest(address _user, uint256 _amount) external onlyStake {
    vests[_user].push(
      TokenVest({
        start: block.timestamp,
        end: block.timestamp + fullyVestedPeriod,
        totalWithdraws: withdrawsPerPeriod,
        withdrawsCompleted: 0,
        amount: _amount
      })
    );
    emit CreateVest(_user, _amount);
  }

  function withdraw(uint256 _index) external {
    address _user = msg.sender;
    TokenVest storage _vest = vests[_user][_index];
    require(_vest.amount > 0, 'vest does not exist');
    require(
      _vest.withdrawsCompleted < _vest.totalWithdraws,
      'already withdrew all tokens'
    );

    uint256 _tokensPerWithdrawPeriod = _vest.amount / _vest.totalWithdraws;
    uint256 _withdrawsAllowed = getWithdrawsAllowed(_user, _index);

    // make sure the calculated allowed amount doesn't exceed total amount for vest
    _withdrawsAllowed = _withdrawsAllowed > _vest.totalWithdraws
      ? _vest.totalWithdraws
      : _withdrawsAllowed;

    require(
      _vest.withdrawsCompleted < _withdrawsAllowed,
      'currently vesting, please wait for next withdrawable time period'
    );

    uint256 _withdrawsToComplete = _withdrawsAllowed - _vest.withdrawsCompleted;

    _vest.withdrawsCompleted = _withdrawsAllowed;
    _p.transfer(_user, _tokensPerWithdrawPeriod * _withdrawsToComplete);
    _p.addToBuyTracker(
      _user,
      _tokensPerWithdrawPeriod * _withdrawsToComplete
    );

    // clean up/remove vest entry if it's completed
    if (_vest.withdrawsCompleted == _vest.totalWithdraws) {
      vests[_user][_index] = vests[_user][vests[_user].length - 1];
      vests[_user].pop();
    }

    emit Withdraw(
      _user,
      _index,
      _tokensPerWithdrawPeriod * _withdrawsToComplete
    );
  }

  function getWithdrawsAllowed(address _user, uint256 _index)
    public
    view
    returns (uint256)
  {
    TokenVest memory _vest = vests[_user][_index];
    uint256 _secondsPerWithdrawPeriod = (_vest.end - _vest.start) /
      _vest.totalWithdraws;
    return (block.timestamp - _vest.start) / _secondsPerWithdrawPeriod;
  }

  function getUserVests(address _user)
    external
    view
    returns (TokenVest[] memory)
  {
    return vests[_user];
  }

  function getP() external view returns (address) {
    return address(_p);
  }

  function addStakingContract(address _contract) external onlyOwner {
    stakeContracts.push(_contract);
  }
}

contract PStake is ERC721Enumerable, Ownable {
  using Strings for uint256;
  using Counters for Counters.Counter;

  uint256 private constant ONE_YEAR = 365 days;
  uint256 private constant ONE_WEEK = 7 days;
  uint16 private constant PERCENT_DENOMENATOR = 10000;

  IERC20 internal stakeToken;
  IP internal p;
  IPVester internal vester;
  IStakeRewards internal rewards;

  struct AprLock {
    uint16 apr;
    uint256 lockTime;
  }
  AprLock[] internal _aprLockOptions;

  struct Stake {
    uint256 created;
    uint256 amountStaked;
    uint256 amountPBaseEarn;
    uint16 apr;
    uint256 lockTime;
  }
  // tokenId => Stake
  mapping(uint256 => Stake) public stakes;
  // tokenId => amount
  mapping(uint256 => uint256) public yieldClaimed;
  // tokenId => timestamp
  mapping(uint256 => uint256) public lastClaim;
  // tokenId => boolean
  mapping(uint256 => bool) public isBlacklisted;

  Counters.Counter internal _ids;
  string private baseTokenURI; // baseTokenURI can point to IPFS folder like https://ipfs.io/ipfs/{cid}/ while
  address public paymentAddress;
  address public royaltyAddress;

  // Royalties basis points (percentage using 2 decimals - 1000 = 100, 500 = 50, 0 = 0)
  uint256 private royaltyBasisPoints = 50; // 5%

  // array of all the NFT token IDs owned by a user
  mapping(address => uint256[]) public allUserOwned;
  // the index in the token ID array at allUserOwned to save gas on operations
  mapping(uint256 => uint256) public ownedIndex;

  mapping(uint256 => uint256) public tokenMintedAt;
  mapping(uint256 => uint256) public tokenLastTransferred;

  event StakeTokens(
    address indexed user,
    uint256 indexed tokenId,
    uint256 amountStaked,
    uint256 lockOptionIndex
  );
  event UnstakeTokens(address indexed user, uint256 indexed tokenId);
  event SetAnnualApr(uint256 indexed newApr);
  event SetPaymentAddress(address indexed user);
  event SetRoyaltyAddress(address indexed user);
  event SetRoyaltyBasisPoints(uint256 indexed _royaltyBasisPoints);
  event SetBaseTokenURI(string indexed newUri);
  event AddAprLockOption(uint16 indexed apr, uint256 lockTime);
  event RemoveAprLockOption(
    uint256 indexed index,
    uint16 indexed apr,
    uint256 lockTime
  );
  event UpdateAprLockOption(
    uint256 indexed index,
    uint16 indexed oldApr,
    uint256 oldLockTime,
    uint16 newApr,
    uint256 newLockTime
  );
  event SetTokenBlacklist(uint256 indexed tokenId, bool isBlacklisted);

  constructor(
    string memory _name,
    string memory _symbol,
    address _stakeToken,
    address _p,
    address _vester,
    address _rewards,
    string memory _baseTokenURI
  ) ERC721(_name, _symbol) {
    stakeToken = IERC20(_stakeToken);
    p = IP(_p);
    vester = IPVester(_vester);
    rewards = IStakeRewards(_rewards);
    baseTokenURI = _baseTokenURI;
  }

  function stake(uint256 _amount, uint256 _lockOptIndex) external virtual {
    _stake(msg.sender, _amount, _amount, _lockOptIndex, true);
  }

  function _stake(
    address _user,
    uint256 _amountStaked,
    uint256 _amountPBaseEarn,
    uint256 _lockOptIndex,
    bool _transferStakeToken
  ) internal {
    require(_lockOptIndex < _aprLockOptions.length, 'invalid lock option');
    _amountStaked = _amountStaked == 0
      ? stakeToken.balanceOf(_user)
      : _amountStaked;
    _amountPBaseEarn = _amountPBaseEarn == 0
      ? _amountStaked
      : _amountPBaseEarn;
    require(
      _amountStaked > 0 && _amountPBaseEarn > 0,
      'must stake and be earning at least some tokens'
    );
    if (_transferStakeToken) {
      stakeToken.transferFrom(_user, address(this), _amountStaked);
    }

    _ids.increment();
    stakes[_ids.current()] = Stake({
      created: block.timestamp,
      amountStaked: _amountStaked,
      amountPBaseEarn: _amountPBaseEarn,
      apr: _aprLockOptions[_lockOptIndex].apr,
      lockTime: _aprLockOptions[_lockOptIndex].lockTime
    });
    _safeMint(_user, _ids.current());
    tokenMintedAt[_ids.current()] = block.timestamp;

    emit StakeTokens(_user, _ids.current(), _amountStaked, _lockOptIndex);
  }

  function unstake(uint256 _tokenId) public {
    address _user = msg.sender;
    Stake memory _tokenStake = stakes[_tokenId];
    require(
      _user == ownerOf(_tokenId),
      'only the owner of the staked tokens can unstake'
    );
    bool _isUnstakingEarly = block.timestamp <
      _tokenStake.created + _tokenStake.lockTime;

    // send back original tokens staked
    // if unstaking early based on lock period, only get a portion back
    if (_isUnstakingEarly) {
      uint256 _timeStaked = block.timestamp - _tokenStake.created;
      uint256 _earnedAmount = (_tokenStake.amountStaked * _timeStaked) /
        _tokenStake.lockTime;
      stakeToken.transfer(_user, _earnedAmount);
      if (address(stakeToken) == address(p)) {
        p.burn(_tokenStake.amountStaked - _earnedAmount);
      } else {
        stakeToken.transfer(owner(), _tokenStake.amountStaked - _earnedAmount);
      }
    } else {
      stakeToken.transfer(_user, _tokenStake.amountStaked);
    }

    // check and create new vest if yield available to be claimed
    uint256 _totalEarnedAmount = getTotalEarnedAmount(_tokenId);
    if (_totalEarnedAmount > yieldClaimed[_tokenId]) {
      _createVestAndMint(_user, _totalEarnedAmount - yieldClaimed[_tokenId]);
    }

    // this NFT is useless after the user unstakes
    _burn(_tokenId);

    emit UnstakeTokens(_user, _tokenId);
  }

  function unstakeMulti(uint256[] memory _tokenIds) external {
    for (uint256 i = 0; i < _tokenIds.length; i++) {
      unstake(_tokenIds[i]);
    }
  }

  function claimAndVestRewards(uint256 _tokenId) public {
    require(!isBlacklisted[_tokenId], 'blacklisted NFT');

    // user can only claim and vest rewards up to once a week
    require(block.timestamp > lastClaim[_tokenId] + ONE_WEEK);
    lastClaim[_tokenId] = block.timestamp;

    uint256 _totalEarnedAmount = getTotalEarnedAmount(_tokenId);
    require(
      _totalEarnedAmount > yieldClaimed[_tokenId],
      'must have some yield to claim'
    );
    _createVestAndMint(
      ownerOf(_tokenId),
      _totalEarnedAmount - yieldClaimed[_tokenId]
    );
    yieldClaimed[_tokenId] = _totalEarnedAmount;
  }

  function claimAndVestRewardsMulti(uint256[] memory _tokenIds) external {
    for (uint256 i = 0; i < _tokenIds.length; i++) {
      claimAndVestRewards(_tokenIds[i]);
    }
  }

  function _createVestAndMint(address _user, uint256 _amount) internal {
    // store metadata for earned tokens in vesting contract for user who is unstaking
    vester.createVest(_user, _amount);
    // mint earned tokens to vesting contract
    p.stakeMintToVester(_amount);
  }

  // Support royalty info - See {EIP-2981}: https://eips.ethereum.org/EIPS/eip-2981
  function royaltyInfo(uint256, uint256 _salePrice)
    external
    view
    returns (address receiver, uint256 royaltyAmount)
  {
    return (royaltyAddress, (_salePrice * royaltyBasisPoints) / 1000);
  }

  function tokenURI(uint256 _tokenId)
    public
    view
    virtual
    override
    returns (string memory)
  {
    require(_exists(_tokenId), 'token does not exist');
    return string(abi.encodePacked(_baseURI(), _tokenId.toString(), '.json'));
  }