// File: ApymonPack.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC721Receiver.sol";
import './IERC1155Receiver.sol';
import "./ERC165.sol";
import './Events.sol';
import './Ownable.sol';

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
}

interface IERC721 {
    function ownerOf(uint256 tokenId) external view returns (address owner);
    function totalSupply() external view returns (uint256);
    function exists(uint256 tokenId) external view returns (bool);
    function approve(address to, uint256 tokenId) external;
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function mintCreature() external returns (uint256 creatureId);
}

interface IERC1155 {
    function setApprovalForAll(address operator, bool approved) external;
    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes calldata data
    ) external;
}

interface ISwap {
    function swapErc20(
        uint256 eggId,
        address inToken,
        uint256 inAmount,
        address outToken,
        uint8 router,
        address to
    ) external;
    function swapErc721(
        uint256 eggId,
        address inToken,
        uint256 inId,
        address outToken,
        uint8 router,
        address to
    ) external;
    function swapErc1155(
        uint256 eggId,
        address inToken,
        uint256 inId,
        uint256 inAmount,
        address outToken,
        uint256 outId,
        uint8 router,
        address to
    ) external;
}

contract ApymonPack is ERC165, IERC1155Receiver, IERC721Receiver, Context, Events, Ownable {

    struct Token {
        uint8 tokenType; // 1: ERC20, 2: ERC721, 3: ERC1155
        address tokenAddress;
    }

    // Token types
    uint8 private constant TOKEN_TYPE_ERC20 = 1;
    uint8 private constant TOKEN_TYPE_ERC721 = 2;
    uint8 private constant TOKEN_TYPE_ERC1155 = 3;

    uint256 private constant MAX_EGG_SUPPLY = 6400;

    // Mapping from egg ID -> token(erc20) -> balance
    mapping(uint256 => mapping(address => uint256)) private _insideERC20TokenBalances;

    // Mapping from egg ID -> token(erc1155) -> tokenId -> balance
    mapping(uint256 => mapping(address => mapping(uint256 => uint256))) private _insideERC1155TokenBalances;

    // Mapping from egg ID -> tokens
    mapping(uint256 => Token[]) private _insideTokens;

    // Mapping from egg ID -> token(erc721 or erc1155) -> ids
    mapping(uint256 => mapping(address => uint256[])) private _insideTokenIds;

    // Mapping from egg ID -> locked time
    mapping(uint256 => uint256) private _lockedTimestamp;

    // Mapping from egg ID -> opened
    mapping(uint256 => bool) private _opened;

    IERC721 public _apymon;

    bool public _enableClose;
    ISwap public _swap;

    modifier onlyEggOwner(uint256 eggId) {
        require(_apymon.exists(eggId));
        require(_apymon.ownerOf(eggId) == msg.sender);
        _;
    }

    modifier unlocked(uint256 eggId) {
        require(
            _lockedTimestamp[eggId] == 0 ||
            _lockedTimestamp[eggId] < block.timestamp
        );
        _;
    }

    modifier opened(uint256 eggId) {
        require(isOpened(eggId));
        _;
    }

    constructor(
        address apymon
    ) {
        _apymon = IERC721(apymon);
        _enableClose = false;
    }

    // View functions

    /**
     * @dev check if egg has been locked.
     */
    function existsId(
        uint256 eggId,
        address token,
        uint256 id
    ) public view returns (bool) {
        uint256[] memory ids = _insideTokenIds[eggId][token];

        for (uint256 i; i < ids.length; i++) {
            if (ids[i] == id) {
                return true;
            }
        }

        return false;
    }

    /**
     * @dev check if egg has been locked.
     */
    function isLocked(
        uint256 eggId
    ) external view returns (bool locked, uint256 endTime) {
        if (
            _lockedTimestamp[eggId] == 0 ||
            _lockedTimestamp[eggId] < block.timestamp
        ) {
            locked = false;
        } else {
            locked = true;
            endTime = _lockedTimestamp[eggId];
        }
    }

    /**
     * @dev check if egg opened or not.
     */
    function isOpened(
        uint256 eggId
    ) public view returns (bool) {
        return _opened[eggId];
    }

    /**
     * @dev check if claimed creature for certain egg.
     */
    function isClaimedCreature(
        uint256 eggId
    ) public view returns (bool) {
        return _apymon.exists(eggId + MAX_EGG_SUPPLY);
    }

    /**
     * @dev check if tokenId exists in egg
     */
    function getInsideTokensCount(
        uint256 eggId
    ) public view opened(eggId) returns (
        uint256 erc20Len,
        uint256 erc721Len,
        uint256 erc1155Len
    ) {
        Token[] memory tokens = _insideTokens[eggId];
        for (uint256 i; i < tokens.length; i++) {
            Token memory token = tokens[i];
            if (token.tokenType == TOKEN_TYPE_ERC20) {
                erc20Len += 1;
            }
            if (token.tokenType == TOKEN_TYPE_ERC721) {
                erc721Len += 1;
            }
            if (token.tokenType == TOKEN_TYPE_ERC1155) {
                erc1155Len += 1;
            }
        }
    }

    /**
     * @dev get tokens by eggId
     */
    function getTokens(
        uint256 eggId
    ) external view opened(eggId) returns (
        uint8[] memory tokenTypes,
        address[] memory tokenAddresses
    ) {
        Token[] memory tokens = _insideTokens[eggId];
        
        tokenTypes = new uint8[](tokens.length);
        tokenAddresses = new address[](tokens.length);

        for (uint256 i; i < tokens.length; i++) {
            tokenTypes[i] = tokens[i].tokenType;
            tokenAddresses[i] = tokens[i].tokenAddress;
        }        
    }

    /**
     * @dev get ERC20 token info
     */
    function getERC20Tokens(
        uint256 eggId
    ) public view opened(eggId) returns (
        address[] memory addresses,
        uint256[] memory tokenBalances
    ) {
        Token[] memory tokens = _insideTokens[eggId];
        (
            uint256 erc20Len,
            ,
        ) = getInsideTokensCount(eggId);
        
        tokenBalances = new uint256[](erc20Len);
        addresses = new address[](erc20Len);
        uint256 j;

        for (uint256 i; i < tokens.length; i++) {
            Token memory token = tokens[i];
            if (token.tokenType == TOKEN_TYPE_ERC20) {
                addresses[j] = token.tokenAddress;
                tokenBalances[j] = _insideERC20TokenBalances[eggId][token.tokenAddress];
                j++;
            }
        }        
    }

    /**
     * @dev get ERC721 token info
     */
    function getERC721Tokens(
        uint256 eggId
    ) public view opened(eggId) returns (
        address[] memory addresses,
        uint256[] memory tokenBalances
    ) {
        Token[] memory tokens = _insideTokens[eggId];
        (
            ,
            uint256 erc721Len
            ,
        ) = getInsideTokensCount(eggId);
        
        tokenBalances = new uint256[](erc721Len);
        addresses = new address[](erc721Len);
        uint256 j;

        for (uint256 i; i < tokens.length; i++) {
            Token memory token = tokens[i];
            if (token.tokenType == TOKEN_TYPE_ERC721) {
                addresses[j] = token.tokenAddress;
                tokenBalances[j] = _insideTokenIds[eggId][token.tokenAddress].length;
                j++;
            }
        }
    }

    /**
     * @dev get ERC721 or ERC1155 ids
     */
    function getERC721OrERC1155Ids(
        uint256 eggId,
        address insideToken
    ) public view opened(eggId) returns (uint256[] memory) {
        return _insideTokenIds[eggId][insideToken];
    }

    /**
     * @dev get ERC1155 token addresses info
     */
    function getERC1155Tokens(
        uint256 eggId
    ) public view opened(eggId) returns (address[] memory addresses) {
        Token[] memory tokens = _insideTokens[eggId];
        (
            ,
            ,
            uint256 erc1155Len
        ) = getInsideTokensCount(eggId);
        
        addresses = new address[](erc1155Len);
        uint256 j;

        for (uint256 i; i < tokens.length; i++) {
            Token memory token = tokens[i];
            if (token.tokenType == TOKEN_TYPE_ERC1155) {
                addresses[j] = token.tokenAddress;
                j++;
            }
        }
    }

    /**
     * @dev get ERC1155 token balances by ids
     */
    function getERC1155TokenBalances(
        uint256 eggId,
        address insideToken,
        uint256[] memory tokenIds
    ) public view opened(eggId) returns (uint256[] memory tokenBalances) {
        tokenBalances = new uint256[](tokenIds.length);
        for (uint256 i; i < tokenIds.length; i++) {
            tokenBalances[i] = _insideERC1155TokenBalances[eggId][insideToken][tokenIds[i]];
        }
    }

    // Write functions

    /**
     * @dev lock egg.
     */
    function lockEgg(
        uint256 eggId,
        uint256 timeInSeconds
    ) external onlyEggOwner(eggId) opened(eggId) unlocked(eggId) {
        _lockedTimestamp[eggId] = block.timestamp + timeInSeconds;
        emit LockedEgg(
            eggId,
            msg.sender,
            block.timestamp,
            block.timestamp + timeInSeconds
        );
    }

    function setEnableClose(bool enabled) external onlyOwner {
        _enableClose = enabled;
    }

    function setSwap(address swap) external onlyOwner {
        _swap = ISwap(swap);
    }

    /**
     * @dev open egg.
     */
    function openEgg(
        uint256 eggId,
        bool isClaimCreature
    ) external onlyEggOwner(eggId) {
        _opened[eggId] = true;
        emit OpenedEgg(
            eggId,
            msg.sender
        );

        if (isClaimCreature && !isClaimedCreature(eggId)) {
            claimCreature(eggId);
        }
    }

    /**
     * @dev open egg.
     */
    function closeEgg(
        uint256 eggId
    ) external onlyEggOwner(eggId) {
        require(_enableClose == true);
        _opened[eggId] = false;
        emit ClosedEgg(
            eggId,
            msg.sender
        );
    }

    /**
     * @dev deposit erc20 tokens into egg.
     */
    function depositErc20IntoEgg(
        uint256 eggId,
        address[] memory tokens,
        uint256[] memory amounts
    ) external {
        require(
            tokens.length > 0 &&
            tokens.length == amounts.length
        );

        for (uint256 i; i < tokens.length; i++) {
            require(tokens[i] != address(0));
            IERC20 iToken = IERC20(tokens[i]);

            uint256 prevBalance = iToken.balanceOf(address(this));
            iToken.transferFrom(
                msg.sender,
                address(this),
                amounts[i]
            );
            uint256 receivedAmount = iToken.balanceOf(address(this)) - prevBalance;

            _increaseInsideTokenBalance(
                eggId,
                TOKEN_TYPE_ERC20,
                tokens[i],
                receivedAmount
            );

            if (isOpened(eggId)) {
                emit DepositedErc20IntoEgg(
                    eggId,
                    msg.sender,
                    tokens[i],
                    receivedAmount
                );
            }
        }
    }

    /**
     * @dev withdraw erc20 tokens from egg.
     */
    function withdrawErc20FromEgg(
        uint256 eggId,
        address[] memory tokens,
        uint256[] memory amounts,
        address to
    ) public onlyEggOwner(eggId) unlocked(eggId) opened(eggId) {
        require(
            tokens.length > 0 &&
            tokens.length == amounts.length
        );

        for (uint256 i; i < tokens.length; i++) {
            require(tokens[i] != address(0));
            IERC20 iToken = IERC20(tokens[i]);

            iToken.transfer(to, amounts[i]);

            _decreaseInsideTokenBalance(
                eggId,
                TOKEN_TYPE_ERC20,
                tokens[i],
                amounts[i]
            );
            emit WithdrewErc20FromEgg(
                eggId,
                msg.sender,
                tokens[i],
                amounts[i],
                to
            );
        }
    }

    /**
     * @dev send erc20 tokens from my egg to another egg.
     */
    function sendErc20(
        uint256 fromEggId,
        address[] memory tokens,
        uint256[] memory amounts,
        uint256 toEggId
    ) public onlyEggOwner(fromEggId) unlocked(fromEggId) opened(fromEggId) {
        require(fromEggId != toEggId);
        require(
            tokens.length > 0 &&
            tokens.length == amounts.length
        );

        for (uint256 i; i < tokens.length; i++) {
            require(tokens[i] != address(0));
            require(_apymon.exists(toEggId));

            _decreaseInsideTokenBalance(
                fromEggId,
                TOKEN_TYPE_ERC20,
                tokens[i],
                amounts[i]
            );

            _increaseInsideTokenBalance(
                toEggId,
                TOKEN_TYPE_ERC20,
                tokens[i],
                amounts[i]
            );

            emit SentErc20(
                fromEggId,
                msg.sender,
                tokens[i],
                amounts[i],
                toEggId
            );
        }
    }

    /**
     * @dev deposit erc721 tokens into egg.
     */
    function depositErc721IntoEgg(
        uint256 eggId,
        address token,
        uint256[] memory tokenIds
    ) external {
        require(token != address(0));

        for (uint256 i; i < tokenIds.length; i++) {
            require(
                token != address(this) ||
                (token == address(this) && eggId != tokenIds[i])
            );
            IERC721 iToken = IERC721(token);
            
            iToken.safeTransferFrom(
                msg.sender,
                address(this),
                tokenIds[i]
            );

            _putInsideTokenId(
                eggId,
                token,
                tokenIds[i]
            );

            if (isOpened(eggId)) {
                emit DepositedErc721IntoEgg(
                    eggId,
                    msg.sender,
                    token,
                    tokenIds[i]
                );
            }
        }
    }

    /**
     * @dev withdraw erc721 token from egg.
     */
    function withdrawErc721FromEgg(
        uint256 eggId,
        address token,
        uint256[] memory tokenIds,
        address to
    ) public onlyEggOwner(eggId) unlocked(eggId) opened(eggId) {
        require(token != address(0));
        IERC721 iToken = IERC721(token);

        for (uint256 i; i < tokenIds.length; i++) {
            address tokenOwner = iToken.ownerOf(tokenIds[i]);

            require(tokenOwner == address(this));

            iToken.safeTransferFrom(
                tokenOwner,
                to,
                tokenIds[i]
            );

            _popInsideTokenId(
                eggId,
                token,
                tokenIds[i]
            );

            emit WithdrewErc721FromEgg(
                eggId,
                msg.sender,
                token,
                tokenIds[i],
                to
            );
        }
    }

    /**
     * @dev send erc721 tokens from my egg to another egg.
     */
    function sendErc721(
        uint256 fromEggId,
        address token,
        uint256[] memory tokenIds,
        uint256 toEggId
    ) public onlyEggOwner(fromEggId) unlocked(fromEggId) opened(fromEggId) {
        require(fromEggId != toEggId);
        require(token != address(0));
        require(_apymon.exists(toEggId));

        for (uint256 i; i < tokenIds.length; i++) {
            _popInsideTokenId(
                fromEggId,
                token,
                tokenIds[i]
            );

            _putInsideTokenId(
                toEggId,
                token,
                tokenIds[i]
            );

            emit SentErc721(
                fromEggId,
                msg.sender,
                token,
                tokenIds[i],
                toEggId
            );
        }
    }

    /**
     * @dev deposit erc1155 token into egg.
     */
    function depositErc1155IntoEgg(
        uint256 eggId,
        address token,
        uint256[] memory tokenIds,
        uint256[] memory amounts
    ) external {
        require(token != address(0));
        IERC1155 iToken = IERC1155(token);

        for (uint256 i; i < tokenIds.length; i++) {
            iToken.safeTransferFrom(
                msg.sender,
                address(this),
                tokenIds[i],
                amounts[i],
                bytes("")
            );

            _putInsideTokenIdForERC1155(
                eggId,
                token,
                tokenIds[i]
            );

            _increaseInsideERC1155TokenBalance(
                eggId,
                TOKEN_TYPE_ERC1155,
                token,
                tokenIds[i],
                amounts[i]
            );

            if (isOpened(eggId)) {
                emit DepositedErc1155IntoEgg(
                    eggId,
                    msg.sender,
                    token,
                    tokenIds[i],
                    amounts[i]
                );
            }
        }
    }

    /**
     * @dev withdraw erc1155 token from egg.
     */
    function withdrawErc1155FromEgg(
        uint256 eggId,
        address token,
        uint256[] memory tokenIds,
        uint256[] memory amounts,
        address to
    ) public onlyEggOwner(eggId) unlocked(eggId) opened(eggId) {
        require(token != address(0));
        IERC1155 iToken = IERC1155(token);

        for (uint256 i; i < tokenIds.length; i++) {
            uint256 tokenId = tokenIds[i];
            uint256 amount = amounts[i];

            iToken.safeTransferFrom(
                address(this),
                to,
                tokenId,
                amount,
                bytes("")
            );

            _decreaseInsideERC1155TokenBalance(
                eggId,
                token,
                tokenId,
                amount
            );

            _popInsideTokenIdForERC1155(
                eggId,
                token,
                tokenId
            );

            _popERC1155FromEgg(
                eggId,
                token,
                tokenId
            );
            emit WithdrewErc1155FromEgg(
                eggId,
                msg.sender,
                token,
                tokenId,
                amount,
                to
            );
        }
    }

    /**
     * @dev send erc1155 token from my egg to another egg.
     */
    function sendErc1155(
        uint256 fromEggId,
        address token,
        uint256[] memory tokenIds,
        uint256[] memory amounts,
        uint256 toEggId
    ) public onlyEggOwner(fromEggId) unlocked(fromEggId) opened(fromEggId) {
        require(fromEggId != toEggId);
        require(token != address(0));
        require(_apymon.exists(toEggId));

        for (uint256 i; i < tokenIds.length; i++) {
            uint256 tokenId = tokenIds[i];
            uint256 amount = amounts[i];

            _decreaseInsideERC1155TokenBalance(
                fromEggId,
                token,
                tokenId,
                amount
            );

            _increaseInsideERC1155TokenBalance(
                toEggId,
                TOKEN_TYPE_ERC1155,
                token,
                tokenId,
                amount
            );

            _popInsideTokenIdForERC1155(
                fromEggId,
                token,
                tokenId
            );

            _putInsideTokenIdForERC1155(
                toEggId,
                token,
                tokenId
            );

            _popERC1155FromEgg(
                fromEggId,
                token,
                tokenId
            );
            emit SentErc1155(
                fromEggId,
                msg.sender,
                token,
                tokenId,
                amount,
                toEggId
            );
        }
    }

    /**
     * @dev withdraw all of inside tokens into specific address.
     */
    function withdrawAll(
        uint256 eggId,
        address to
    ) external {
        require(to != address(0));
        (address[] memory erc20Addresses, uint256[] memory erc20Balances) = getERC20Tokens(eggId);
        withdrawErc20FromEgg(
            eggId,
            erc20Addresses,
            erc20Balances,
            to
        );

        (address[] memory erc721Addresses, ) = getERC721Tokens(eggId);
        for (uint256 a; a < erc721Addresses.length; a++) {
            uint256[] memory ids = getERC721OrERC1155Ids(
                eggId,
                erc721Addresses[a]
            );
            withdrawErc721FromEgg(
                eggId,
                erc721Addresses[a],
                ids,
                to
            );
        }

        address[] memory erc1155Addresses = getERC1155Tokens(eggId);
        for (uint256 a; a < erc1155Addresses.length; a++) {
            uint256[] memory ids = getERC721OrERC1155Ids(
                eggId,
                erc1155Addresses[a]
            );
            uint256[] memory tokenBalances = getERC1155TokenBalances(
                eggId,
                erc1155Addresses[a],
                ids
            );
            withdrawErc1155FromEgg(
                eggId,
                erc1155Addresses[a],
                ids,
                tokenBalances,
                to
            );
        }
    }

    /**
     * @dev send all of inside tokens to specific egg.
     */
    function sendAll(
        uint256 fromEggId,
        uint256 toEggId
    ) external {
        (
            address[] memory erc20Addresses,
            uint256[] memory erc20Balances
        ) = getERC20Tokens(fromEggId);
        sendErc20(
            fromEggId,
            erc20Addresses,
            erc20Balances,
            toEggId
        );

        (
            address[] memory erc721Addresses
            ,
        ) = getERC721Tokens(fromEggId);
        for (uint256 a; a < erc721Addresses.length; a++) {
            uint256[] memory ids = getERC721OrERC1155Ids(
                fromEggId,
                erc721Addresses[a]
            );
            sendErc721(
                fromEggId,
                erc721Addresses[a],
                ids,
                toEggId
            );
        }

        address[] memory erc1155Addresses = getERC1155Tokens(fromEggId);
        for (uint256 a; a < erc1155Addresses.length; a++) {
            uint256[] memory ids = getERC721OrERC1155Ids(
                fromEggId,
                erc1155Addresses[a]
            );
            uint256[] memory tokenBalances = getERC1155TokenBalances(
                fromEggId,
                erc1155Addresses[a],
                ids
            );
            sendErc1155(
                fromEggId,
                erc1155Addresses[a],
                ids,
                tokenBalances,
                toEggId
            );
        }
    }
    
    /**
     * @dev external function to increase token balance of egg
     */
    function increaseInsideTokenBalance(
        uint256 eggId,
        uint8 tokenType,
        address token,
        uint256 amount
    ) external {
        require(msg.sender != address(0));
        require(msg.sender == address(_apymon));

        _increaseInsideTokenBalance(
            eggId,
            tokenType,
            token,
            amount
        );
    }

    /**
     * @dev external function to put creature into egg
     * Must be called by egg owner
     */
    function claimCreature(
        uint256 eggId
    ) public onlyEggOwner(eggId) {
        uint256 creatureId = _apymon.mintCreature();

        _putInsideTokenId(
            eggId,
            address(_apymon),
            creatureId
        );

        emit DepositedErc721IntoEgg(
            eggId,
            address(this),
            address(_apymon),
            creatureId
        );
    }

    function swapErc20(
        uint256 eggId,
        address inToken,
        uint256 inAmount,
        address outToken,
        uint8 router,
        address to
    ) external onlyEggOwner(eggId) unlocked(eggId) opened(eggId) {
        require(address(_swap) != address(0));
        require(_insideERC20TokenBalances[eggId][inToken] >= inAmount);

        IERC20(inToken).approve(address(_swap), inAmount);

        _swap.swapErc20(
            eggId,
            inToken,
            inAmount,
            outToken,
            router,
            to
        );
        emit SwapedErc20(
            msg.sender,
            eggId,
            inToken,
            inAmount,
            outToken,
            to
        );

        _decreaseInsideTokenBalance(
            eggId,
            TOKEN_TYPE_ERC20,
            inToken,
            inAmount
        );
    }

    function swapErc721(
        uint256 eggId,
        address inToken,
        uint256 inId,
        address outToken,
        uint8 router,
        address to
    ) external onlyEggOwner(eggId) unlocked(eggId) opened(eggId) {
        require(address(_swap) != address(0));
        require(existsId(eggId, inToken, inId));
        
        IERC721(inToken).approve(address(_swap), inId);

        _swap.swapErc721(
            eggId,
            inToken,
            inId,
            outToken,
            router,
            to
        );
        emit SwapedErc721(
            msg.sender,
            eggId,
            inToken,
            inId,
            outToken,
            to
        );

        _popInsideTokenId(
            eggId,
            inToken,
            inId
        );
    }

    function swapErc1155(
        uint256 eggId,
        address inToken,
        uint256 inId,
        uint256 inAmount,
        address outToken,
        uint256 outId,
        uint8 router,
        address to
    ) external onlyEggOwner(eggId) unlocked(eggId) opened(eggId) {
        require(address(_swap) != address(0));
        require(existsId(eggId, inToken, inId));
        require(
            _insideERC1155TokenBalances[eggId][inToken][inId] >= inAmount
        );

        IERC1155(inToken).setApprovalForAll(address(_swap), true);

        _swap.swapErc1155(
            eggId,
            inToken,
            inId,
            inAmount,
            outToken,
            outId,
            router,
            to
        );
        emit SwapedErc1155(
            msg.sender,
            eggId,
            inToken,
            inId,
            inAmount,
            outToken,
            outId,
            to
        );

        _decreaseInsideERC1155TokenBalance(
            eggId,
            inToken,
            inId,
            inAmount
        );

        _popInsideTokenIdForERC1155(
            eggId,
            inToken,
            inId
        );

        _popERC1155FromEgg(
            eggId,
            inToken,
            inId
        );
    }

    function _popERC1155FromEgg(
        uint256 eggId,
        address token,
        uint256 tokenId
    ) private {
        uint256[] memory ids = _insideTokenIds[eggId][token];
        if (
            _insideERC1155TokenBalances[eggId][token][tokenId] == 0 && 
            ids.length == 0
        ) {
            delete _insideERC1155TokenBalances[eggId][token][tokenId];
            delete _insideTokenIds[eggId][token];
            _popTokenFromEgg(
                eggId,
                TOKEN_TYPE_ERC1155,
                token
            );
        }
    }
    
    /**
     * @dev private function to increase token balance of egg
     */
    function _increaseInsideTokenBalance(
        uint256 eggId,
        uint8 tokenType,
        address token,
        uint256 amount
    ) private {
        _insideERC20TokenBalances[eggId][token] += amount;
        _putTokenIntoEgg(
            eggId,
            tokenType,
            token
        );
    }

    /**
     * @dev private function to increase erc1155 token balance of egg
     */
    function _increaseInsideERC1155TokenBalance(
        uint256 eggId,
        uint8 tokenType,
        address token,
        uint256 tokenId,
        uint256 amount
    ) private {
        _insideERC1155TokenBalances[eggId][token][tokenId] += amount;
        _putTokenIntoEgg(
            eggId,
            tokenType,
            token
        );
    }

    /**
     * @dev private function to decrease token balance of egg
     */
    function _decreaseInsideTokenBalance(
        uint256 eggId,
        uint8 tokenType,
        address token,
        uint256 amount
    ) private {
        require(_insideERC20TokenBalances[eggId][token] >= amount);
        _insideERC20TokenBalances[eggId][token] -= amount;
        if (_insideERC20TokenBalances[eggId][token] == 0) {
            delete _insideERC20TokenBalances[eggId][token];
            _popTokenFromEgg(
                eggId,
                tokenType,
                token
            );
        }
    }

    /**
     * @dev private function to decrease erc1155 token balance of egg
     */
    function _decreaseInsideERC1155TokenBalance(
        uint256 eggId,
        address token,
        uint256 tokenId,
        uint256 amount
    ) private {
        require(_insideERC1155TokenBalances[eggId][token][tokenId] >= amount);
        _insideERC1155TokenBalances[eggId][token][tokenId] -= amount;
    }

    /**
     * @dev private function to put a token id to egg
     */
    function _putInsideTokenId(
        uint256 eggId,
        address token,
        uint256 tokenId
    ) private {
        uint256[] storage ids = _insideTokenIds[eggId][token];
        ids.push(tokenId);
    }

    /**
     * @dev private function to put a token id to egg in ERC1155
     */
    function _putInsideTokenIdForERC1155(
        uint256 eggId,
        address token,
        uint256 tokenId
    ) private {
        uint256[] storage ids = _insideTokenIds[eggId][token];
        bool isExist;
        for (uint256 i; i < ids.length; i++) {
            if (ids[i] == tokenId) {
                isExist = true;
            }
        }
        if (!isExist) {
            ids.push(tokenId);
        }
    }

    /**
     * @dev private function to pop a token id from egg
     */
    function _popInsideTokenId(
        uint256 eggId,
        address token,
        uint256 tokenId
    ) private {
        uint256[] storage ids = _insideTokenIds[eggId][token];
        for (uint256 i; i < ids.length; i++) {
            if (ids[i] == tokenId) {
                ids[i] = ids[ids.length - 1];
                ids.pop();
            }
        }

        if (ids.length == 0) {
            delete _insideTokenIds[eggId][token];
        }
    }

    /**
     * @dev private function to pop a token id from egg in ERC1155
     */
    function _popInsideTokenIdForERC1155(
        uint256 eggId,
        address token,
        uint256 tokenId
    ) private {
        uint256 tokenBalance = _insideERC1155TokenBalances[eggId][token][tokenId];
        if (tokenBalance <= 0) {
            delete _insideERC1155TokenBalances[eggId][token][tokenId];
            _popInsideTokenId(
                eggId,
                token,
                tokenId
            );
        }
    }

    /**
     * @dev put token(type, address) to egg
     */
    function _putTokenIntoEgg(
        uint256 eggId,
        uint8 tokenType,
        address tokenAddress
    ) private {
        Token[] storage tokens = _insideTokens[eggId];
        bool exists = false;
        for (uint256 i; i < tokens.length; i++) {
            if (
                tokens[i].tokenType == tokenType &&
                tokens[i].tokenAddress == tokenAddress
            ) {
                exists = true;
                break;
            }
        }

        if (!exists) {
            tokens.push(Token({
                tokenType: tokenType,
                tokenAddress: tokenAddress
            }));
        }
    }

    /**
     * @dev pop token(type, address) from egg
     */
    function _popTokenFromEgg(
        uint256 eggId,
        uint8 tokenType,
        address tokenAddress
    ) private {
        Token[] storage tokens = _insideTokens[eggId];
        for (uint256 i; i < tokens.length; i++) {
            if (
                tokens[i].tokenType == tokenType &&
                tokens[i].tokenAddress == tokenAddress
            ) {
                tokens[i] = tokens[tokens.length - 1];
                tokens.pop();
                break;
            }
        }

        if (tokens.length == 0) {
            delete _insideTokens[eggId];
        }
    }
   
    function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata
    ) external pure override returns (bytes4) {
        return this.onERC721Received.selector;
    }

    function onERC1155Received(
        address,
        address,
        uint256,
        uint256,
        bytes calldata
    ) external pure override returns(bytes4) {
        return 0xf23a6e61;
    }

    function onERC1155BatchReceived(
        address,
        address,
        uint256[] calldata,
        uint256[] calldata,
        bytes calldata
    ) external pure override returns(bytes4) {
        return 0xbc197c81;
    }
}

