contract SerumNFT is
    ERC721EnumerableUpgradeable,
    OwnableUpgradeable,
    ReentrancyGuardUpgradeable,
    ERC721URIStorageUpgradeable
{
    using SafeMath for uint256;
    
    
    // counter for token Ids
    using CountersUpgradeable for CountersUpgradeable.Counter;
    
    CountersUpgradeable.Counter private _tokenIds;
    CountersUpgradeable.Counter private _collectionIds;

    // NFT tokens Mappings
    // Mapping from TokenID to TokenMeta data
    mapping(uint256 => TokenMeta) private _tokenMeta;
    // Mapping from TokenID to CollectionMeta data
    mapping(uint256 => CollectionMeta) private _tokenCollection;
    // Mapping from TokenID to CardMeta data
    mapping(uint256 => CardMeta) private _tokenCard;

    // Mapping from collection id to collection meta
    mapping(uint256 => CollectionMeta) private _collectionMeta;

    // baseURI of token
    string private baseURI;

    //transformed nft contract
    ITransformedNFT private _transformedNftContract;

    //trwasury wallet
    address private treasuryWallet;
    address private adminWallet;

    // token metadata structure
    struct TokenMeta {
        uint256 id;
        string name;
        address owner;
        uint256 price;
        uint256 transformationFee;
        string fullUri;
        string emptyUri;
        string description;
        string category; 
        uint256 status;  // full = 2 or empty = 1
        uint256 classify; // 1 generic , 2 collection , 3 card
        address collectionAddress; // to be added incase classify not equal 2 or 3
        uint256 cardNum; // to be added incase classify equal 3 
    }

    // collection specific metadata structure
    struct CollectionMeta { 
        address collection_contract;
        string collection_name;
    }

    // card specific metadata structure
    struct CardMeta {
        address card_contract;
        uint card_id;
    }

    
    // set the address of the transformed nft contract
    function setTransformedContract (address transformedContract_) public onlyOwner{
       _transformedNftContract = ITransformedNFT(transformedContract_);
    }

    //set the treasury wallet
    function setTreasuryWallet (address wallet) public onlyOwner {
        treasuryWallet = wallet;
    }

    //set the treasury wallet
    function setAdminRole (address to) public onlyOwner {
        adminWallet = to;
    }

     /**
     * @dev initializes function is added instead of constructor() in "@openzeppelin/contracts"
     */
    function initialize () public initializer {
        OwnableUpgradeable.__Ownable_init();
        ReentrancyGuardUpgradeable.__ReentrancyGuard_init();
        ERC721Upgradeable.__ERC721_init(
            "Serum NFTs",
            "SN"
        );
        setBaseURI("ipfs://");
    }

    function setBaseURI(string memory _newBaseURI) public virtual onlyOwner {
        baseURI = _newBaseURI;
    }
    
    function _baseURI() internal view virtual override returns (string memory) {
        return baseURI;
    }
  
     /**
     * @dev sets token meta
     * @param _tokenId uint256 token ID (token number)
     * @param _meta TokenMeta
     *
     * Requirements:
     * `tokenId` must exist
     * `owner` must the msg.owner
     */
    function _setTokenMeta(uint256 _tokenId, TokenMeta memory _meta) private {
        require(_exists(_tokenId));
        require(ownerOf(_tokenId) == _msgSender() || owner() == _msgSender() || adminWallet == _msgSender());
        _tokenMeta[_tokenId] = _meta;
    }

    /**
     * @dev returns tokenId metadata
     * @param _tokenId uint256 token ID (token number)
     *
     * Requirements:
     * `tokenId` must exist
     */
    function tokenMeta(uint256 _tokenId) public view returns (TokenMeta memory) {
        require(_exists(_tokenId));
        return _tokenMeta[_tokenId];
    }


    function getOwner(address add, uint256 id) public view returns (address) {
        return IERC721Upgradeable(add).ownerOf(id);
    }

    /** From the inherited contracs */
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC721Upgradeable, ERC721EnumerableUpgradeable)
        returns (bool)
    {
        return interfaceId == type(IERC721EnumerableUpgradeable).interfaceId || super.supportsInterface(interfaceId);
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal override(ERC721Upgradeable, ERC721EnumerableUpgradeable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function _burn(uint256 tokenId) internal override(ERC721URIStorageUpgradeable, ERC721Upgradeable) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721URIStorageUpgradeable, ERC721Upgradeable)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }
    
    /** end of inherited contracts */


    /** Get All Serums*/
    function getAllTokens () public view virtual returns (TokenMeta[] memory) {
        TokenMeta [] memory allTokens = new TokenMeta[] (_tokenIds.current());
        for (uint  i = 0; i < _tokenIds.current() ; i++){
            allTokens[i] = _tokenMeta[i+1];
        }
        return allTokens;
    }

    /** Get serums of owner*/
    function getOwnerTokens (address _owner) public view virtual returns (TokenMeta[] memory){
        uint numOwnerTokens = ERC721Upgradeable.balanceOf(_owner);
        TokenMeta[] memory ownerTokens = new TokenMeta[](numOwnerTokens);
        for (uint i = 0; i < numOwnerTokens ; i++){
            ownerTokens[i] =_tokenMeta[tokenOfOwnerByIndex(_owner, i)];
        }
        return ownerTokens;
    }

         
    // add new collection 
    function setCollection (address _collectionAddress, string memory _collectionName) public onlyOwner{
        _collectionIds.increment();
        uint256 newCollectionId = _collectionIds.current();
        CollectionMeta memory _collection = CollectionMeta (_collectionAddress,_collectionName);
        _collectionMeta[newCollectionId] = _collection;   
    }

    // get collection name by address
    function getAllCollections() public view virtual returns (CollectionMeta[] memory) {
        CollectionMeta [] memory allCollections = new CollectionMeta[] (_collectionIds.current());
        for (uint  i = 0; i < _collectionIds.current() ; i++){
            allCollections[i] = _collectionMeta[i+1];
        }
        return allCollections;
    }


    /**
     * @dev purchase _tokenId
     * @param _fullTokenURI string memory tokenURI
     * @param _emptyTokenURI string memory empty tokenURI
     * @param _name string memory token name
     * @param _description string memory for description
     * @param _category serum category
     * @param _price uint256 token price
     * @param _transformationFee fees of applying serum 
     * @param _classify int for serum type
     * Requirements:
     * '_price' must be positive
     */
    function mintCollectable(
        
        address _owner,
        string memory _fullTokenURI,
        string memory _emptyTokenURI,
        string memory _name,
        string memory _description,
        string memory _category,
        uint256 _price,
        uint256 _transformationFee,
        uint256 _classify,
        address _collectionAddress,
        uint256 _cardNum

    ) public returns (uint256) {
        require(_msgSender() == adminWallet || _msgSender() == owner(), 'you are not authorized to add new serum');
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(_owner, newItemId);
        _setTokenURI(newItemId, _fullTokenURI);
       
        TokenMeta memory meta = TokenMeta(
            newItemId,
            _name,
            _owner,
            _price,
            _transformationFee,
            _fullTokenURI,
            _emptyTokenURI,
            _description,
            _category,
            2, //Full by default at minting
            _classify,
            _collectionAddress,
            _cardNum     
        );

        _setTokenMeta(newItemId, meta);
        return newItemId;
    }
    
    /** refil serum !! */
    function refilSerum(uint256 _serumId) public payable {
        require(msg.sender == adminWallet || msg.sender == owner() || msg.sender == ownerOf(_serumId),
         'you are not authorized to refil serum');
        require(_tokenMeta[_serumId].status == 1, "serum is already full");

        uint256 _price = _tokenMeta[_serumId].price;
        uint256 _refilFeePercentage = _price.mul(80);
        uint256 _refilFees = _refilFeePercentage.div(100);

        require(msg.value >= _refilFees, 'insufficient amount');

        _tokenMeta[_serumId].status = 2;
        _setTokenURI(_serumId, _tokenMeta[_serumId].fullUri);
        payable(treasuryWallet).transfer(msg.value); // transfer money to sender 
    }

    /**
     * @dev purchase _tokenId
     * @param _serumId token ID
     * @param _owner address of the owner
     * @param _originalNftContract original nft contract
     * @param _originalNftId original nft id
     * @param _transformedUri URI for the generated nft
     * @param _name URI for the generated nft
     * Requirements:
     * caller must be the owner
     */
   function transform(
        address payable _owner,
        string memory _transformedUri,
        string memory _name,
        address _originalNftContract,
        uint256 _originalNftId,
        uint256 _serumId
    ) public payable returns (uint256) {
        // require tokenId to exist, if not code panics
        require(_exists(_serumId), "ERC721Metadata: nonexistent token");
        // require owner of the tokenId to be the one calling the function,
        // if not code panics
        require(
            ownerOf(_serumId) == _msgSender() || owner() == _msgSender(),
            "Ownable: caller is not the owner"
        );

        // check address of sender
        require(msg.sender != address(0) , "Address of sender is not valid");


        // check treasury wallet
        require(treasuryWallet != address(0) , "Address of receiver is not valid");

        // check msg value to be bigger than fee
        require(
            msg.value >= _tokenMeta[_serumId].transformationFee,
            "Please submit the asking price in order to complete the purchase"
        );

        // require caller to be owner of the orignal nft
        require(IERC721Upgradeable(_originalNftContract).ownerOf(_originalNftId) == _msgSender(), "caller is not the owner of original nft");
        //require serum to be full (2) (not used)
        require(_tokenMeta[_serumId].status == 2, "Serum is already used");

        // require the serum to be general or incase of collection specific or card specific to match the collection address and card id    
        require(
            (_tokenMeta[_serumId].classify == 1) ||
            (_tokenMeta[_serumId].classify == 2  && _tokenMeta[_serumId].collectionAddress == _originalNftContract) ||
            (_tokenMeta[_serumId].classify == 3  && _tokenMeta[_serumId].collectionAddress == _originalNftContract 
                && _tokenMeta[_serumId].cardNum  == _originalNftId)
            , "Serum is not valid for this NFT"
        );

        /** send MATIC transformation fees to treasury wallet */
        payable(treasuryWallet).transfer(msg.value);
        /** call the contract to mint */
        uint256 TransformedNFTId = _transformedNftContract.mintCollectable(
            _owner,
            _transformedUri,
            _name,
            _originalNftContract,
            _originalNftId,
            _serumId,
            _tokenMeta[_serumId].classify
        );
        //change state of the serum nft incase transaction done successfully
        _tokenMeta[_serumId].status = 1;
        _setTokenURI(_serumId, _tokenMeta[_serumId].emptyUri); // change token uri
        return TransformedNFTId;
    }


    /**
     * @dev purchase new serum
     * @param _serumId serum ID
     * @param _owner user address
     * Requirements:
     * existence of serum
     */
    function MintUser(address payable _owner, uint256 _serumId) 
        public payable returns (uint256) {
        
        // check existence of serum and owner of the serum is the deployer
        require(_exists(_serumId) && ownerOf(_serumId) == owner(), "Serum not exist");

        // check address of sender
        require(msg.sender != address(0) , "Address of sender is not valid");

        // check treasury wallet
        require(treasuryWallet != address(0) , "Address of receiver is not valid");

        // check msg value to be bigger than fee
        require(
            msg.value >= _tokenMeta[_serumId].price,
            "Please submit the asking price in order to complete the purchase"
        );

        /** send MATIC transformation fees to treasury wallet */
        payable(treasuryWallet).transfer(msg.value);

        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();
        _mint(_owner, newItemId);

        _setTokenURI(newItemId, _tokenMeta[_serumId].fullUri);
        _setTokenMeta(newItemId, _tokenMeta[_serumId]);
        _tokenMeta[newItemId].owner = _owner;
        _tokenMeta[newItemId].id = newItemId;

        return newItemId;
    }       
}
function getAllCollections() public view virtual returns (CollectionMeta[] memory) {
        CollectionMeta [] memory allCollections = new CollectionMeta[] (_collectionIds.current());
        for (uint  i = 0; i < _collectionIds.current() ; i++){
            allCollections[i] = _collectionMeta[i+1];
        }
        return allCollections;
    }
// get collection name by address