contract Shiny_Media is EIP712, Ownable {
    using SafeMath for uint256;

    struct Token {
        bool _isFungible;
        uint256 _totalSupply;
        string _title;
        string _uri;
        address _artist;
        uint256 _bidDuration;
        uint256 _price;
    }

    event MintToken(
        uint256 _totalSupply,
        uint256 _royaltyPoints,
        string _uri,
        string _title,
        address _owner
    );

    event TokenCounter(uint256);

    event Claim(uint _tokenID, address _tokenOwner, address _recipient);
    event Buy(uint _tokenID, address _from , address _recipient);

    address private _ERC1155Address;
    address private _marketAddress;
    address private _ERC721Address;

    address private _adminAddress;

    uint256 private _tokenCounter;

    // TokenHash => tokenID
    mapping(bytes32 => uint256) private _tokenHashToTokenID;

    // tokenID => Owner
    mapping(uint256 => address) private _nftToOwners;

    // tokenID => Creator
    mapping(uint256 => address) private _nftToCreators;

    // tokenID => Token
    mapping(uint256 => Token) private _tokenIDToToken;

    // userAddress => (tokenID => (category => QTY))
    mapping(address => mapping(uint256 => mapping(string => uint256)))
        private _tokenSellingTypes;

    modifier whenTokenExist(uint256 _tokenID) {
        require(
            _tokenIDToToken[_tokenID]._artist != address(0),
            "Media: The Token Does Not Exist!"
        );
        _;
    }

    modifier onlyAdmin() {
        require(_adminAddress == msg.sender, "Media: Unauthorized Access!");
        _;
    }

    fallback() external {}

    receive() external payable {}

    constructor(
        address _ERC1155,
        address _ERC721,
        address _market,
        address adminAddress_
    ) EIP712("", "1.0.0") Ownable() {
        _ERC1155Address = _ERC1155;
        _ERC721Address = _ERC721;
        _marketAddress = _market;
        _adminAddress = adminAddress_;
    }

    // *** Private Methods Start ***
    function _hash(
        Token memory _token,
        uint16 _royaltyPoints,
        string memory _tokenSellingType
    ) private pure returns (bytes32) {
        bytes32 typeHash = keccak256(
            abi.encodePacked(
                "bool _isFungible",
                "uint256 _totalSupply",
                "string _title",
                "string _uri",
                "address _artist",
                "uint256 _bidDuration",
                "uint256 _price",
                "uint16 _royaltyPoints",
                "string _tokenSellingType"
            )
        );
        bytes32 valueHash = keccak256(
            abi.encodePacked(
                _token._isFungible,
                _token._totalSupply,
                _token._title,
                _token._uri,
                _token._artist,
                _token._bidDuration,
                _token._price,
                _royaltyPoints,
                _tokenSellingType
            )
        );
        return keccak256(abi.encodePacked(typeHash, valueHash));
    }

    function _verify(bytes32 digest, bytes memory signature)
        private
        view
        returns (bool)
    {
        return ECDSA.recover(digest, signature) == _adminAddress;
    }

    function getTokenHash(
        string memory _uri,
        string memory _title,
        uint256 _totalSupply
    ) private pure returns (bytes32) {
        return keccak256(abi.encodePacked(_uri, _title, _totalSupply));
    }

    function mint(
        Token memory _newToken,
        uint16 _royaltyPoints,
        string calldata _tokenSellingType,
        bytes calldata signature
    ) private {
        // Verify signature
        require(
            _verify(
                _hash(_newToken, _royaltyPoints, _tokenSellingType),
                signature
            ),
            "Media: Invalid Signature!"
        );
        _mintToken(_newToken, _royaltyPoints, _tokenSellingType);
    }

    function _mintToken(
        Token memory _newToken,
        uint16 _royaltyPoints,
        string calldata _tokenSellingType
    ) private returns (bool) {
        require(
            !(_newToken._totalSupply > 1 && !_newToken._isFungible),
            "Media: Multiple Copies Cannot Be Non Fungible!"
        );

        require(
            !(_newToken._totalSupply > 1 &&
                keccak256(abi.encodePacked(_tokenSellingType)) ==
                keccak256(abi.encodePacked("Auction"))),
            "Media: Multiple Copies Cannot Be On Auction"
        );

        require(
            keccak256(abi.encodePacked(_tokenSellingType)) ==
                keccak256(abi.encodePacked("Auction")) ||
                keccak256(abi.encodePacked(_tokenSellingType)) ==
                keccak256(abi.encodePacked("Fixed Sale")) ||
                keccak256(abi.encodePacked(_tokenSellingType)) ==
                keccak256(abi.encodePacked("Unlockable")),
            "Media: Invalid Selling Type Provided!"
        );

        // Calculate hash of the Token
        bytes32 tokenHash = getTokenHash(
            _newToken._uri,
            _newToken._title,
            _newToken._totalSupply
        );

        // Check if Token with same data exists
        require(
            _tokenHashToTokenID[tokenHash] == 0,
            "Media: Token With Same Data Already Exist!"
        );

        // Increase the Token Counter
        _tokenCounter++;
        // Store the hash
        _tokenHashToTokenID[tokenHash] = _tokenCounter;
        // Set the Token
        _tokenIDToToken[_tokenCounter] = _newToken;

        if (_newToken._isFungible) {
            ERC1155Minter(_ERC1155Address).mint(
                _tokenCounter,
                _newToken._artist,
                _newToken._totalSupply,
                _royaltyPoints,
                _newToken._uri
            );
        } else {
            ERC721Minter(_ERC721Address).mintToken(
                _tokenCounter,
                _newToken._artist,
                _royaltyPoints,
                _newToken._uri
            );
        }

        _tokenSellingTypes[_newToken._artist][_tokenCounter][
            _tokenSellingType
        ] = _newToken._totalSupply;

        emit MintToken(
            _newToken._totalSupply,
            _royaltyPoints,
            _newToken._uri,
            _newToken._title,
            _newToken._artist
        );
        emit TokenCounter(_tokenCounter);
        return true;
    }

    function _transfer(
        uint256 _tokenID,
        address _from,
        address _recipient,
        uint256 _amount
    ) private {
        if (_tokenIDToToken[_tokenID]._isFungible) {
            ERC1155Minter(_ERC1155Address).transferToken(
                _from,
                _recipient,
                _tokenID,
                _amount
            );
        } else {
            ERC721Minter(_ERC721Address).transferToken(
                _from,
                _recipient,
                _tokenID
            );
        }
    }
    // *** Private Methods End ***

    // *** Admin Methods Start ***
    function mintToken(
        Token memory _newToken,
        uint16 _royaltyPoints,
        string calldata _tokenSellingType,
        bytes calldata _signature
    ) public onlyAdmin {
        mint(_newToken, _royaltyPoints, _tokenSellingType, _signature);
    }

    function mintAndTransfer(
        Token memory _newToken,
        uint16 _royaltyPoints,
        string calldata _tokenSellingType,
        bytes calldata _signature,
        uint256 _quantity,
        address _recipient
    ) external onlyAdmin {
        // Calculate hash of the Token
        bytes32 tokenHash = getTokenHash(
            _newToken._uri,
            _newToken._title,
            _newToken._totalSupply
        );
        if (_tokenHashToTokenID[tokenHash] == 0) {
            mintToken(_newToken, _royaltyPoints, _tokenSellingType, _signature);
        }
        _transfer(
            _tokenHashToTokenID[tokenHash],
            _newToken._artist,
            _recipient,
            _quantity
        );
    }
    // *** Admin Methods End ***

    function getAdminAddress() external view returns (address) {
        return _adminAddress;
    }

    // *** User Methods Start ***
    function buyNow(
        Token memory _newToken,
        uint16 _royaltyPoints,
        string calldata _tokenSellingType,
        bytes calldata _signature,
        uint256 _quantity
    ) external payable {
        require(
            keccak256(abi.encodePacked(_tokenSellingType)) ==
                keccak256(abi.encodePacked("Fixed Sale")),
            "Media: Invalid Selling Type!"
        );
        require(_newToken._artist != msg.sender, "Media: Owner Cannot Buy!");
        require(_quantity >= 1, "Media: Invalid Quantity!");
        bytes32 tokenHash = getTokenHash(
            _newToken._uri,
            _newToken._title,
            _newToken._totalSupply
        );
        if (_tokenHashToTokenID[tokenHash] == 0) {
            mint(_newToken, _royaltyPoints, _tokenSellingType, _signature);
        }

        require(
            _tokenSellingTypes[_newToken._artist][
                _tokenHashToTokenID[tokenHash]
            ]["Fixed Sale"] >= 1,
            "Media: Token Is Not On Sale!"
        );

        if (!_newToken._isFungible) {
            require(
                ERC721Minter(_ERC721Address).ownerOf(
                    _tokenHashToTokenID[tokenHash]
                ) == _newToken._artist,
                "Media: Token Already Sold!"
            );
        }
        if (_newToken._price != 0) {
            if (_quantity == 1)
                require(
                    msg.value == _newToken._price,
                    "Media: Wrong Amount Transferred!"
                );
            else
                require(
                    msg.value == _newToken._price.mul(_quantity),
                    "Media: Wrong Amount Transferred!"
                );
        }

        _transfer(
            _tokenHashToTokenID[tokenHash],
            _newToken._artist,
            msg.sender,
            _quantity
        );
        _tokenSellingTypes[msg.sender][_tokenHashToTokenID[tokenHash]][
            "Unlockable"
        ] = _tokenSellingTypes[msg.sender][_tokenHashToTokenID[tokenHash]][
            "Unlockable"
        ].add(1);

        payable(_marketAddress).transfer(msg.value);
        IShiny_Market(_marketAddress)._divideMoney(
            msg.value,
            _adminAddress,
            _newToken._artist
        );
        emit Buy(_tokenHashToTokenID[tokenHash], _newToken._artist, msg.sender);
    }

    function bid(uint256 _tokenID) external payable whenTokenExist(_tokenID) {
        require(msg.value != 0, "Media: Cannon Bid With Zero Amount!");

        Token memory token = _tokenIDToToken[_tokenID];
        if (token._isFungible) {
            // ERC1155 Token
            require(
                ERC1155Minter(_ERC1155Address).balanceOf(
                    token._artist,
                    _tokenID
                ) >= 1,
                "Media: Owner Does Not Have Token!"
            );
        } else {
            // ERC721 Token
            require(
                ERC721Minter(_ERC721Address).ownerOf(_tokenID) != msg.sender,
                "Media: Owner Cannot Bid!"
            );
            require(
                ERC721Minter(_ERC721Address).ownerOf(_tokenID) == token._artist,
                "Media: Token Already Sold!"
            );
        }
        require(
            _tokenSellingTypes[token._artist][_tokenID]["Auction"] == 1,
            "Media: Token Is Not On Auction!"
        );

        // Transfer Bid Amount to Market
        payable(_marketAddress).transfer(msg.value);
        IShiny_Market(_marketAddress).bid(
            _tokenID,
            msg.sender,
            msg.value,
            token._bidDuration
        );
    }

    function claim(uint256 _tokenID) external whenTokenExist(_tokenID) {
        IShiny_Market(_marketAddress).claimNFT(
            _tokenID,
            msg.sender,
            _adminAddress,
            _tokenIDToToken[_tokenID]._artist
        );

        _transfer(_tokenID, _tokenIDToToken[_tokenID]._artist, msg.sender, 1);

        _tokenSellingTypes[_tokenIDToToken[_tokenID]._artist][_tokenID][
            "Auction"
        ] = _tokenSellingTypes[_tokenIDToToken[_tokenID]._artist][_tokenID][
            "Auction"
        ].sub(1);

        // Add Tokens to Unlockable selling type for the New Owner
        _tokenSellingTypes[msg.sender][_tokenID][
            "Unlockable"
        ] = _tokenSellingTypes[msg.sender][_tokenID]["Unlockable"].add(1);

        emit Claim(_tokenID, _tokenIDToToken[_tokenID]._artist, msg.sender);
    }
    // *** User Methods End ***
}
function mintToken(
    Token memory _newToken,
    uint16 _royaltyPoints,
    string calldata _tokenSellingType,
    bytes calldata _signature
) public onlyAdmin {
    mint(_newToken, _royaltyPoints, _tokenSellingType, _signature);
}

// *** Private Methods End ***
// *** Admin Methods Start ***