// File: Context.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/*
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691
        return msg.data;
    }
}

// File: ERC165.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC165.sol";

/**
 * @dev Implementation of the {IERC165} interface.
 *
 * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check
 * for the additional interface id that will be supported. For example:
 *
 * ```solidity
 * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
 *     return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
 * }
 * ```
 *
 * Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation.
 */
abstract contract ERC165 is IERC165 {
    /*
     * bytes4(keccak256('supportsInterface(bytes4)')) == 0x01ffc9a7
     */
    bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7;

    /**
     * @dev Mapping of interface ids to whether or not it's supported.
     */
    mapping(bytes4 => bool) private _supportedInterfaces;

    constructor () {
        // Derived contracts need only register support for their own interfaces,
        // we register support for ERC165 itself here
        _registerInterface(_INTERFACE_ID_ERC165);
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     *
     * Time complexity O(1), guaranteed to always use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return _supportedInterfaces[interfaceId];
    }

    /**
     * @dev Registers the contract as an implementer of the interface defined by
     * `interfaceId`. Support of the actual ERC165 interface is automatic and
     * registering its interface id is not required.
     *
     * See {IERC165-supportsInterface}.
     *
     * Requirements:
     *
     * - `interfaceId` cannot be the ERC165 invalid interface (`0xffffffff`).
     */
    function _registerInterface(bytes4 interfaceId) internal virtual {
        require(interfaceId != 0xffffffff, "ERC165: invalid interface id");
        _supportedInterfaces[interfaceId] = true;
    }
}

