contract KrazykarensNFT is ERC721, Ownable, Pausable, ReentrancyGuard {
    using Strings for uint256;
    using SafeMath for uint256;
    using SafeMath for uint8;

    uint256 public constant MAX_SUPPLY = 6666;
    uint256 public constant MINT_PRICE = 0.04 ether;
    uint256 public constant MINT_PRICE_PUBLIC = 0.08 ether;
    uint256 public constant MAX_MINT_PER_TX = 10;
    uint256 public constant MAX_MINT_PER_WL = 2;
    uint256 public constant GIVEAWAY_SUPPLY = 50;
    uint256 public totalSupply = 0;

    bool public saleStarted = false;
    bool public preSaleStarted = false;
    bool public revealed = false;

    string public baseExtension = ".json";
    string public baseURI;
    string public notRevealedURI;

    // Merkle Tree Root
    bytes32 private _merkleRoot;

    // Team wallet
    address[] private _royaltyAddresses = [
        0xb20F2a4601aED75B886CC5B84E28a0D65a7Bfd48, // Dev wallet address
        0xf3Dd09D30249Ca3FB34251aBC144ad4409d79597  // Team wallet address
    ];

    mapping(address => uint256) private _royaltyShares;
    mapping(address => uint256) balanceOfAddress;

    constructor(
        string memory _name,
        string memory _symbol,
        string memory _initBaseURI,
        string memory _initNotRevealedURI
    ) ERC721(_name, _symbol) {
        setBaseURI(_initBaseURI);
        setNotRevealedURI(_initNotRevealedURI);

        _royaltyShares[_royaltyAddresses[0]] = 1;  // Royalty for dev wallet
        _royaltyShares[_royaltyAddresses[1]] = 99; // Royalty for team wallet
    }

    /// @dev    Getter for the base URI 
    /// @return String of the base URI
    function _baseURI() internal view virtual override returns (string memory) {
        return baseURI;
    }

    /// @dev Mint NFTs for giveway
    function mintForGiveaway() external onlyOwner {
        require(totalSupply == 0, "MINT_ALREADY_STARTED");

        for (uint256 i = 1; i <= GIVEAWAY_SUPPLY; i++) {
            _safeMint(msg.sender, totalSupply + i);
        }
        totalSupply += GIVEAWAY_SUPPLY;
    }

    /// @dev   Admin mint for allocated NFTs
    /// @param _amount Number of NFTs to mint
    /// @param _to NFT receiver
    function mintAdmin(uint256 _amount, address _to) external onlyOwner {
        require(totalSupply + _amount <= MAX_SUPPLY, "MAX_SUPPLY_REACHED");
        require(totalSupply >= GIVEAWAY_SUPPLY, "GIVEAWAY_NOT_MINTED");
        require(msg.sender == owner(), "MINT_NOT_OWNER");


        for (uint256 i = 1; i <= _amount; i++) {
            _safeMint(_to, totalSupply + i);
        }
        totalSupply += _amount;
    }

    /// @dev    Add a hashed address to the merkle tree as a leaf
    /// @param  account Leaf address for MerkleTree
    /// @return bytes32 hashed version of the merkle leaf address
    function _leaf(address account) private pure returns (bytes32) {
        return keccak256(abi.encodePacked(account));
    }

    /// @dev    Verify the whitelist using the merkle tree
    /// @param  leaf Hashed address leaf from _leaf() to search for
    /// @param  proof Submitted root proof from MerkleTree
    /// @return bool True if address is allowed to mint
    function verifyWhitelist(bytes32 leaf, bytes32[] memory proof)
        private
        view
        returns (bool)
    {
        bytes32 computedHash = leaf;

        for (uint256 i = 0; i < proof.length; i++) {
            bytes32 proofElement = proof[i];

            if (computedHash < proofElement) {
                computedHash = keccak256(
                    abi.encodePacked(computedHash, proofElement)
                );
            } else {
                computedHash = keccak256(
                    abi.encodePacked(proofElement, computedHash)
                );
            }
        }

        return computedHash == _merkleRoot;
    }

    /// @dev   General whitelist presale mint
    /// @param _count Amount to mint
    /// @param _proof Root merkle proof to submit
    function mintWhitelist(uint256 _count, bytes32[] memory _proof)
        external
        payable
    {
        require(preSaleStarted, "MINT_NOT_STARTED");
        require(
            verifyWhitelist(_leaf(msg.sender), _proof) == true,
            "ADDRESS_INVALID"
        );
        require(
            _count > 0 && balanceOfAddress[msg.sender] + _count <= MAX_MINT_PER_WL,
            "COUNT_INVALID"
        );
        require(totalSupply + _count <= MAX_SUPPLY, "MAX_SUPPLY_REACHED");
        require(totalSupply >= GIVEAWAY_SUPPLY, "GIVEAWAY_NOT_MINTED");

        if (msg.sender != owner()) {
            require(msg.value >= MINT_PRICE * _count);
        }

        for (uint256 i = 1; i <= _count; i++) {
            _safeMint(msg.sender, totalSupply + i);
        }

        totalSupply += _count;
        balanceOfAddress[msg.sender] += _count;
    }

    /// @dev   Mint a single NFT
    /// @param _count Number of NFTs to mint
    function mint(uint256 _count) public payable {
        require(saleStarted, "MINT_NOT_STARTED");
        require(
            _count > 0 && balanceOfAddress[msg.sender] + _count <= MAX_MINT_PER_TX,
            "COUNT_INVALID"
        );
        require(totalSupply >= GIVEAWAY_SUPPLY, "GIVEAWAY_NOT_MINTED");
        require(totalSupply + _count <= MAX_SUPPLY, "MAX_SUPPLY_REACHED");

        if (msg.sender != owner()) {
            require(msg.value >= MINT_PRICE_PUBLIC * _count);
        }

        for (uint256 i = 1; i <= _count; i++) {
            _safeMint(msg.sender, totalSupply + i);
        }

        totalSupply += _count;
        balanceOfAddress[msg.sender] += _count;
    }

    /// @dev    Override the current tokenURI
    /// @param  tokenId Token ID that you want to override
    /// @return String of new tokenURI
    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        require(_exists(tokenId), "Token does not exist");

        string memory currentBaseURI = "";
        if (revealed == false) {
            currentBaseURI = notRevealedURI;

            return currentBaseURI;
        }
        else {
            currentBaseURI = _baseURI();

            return
            bytes(currentBaseURI).length > 0
                ? string(
                    abi.encodePacked(
                        currentBaseURI,
                        tokenId.toString(),
                        baseExtension
                    )
                )
                : "";
        }
        
    }

    /// @dev Withdraw ether from the smart contract
    function withdraw() external onlyOwner {
        require(address(this).balance > 0, "EMPTY_BALANCE");
        uint256 balance = address(this).balance;

        for (uint256 i = 0; i < _royaltyAddresses.length; i++) {
            payable(_royaltyAddresses[i]).transfer(
                balance.div(100).mul(_royaltyShares[_royaltyAddresses[i]])
            );
        }
    }

    /// @dev Set the sale to starting
    /// @param _hasStarted Boolean to set sale to starting
    function setSaleStarted(bool _hasStarted) external onlyOwner {
        require(saleStarted != _hasStarted, "SALE_STARTED_ALREADY_SET");
        saleStarted = _hasStarted;
    }

    /// @dev   Set the presale to starting
    /// @param _hasStarted Boolean to set presale to starting
    function setPreSaleStarted(bool _hasStarted) external onlyOwner {
        require(preSaleStarted != _hasStarted, "PRESALE_STARTED_ALREADY_SET");
        preSaleStarted = _hasStarted;
    }

    /// @dev   Set a new base URI for the NFT metadata
    /// @param _newBaseURI String of new baseURI
    function setBaseURI(string memory _newBaseURI) public onlyOwner {
        baseURI = _newBaseURI;
    }

    /// @dev Open the reveal gates!
    function reveal() public onlyOwner {
        revealed = true;
    }

    /// @dev   Set the new tokenURI for a specific token
    /// @param _notRevealedURI String of not revealed URI
    function setNotRevealedURI(string memory _notRevealedURI) public onlyOwner {
        notRevealedURI = _notRevealedURI;
    }

    /// @dev Set the merkle root
    /// @param _merkleRootValue String of merkle root
    /// @return Bytes32 merkle root
    function setMerkleRoot(bytes32 _merkleRootValue)
        external
        onlyOwner
        returns (bytes32)
    {
        _merkleRoot = _merkleRootValue;
        return _merkleRoot;
    }

    /// @dev Pause the contract as an emergency switch
    function pause() external onlyOwner {
        require(!paused(), "ALREADY_PAUSED");
        _pause();
    }

    /// @dev Unpause the contract as an emergency switch
    function unpause() external onlyOwner {
        require(paused(), "ALREADY_UNPAUSED");
        _unpause();
    }
}
/**
 * @title A contract for Krazy Karens
 * @author H!ro
 * @notice NFT Minting
 */
function _leaf(address account) private pure returns (bytes32) {
    return keccak256(abi.encodePacked(account));
}

/// @dev    Add a hashed address to the merkle tree as a leaf
/// @param  account Leaf address for MerkleTree
/// @return bytes32 hashed version of the merkle leaf address