// Contract: MythicJourney
// Target function: function onERC1155Received(
    ) 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
            )
        );
    }

    // @notice updateActiveJourney update an existing ActiveJourneys entry
    // @params owner is the owner of the Journey
    // @params keyTokenId is the token id of the Liquid Key
    // @params nftAddress is the address of the passive Journey NFT contract
    // @params nftTokenId is the token of the passive Journey NFT
    // @params artifactId is the token id of the artifact burned to activate
    // @params mythicWorld is the name (string) of the Mythic World for Journey
    function updateActiveJourney(
        address owner,
        uint256 keyTokenId,
        address nftAddress,
        uint256 nftTokenId,
        uint256 artifactId,
        string memory mythicWorld
    ) public whenNotPaused nonReentrant {
        // Make sure the keyTokenId is in the contract
        require(
            IERC721(LiquidKeys).ownerOf(keyTokenId) == address(this),
            "You are not on a Journey and not the contract owner"
        );
        // Make sure the owner and the keyTokenId are written to the contract
        // or we are the owner of the contract
        require(
            isActiveJourneyKey(owner, keyTokenId) ||
                msg.sender == LiquidDeployer,
            "You are not the owner of this nft or the owner of the contract"
        );

        // TODO: Make sure we own the nftAddress & nftTokenId
        require(
            IERC721(nftAddress).ownerOf(nftTokenId) == msg.sender,
            "You do not own the NFT for this Journey"
        );

        for (uint256 i = 0; i < ActiveJourneys.length; i++) {
            if (
                ActiveJourneys[i].keyTokenId == keyTokenId &&
                ActiveJourneys[i].owner == owner
            ) {
                ActiveJourneys[i].nftAddress = nftAddress;
                ActiveJourneys[i].nftTokenId = nftTokenId;
                ActiveJourneys[i].artifactId = artifactId;
                ActiveJourneys[i].mythicWorld = mythicWorld;
            }
        }
    }

    // @notice endActiveJourney ends an ActiveJourney for a given owner
    // @params owner the address of the owner
    // @params keyTokenId the key token id
    function endActiveJourney(address owner, uint256 keyTokenId)
        public
        whenNotPaused
        nonReentrant
    {
        // Make sure the keyTokenId is in the contract
        require(
            IERC721(LiquidKeys).ownerOf(keyTokenId) == address(this) ||
                msg.sender == LiquidDeployer,
            "You are not on a Journey and not the contract owner"
        );

        // Make sure the owner and the keyTokenId are written to the contract
        // or we are the owner of the contract
        require(
            isActiveJourneyKey(owner, keyTokenId) ||
                msg.sender == LiquidDeployer,
            "You are not the owner of this nft or the owner of the contract"
        );

        // Transfer the key into the wallet of the original owner
        IERC721(LiquidKeys).safeTransferFrom(
            address(this),
            msg.sender,
            keyTokenId,
            "Not withdrawn: Transfer failed."
        );

        removeJourneyEntry(owner, keyTokenId);
    }

    // ------------------------------------------------------------------
    // Random Number generation, calculations, and other helper functions
    // ------------------------------------------------------------------

    // @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;
    }

    // ---------------------------------------------------------------
    // Private functions to make life so much easier for this contract
    // ---------------------------------------------------------------

    // @notice storeLiquidKey stores a key in the contract (requires a Liquid Key)
    // @params tokenId of the Liquid Keys NFT
    //         Requires the token to be approved before sending it to this contract
    //
    function storeLiquidKey(uint256 tokenId) private returns (bool) {
        // Check if the contract is approved to transfer the specified token
        require(
            IERC721(LiquidKeys).getApproved(tokenId) == address(this),
            "Journeys Contract is not approved to transfer this token"
        );

        // Make sure the Liquid Key is owned
        require(
            IERC721(LiquidKeys).ownerOf(tokenId) == msg.sender,
            "You do not own that Liquid Key (nice try)."
        );

        // Transfer the key from the sender to the contract
        IERC721(LiquidKeys).safeTransferFrom(
            msg.sender,
            address(this),
            tokenId,
            "Failed to transfer token to contract"
        );

        // Return true if the key transfer was successful
        return true;
    }

    function removeJourneyEntry(address owner, uint256 keyTokenId) private {
        // Find the index of the journey with the specified key
        for (uint256 i = 0; i < ActiveJourneys.length; i++) {
            if (
                ActiveJourneys[i].keyTokenId == keyTokenId &&
                ActiveJourneys[i].owner == owner
            ) {
                delete ActiveJourneys[i];
                break;
            }
        }
    }

    event ArtifactBurned(
        address indexed _sender,
        uint256 _artifactTokenId,
        uint256 _totalToBurn
    );

    // @notice burnMyArtifacts call the Mythic Artifacts contract function
    // @params artifactTokenId the token id to "burn"
    // @params totalToBurn the total artifacts to burn for the token id given
    //
    function burnMyArtifacts(uint256 artifactTokenId, uint256 totalToBurn)
        private
    {
        // We must own at least totalToBurn of token
        require(
            ArtifactsContract.balanceOf(msg.sender, artifactTokenId) >=
                totalToBurn,
            "You do not have enough of this Artifact to burn"
        );

        ArtifactsContract.safeTransferFrom(
            msg.sender,
            address(this),
            artifactTokenId,
            totalToBurn,
            "Artifact transfer failed."
        );

        // log the artifact burn event
        emit ArtifactBurned(msg.sender, artifactTokenId, totalToBurn);
    }