// File: Events.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Events {
    event LockedEgg(
        uint256 eggId,
        address indexed owner,
        uint256 startTimestamp,
        uint256 endTimestamp
    );

    event OpenedEgg(
        uint256 eggId,
        address indexed owner
    );

    event ClosedEgg(
        uint256 eggId,
        address indexed owner
    );

    event DepositedErc20IntoEgg(
        uint256 eggId,
        address indexed owner,
        address indexed erc20Token,
        uint256 amount
    );

    event WithdrewErc20FromEgg(
        uint256 eggId,
        address indexed owner,
        address indexed erc20Token,
        uint256 amount,
        address indexed to
    );

    event SentErc20(
        uint256 fromEggId,
        address indexed owner,
        address indexed erc20Token,
        uint256 amount,
        uint256 toEggId
    );

    event DepositedErc721IntoEgg(
        uint256 eggId,
        address indexed owner,
        address indexed erc721Token,
        uint256 tokenId
    );

    event WithdrewErc721FromEgg(
        uint256 eggId,
        address indexed owner,
        address indexed erc721Token,
        uint256 tokenId,
        address indexed to
    );

    event SentErc721(
        uint256 fromEggId,
        address indexed owner,
        address indexed erc721Token,
        uint256 tokenId,
        uint256 toEggId
    );

    event DepositedErc1155IntoEgg(
        uint256 eggId,
        address indexed owner,
        address indexed erc1155Token,
        uint256 tokenId,
        uint256 amount
    );

    event WithdrewErc1155FromEgg(
        uint256 eggId,
        address indexed owner,
        address indexed erc1155Token,
        uint256 tokenId,
        uint256 amount,
        address indexed to
    );

    event SentErc1155(
        uint256 fromEggId,
        address indexed owner,
        address indexed erc1155Token,
        uint256 tokenId,
        uint256 amount,
        uint256 toEggId
    );

    event SwapedErc20(
        address indexed owner,
        uint256 eggId,
        address inToken,
        uint256 inAmount,
        address outToken,
        address indexed to
    );

    event SwapedErc721(
        address indexed owner,
        uint256 eggId,
        address inToken,
        uint256 inId,
        address outToken,
        address indexed to
    );

    event SwapedErc1155(
        address indexed owner,
        uint256 eggId,
        address inToken,
        uint256 inId,
        uint256 inAmount,
        address outToken,
        uint256 outId,
        address indexed to
    );
}
// File: IERC1155Receiver.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC165.sol";

