contract CommuNFTy is ERC721Enumerable, Ownable, ReentrancyGuard {
    using Strings for uint256;

    string private _baseTokenURI = "https://mint.communfty.io/NFTs/metadatas/";
    string private _contractURI = "";

    //toggle the sale
    bool public isSaleActive = true;

    //allow only whitelisted to mint first
    bool public onlyWhiteListed = true;
    mapping(address => bool) public whitelist;

    //Limit minting to one per address
    mapping(address => bool) public listDidMint;

    //no more than 1k
    uint256 public constant totalTokenToMint = 1000;

    //the enumerable part from ERC721Enumerable
    uint256 public tokenIndex = 0;

    //Current expedition mint infos
    uint256 public currentFloorPrice = 100000000000000000; //0.1 ETH
    uint256 public currentTokenToMint = 60;

    //events
    event AddedToWhitelist(address indexed account);
    event RemovedFromWhitelist(address indexed account);
    event StartedNewExpedition(uint256 totalNFTs, uint256 floorPrice);
    event MintedNFT(uint256 idToken, uint256 price);
    event PayProvider(uint256 amount, address indexed account);
    event AllowOnlyWhiteListToMint(bool status);
    event PausedMints(bool status);

    constructor() ERC721("CommuNFTy", "C-NFTy") {
        emit StartedNewExpedition(currentTokenToMint, currentFloorPrice);
    }

    /*
     * This is how you get one
     */

    function acquire() public payable nonReentrant {
        require(isSaleActive, "sale is not active");
        require(
            !onlyWhiteListed || whitelist[_msgSender()],
            "Only whitelisted addresses are allowed to mint at this stage."
        );
        require(currentFloorPrice == msg.value, "exact value in ETH needed");
        require(tokenIndex <= totalTokenToMint, "No more Token can be minted");
        require(
            tokenIndex <= currentTokenToMint,
            "No more Token can be minted in the current expedition"
        );
        require(
            !listDidMint[_msgSender()],
            "You can't mint more then one NFT on mint"
        );
        _mintToken(_msgSender());
    }

    // in case tokens are not sold, admin can mint them for giveaways, airdrops etc
    function adminMint(uint256 amount) public onlyOwner {
        require(amount > 0, "minimum 1 token");
        require(
            tokenIndex + amount <= totalTokenToMint,
            "No more Token can be minted"
        );
        require(
            tokenIndex + amount <= currentTokenToMint,
            "No more Token can be minted in the current expedition"
        );
        for (uint256 i = 0; i < amount; i++) {
            _mintToken(_msgSender());
        }
    }

    /*
     * Internal mint function
     */
    function _mintToken(address destinationAddress) private {
        tokenIndex++;
        require(!_exists(tokenIndex), "Token already exist.");
        _safeMint(destinationAddress, tokenIndex);
        listDidMint[destinationAddress] = true;
    }

    /*
     * Helper function
     */
    function tokensOfOwner(address _owner)
        external
        view
        returns (uint256[] memory)
    {
        uint256 tokenCount = balanceOf(_owner);
        if (tokenCount == 0) {
            // Return an empty array
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            uint256 index;
            for (index = 0; index < tokenCount; index++) {
                result[index] = tokenOfOwnerByIndex(_owner, index);
            }
            return result;
        }
    }

    function exists(uint256 _tokenId) external view returns (bool) {
        return _exists(_tokenId);
    }

    function isApprovedOrOwner(address _spender, uint256 _tokenId)
        external
        view
        returns (bool)
    {
        return _isApprovedOrOwner(_spender, _tokenId);
    }

    function stopSale() external onlyOwner {
        isSaleActive = false;
        emit PausedMints(isSaleActive);
    }

    function startSale() external onlyOwner {
        isSaleActive = true;
        emit PausedMints(isSaleActive);
    }

    function allowOnlyWhitelistedToMint() external onlyOwner {
        onlyWhiteListed = true;
        emit AllowOnlyWhiteListToMint(onlyWhiteListed);
    }

    function allowEveryoneToMint() external onlyOwner {
        onlyWhiteListed = false;
        emit AllowOnlyWhiteListToMint(onlyWhiteListed);
    }

    function tokenURI(uint256 _tokenId)
        public
        view
        override
        returns (string memory)
    {
        require(
            _exists(_tokenId),
            "ERC721Metadata: URI query for nonexistent token"
        );
        return
            string(
                abi.encodePacked(_baseTokenURI, _tokenId.toString(), ".json")
            );
    }

    function setBaseURI(string memory newBaseURI) public onlyOwner {
        _baseTokenURI = newBaseURI;
    }

    function setContractURI(string memory newuri) public onlyOwner {
        _contractURI = newuri;
    }

    function contractURI() public view returns (string memory) {
        return _contractURI;
    }

    function withdrawEarnings() public onlyOwner {
        payable(msg.sender).transfer(address(this).balance);
    }

    function payProvider(address provider, uint256 amount) public onlyOwner {
        payable(provider).transfer(amount);
        emit PayProvider(amount, provider);
    }

    function reclaimERC20(IERC20 erc20Token) public onlyOwner {
        erc20Token.transfer(msg.sender, erc20Token.balanceOf(address(this)));
    }

    //WhiteListing utilities

    //Add Bulk Whitelist
    function addBulkWhiteList(address[] memory list) public onlyOwner {
        for (uint256 i = 0; i < list.length; i++) {
            whiteListAdd(list[i]);
        }
    }

    //Remove Bulk Whitelist
    function removeBulkWhiteList(address[] memory list) public onlyOwner {
        for (uint256 i = 0; i < list.length; i++) {
            whiteListRemove(list[i]);
        }
    }

    //Add To WhiteList
    function whiteListAdd(address _address) private {
        whitelist[_address] = true;
        emit AddedToWhitelist(_address);
    }

    //Remove from WhiteList
    function whiteListRemove(address _address) private {
        whitelist[_address] = false;
        emit RemovedFromWhitelist(_address);
    }

    //Expedition utilities
    function setNextEpiditionAmountAndFloorPrice(
        uint256 amount,
        uint256 newFloorPrice
    ) public onlyOwner {
        currentTokenToMint += amount;
        currentFloorPrice = newFloorPrice;
        emit StartedNewExpedition(amount, newFloorPrice);
    }
}
function setNextEpiditionAmountAndFloorPrice(
        uint256 amount,
        uint256 newFloorPrice
    ) public onlyOwner {
        currentTokenToMint += amount;
        currentFloorPrice = newFloorPrice;
        emit StartedNewExpedition(amount, newFloorPrice);
    }
//Expedition utilities