contract Soulcops is ERC721EnumerableM, Ownable {
    using Strings for uint256;
    enum Stage {
        Shutdown,
        Presale,
        Publicsale
    }

    Stage public stage;
    
    bytes32 public root;

    uint256 public constant MAX_RESERVED = 500;
    uint256 public constant MAX_PRESALE = 2000 + MAX_RESERVED;
    uint256 public constant TOTAL = 10000;
    uint256 public constant PRICE = 0.08 ether;
    uint256 public constant PRE_PRICE = 0.068 ether;
    uint256 public constant MAX_NFT_SALE = 3;
    uint256 public reservedQtyMinted;
    uint256 public presaleQtyMinted;
    
    mapping(address => uint256) public listPresalePurchases;
    mapping(address => uint256) public listSalePurchases;

    string private _contractURI;
    string private _baseTokenURI;
    string private _defaultTokenURI;

    address SOULCOPS_WALLET = 0x71D54d6Dd26339B4d69C5cd922C4846aC6fb5E95;

    constructor(
        string memory defaultTokenURI,
        bytes32 merkleroot
    ) 
        ERC721M("Soul Cops", "SOULCOPS")  
    {
        _defaultTokenURI = defaultTokenURI;
        root = merkleroot;
    }
    
    function setMerkleRoot(bytes32 merkleroot) onlyOwner public 
    {
        root = merkleroot;
    }

     // get remaining pre sale qty
    function presaleQtyRemaining() public view returns (uint256) {
        return MAX_PRESALE - reservedQtyMinted;
    }

    // get remaining sale qty
    function saleQtyRemaining() external view returns (uint256) {
        return TOTAL - presaleQtyMinted - reservedQtyMinted;
    }
    
    // gift token to spesfic address
    function gifting(address receiver,uint256 tokenQuantity) external onlyOwner {
        uint256 supply = totalSupply();
        require(supply <= TOTAL, "All Soulcops minted");
        require(reservedQtyMinted + tokenQuantity <= MAX_RESERVED, "Reserved Empty");
      
        for (uint256 i = 0; i < tokenQuantity; i++) {
            reservedQtyMinted++;
            _safeMint(receiver, supply++);
        }
    }

    // presale sale buy
    function presaleBuy(uint256 tokenQuantity, uint256 allowance, bytes32[] calldata proof) external payable {
        uint256 supply = totalSupply();
        require(_verify(_leaf(msg.sender, allowance), proof), "Invalid merkle proof");
        require(stage==Stage.Presale, "The presale not active");
        require(supply + tokenQuantity <= TOTAL, "Minting would exceed the sale allocation");
        require(presaleQtyMinted + tokenQuantity <= presaleQtyRemaining(), "Minting would exceed the presale allocation");
        require(listPresalePurchases[msg.sender] + tokenQuantity <= allowance, "You can not mint exceeds maximum NFT");
        require(PRE_PRICE * tokenQuantity == msg.value, "ETH sent not match with total purchase");
        
        listPresalePurchases[msg.sender] += tokenQuantity;

        for (uint256 i = 0; i < tokenQuantity; i++) {
            presaleQtyMinted++;
            _safeMint(msg.sender, supply++);
        }
    }
    
    // public sale buy
    function buy(uint256 tokenQuantity) external payable {
        uint256 supply = totalSupply();
        require(stage==Stage.Publicsale, "The sale not active");
        require(supply + tokenQuantity <= TOTAL, "Minting would exceed the sale allocation");
        require(listSalePurchases[msg.sender] + tokenQuantity <= MAX_NFT_SALE, "You can not mint exceeds maximum NFT");
        require(PRICE * tokenQuantity == msg.value, "ETH sent not match with total purchase");

        listSalePurchases[msg.sender] += tokenQuantity;

        for (uint256 i = 0; i < tokenQuantity; i++) {
            _safeMint(msg.sender, supply++);
        }
    }

    // get all token by owner addresss
    function tokensOfOwner(address _owner) external view returns(uint256[] memory) {
        uint256 tokenCount = balanceOf(_owner);

        uint256[] memory tokensId = new uint256[](tokenCount);
        for(uint256 i; i < tokenCount; i++){
            tokensId[i] = tokenOfOwnerByIndex(_owner, i);
        }
        return tokensId;
    }
    
    function withdraw() external onlyOwner {
        Address.sendValue(payable(SOULCOPS_WALLET), address(this).balance);
    }

    // list address presale purchased count
    function presalePurchasedCount(address addr) external view returns (uint256) {
        return listPresalePurchases[addr];
    }
    
    // change stage presale, sale, and shutdown
    function setStage(Stage _stage) external {
        stage = _stage;
    }

    function setContractURI(string calldata URI) external onlyOwner {
        _contractURI = URI;
    }
    
    function setBaseURI(string calldata URI) external onlyOwner {
        _baseTokenURI = URI;
    }
    
    function setDefaultTokenURI(string calldata URI) external onlyOwner {
        _defaultTokenURI = URI;
    }
    
    function contractURI() public view returns (string memory) {
        return _contractURI;
    }

    function baseURI() public view returns (string memory) {
        return _baseTokenURI;
    }

    // get tokenURI by index, add default base uri
    function tokenURI(uint256 tokenId) external override view returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        string memory _baseURI = baseURI();
        return bytes(_baseURI).length > 0 ? string(abi.encodePacked(_baseURI, tokenId.toString())) : _defaultTokenURI;
    }


    function _leaf(address account, uint256 allowance)internal pure returns (bytes32)
    {
        return keccak256(abi.encodePacked(account,allowance));
    }

    function _verify(bytes32 leaf, bytes32[] memory proof) internal view returns (bool)
    {
        return MerkleProof.verify(proof, root, leaf);
    }
    
}
// Created by thealphalets.com
function gifting(address receiver,uint256 tokenQuantity) external onlyOwner {
        uint256 supply = totalSupply();
        require(supply <= TOTAL, "All Soulcops minted");
        require(reservedQtyMinted + tokenQuantity <= MAX_RESERVED, "Reserved Empty");
      
        for (uint256 i = 0; i < tokenQuantity; i++) {
            reservedQtyMinted++;
            _safeMint(receiver, supply++);
        }
    }
// gift token to spesfic address