/**
 * _Available since v3.1._
 */
interface IERC1155Receiver is IERC165 {

    /**
        @dev Handles the receipt of a single ERC1155 token type. This function is
        called at the end of a `safeTransferFrom` after the balance has been updated.
        To accept the transfer, this must return
        `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`
        (i.e. 0xf23a6e61, or its own function selector).
        @param operator The address which initiated the transfer (i.e. msg.sender)
        @param from The address which previously owned the token
        @param id The ID of the token being transferred
        @param value The amount of tokens being transferred
        @param data Additional data with no specified format
        @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed
    */
    function onERC1155Received(
        address operator,
        address from,
        uint256 id,
        uint256 value,
        bytes calldata data
    )
        external
        returns(bytes4);

    /**
        @dev Handles the receipt of a multiple ERC1155 token types. This function
        is called at the end of a `safeBatchTransferFrom` after the balances have
        been updated. To accept the transfer(s), this must return
        `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`
        (i.e. 0xbc197c81, or its own function selector).
        @param operator The address which initiated the batch transfer (i.e. msg.sender)
        @param from The address which previously owned the token
        @param ids An array containing ids of each token being transferred (order and length must match values array)
        @param values An array containing amounts of each token being transferred (order and length must match ids array)
        @param data Additional data with no specified format
        @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed
    */
    function onERC1155BatchReceived(
        address operator,
        address from,
        uint256[] calldata ids,
        uint256[] calldata values,
        bytes calldata data
    )
        external
        returns(bytes4);
}

// File: IERC165.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC165 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-165[EIP].
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others ({ERC165Checker}).
 *
 * For an implementation, see {ERC165}.
 */
interface IERC165 {
    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

// File: IERC721Receiver.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
 * @title ERC721 token receiver interface
 * @dev Interface for any contract that wants to support safeTransfers
 * from ERC721 asset contracts.
 */
interface IERC721Receiver {
    /**
     * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
     * by `operator` from `from`, this function is called.
     *
     * It must return its Solidity selector to confirm the token transfer.
     * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted.
     *
     * The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`.
     */
    function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4);
}

// File: Ownable.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./Context.sol";
/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor () {
        address msgSender = _msgSender();
        _owner = msgSender;
        emit OwnershipTransferred(address(0), msgSender);
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
        _;
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        emit OwnershipTransferred(_owner, address(0));
        _owner = address(0);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        emit OwnershipTransferred(_owner, newOwner);
        _owner = newOwner;
    }
}