contract ElysiumClub is ERC721Enumerable, Ownable, RoyaltiesV2Impl {

    using SafeMath for uint256;
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    string private _dataHostBaseURI = "ipfs://QmWHx293w768mZkYnqVswPYVzWzUwjCK3qNfqQnYSpVsZ1/";
    string private _lockedDataHostBaseURI = "ipfs://QmbdjoBLUMCkqZKfXB7n8YSjkVYAPVxrX4UEMkFDgWHNSH/";
    string private _contractURI = "https://ipfs.io/ipfs/Qmer1bmCfytNAswiTey2jGxZBBf1NXVwPbUSZoQtzAXAMC";
    string private _placeHolderHash = "ipfs://QmSkArc6e1s7no6F1kEhgnoXQc6fqeaiNTfBxf5z66a2kJ";

    uint256 public maxMints = 6888;
    bool public publicBuyEnabled = false;
    bool public treasuresRevealed = false;

    mapping(address => bool) private isCollabPartner;
    mapping(address => uint256) private _userMints;
    
    uint256 public maxUserMints = 10;

    uint256 private _price = 300000000000000000;
    uint256 private _collabPrice = 400000000000000000;
    
    uint96 private _raribleRoyaltyPercentage = 500;
    address payable _beneficiary = payable(address(0x12CcBd4b15052f261FD472731Db4F353ffc5ee89));
    address payable _raribleBeneficiary = payable(address(0x12CcBd4b15052f261FD472731Db4F353ffc5ee89));

    event BeneficiaryChanged(address payable indexed previousBeneficiary, address payable indexed newBeneficiary);
    event RaribleBeneficiaryChanged(address payable indexed previousBeneficiary, address payable indexed newBeneficiary);
    event BeneficiaryPaid(address payable beneficiary, uint256 amount);
    event PriceChange(uint256 previousPrice, uint256 newPrice);
    event RaribleRoyaltyPercentageChange(uint96 previousPercentage, uint96 newPercentage);
    event BaseURIChanged(string previousBaseURI, string newBaseURI);
    event ContractBaseURIChanged(string previousBaseURI, string newBaseURI);
    event ContractURIChanged(string previousURI, string newURI);
    event PublicBuyEnabled(bool enabled);
    event PermanentURI(string _value, uint256 indexed _id);

    constructor( string memory name, string memory symbol
    ) ERC721(name, symbol) Ownable() {
        emit BeneficiaryChanged(payable(address(0)), _beneficiary);
        emit RaribleBeneficiaryChanged(payable(address(0)), _raribleBeneficiary);
        emit RaribleRoyaltyPercentageChange(0, _raribleRoyaltyPercentage);
    }

    function _mintToken(address owner) internal returns (uint) {

        _tokenIds.increment();
        uint256 id = _tokenIds.current();
        require(id <= maxMints, "Cannot mint more than max");

        _safeMint(owner, id);
        _setRoyalties(id, _raribleBeneficiary, _raribleRoyaltyPercentage);

        emit PermanentURI(_tokenURI(id), id);

        return id;
    }

    /**
    * @dev Public mint function to mint one token
    */
    function mintToken() external payable returns (uint256) {

        require(publicBuyEnabled, "Public buy is not enabled yet");
        require(_userMints[msg.sender] <= maxUserMints, "Already minted maximum");

        uint256 mintPrice = isCollabPartner[_msgSender()] ? _collabPrice : _price;
        require(msg.value >= mintPrice, "Invalid value sent");

        uint256 id = _mintToken(_msgSender());

        (bool sent, ) = _beneficiary.call{value : msg.value}("");
        require(sent, "Failed to pay beneficiary");
        emit BeneficiaryPaid(_beneficiary, msg.value);
        
        _userMints[msg.sender] = _userMints[msg.sender] + 1;

        return id;
    }

    /**
    * @dev Public mint function to mint multiple tokens at once
    * @param count The amount of tokens to mint
    */
    function mintMultipleToken(uint256 count) external payable returns (uint256) {

        require(publicBuyEnabled, "Public buy is not enabled yet");
        require(_userMints[msg.sender] + count <= maxUserMints, "Already minted maximum");

        uint256 mintPrice = isCollabPartner[_msgSender()] ? _collabPrice : _price;
        require(msg.value >= mintPrice.mul(count), "Invalid value sent");

        for (uint256 i = 0; i < count; i++) {
            _mintToken(msg.sender);
        }

        (bool sent, ) = _beneficiary.call{value : msg.value}("");
        require(sent, "Failed to pay beneficiary");
        emit BeneficiaryPaid(_beneficiary, msg.value);

        _userMints[msg.sender] = _userMints[msg.sender] + count;

        return count;
    }

    /**
    * @dev Admin function to mint on token to multiple addresses
    * @param addresses Array of addresses to mint to
    */
    function airdrop(address[] memory addresses) external onlyOwner {
        for (uint256 i = 0; i < addresses.length; i++) {
            _mintToken(addresses[i]);
        }
    }

    /**
    * @dev Admin function to mint many tokens
    * @param count The count to mint
    * @param receiver The receiver to mint to
    */
    function mintMany(uint256 count, address receiver) external onlyOwner {
        for (uint256 i = 0; i < count; i++) {
            _mintToken(receiver);
        }
    }

    /**
    * @dev Get opensea royalty beneficiary
    */
    function getBeneficiary() external view returns (address) {
        return _beneficiary;
    }

    /**
    * @dev Set opensea royalty beneficiary
    * @param newBeneficiary The new opensea royalty beneficiary
    */
    function setBeneficiary(address payable newBeneficiary) external onlyOwner {
        require(newBeneficiary != address(0), "Beneficiary: new beneficiary is the zero address");
        address payable prev = _beneficiary;
        _beneficiary = newBeneficiary;
        emit BeneficiaryChanged(prev, _beneficiary);
    }

    /**
    * @dev Set rarible royalty beneficiary
    * @param newBeneficiary The new rarible royalty beneficiary
    */
    function setRaribleBeneficiary(address payable newBeneficiary) external onlyOwner {
        require(newBeneficiary != address(0), "Beneficiary: new rarible beneficiary is the zero address");
        address payable prev = _raribleBeneficiary;
        _raribleBeneficiary = newBeneficiary;
        emit RaribleBeneficiaryChanged(prev, _raribleBeneficiary);
    }

    /**
    * @dev Get the current mint price
    */
    function getPrice() external view returns (uint256)  {
        return _price;
    }

    /**
    * @dev Set the mint price
    * @param price The new price
    */
    function setPrice(uint256 price) external onlyOwner {
        uint256 prev = _price;
        _price = price;
        emit PriceChange(prev, _price);
    }

    /**
    * @dev Set the collab  list reduced mint price
    * @param price The new price
    */
    function setCollabPrice(uint256 price) external onlyOwner {
        _collabPrice = price;
    }
    
    /**
    * @dev Set rarible global royalty percentage
    * @param percentage The new rarible percentage
    */
    function setRaribleRoyaltyPercentage(uint96 percentage) external onlyOwner {
        uint96 prev = _raribleRoyaltyPercentage;
        _raribleRoyaltyPercentage = percentage;
        emit RaribleRoyaltyPercentageChange(prev, _raribleRoyaltyPercentage);
    }

    /**
    * @dev Set the base uri for all unlocked token
    * @param dataHostBaseURI The new base uri
    */
    function setDataHostURI(string memory dataHostBaseURI) external onlyOwner {
        string memory prev = _dataHostBaseURI;
        _dataHostBaseURI = dataHostBaseURI;
        emit BaseURIChanged(prev, _dataHostBaseURI);
    }

    /**
    * @dev Set the base URI for the locked metadata
    * @param lockedDataHostBaseURI new base uri
    */
    function setLockedDataHostBaseURI(string memory lockedDataHostBaseURI) external onlyOwner {
        _lockedDataHostBaseURI = lockedDataHostBaseURI;
    }

    /**
    * @dev Set the contract uri for opensea standart
    * @param contractURI_ The new contract uri
    */
    function setContractURI(string memory contractURI_) external onlyOwner {
        string memory prev = _contractURI;
        _contractURI = contractURI_;
        emit ContractURIChanged(prev, _contractURI);
    }

    /**
    * @dev Get the contract uri for opensea standart
    */
    function contractURI() external view returns (string memory) {
        return _contractURI;
    }

    function _tokenURI(uint256 tokenId) internal view returns (string memory) {
        return string(abi.encodePacked(_dataHostBaseURI, Strings.toString(tokenId)));
    }

    function _lockedTokenURI(uint256 tokenId) internal view returns (string memory) {
        return string(abi.encodePacked(_lockedDataHostBaseURI, Strings.toString(tokenId)));
    }

    /**
    * @dev Get the token URI of a specific id, will return locked metadata if the token is locked
    * @param tokenId The token id
    */
    function tokenURI(uint256 tokenId) external view override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        if(treasuresRevealed){
            if(_getLockTime(tokenId) > block.timestamp){
                return _lockedTokenURI(tokenId);
            }
            return _tokenURI(tokenId);
        }
        return _placeHolderHash;
    }

    function _setRoyalties(uint _tokenId, address payable _royaltiesReceipientAddress, uint96 _percentageBasisPoints) internal {
        LibPart.Part[] memory _royalties = new LibPart.Part[](1);
        _royalties[0].value = _percentageBasisPoints;
        _royalties[0].account = _royaltiesReceipientAddress;
        _saveRoyalties(_tokenId, _royalties);
    }

    /**
    * @dev Set Opensea royalties
    * @param _tokenId The token id
    * @param _royaltiesReceipientAddress The royalty receiver address
    * @param _percentageBasisPoints The royalty percentage in basis points
    */
    function setRoyalties(uint256 _tokenId, address payable _royaltiesReceipientAddress, uint96 _percentageBasisPoints) external onlyOwner {
        _setRoyalties(_tokenId, _royaltiesReceipientAddress, _percentageBasisPoints);
    }

    /**
    * @dev Set placeholder hash for unrevealed metadata
    * @param placeHolderHash new placeholder uri
    */
    function setPlaceHolderHash(string memory placeHolderHash) external onlyOwner {
        _placeHolderHash = placeHolderHash;
    }
    
    /**
    * @dev Admin function to reveal the actual metadata
    */
    function revealTreasures() external onlyOwner {
        treasuresRevealed = !treasuresRevealed;
    }

    /**
    * @dev Admin function to release a token from lock
    * @param tokenId the token id to unlock
    */
    function safetyUnlock(uint256 tokenId) external onlyOwner {
        _safetyUnlock(tokenId);
    }

    /**
    * @dev Adds address to collab reduces price
    * @param collabAddresses collab address to add
    */
    function addCollabPartner(address[] memory collabAddresses) external onlyOwner {
        for(uint256 i = 0; i < collabAddresses.length; i++){
            isCollabPartner[collabAddresses[i]] = true;
        }
    }

    /**
    * @dev Removes address from collab reduces price
    * @param collabAddress collab address to remove from list
    */
    function removeCollabPartner(address collabAddress) external onlyOwner {
        require(isCollabPartner[collabAddress] == true, "Not marked as partner");
        isCollabPartner[collabAddress] = false;
    }

    /**
    * @dev Switches the public sale
    * @param enabled true if the public mint should be enabled
    */
    function enablePublicBuy(bool enabled) external onlyOwner {
        require(publicBuyEnabled != enabled, "Already set");
        publicBuyEnabled = enabled;
        emit PublicBuyEnabled(publicBuyEnabled);
    }

    /**
    * @dev Sets max allowed mints per user address
    * @param _maxUserMints The maximum mint count per user address.
    */
    function setMaxUserMints(uint256 _maxUserMints) external onlyOwner {
        maxUserMints = _maxUserMints;
    }
    
    /**
    * @dev Count of mints from user
    * @param user The address of the user.
    */
    function userMints(address user) external view returns (uint256) {
        return _userMints[user];
    }
    

    /**
    * @dev Get the lock time of a token
    * @param tokenId The token id
    */
    function getLockTime(uint256 tokenId) external view returns(uint256) {
        return _getLockTime(tokenId);
    }

    /**
    * @dev Public function to lock a token, used in the dApp to complete a boocking process.
    * A locked token cannot be transferred or unlocked. There is a admin safety to unlock tokens in case of cancel.
    * @param tokenId The token id to lock
    * @param lockTime The time to lock the token as a unix timestamp
    */
    function lockForBooking(uint256 tokenId, uint256 lockTime) external {
        _lockForBooking(tokenId, lockTime);
    }

    /**
    * @dev Support Interface for Rarible royalty implementation
    */
    function supportsInterface(bytes4 interfaceId) public view override(ERC721Enumerable) returns (bool) {
        if(interfaceId == LibRoyaltiesV2._INTERFACE_ID_ROYALTIES) {
            return true;
        }
        return super.supportsInterface(interfaceId);
    }

    function getTime() external view returns (uint256) {
        return block.timestamp;
    }
}
function revealTreasures() external onlyOwner {
    treasuresRevealed = !treasuresRevealed;
}

/**
 * @dev Admin function to reveal the actual metadata
 */