contract Graveyard is ERC721Enumerable {
    using GraveyardLibrary for uint8;
    
    // While zombies are false, minting graveyards is not possible
    // bool public ZOMBIES = false;

    // uint256 public DEFAULT_DIGGING_TIME = 86400;
    uint256 public MAX_SUPPLY = 5444;
    uint256 SEED_NONCE = 0;

    // Mappings for the traits and their hashes
    mapping(uint256 => GraveyardLibrary.Trait[]) public traitTypes;
    mapping(string => bool) hashToMinted;
    mapping(uint256 => string) internal tokenIdToHash;

    // Mapping of graveyardId to store the time when the zombie can be claimed
    mapping(uint256 => uint256) internal tokenIdToTargetTime;

    // Mapping of graveyardId to staker
    mapping(uint256 => address) internal tokenIdToStaker;    
    mapping(address => uint256[]) internal stakerToTokenIds;

    // Mapping of graveyardId to staked zombie ids 
    mapping(uint256 => uint256[]) internal graveyardIdToCatIds;

    address nullAddress = 0x0000000000000000000000000000000000000000;

    // Mapping of graveyardId to store if a zombie has been claimed or not
    mapping(uint256 => bool) internal tokenIdToYield;

    mapping(uint256 => bool) internal katzIdUsedForMint;

    // The ZombieKat contract address
    address public zombiesAddress;

    // String arrays, we were not able to store this in the library. Sing the alphabet while you read this.
    string[] LETTERS = [
        "a",
        "b",
        "c",
        "d",
        "e",
        "f",
        "g",
        "h",
        "i",
        "j",
        "k",
        "l",
        "m",
        "n",
        "o",
        "p",
        "q",
        "r",
        "s",
        "t",
        "u",
        "v",
        "w",
        "x",
        "y",
        "z"
    ];

    // Array to store the rarity of the graves
    uint16[][8] internal TIERS;

    // The miece contract address and the contract owner. 
    address mieceAddress;
    address _owner;

    constructor() ERC721("cMyGraves", "Grave") {
        _owner = msg.sender;

        TIERS[0] = [100, 200, 800, 800, 1200, 2000, 4900]; // arm
        TIERS[1] = [400, 1000, 3200, 5400];
        TIERS[2] = [500, 1400, 1600, 6500];
        TIERS[3] = [100, 400, 500, 9000];
        TIERS[4] = [200, 400, 1100, 1100, 2400, 2400, 2400];
        TIERS[5] = [2500, 2500, 2500, 2500];
        TIERS[6] = [2500, 2500, 2500, 2500];
        TIERS[7] = [40, 140, 1420, 1800, 2000, 2200, 2400 ];

    }

    /**
     * @dev Returns the staked zombies ids 
     * @param staker  The address of the staker
     */   
    function getCatsStaked(address staker)
        public
        view
        returns (uint256[] memory)
    {
        return stakerToTokenIds[staker];
    }
    
    /**
     * @dev helper function to remove a token id from the array associated to the staker
     * @param staker  The address of the staker
     * @param index Index
     */   
    function remove(address staker, uint256 index) internal {
        if (index >= stakerToTokenIds[staker].length) return;

        for (uint256 i = index; i < stakerToTokenIds[staker].length - 1; i++) {
            stakerToTokenIds[staker][i] = stakerToTokenIds[staker][i + 1];
        }
        stakerToTokenIds[staker].pop();
    }

    /**
     * @dev helper function to maintain the list of staked ids of an address
     * @param staker  The address of the staker
     * @param tokenId  The Staked Zombie Ids
     */
    function removeTokenIdFromStaker(address staker, uint256 tokenId) internal {
        for (uint256 i = 0; i < stakerToTokenIds[staker].length; i++) {
            if (stakerToTokenIds[staker][i] == tokenId) {
                //This is the tokenId to remove;
                remove(staker, i);
            }
        }
    }

    /**
     * @dev Initialize the digging op and sets the target time when user can claim a zombie
     * @param graveId  Target Grave Id
     */
    
    function initDigTime(uint256 graveId, uint256 multipler) internal {
        tokenIdToTargetTime[graveId] =
            block.timestamp +
            (86400 * multipler);
    }

    /**
     * @dev Unstakes the zombies from the grave, but only if the zombie has been claimed first. 
     * @param graveId  Grave Id
     */
    function claimAndUnstake(uint256 graveId) public {
        claimZombie(graveId);

        require(
            tokenIdToYield[graveId] == true
            // "You need to claim the zombie first"
        );

        for (uint256 i = 0; i < graveyardIdToCatIds[graveId].length; i++) {
            require(
                tokenIdToStaker[graveyardIdToCatIds[graveId][i]] == msg.sender
                // "Message Sender was not original staker!"
            );

            IERC721(zombiesAddress).transferFrom(
                address(this),
                msg.sender,
                graveyardIdToCatIds[graveId][i]
            );

            removeTokenIdFromStaker(
                msg.sender,
                graveyardIdToCatIds[graveId][i]
            );

            tokenIdToStaker[graveyardIdToCatIds[graveId][i]] = nullAddress;
        }
    }

    /**
     * @dev Moves staked zombies from one grave to an another
     * @param fromGraveId   Source Grave Id
     * @param toGraveId  Target Grave Id
     */

    function claimAndMove(uint256 fromGraveId, uint256 toGraveId) public {
        claimZombie(fromGraveId);

        require(
            tokenIdToYield[fromGraveId] == true
            // "You need to withdraw the zombie cat first."
        );

        require(
            ownerOf(toGraveId) == msg.sender
            // "You can only dig your own graves"
        );

        for (uint256 i = 0; i < graveyardIdToCatIds[fromGraveId].length; i++) {
            require(
                tokenIdToStaker[graveyardIdToCatIds[fromGraveId][i]] ==
                    msg.sender
                // "Message Sender was not original staker!"
            );
            graveyardIdToCatIds[toGraveId].push(
                graveyardIdToCatIds[fromGraveId][i]
            );
        }
        delete graveyardIdToCatIds[fromGraveId]; // Maybe we don't need this

        initDigTime(toGraveId, 7);
    }

    /**
     * @dev Stake zombies in a grave
     * @param graveId   The grave id
     * @param tokenIds  Ids of the zombies
     */

    function stakeByIds(uint256 graveId, uint256[] memory tokenIds) public {
        require(
            tokenIds.length == 2
            // "You need two cats to dig!"
        );

        require(
            graveyardIdToCatIds[graveId].length == 0
            // "There are already cats digging this grave"
        );

        require(
            ownerOf(graveId) == msg.sender
            // "You can only dig your own graves"
        );

        for (uint256 i = 0; i < tokenIds.length; i++) {
            require(
                IERC721(zombiesAddress).ownerOf(tokenIds[i]) == msg.sender &&
                    tokenIdToStaker[tokenIds[i]] == nullAddress
                // "Token must be stakable by you!"
            );

            IERC721(zombiesAddress).transferFrom(
                msg.sender,
                address(this),
                tokenIds[i]
            );

            stakerToTokenIds[msg.sender].push(tokenIds[i]);

            graveyardIdToCatIds[graveId].push(tokenIds[i]);

            tokenIdToStaker[tokenIds[i]] = msg.sender;
        }

        initDigTime(graveId, 7);
    }

    /**
     * @dev Returns the list of ids staked in a grave
     * @param graveId The grave id
     */
    function getCatsStakedByGraveId(uint256 graveId)
        public
        view
        returns (uint256[] memory)
    {
        return graveyardIdToCatIds[graveId];
    }
    

    /**
     * @dev Returns seconds lefts from a digging completion
     * @param graveId The tokenId to return the time lefts
     */ 
    function getDiggingCompletionTime(uint256 graveId)
        public
        view
        returns (uint256)
    {
        require(tokenIdToTargetTime[graveId] != 0
        // , "Digging not started"
        );
        require(tokenIdToYield[graveId] != true
        //, "Digging already finished"
        );

        return tokenIdToTargetTime[graveId];
    }

    /**
     * @dev Claims the zombie from a grave and burn the grave. 
     * @param tokenId The grave Id to claim the zombie from
     */
     function claimZombie(uint256 tokenId) internal {
         require(
            ownerOf(tokenId) == msg.sender
            // "You cannot withdraw from someone else grave"
        );
        require(
            tokenIdToTargetTime[tokenId] < block.timestamp
            //  "This grave digging is not finished yet"
        );

        // Zombie Contract will mint a Zombie with the same hash as the Current Grave
        IZombie(zombiesAddress).mintZombie(
            msg.sender,
            _tokenIdToHash(tokenId)
        );

        tokenIdToYield[tokenId] = true;

        // After the zombie claimed we burn the graveyard
        _transfer(
            msg.sender,
            0x000000000000000000000000000000000000dEaD,
            tokenId
        );
     }

    /**
     * @dev Claims the zombie from a grave and burn the grave. 
     * @param tokenIds The grave Ids to claim the zombie from
     */
    function claimZombies(uint256[] memory tokenIds) public {
        for (uint256 i = 0; i < tokenIds.length; i++) {
            require(
                graveyardIdToCatIds[tokenIds[i]].length == 0
                // "There are cats digging the grave, can't only claim yielded zombie"
            );

            claimZombie(tokenIds[i]);
        }
    }

    /**
     * @dev Fasten up the digging process by spending mice on the Grave. You can include the same GraveId multiple time, it decreases the digging time with a day.
     * @param tokenIds The graveyard Ids. 
     */
    function fastenDigging(uint256[] memory tokenIds) public {
        for (uint256 i = 0; i < tokenIds.length; i++) {
            IMiece(mieceAddress).burnFrom(
                msg.sender,
                GraveyardLibrary.currentFastenCost(
                    // IZombie(zombiesAddress).totalSupply()
                )
            );

            tokenIdToTargetTime[tokenIds[i]] =
                tokenIdToTargetTime[tokenIds[i]] -
                86400;
        }
    }

    /*
  __  __ _     _   _             ___             _   _             
 |  \/  (_)_ _| |_(_)_ _  __ _  | __|  _ _ _  __| |_(_)___ _ _  ___
 | |\/| | | ' \  _| | ' \/ _` | | _| || | ' \/ _|  _| / _ \ ' \(_-<
 |_|  |_|_|_||_\__|_|_||_\__, | |_| \_,_|_||_\__|\__|_\___/_||_/__/
                         |___/                                     
   */

    /**
     * @dev Generates a 9 digit hash from a tokenId, address, and random number.
     * @param _t The token id to be used within the hash.
     * @param _a The address to be used within the hash.
     * @param _c The custom nonce to be used within the hash.
     */
    function hash(
        uint256 _t,
        address _a,
        uint256 _c
    ) internal returns (string memory) {
        require(_c < 10);

        // This will generate a 9 character string.
        //The last 8 digits are random, the first is 0, due to the mouse not being burned.
        string memory currentHash = "0";

        for (uint8 i = 0; i < 8; i++) {
            SEED_NONCE++;
            uint16 _randinput = uint16(
                uint256(
                    keccak256(
                        abi.encodePacked(
                            block.timestamp,
                            block.difficulty,
                            _t,
                            _a,
                            _c,
                            SEED_NONCE
                        )
                    )
                ) % 10000
            );

            currentHash = string(
                abi.encodePacked(
                    currentHash,
                    GraveyardLibrary.rarityGen(_randinput, i, TIERS)
                )
            );
        }

        if (hashToMinted[currentHash]) return hash(_t, _a, _c + 1);

        return currentHash;
    }

    /**
     * @dev Returns the current miece cost of mint.
     */
    function currentMieceCost() public view returns (uint256) {
        return GraveyardLibrary.currentMieceCost(totalSupply());
    }


    /**
     * @dev Checks if a Katz has been used for minting.
     */
    function checkKatzUsage(uint256 katzId) public view returns (bool) {
        return katzIdUsedForMint[ katzId ];
    }

    /**
     * @dev Mints a Grave, this is to avoid code duplication.
     */
    function mintGrave(uint256 katzId) public {
        uint256 _totalSupply = totalSupply();
        require(_totalSupply < MAX_SUPPLY);

        // FIXME: Add these before deployment
        // require(_totalSupply > 4);
        // require(!GraveyardLibrary.isContract(msg.sender));
        
        uint256 thisTokenId = _totalSupply;

        tokenIdToHash[thisTokenId] = hash(thisTokenId, msg.sender, 0);

        hashToMinted[tokenIdToHash[thisTokenId]] = true;

        IMiece(mieceAddress).burnFrom(msg.sender, currentMieceCost());

        // First 1000 graves yields automatically after 7 days. 
        if ( _totalSupply < 1000 ) {

            bool doesListContainElement = false;

            uint256[] memory stakedIds = IMiece(mieceAddress).getTokensStaked(msg.sender);

            for (uint i=0; i < stakedIds.length; i++) {
                if (katzId == stakedIds[i]) {
                    doesListContainElement = true;
                }
            }
            require(doesListContainElement == true);

            require(katzIdUsedForMint[ katzId ] == false);

            katzIdUsedForMint[ katzId ] = true;
                                                            // require(stakedIds.length > 0);    
           initDigTime(thisTokenId, 7);
        }
        
        _mint(msg.sender, thisTokenId);
    }
 

  

    function mintReserve(uint256 _times) public onlyOwner {
        // ZOMBIES = true;
        for(uint256 i=0; i< _times; i++){
            uint256 thisTokenId = totalSupply();
            require(thisTokenId < 50);
            tokenIdToHash[thisTokenId] = hash(thisTokenId, msg.sender, 0);
            hashToMinted[tokenIdToHash[thisTokenId]] = true;
            initDigTime(thisTokenId, 7);
            _mint(msg.sender, thisTokenId);
        }
    }
 

    /*
 ____     ___   ____  ___        _____  __ __  ____     __ ______  ____  ___   ____   _____
|    \   /  _] /    ||   \      |     ||  |  ||    \   /  ]      ||    |/   \ |    \ / ___/
|  D  ) /  [_ |  o  ||    \     |   __||  |  ||  _  | /  /|      | |  ||     ||  _  (   \_ 
|    / |    _]|     ||  D  |    |  |_  |  |  ||  |  |/  / |_|  |_| |  ||  O  ||  |  |\__  |
|    \ |   [_ |  _  ||     |    |   _] |  :  ||  |  /   \_  |  |   |  ||     ||  |  |/  \ |
|  .  \|     ||  |  ||     |    |  |   |     ||  |  \     | |  |   |  ||     ||  |  |\    |
|__|\_||_____||__|__||_____|    |__|    \__,_||__|__|\____| |__|  |____|\___/ |__|__| \___|
                                                                                           
*/

    /**
     * @dev Returns the SVG and metadata for a token Id
     * @param _tokenId The tokenId to return the SVG and metadata for.
     */

    function tokenURI(uint256 _tokenId)
        public
        view
        override
        returns (string memory)
    {
        require(_exists(_tokenId));

        string memory tokenHash = _tokenIdToHash(_tokenId);
        
        return
            GraveyardLibrary.tokenURIData(
                _tokenId,
                tokenHash,
                traitTypes,
                LETTERS,
                tokenIdToTargetTime[_tokenId]
            );
    }


    /**
     * @dev Returns a hash for a given tokenId
     * @param _tokenId The tokenId to return the hash for.
     */
    function _tokenIdToHash(uint256 _tokenId)
        public
        view
        returns (string memory)
    {
        string memory tokenHash = tokenIdToHash[_tokenId];
        //If this is a burned token, override the previous hash
        if (tokenIdToYield[_tokenId]) {
            tokenHash = string(
                abi.encodePacked(
                    "1",
                    GraveyardLibrary.substring(tokenHash, 1, 9)
                )
            );
        }

        return tokenHash;
    }

    /**
     * @dev Returns the wallet of a given wallet. Mainly for ease for frontend devs.
     * @param _wallet The wallet to get the tokens of.
     */
    function walletOfOwner(address _wallet)
        public
        view
        returns (uint256[] memory)
    {
        uint256 tokenCount = balanceOf(_wallet);

        uint256[] memory tokensId = new uint256[](tokenCount);
        for (uint256 i; i < tokenCount; i++) {
            tokensId[i] = tokenOfOwnerByIndex(_wallet, i);
        }
        return tokensId;
    }

    /*

  ___   __    __  ____     ___  ____       _____  __ __  ____     __ ______  ____  ___   ____   _____
 /   \ |  |__|  ||    \   /  _]|    \     |     ||  |  ||    \   /  ]      ||    |/   \ |    \ / ___/
|     ||  |  |  ||  _  | /  [_ |  D  )    |   __||  |  ||  _  | /  /|      | |  ||     ||  _  (   \_ 
|  O  ||  |  |  ||  |  ||    _]|    /     |  |_  |  |  ||  |  |/  / |_|  |_| |  ||  O  ||  |  |\__  |
|     ||  `  '  ||  |  ||   [_ |    \     |   _] |  :  ||  |  /   \_  |  |   |  ||     ||  |  |/  \ |
|     | \      / |  |  ||     ||  .  \    |  |   |     ||  |  \     | |  |   |  ||     ||  |  |\    |
 \___/   \_/\_/  |__|__||_____||__|\_|    |__|    \__,_||__|__|\____| |__|  |____|\___/ |__|__| \___|
                                                                                                     


    */

    
    /**
     * @dev Add a trait type
     * @param _traitTypeIndex The trait type index
     * @param traits Array of traits to add
     */

    function addTraitType(
        uint256 _traitTypeIndex,
        GraveyardLibrary.Trait[] memory traits
    ) public onlyOwner {
        for (uint256 i = 0; i < traits.length; i++) {
            traitTypes[_traitTypeIndex].push(
                GraveyardLibrary.Trait(
                    traits[i].traitName,
                    traits[i].traitType,
                    traits[i].pixels,
                    traits[i].pixelCount
                )
            );
        }

        return;
    }

    /**
     * @dev Sets the miece, cats and zombie contract addresses
     * @param _mieceAddress The miece address
     * @param _zombiesAddress The cats address
     */
    function setContractAddresses(
        address _mieceAddress,
        address _zombiesAddress
    ) public onlyOwner {
        mieceAddress = _mieceAddress;
        zombiesAddress = _zombiesAddress;
        return;
    }

    /**
     * @dev Transfers ownership
     * @param _newOwner The new owner
     */
    // function transferOwnership(address _newOwner) public onlyOwner {
    //     _owner = _newOwner;
    // }
    

    /**
     * @dev Modifier to only allow owner to call functions
     */
    modifier onlyOwner() {
        require(_owner == msg.sender);
        _;
    }
}
/*
 
 ___  ___      _____                          
 |  \/  |     |  __ \                         
 ___| .  . |_   _| |  \/_ __ __ ___   _____  ___ 
 / __| |\/| | | | | | __| '__/ _` \ \ / / _ \/ __|
 | (__| |  | | |_| | |_\ \ | | (_| |\ V /  __/\__ \
 \___\_|  |_/\__, |\____/_|  \__,_| \_/ \___||___/
 __/ |                               
 |___/                                
 
 */
function setContractAddresses(
    address _mieceAddress,
    address _zombiesAddress
) public onlyOwner {
    mieceAddress = _mieceAddress;
    zombiesAddress = _zombiesAddress;
    return;
}

/**
 * @dev Sets the miece, cats and zombie contract addresses
 * @param _mieceAddress The miece address
 * @param _zombiesAddress The cats address
 */