// Contract: MythicGateway
// Target function: function endJourney(address myAddress, uint256 myKeyId)
256 keyTokenId;
        address nftAddress;
        uint256 nftTokenId;
        uint256 artifactId;
        string mythicWorld;
        uint256 startTime;
    }

    // Define a mapping where the key is an address and the value
    // is an array of the struct we defined earlier
    mapping(address => Journeys[]) public activeJourneys;

    // Setup the contracts that will controll or be controlled by the game
    address private gateway = 0x866cfDa1B7cD90Cd250485cd8b700211480845D7;
    address private weapons = 0x866cfDa1B7cD90Cd250485cd8b700211480845D7;
    address private enhancements = 0x866cfDa1B7cD90Cd250485cd8b700211480845D7;
    address private overseer = 0x866cfDa1B7cD90Cd250485cd8b700211480845D7;

    // METL Token
    address public METLToken = 0xFcbE615dEf610E806BB64427574A2c5c1fB55510;

    // Specify the IERC20 interface to the METL Token
    IERC20 METLTokenAddress = IERC20(address(METLToken));

    // Setup the blocks, the rates, and how we know Journey time
    uint256 JourneyBlockRate = 222208; // 30 days
    uint256 JourneyDayRate = 7450; // 1 day

    // Setup the probability of finding anything
    // NOTE: This should be done in the sub-contracts, so we
    //       do not paint ourselves into a forever corner.
    //
    uint256 private probability = 2; // 100* (1-(tokenId/(tokenId+2)))
    uint256 private drop_chance = 64; // 100*(tokenId/64)
    uint256 private changeNameCost = 10; // 10 METL

    // ------ NFT Names -------------------------------------------------------------------
    // Define a struct to represent an NFT and the Name of that NFT
    struct NFT {
        uint256 tokenId;
        string name;
    }

    // Define a mapping where the key is the NFT contract address and the value is an array of NFTs
    mapping(address => NFT[]) public nfts;

    // ----- PAUSE and UNPAUSE ----------------------------------------------------------------
    // Pause and unpause the contract, just in case
    function pause() public onlyOwner {
        _pause();
    }

    function unpause() public onlyOwner {
        _unpause();
    }

    function addJourney(
        address addr,
        uint256 keyTokenId,
        address nftAddress,
        uint256 nftTokenId,
        uint256 artifactId,
        string memory mythicWorld
    ) public whenNotPaused nonReentrant {
        // require the sender be the address
        require(addr == msg.sender, "You are not the sender, go fck");

        // Create a new struct containing the values we want to store in the array
        Journeys memory journey = Journeys(
            keyTokenId,
            nftAddress,
            nftTokenId,
            artifactId,
            mythicWorld,
            block.timestamp
        );

        // Add the struct to the array in the mapping using the address as the key
        activeJourneys[addr].push(journey);
    }

    function removeJourney(address addr, uint256 oldKeyTokenId)
        public
        whenNotPaused
        nonReentrant
    {
        // require the sender be the address
        require(addr == msg.sender, "You are not the sender, go fck");

        // Retrieve the array of journeys associated with the given address
        Journeys[] memory addrJourneys = activeJourneys[addr];

        // Iterate through the journeys in the current array
        for (uint256 i = 0; i < addrJourneys.length; i++) {
            // Check if the current journey has the desired old keyTokenId
            if (addrJourneys[i].keyTokenId == oldKeyTokenId) {
                // Replace the current journey with the new entry
                addrJourneys[i].nftAddress = address(0);
                addrJourneys[i].keyTokenId = 0;
                addrJourneys[i].startTime = 0;
                addrJourneys[i].mythicWorld = "";

                // Return from the function after the item has been replaced
                return;
            }
        }
    }

    function getJourneyByIndex(address addr, uint256 index)
        public
        view
        returns (Journeys memory)
    {
        // Retrieve the array of journeys associated with the given address
        Journeys[] memory journeys = activeJourneys[addr];

        // Check that the index is valid (i.e. less than the length of the journeys array)
        require(index < journeys.length, "Invalid index");

        // Return the journey at the specified index
        return journeys[index];
    }

    function getJourneyTokenAddressIdAndArtifact(
        address addr,
        uint256 keyTokenId
    )
        public
        view
        returns (
            address,
            uint256,
            string memory,
            uint256,
            uint256
        )
    {
        // Retrieve the array of journeys associated with the given address
        Journeys[] memory addrJourneys = activeJourneys[addr];

        // Iterate through the journeys in the current array
        for (uint256 i = 0; i < addrJourneys.length; i++) {
            // Check if the current journey has the desired keyTokenId
            if (addrJourneys[i].keyTokenId == keyTokenId) {
                // Return the journey if the keyTokenId matches
                return (
                    addrJourneys[i].nftAddress,
                    addrJourneys[i].nftTokenId,
                    addrJourneys[i].mythicWorld,
                    addrJourneys[i].artifactId,
                    addrJourneys[i].startTime
                );
            }
        }

        // Return an empty journey if no matching keyTokenId was found
        return (address(0), 0, "", 0, 0);
    }

    function getJourneyByKeyTokenId(address addr, uint256 keyTokenId)
        public
        view
        returns (Journeys memory)
    {
        // Retrieve the array of journeys associated with the given address
        Journeys[] memory addrJourneys = activeJourneys[addr];

        // Iterate through the journeys in the current array
        for (uint256 i = 0; i < addrJourneys.length; i++) {
            // Check if the current journey has the desired keyTokenId
            if (addrJourneys[i].keyTokenId == keyTokenId) {
                // Return the journey if the keyTokenId matches
                return addrJourneys[i];
            }
        }

        // Return an empty journey if no matching keyTokenId was found
        return Journeys(0, address(0), 0, 0, "", 0);
    }

    // Unlock Mythic Gateway (requires a Liquid Key)
    function storeLiquidKey(uint256 tokenId)
        external
        whenNotPaused
        nonReentrant
        returns (bool)
    {
        // Cannot be the LiquidKeys contract
        require(msg.sender != LiquidKeys, "Invalid NFT contract address");

        // Make sure the Liquid Key is owned
        require(
            IERC721(LiquidKeys).ownerOf(tokenId) == msg.sender,
            "You do not own that Liquid Key (nice try, asshole)."
        );

        // Transfer the key from the sender to the contract
        IERC721(LiquidKeys).safeTransferFrom(
            msg.sender,
            address(this),
            tokenId,
            "Transfer from Member to Mythic Gateway"
        );

        // Return true if the key transfer was successful
        return true;
    }

    // @notice Generate random number based on totalnumbers
    // @param totalnumbers The Maximum number to return (i.e. 100 returns 0-99)
    function RandomNumber(uint256 totalnumbers) public view returns (uint256) {
        totalnumbers = totalnumbers + 1; // add one, because starting at 0 sux
        // Create a new seed using a combination of the current block's properties
        uint256 seed = uint256(
            keccak256(
                abi.encodePacked(
                    block.timestamp,
                    block.difficulty,
                    uint256(keccak256(abi.encodePacked(block.coinbase))) /
                        block.timestamp,
                    block.gaslimit,
                    uint256(keccak256(abi.encodePacked(msg.sender))) /
                        block.timestamp,
                    block.number
                )
            )
        );

        // Return the result of the modulo operation between the seed and
        // the total number of possible numbers
        return seed % totalnumbers;
    }

    function getJourneyDetails(address addr, uint256 keyTokenId)
        public
        view
        returns (Journeys memory)
    {
        // Retrieve the array of journeys associated with the given address
        Journeys[] memory addrJourneys = activeJourneys[addr];

        // Iterate through the journeys in the current array
        for (uint256 i = 0; i < addrJourneys.length; i++) {
            // Check if the current journey has the desired keyTokenId
            if (addrJourneys[i].keyTokenId == keyTokenId) {
                // Return the journey if the keyTokenId matches
                return addrJourneys[i];
            }
        }

        // Return an empty journey if no matching keyTokenId was found
        Journeys memory journey = Journeys(0, address(0), 0, 0, "", 0);
        return journey;
    }

    // ---- Probability and Drop Chances ---------------------------------------------
    /* @notice getProbability sets the probability based on the token
     * @param tokenId the token to use
     * @returns the likelihood of an item or METL being dropped
     */
    function getProbability(uint256 tokenId) external view returns (uint256) {
        uint256 p = 100 * (1 - (tokenId / (tokenId + probability)));
        return p;
    }

    /* @notice getDropChance sets the drop chance based on the token
     * @param tokenId the token to use
     * @returns the likelihood of an item or METL being dropped
     */
    function getDropChance(uint256 tokenId) external view returns (uint256) {
        uint256 p = 100 * (tokenId / drop_chance);
        return p;
    }

    /**
     * @notice Set the probability
     * @param newnum the new number for the percent chance (0-100)
     */
    function setProbability(uint256 newnum) external onlyOwner {
        probability = newnum;
    }

    /**
     * @notice Set the drop chance
     * @param newnum the new number for the percent chance (0-100)
     */
    function setDropChance(uint256 newnum) external onlyOwner {
        drop_chance = newnum;
    }

    // Call an external function and get a drop if we qualify
    // for a random drop

    // Start the Journey (with each individual NFT tied to a specific key)
    //
    // 1. Place a key in the Mythic Gateway
    // 2. Select an NFT (Legend, Invader, Azuki Ape, or Tiger)
    // 3. Burn an Artifact and write the Token Id into the mapping
    //
    function OpenGateway(
        uint256 liquidKeyTokenId,
        address nftAddress,
        uint256 nftTokenId,
        uint256 artifactTokenId,
        string memory mythicWorld
    ) external whenNotPaused nonReentrant {
        // Check if the sender owns the required assets
        require(
            IERC721(LiquidKeys).ownerOf(liquidKeyTokenId) == msg.sender &&
                IERC721(nftAddress).ownerOf(nftTokenId) == msg.sender &&
                IERC1155(Artifacts).balanceOf(msg.sender, artifactTokenId) > 0,
            "You do not own the required assets"
        );

        // Burn the artifact to open the gateway
        require(
            burnArtifactOpenGateway(Artifacts).openGateway(
                msg.sender,
                artifactTokenId
            ),
            "Burn failed."
        );

        // Transfer the Liquid Key to the contract
        IERC721(LiquidKeys).safeTransferFrom(
            msg.sender,
            address(this),
            liquidKeyTokenId,
            "Transfer Liquid Key from Member to Mythic Gateway"
        );

        // Add the journey information to the contract
        addJourney(
            msg.sender,
            liquidKeyTokenId,
            nftAddress,
            nftTokenId,
            artifactTokenId,
            mythicWorld
        );
    }

    function payWithMETL(uint256 amount)
        public
        whenNotPaused
        nonReentrant
        returns (bool)
    {
        // First, check that the contract has been approved to transfer the desired amount of tokens
        // from the sender's account. This requires the sender to have previously called the approve()
        // method on the ERC-20 contract, allowing the contract to transfer the tokens on their behalf.
        require(
            IERC20(METLToken).allowance(msg.sender, address(this)) >=
                amount * 1 ether,
            "Missing approval"
        );

        // If the approval check passes, transfer the desired amount of tokens from the sender's
        // account to the contract's account using the transferFrom() method.
        bool success = IERC20(METLToken).transferFrom(
            msg.sender,
            address(this),
            amount * 1 ether
        );

        // Return the result of the transfer, indicating whether it was successful or not.
        return success;
    }

    // ------ NFT Name Functions ----------------------------------------------------------
    // Function to add an NFT to the mapping
    function addNFTName(
        address contractAddress,
        uint256 tokenId,
        string memory name
    ) public whenNotPaused nonReentrant {
        // Require x METL to change the name of your NFT 😈
        bool success = IERC20(METLToken).transferFrom(
            address(this),
            msg.sender,
            changeNameCost * 1 ether
        );

        // Require the user to pay METL to change the name of their NFT
        require(success, "Payment of METL failed");

        // Check that the caller is the owner of the NFT with the specified token ID
        require(
            IERC721(contractAddress).ownerOf(tokenId) == msg.sender,
            "Only the owner of the NFT can add its name"
        );

        // Create a new struct containing the values we want to store in the array
        NFT memory nft = NFT(tokenId, name);

        // Add the struct to the array in the mapping using the contract address as the key
        nfts[contractAddress].push(nft);
    }

    function getAllNFTNames(address contractAddress)
        public
        view
        returns (NFT[] memory)
    {
        // Retrieve the array of NFTs associated with the given contract address
        NFT[] memory localnfts = nfts[contractAddress];

        // Create a new array to hold all the NFTs
        NFT[] memory allNFTs = new NFT[](localnfts.length);

        // Iterate over all the NFTs in the array
        for (uint256 i = 0; i < localnfts.length; i++) {
            // Add each NFT to the allNFTs array
            allNFTs[i] = localnfts[i];
        }

        // Return the allNFTs array
        return allNFTs;
    }

    function getNFTName(address contractAddress, uint256 tokenId)
        public
        view
        returns (NFT memory)
    {
        // Retrieve the array of NFTs associated with the given contract address
        NFT[] memory localnfts = nfts[contractAddress];

        // Iterate over all the NFTs in the array
        for (uint256 i = 0; i < localnfts.length; i++) {
            // Check if the current NFT has the desired token id
            if (localnfts[i].tokenId == tokenId) {
                // Return the NFT if the token id matches
                return localnfts[i];
            }
        }

        // Return an empty NFT if no matching token id was found
        return NFT(0, "");
    }

    // Remove Key from Gateway
    // When the key is removed, also destroy the Key map with the
    // nft, token, and artifact id
    function withdrawNFT(uint256 liquidKeyTokenId)
        external
        whenNotPaused
        nonReentrant
    {
        // Check if the contract owns the given token
        require(
            IERC721(LiquidKeys).ownerOf(liquidKeyTokenId) == address(this),
            "Token is not owned by the contract"
        );

        // Transfer the NFT to the caller
        IERC721(LiquidKeys).safeTransferFrom(
            address(this),
            msg.sender,
            liquidKeyTokenId,
            "Not withdrawn: Transfer failed"
        );
    }

    /**
     * @notice Send some amount of METL
     * @param amount (the amount of METL, without the wei)
     *        such as 2 for 2 METL, not 2 x 10^18
     */
    function SendMETL(uint256 amount)
        private
        whenNotPaused
        nonReentrant
        returns (bool)
    {
        // Send the specified amount of METL to the msg.sender
        bool success = METLTokenAddress.transfer(msg.sender, amount * 1 ether);
        require(success, "Could not transfer token. Insufficient balance?");
        return success;
    }