contract Generascope is ERC721Enumerable, Ownable {
    using SafeMath for uint256;
    using Strings for uint256;
    using ECDSA for bytes32;

    uint256 public constant GS_PRIVATE = 858;
    uint256 public constant GS_PUBLIC = 253;
    uint256 public constant GS_MAX = GS_PRIVATE + GS_PUBLIC;
    uint256 public constant GS_PRICE = 0.3 ether;
    uint256 public constant GS_PURCHASE_LIMIT = 2;

    mapping(address => bool) public presalerList;
    mapping(address => uint256) public allPurchases;

    string private _contractURI;
    string private _tokenBaseURI;

    uint256 public publicAmountMinted;
    uint256 public privateAmountMinted;
    
    bool public presaleLive;
    bool public saleLive;
    bool public locked;

    constructor() ERC721("Generascope", "GS") {}

    modifier notLocked() {
        require(!locked, "Contract metadata methods are locked");
        _;
    }

    function addToPresaleList(address[] calldata entries) external onlyOwner {
        for (uint256 i = 0; i < entries.length; i++) {
            address entry = entries[i];
            require(entry != address(0), "NULL_ADDRESS");
            require(!presalerList[entry], "DUPLICATE_ENTRY");

            presalerList[entry] = true;
        }
    }

    function removeFromPresaleList(address[] calldata entries)
        external
        onlyOwner
    {
        for (uint256 i = 0; i < entries.length; i++) {
            address entry = entries[i];
            require(entry != address(0), "NULL_ADDRESS");

            presalerList[entry] = false;
        }
    }

    function buy(uint256 tokenQuantity) external payable {
        require(saleLive, "SALE_CLOSED");
        require(!presaleLive, "ONLY_PRESALE");
        require(totalSupply() < GS_MAX, "OUT_OF_STOCK");
        require(
            privateAmountMinted.add(publicAmountMinted).add(tokenQuantity) <= GS_MAX,
            "EXCEED_AVAILABLE"
        );
        require(
            allPurchases[msg.sender].add(tokenQuantity) <=
                GS_PURCHASE_LIMIT,
            "EXCEED_ALLOC"
        );
        require(GS_PRICE.mul(tokenQuantity) == msg.value, "INCORRECT_ETH_AMOUNT");

        for (uint256 i = 0; i < tokenQuantity; i++) {
            publicAmountMinted++;
            allPurchases[msg.sender]++;
            _safeMint(msg.sender, totalSupply() + 1);
        }
    }

    function presaleBuy(uint256 tokenQuantity) external payable {
        require(!saleLive && presaleLive, "PRESALE_CLOSED");
        require(presalerList[msg.sender], "NOT_QUALIFIED");
        require(totalSupply() < GS_MAX, "OUT_OF_STOCK");
        require(
            privateAmountMinted.add(tokenQuantity) <= GS_PRIVATE,
            "EXCEED_PRIVATE"
        );
        require(
            allPurchases[msg.sender].add(tokenQuantity) <=
                GS_PURCHASE_LIMIT,
            "EXCEED_ALLOC"
        );
        require(GS_PRICE.mul(tokenQuantity) == msg.value, "INCORRECT_ETH_AMOUNT");

        for (uint256 i = 0; i < tokenQuantity; i++) {
            privateAmountMinted++;
            allPurchases[msg.sender]++;
            _safeMint(msg.sender, totalSupply() + 1);
        }
    }

    function withdraw() external onlyOwner {
        payable(msg.sender).transfer(address(this).balance);
    }

    function isPresaler(address addr) external view returns (bool) {
        return presalerList[addr];
    }

    function allPurchasedCount(address addr)
        external
        view
        returns (uint256)
    {
        return allPurchases[addr];
    }

    // Owner functions for enabling presale and sale
    function lockMetadata() external onlyOwner {
        locked = true;
    }

    function togglePresaleStatus() external onlyOwner {
        presaleLive = !presaleLive;
    }

    function toggleSaleStatus() external onlyOwner {
        saleLive = !saleLive;
    }

    function setContractURI(string calldata URI) external onlyOwner notLocked {
        _contractURI = URI;
    }

    function setBaseURI(string calldata URI) external onlyOwner notLocked {
        _tokenBaseURI = URI;
    }

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

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721)
        returns (string memory)
    {
        require(_exists(tokenId), "Cannot query non-existent token");

        return string(abi.encodePacked(_tokenBaseURI, tokenId.toString(), ".json"));
    }
}
function lockMetadata() external onlyOwner {
    locked = true;
}

// Owner functions for enabling presale and sale