// Contract: MythicGateway
// Target function: function OpenGateway(
 an {URI} event was emitted for `id`, the standard
     * https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value
     * returned by {IERC1155MetadataURI-uri}.
     */
    event URI(string value, uint256 indexed id);

    /**
     * @dev Returns the amount of tokens of token type `id` owned by `account`.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     */
    function balanceOf(address account, uint256 id)
        external
        view
        returns (uint256);

    /**
     * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}.
     *
     * Requirements:
     *
     * - `accounts` and `ids` must have the same length.
     */
    function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids)
        external
        view
        returns (uint256[] memory);

    /**
     * @dev Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`,
     *
     * Emits an {ApprovalForAll} event.
     *
     * Requirements:
     *
     * - `operator` cannot be the caller.
     */
    function setApprovalForAll(address operator, bool approved) external;

    /**
     * @dev Returns true if `operator` is approved to transfer ``account``'s tokens.
     *
     * See {setApprovalForAll}.
     */
    function isApprovedForAll(address account, address operator)
        external
        view
        returns (bool);

    /**
     * @dev Transfers `amount` tokens of token type `id` from `from` to `to`.
     *
     * Emits a {TransferSingle} event.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - If the caller is not `from`, it must have been approved to spend ``from``'s tokens via {setApprovalForAll}.
     * - `from` must have a balance of tokens of type `id` of at least `amount`.
     * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the
     * acceptance magic value.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes calldata data
    ) external;

    /**
     * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}.
     *
     * Emits a {TransferBatch} event.
     *
     * Requirements:
     *
     * - `ids` and `amounts` must have the same length.
     * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the
     * acceptance magic value.
     */
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] calldata ids,
        uint256[] calldata amounts,
        bytes calldata data
    ) external;
}

pragma solidity ^0.8.0;

/* Mythic Gateways and the Mythic Journey [an overview]

   Turn-based RPG which does at least the following:

   1. Requires a Liquid Key to open a Mythic Gateway.
      The Liquid Key must be added to the Mythic Gateway contract.

   2. Requires the user to select an NFT from one of the pre-approved contracts,
      such as Liquid Legends, Liquid Invaders, Azuki Ape Social Club, or
      Typical Tigers.

      a) If a Typical Tiger is selected, also requires a Baby Cub Tiger
         to be permanently stored inside the contract

      - Writes the Token ID of the Liquid Key + the contract of the NFT
        being used along with the Token ID of the NFT being used.

   3. Requires a Mythic Artifact to be burned during the initial setup
      of the Mythic Gateway.

   4. We use the ETH blockrate to determine how many points are gained during
      the time the gateway is open (the NFT is on a "Journey"). Points accrue
      at a specific rate per Journey.

   5. During the Journey, the NFT randomly accumulates items, which are dynamic,
      based on a list of contracts (also dynamic) and periodically the user can
      interact with the contract and claim any item which was randomly
      acquired on the Journey.

   *  Other stuff is proprietary, so... go fack.

 */

// Burn an artifact and open the Gateway
interface burnArtifactOpenGateway {
    function openGateway(address tokenOwner, uint256 tokenId)
        external
        returns (bool);
}

pragma solidity ^0.8.14;

// See notes above for details... this contract is pretty intense
contract MythicGateway is Ownable, ReentrancyGuard, Pausable {
    using EnumerableSet for EnumerableSet.UintSet;
    using Strings for uint256;

    // Setup the contracts for the pieces in the Journey
    address public Legends = 0x372405A6d95628Ad14518BfE05165D397f43dE1D;
    address public Invaders = 0x2f3A9adc5301600Cd9205eF7657cF0733fF71D04;
    address public Tigers = 0xf4744Ec5D846F7f1a0c5d389F590Cc1344eD3fCf;
    address public AzukiApes = 0x813b5c4aE6b188F4581aa1dfdB7f4Aba44AA578B;
    address public LiquidKeys = 0x753412F4FB7245BCF1c0714fDf59ba89110f39b8;
    address public Artifacts = 0x753412F4FB7245BCF1c0714fDf59ba89110f39b8;

    // Setup the Liquid Key Journey (one address and many possible keys)
    // Define a struct to hold the key, ID, and token ID values
    struct Journeys {
        uint256 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