// Contract: MythicJourney
// Target function: function updateActiveJourney(
 not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and make it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        // On the first call to nonReentrant, _notEntered will be true
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        // Any calls to nonReentrant after this point will fail
        _status = _ENTERED;

        _;

        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = _NOT_ENTERED;
    }
}

pragma solidity ^0.8.0;

/**
 * @dev Required interface of an ERC1155 compliant contract, as defined in the
 * https://eips.ethereum.org/EIPS/eip-1155[EIP].
 *
 * _Available since v3.1._
 */
interface IERC1155 is IERC165 {
    /**
     * @dev Emitted when `value` tokens of token type `id` are transferred from `from` to `to` by `operator`.
     */
    event TransferSingle(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256 id,
        uint256 value
    );

    /**
     * @dev Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all
     * transfers.
     */
    event TransferBatch(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256[] ids,
        uint256[] values
    );

    /**
     * @dev Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to
     * `approved`.
     */
    event ApprovalForAll(
        address indexed account,
        address indexed operator,
        bool approved
    );

    /**
     * @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI.
     *
     * If 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;
}

/* 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.

 */

pragma solidity ^0.8.16;

contract MythicJourney is
    Ownable,
    ReentrancyGuard,
    Pausable,
    ERC1155TokenReceiver,
    IERC721Receiver
{
    using Strings for uint256;

    // Setup the contracts for the pieces in the Journey
    address public Tigers = 0xf4744Ec5D846F7f1a0c5d389F590Cc1344eD3fCf;
    address public Cubs = 0x98d3cd2F29a4F5464266f925Fe177018e6c2F9E6;

    address public LiquidDeployer = 0x866cfDa1B7cD90Cd250485cd8b700211480845D7;
    address public LiquidKeys = 0x753412F4FB7245BCF1c0714fDf59ba89110f39b8;
    address public Artifacts = 0xDde7505f40a61032Ed076452f85C0F54DFA208Bd;

    // ----------------------------------------------------------------------------
    // Setup the Liquid Key Journey (one address and many possible keys)
    // Define a struct to hold the key, ID, and token ID values
    struct Journey {
        uint256 keyTokenId;
        address owner;
        address nftAddress;
        uint256 nftTokenId;
        uint256 artifactId;
        string mythicWorld;
        uint256 start;
    }

    Journey[] public ActiveJourneys; // ActiveJourneys
    address[] public JourneyContracts; // Contracts allowed on the ActiveJourneys

    // Populate the contract with all the setup details
    constructor() {
        // Add the Contracts for the Journey items
        JourneyContracts.push(0x372405A6d95628Ad14518BfE05165D397f43dE1D); // Legends
        JourneyContracts.push(0x2f3A9adc5301600Cd9205eF7657cF0733fF71D04); // Invaders
        JourneyContracts.push(0x813b5c4aE6b188F4581aa1dfdB7f4Aba44AA578B); // Azuki Apes
        JourneyContracts.push(0xf4744Ec5D846F7f1a0c5d389F590Cc1344eD3fCf); // Tigers
    }

    IERC1155 ArtifactsContract = IERC1155(address(Artifacts));

    /**
     * @notice setLiquidDeployer set a new deployer contract (fail-safe)
     * @param deployerAddress address of the deployer contract
     */
    function setLiquidDeployer(address deployerAddress) external onlyOwner {
        LiquidDeployer = deployerAddress;
    }

    // -------------------------------------------------------------------------
    // JourneyContracts hold a list of all NFTs that can go on a Journey
    // -------------------------------------------------------------------------

    // @notice isAddressInJourneyContracts helper function to check if address exists
    // @params theContract address of the contract to check
    function isAddressInJourneyContracts(address theContract)
        public
        view
        returns (bool)
    {
        for (uint256 i = 0; i < JourneyContracts.length; i++) {
            if (JourneyContracts[i] == theContract) {
                return true;
            }
        }
        return false;
    }

    // -------------------------------------------------------------------------
    // Functions to add a new activeJourney and manage some of the open Journeys
    // -------------------------------------------------------------------------
    function liquidKeyTokenIdsByOwner(address owner)
        public
        view
        returns (uint256[] memory)
    {
        uint256[] memory liquidKeyTokenIds;
        uint256 counter = 0;
        for (uint256 i = 0; i < ActiveJourneys.length; i++) {
            Journey memory journey = ActiveJourneys[i];
            if (journey.owner == owner) {
                liquidKeyTokenIds[counter] = journey.keyTokenId;
                counter++;
            }
        }
        return liquidKeyTokenIds;
    }

    function allOwners() public view returns (address[] memory) {
        address[] memory owners;
        uint256 counter = 0;
        for (uint256 i = 0; i < ActiveJourneys.length; i++) {
            Journey storage journey = ActiveJourneys[i];
            owners[i] = journey.owner;
            counter++;
        }
        return owners;
    }

    // @notice activeJourneyDetails returns the details of a particular Journey
    // @param address is the address of the Journey wallet
    // @param keyTokenId is the token id of the Liquid Key
    // @returns a tuple of the Journey details
    function activeJourneyDetails(address addr, uint256 keyTokenId)
        public
        view
        returns (Journey memory)
    {
        for (uint256 i = 0; i < ActiveJourneys.length; i++) {
            Journey memory journey = ActiveJourneys[i];
            if (journey.owner == addr && journey.keyTokenId == keyTokenId) {
                return journey;
            }
        }

        // Return an empty journey if no matching keyTokenId was found
        return Journey(0, address(0), address(0), 0, 0, "", 0);
    }

    // @notice isActiveJourneyKey returns true or false if the owner + key exists
    // @params owner the owner of the key (i.e. you)
    // @params key the keyTokenId original stored in the array
    function isActiveJourneyKey(address owner, uint256 key)
        public
        view
        returns (bool)
    {
        Journey memory journey = activeJourneyDetails(owner, key);

        return journey.owner == owner && journey.keyTokenId == key;
    }

    // @notice addActiveJourneys add several journys as the contract owner
    function addActiveJourneys(Journey[] memory _journeys) public onlyOwner {
        // Iterate through the array of journeys to add
        for (uint256 i = 0; i < _journeys.length; i++) {
            Journey memory journey = _journeys[i];
            ActiveJourneys.push(journey);
        }
    }

    event CubNotFound(
        address indexed _sender,
        uint256 keyId,
        uint256 cubTokenId
    );

    // Start the Journey (with each individual NFT tied to a specific key)
    //
    // 1. Place a key in the Mythic Journey contract
    // 2. Select an NFT (Legend, Invader, Azuki Ape, or Tiger)
    // 3. Burn an Artifact and write the Token Id into the mapping
    //
    function newActiveJourney(
        address owner,
        uint256 liquidKeyTokenId,
        address nftAddress,
        uint256 nftTokenId,
        uint256 artifactTokenId,
        string memory mythicWorld,
        uint256 cubTokenId,
        uint256 start
    ) public whenNotPaused {
        require(
            msg.sender == owner || msg.sender == LiquidDeployer,
            "You are not the owner or the contract"
        );

        // Make sure this key is not already on a Journey
        require(
            isActiveJourneyKey(owner, liquidKeyTokenId) == false,
            "Key is already on a Journey"
        );

        // Do we own the key or are we the owner of the contract
        require(
            IERC721(LiquidKeys).ownerOf(liquidKeyTokenId) == msg.sender,
            "You are not the owner of this key"
        );

        // Do we own the nft or are we the contract owner?
        require(
            IERC721(nftAddress).ownerOf(nftTokenId) == msg.sender ||
                msg.sender == LiquidDeployer,
            "You are not the owner of this nft or the owner of the contract"
        );

        // You can only burn an artifact 1-18 or 42
        require(
            (artifactTokenId > 0 && artifactTokenId <= 18) ||
                artifactTokenId == 42,
            "Not a valid Artifact"
        );

        require(bytes(mythicWorld).length > 5, "Mythic World not possible");

        require(
            isAddressInJourneyContracts(nftAddress),
            "This NFT cannot take a Journey"
        );

        // If this is a tiger, we must have already burned a cub
        if (nftAddress == Tigers) {
            // Check to see if Cub is in this contract
            if (IERC721(Cubs).ownerOf(cubTokenId) != address(this)) {
                emit CubNotFound(msg.sender, liquidKeyTokenId, cubTokenId);
                return;
            }
        }

        if (msg.sender != LiquidDeployer) {
            // Transfer the key from the sender to the contract
            require(storeLiquidKey(liquidKeyTokenId), "Key not stored");
            // Burn the necessary artifact
            burnMyArtifacts(artifactTokenId, 1);
        }

        // Make sure the start is reasonable (after 12/12/22 at 12pm)
        start = start < 1670864400 ? block.timestamp : start;

        ActiveJourneys.push(
            Journey(
                liquidKeyTokenId,
                owner,
                nftAddress,
                nftTokenId,
                artifactTokenId,
                mythicWorld,
                start
            )
        );
    }