contract DegenToken is ERC20, Ownable, ReentrancyGuard{

    //@Interfaces
    IERC721 Punkx;

    //@Times
    uint public punkXStart;
    uint public lockoutTime = 10 days;

    //@Rewards
    mapping(uint=>bool) public mintRewardClaimed;
    uint[] public rewards = [13,12,11,10];
    uint[] public multiplier = [0,0,1,2,3,4,5];
    mapping(uint=>uint) externallyClaimed;
    mapping(address=>bool) isApproved;

    //@Staking
    mapping(uint=>stake) public stakedTokens;
    mapping(address=>uint[]) public userStaked;

    //@Emergency
    bool public isPaused;

    //@notice This is the struct containing the individual information regarding the tokens
    struct stake{
        address tokenOwner;
        uint lockTime;
        uint lastClaim;
        uint stakeTime;
        uint lockedPieces;
        uint arrayPosition;
    }

    //@notice This takes in PUNKX NFT's contract address and also sets the start-time to block.timestamp
    constructor(address punkxAddress) ERC20("DEGEN","DEGEN"){
        Punkx = IERC721(punkxAddress);
        punkXStart = block.timestamp;
    }

    //@notice SETTERS
    function deleteRewards () external onlyOwner {
        rewards.pop();
    }

    function deleteMultiplier () external onlyOwner {
        multiplier.pop();
    }

    function addMultiplier (uint value) external onlyOwner {
        multiplier.push(value);
    }

    function addRewards (uint value) external onlyOwner {
        rewards.push(value);
    }

    function ownerMint (uint _amount) external onlyOwner {
        _mint(msg.sender, _amount * 1 ether);
    }

  //@getter
    function getStakedUserTokens(address _query) public view returns (uint[] memory){
        return userStaked[_query];
    }

    //@modifiers
    modifier isNotPaused(){
        require(!isPaused,"Execution is paused");
        _;
    }

    //@notice This function is used to stake tokens
    //@param uint token-ids are taken in a array format
    function stakeTokens (uint[] memory tokenIds,bool lock) external isNotPaused{
        for (uint i=0;i<tokenIds.length;i++) {
            require(Punkx.ownerOf(tokenIds[i]) == msg.sender,"Not Owner"); //Can't stake unowned tokens
            stakedTokens[tokenIds[i]] = stake(msg.sender,0,block.timestamp,block.timestamp,0,userStaked[msg.sender].length);
            userStaked[msg.sender].push(tokenIds[i]);
            Punkx.safeTransferFrom(msg.sender,address(this),tokenIds[i]);
        }
        if(lock){
            require (tokenIds.length > 1,'Min Lock Amount Is 2');
            lockInternal(tokenIds);
        }
    }

    //@notice This function is used to lock in the staked function.
    //@param uint token-ids are taken in an array format. Here the minimum number of tokens passed should be 2.
    function lockTokens(uint[] memory tokenIds) public isNotPaused{
        require (tokenIds.length > 1,'Min Lock Amount Is 2');
        claimRewards(tokenIds);
        lockInternal(tokenIds);
    }

    function lockInternal(uint[] memory tokenIds) internal {
        for(uint i=0;i<tokenIds.length;i++){
            stake storage currentToken = stakedTokens[tokenIds[i]];
            require(currentToken.tokenOwner == msg.sender,"Only owner can lock"); //Needs to be owner to lock
            require(block.timestamp - currentToken.lockTime > lockoutTime,"Already locked"); //Need to not be locked to lock
            currentToken.lockTime = currentToken.lastClaim;
            tokenIds.length > 6 ? currentToken.lockedPieces = 6 : currentToken.lockedPieces = tokenIds.length ;
        }
    }

    //@notice This function is used to unstake the staked tokens. PS: tokens which are locked cannot be unstaked within lockoutTime of locking period
    //@param uint token-ids are taken in an array format.
    function unstakeTokens(uint[] memory tokenIds) external isNotPaused{
        claimRewards(tokenIds);
        for (uint i; i< tokenIds.length; i++) {
            require(Punkx.ownerOf(tokenIds[i]) == address(this),"Token not staked"); //Needs to not be staked
            stake storage currentToken = stakedTokens[tokenIds[i]];
            require(currentToken.tokenOwner == msg.sender,"Only owner can unstake"); //Needs to be sender's token
            require(block.timestamp - currentToken.lockTime > lockoutTime,"Not unlocked"); //Needs to be unlocked
            userStaked[msg.sender][currentToken.arrayPosition] = userStaked[msg.sender][userStaked[msg.sender].length-1];
            stakedTokens[userStaked[msg.sender][currentToken.arrayPosition]].arrayPosition = currentToken.arrayPosition;
            userStaked[msg.sender].pop();
            delete stakedTokens[tokenIds[i]];
            Punkx.safeTransferFrom(address(this), msg.sender, tokenIds[i]);
        }
    }

    //@notice This function is used to reward those first 2222 tokens which were minted before the staking contract was launched
    //@param uint token-ids are taken in an array format
    function rewardFromMint(uint[] memory tokenIds) external isNotPaused{
        require(block.timestamp - punkXStart > 7 days, "It's not birthday yet");
        uint totalAmount;
        for(uint i=0;i<tokenIds.length;i++){
            require(tokenIds[i] < 2222,"Reward only till 2222");
            require (Punkx.ownerOf(tokenIds[i])==msg.sender || stakedTokens[tokenIds[i]].tokenOwner == _msgSender());
            require(!mintRewardClaimed[tokenIds[i]],"Already Claimed");
            mintRewardClaimed[tokenIds[i]] == true;
            totalAmount += 32*rewards[0];
        }
        _mint(msg.sender,totalAmount*1 ether);
    }

    //@notice This function is used to reward those tokens who are staked over 1 day.
    //@param uint token-ids are taken in an array format
    function claimRewards(uint[] memory tokenIds) public nonReentrant isNotPaused{
        uint totalAmount; 
        for(uint i=0;i<tokenIds.length;i++){
            stake storage currentToken = stakedTokens[tokenIds[i]];
            require(currentToken.tokenOwner == msg.sender,"Only owner can claim"); //Needs to be sender's token
            totalAmount += getReward(tokenIds[i]);
            delete externallyClaimed[tokenIds[i]];
            currentToken.lastClaim += ((block.timestamp - currentToken.lastClaim)/ 1 days) * 1 days;
        }
        _mint(msg.sender,totalAmount);
    }

    //@notice This function is to be used by external contract to claim unclaimed rewards
    //@param  from user from which amount needs to be deducted
    //@param amount which is to be deducted
    function claimExternally(address from,uint amount,uint[] memory tokenIds) external isNotPaused {
        require(isApproved[msg.sender],"Not approved caller");
        uint totalAmount;
        for(uint i=0;i<tokenIds.length;i++) {
            stake storage currentToken = stakedTokens[tokenIds[i]];
            require(currentToken.tokenOwner == from,"Only owner can lock"); //Needs to be owner to lock
            uint reward = getReward(tokenIds[i]);
            if(amount > reward + totalAmount) {
                externallyClaimed[tokenIds[i]] = reward;
                totalAmount += reward;
            }
            else {
                externallyClaimed[tokenIds[i]] = amount - totalAmount;
                totalAmount = amount;
                break;
            }
        }
        require(totalAmount == amount,"Not enough DEGEN");
        _mint(msg.sender,amount);
    }

    //@notice This function is to be used to fetch the unclaimed amount value calculation
    //@param  uint token-id is taken
    function getReward(uint tokenId) public view returns(uint){
            require(Punkx.ownerOf(tokenId) == address(this),"Token not staked"); //Needs to not be staked
            stake storage currentToken = stakedTokens[tokenId];
            uint reward;
            if(block.timestamp - currentToken.lockTime <= lockoutTime) {
                //tokenId still in lock period
                uint timeInDays = (block.timestamp - currentToken.lastClaim)/1 days;
                uint combo = currentToken.lockedPieces;
                reward = 1 ether*timeInDays*rewards[tokenId/2222]*(10+multiplier[combo])/10 - externallyClaimed[tokenId]; //Assuming tokenId starts from 1
            }
            else {
                uint unlockTime = currentToken.lockTime + lockoutTime;
                if(block.timestamp > unlockTime && currentToken.lastClaim < unlockTime){
                    //last claim before lock end but current time after
                    uint timeLocked = (currentToken.lockTime + lockoutTime - currentToken.lastClaim)/1 days;
                    uint timeUnlocked = (block.timestamp - (currentToken.lockTime + lockoutTime))/1 days;
                    uint combo = currentToken.lockedPieces;
                    reward = 1 ether*timeLocked*rewards[tokenId/2222]*(10+multiplier[combo])/10;
                    reward += 1 ether*timeUnlocked*rewards[tokenId/2222];
                    reward -= externallyClaimed[tokenId];
                }
                else{
                    //last claim time after lock period
                    uint timeInDays = (block.timestamp - currentToken.lastClaim)/1 days;
                    reward = 1 ether*timeInDays*rewards[tokenId/2222] - externallyClaimed[tokenId];
                }
            }
        return reward;
    }

    function mintExternally(uint _amount) external isNotPaused{
        require(isApproved[msg.sender], "Not approved yet");
        _mint(_msgSender(), _amount);
    }

    function burnExternally(address _from, uint _amount) external isNotPaused{
        require(isApproved[msg.sender], "Not approved yet");
        _burn(_from, _amount);
    }

    function approveAddress(address toBeApproved,bool approveIt) external onlyOwner{
        isApproved[toBeApproved] = approveIt;
    }

    function pauseExecution(bool pause) external onlyOwner{
        isPaused = pause;
    }
    
    function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) public pure returns (bytes4) {
        return 0x150b7a02;
    }
}
//@title A ERC20-token minting and ERC721 staking contract used for Punkx NFT series
//@author Ace, Alfa, Anyx, 0xytocin
//@notice This contract is used for staking Punkx NFT's and mint Degenz tokens according to the tokenomics mentioned in the whitepaper
//@dev All functions are currently working and having the exact value they should have. TESTS ARE YET TO BE DONE.
function claimExternally(address from,uint amount,uint[] memory tokenIds) external isNotPaused {
    require(isApproved[msg.sender],"Not approved caller");
    uint totalAmount;
    for(uint i=0;i<tokenIds.length;i++) {
        stake storage currentToken = stakedTokens[tokenIds[i]];
        require(currentToken.tokenOwner == from,"Only owner can lock"); //Needs to be owner to lock
        uint reward = getReward(tokenIds[i]);
        if(amount > reward + totalAmount) {
            externallyClaimed[tokenIds[i]] = reward;
            totalAmount += reward;
        }
        else {
            externallyClaimed[tokenIds[i]] = amount - totalAmount;
            totalAmount = amount;
            break;
        }
    }
    require(totalAmount == amount,"Not enough DEGEN");
    _mint(msg.sender,amount);
}

//@notice This function is to be used by external contract to claim unclaimed rewards
//@param  from user from which amount needs to be deducted
//@param amount which is to be deducted