contract ABCTestContract is ERC721A, Ownable {
    using Address for address;
    using Strings for uint256;

    //  Format of files stores in ipfs
    string private uriPrefix = "";
    string private uriSuffix = ".json";
    
    //  Init uri for when collection is not revealed
    string public hiddenMetadataUri;

    //  Init values for cost, max supply, max mint amount, and allowlist mapping
    uint256 public whitelistPrice = 0.000066 ether; 
    uint256 public publicPrice = 0.00008 ether; 
    uint256 public maxSupply = 5555;
    uint256 public immutable amountForDevs = 100;
    uint256 public immutable maxMintAmountWhitelist = 3;
    uint256 public immutable maxMintAmountPublic = 5;
    mapping(address => uint256) public allowlistCount; 

    //  Contract will be initalized as paused and not revealed.
    bool public paused = true;
    bool public revealed = false;
    uint32 public publicSaleStartTime;

    bytes32 private whitelistMerkleRoot;
    constructor() ERC721A("ABC Testing OS", "ACBT") {
        // must set hidden uri.
        setHiddenMetadataUri("ipfs://Qmbi3JwrnLUGBVckX8nA73XUSwDRbReg7axztpRVnzxPFa/ABC_Hidden.png.json");

        // set time for public launch
        setPublicSaleStartTime(1646548200);

        // hold back txn before mint for giveaway, collab, and team
        _safeMint(msg.sender, 10);    
    }

    modifier mintCompliance(uint256 _mintAmount) {
        require(totalSupply() + _mintAmount <= maxSupply, "Max supply exceeded!");
        require(!paused, "NFT is not available for mint");
        _;
    }

    // general mint 
    function publicMint(uint256 _mintAmount) public payable mintCompliance(_mintAmount) {
        require(isPublicSaleOn() == true, "Public sale has not begun yet");
        require(_mintAmount > 0 && _numberMinted(msg.sender) + _mintAmount - allowlistCount[msg.sender] <= maxMintAmountPublic, "Invalid mint amount!");
        require(msg.value >= publicPrice * _mintAmount, "Insufficient funds!");
        _safeMint(msg.sender, _mintAmount);
        refundIfOver(publicPrice * _mintAmount);
    }

    // allowlist Mint
    function allowlistMint(bytes32[] calldata merkleProof, uint256 _mintAmount) public payable mintCompliance(_mintAmount) isValidMerkleProof(merkleProof, whitelistMerkleRoot){
        require(_mintAmount > 0 && _numberMinted(msg.sender) + _mintAmount <= maxMintAmountWhitelist, "Invalid mint amount!");
        require(msg.value >= whitelistPrice * _mintAmount, "Insufficient funds!");
        allowlistCount[msg.sender]+=_mintAmount;
        _safeMint(msg.sender, _mintAmount);
        refundIfOver(whitelistPrice * _mintAmount);
    }

    // for marketing etc.
    function devMint(uint256 _mintAmount) external onlyOwner {
        require(totalSupply() + _mintAmount <= amountForDevs, "Too many already minted before dev mint");
        _safeMint(msg.sender, _mintAmount);
        
    }

    //  refund if value sent if more than price
    function refundIfOver(uint256 price) private {
        require(msg.value >= price, "Need to send more ETH.");
        if (msg.value > price) {
        payable(msg.sender).transfer(msg.value - price);
        }
    }

    // check if reached public sale 
    function isPublicSaleOn() public view returns (bool) {
        return block.timestamp >= publicSaleStartTime;
    }

    //  set merkleroot node
    function setWhitelistMerkleRoot(bytes32 merkleRoot) external onlyOwner {
        whitelistMerkleRoot = merkleRoot;
    }

    //  verify merkletree 
    modifier isValidMerkleProof(bytes32[] calldata merkleProof, bytes32 root) {
        require(MerkleProof.verify(
                merkleProof,
                root,
                keccak256(abi.encodePacked(msg.sender))),
            "Address does not exist in list"
        );
        _;
    }

    // set public sale start time
    function setPublicSaleStartTime(uint32 timestamp) public onlyOwner{
        publicSaleStartTime = timestamp;
    }

    //  reveal 
    function toggleReveal() public onlyOwner {
        revealed = !revealed;
    }
    
    function setHiddenMetadataUri(string memory _hiddenMetadataUri) public onlyOwner {
        hiddenMetadataUri = _hiddenMetadataUri;
    }

    function setMaxSupply(uint32 _maxSupply) public onlyOwner {
        maxSupply = _maxSupply;
    }

    function setUriPrefix(string memory _uriPrefix) public onlyOwner {
        uriPrefix = _uriPrefix;
    }

    function setUriSuffix(string memory _uriSuffix) public onlyOwner {
        uriSuffix = _uriSuffix;
    }

    function setPause(bool _state) public onlyOwner {
        paused = _state;
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        if (!_exists(tokenId)) revert URIQueryForNonexistentToken();
        if (revealed == false) {
            return hiddenMetadataUri;
        }
        string memory baseURI = _baseURI();
        return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, tokenId.toString(), uriSuffix)) : '';
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return uriPrefix;
    }

    function withdraw() public payable onlyOwner {
        // This will payout the owner the contract balance.
        // Do not remove this otherwise you will not be able to withdraw the funds.
        // =============================================================================
        (bool os, ) = payable(owner()).call{value: address(this).balance}("");
        require(os);
        // =============================================================================
    }
}
function refundIfOver(uint256 price) private {
    require(msg.value >= price, "Need to send more ETH.");
    if (msg.value > price) {
    payable(msg.sender).transfer(msg.value - price);
    }
}

//  refund if value sent if more than price