contract BlackBoxPrtcl is
    Ownable,
    ERC721,
    ERC721Burnable,
    ERC721Pausable,
    ReentrancyGuard
{
   
    /// Variables

    using SafeMath for uint256;
    using SafeMath for uint32;
    using Strings for uint256;
    using Address for address;
    using MerkleProof for bytes32[];
    using Counters for Counters.Counter;

    /// Contract States

    enum BoxStatus {
        DONOTOPEN,
        SOCIETY,
        FREEMINT,
        STEALTH
    }

    /// TODO:change

    Counters.Counter private _tokenIds;
    bytes32 private _merkleRoot;
    bool public locked;
    bool public revealed;
    BoxStatus public _boxStatus;
    string private _baseTokenURI;
    string private _baseExtension;
    string public _notRevealedUri;
    address payable private _mintingBeneficiary;
    uint32 private _BBOXES;
    uint32 private _STEALTH_MINT_LIMIT;
    uint256 public PRICE;
    mapping(address => bool) private _claimed;

    /// Events

    event BoxMinted(address indexed to, uint256 indexed tokenId);

    
    event StatusChanged(BoxStatus indexed _boxStatus);

    /// Modifiers

    modifier onlyIfAvailable(uint32 _amount) {
        require(_boxStatus != BoxStatus.DONOTOPEN, "DO NOT OPEN");
        require(
            (_tokenIds.current()).add(_amount) <= _BBOXES,
            "Our supply of Black Boxes has run out"
        );
        require(_amount > 0, "Oh so you dont want ANY boxes");
        require(
            msg.value == PRICE.mul(_amount),
            "Ether sent mismatch with mint price, silly"
        );
        _;
    }

    modifier notLocked() {
        require(!locked, "Metadata URIs are locked");
        _;
    }

    
    modifier notRevealed() {
        require(!revealed, "Art is already revealed");
        _;
    }

    
    modifier Revealed() {
        require(revealed, "Art is not revealed");
        _;
    }

    /// What we imagine might be inside the box, but we didn't open any, so we really don't know

    constructor(
        string memory name,
        string memory symbol,
        string memory baseTokenURI,
        string memory notRevealedUri,
        bytes32 merkleRoot,
        address mbeneficiary
    ) ERC721(name, symbol) {
        _boxStatus = BoxStatus.DONOTOPEN;

        _merkleRoot = merkleRoot;

        PRICE = 0.1 ether;

        _BBOXES = 4096;

        _STEALTH_MINT_LIMIT = 10;

        _baseExtension = ".json";
        _baseTokenURI = baseTokenURI;
        _notRevealedUri = notRevealedUri;

        _mintingBeneficiary = payable(mbeneficiary);
    }


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

   
    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        return
            revealed
                ? string(
                    abi.encodePacked(super.tokenURI(tokenId), _baseExtension)
                )
                : _notRevealedUri;
    }

    
    function changeMintCost(uint256 mintCost) public onlyOwner {
        require(
            PRICE != mintCost,
            "mint Cost cannot be same as previous"
        );
        PRICE = mintCost;
    }

   
    function Enter(address _to, uint32 _amount) private {
        require(_to != address(0), "Address cannot be 0");

        for (uint32 i = _amount; i > 0; i--) {
            _tokenIds.increment();

            _safeMint(_to, _tokenIds.current());

            emit BoxMinted(_to, _tokenIds.current());
        }
    }

    /// Society + Free Mint + Blacklisted T1/T2/T3


    function freemint(
        uint32 _amount,
        bytes32[] memory _proof,
        uint32 freeTokensAmount,
        uint32 tier
    ) private {
        require(
            _boxStatus >= BoxStatus(tier),
            "please wait the next sale phase to mint"
        );

        if (_claimed[msg.sender]) {
            require(
                balanceOf(msg.sender).add(_amount) <= 10 + freeTokensAmount,
                "Limit Exceeded, Ser"
            );

            Enter(msg.sender, _amount);
        } else {
            require(
                balanceOf(msg.sender).add(_amount) <= 10,
                "Limit Exceeded, Ser"
            );

            isAllowedFreeMint(_proof, freeTokensAmount, tier);

            _claimed[msg.sender] = true;

            Enter(msg.sender, uint32(_amount.add(freeTokensAmount)));
        }
    }

    /// LET'S GET STEALTHYYYYY

    function stealthMint(uint32 _amount) private {
        require(
            balanceOf(msg.sender).add(_amount) <= _STEALTH_MINT_LIMIT,
            "YOU HAVE HAD ENOUGH BOXES, RELAX"
        );
        Enter(msg.sender, _amount);
    }

    
    function mint(
        uint32 _amount,
        bytes32[] memory _proof,
        uint32 freeTokensAmount,
        uint32 tier
    ) external payable whenNotPaused onlyIfAvailable(_amount) nonReentrant {
        if (_boxStatus == BoxStatus.STEALTH) {
            stealthMint(_amount);
        } else {
            freemint(_amount, _proof, freeTokensAmount, tier);
        }
    }

    
    function adminMint(address to, uint32 _amount)
        external
        whenNotPaused
        nonReentrant
        onlyOwner
    {
        require(to != address(0), "Address cannot be 0");
        require(
            (_tokenIds.current()).add(_amount) <= _BBOXES,
            "Supply of Black Boxes has run out"
        );
        require(_amount > 0, "Amount must be greater than 0");
        Enter(to, _amount);
    }

    /// Gearshift

    function setStatus(BoxStatus _status) external onlyOwner {
        require(_boxStatus != _status, "Status cannot be the same");
        _boxStatus = _status;

        emit StatusChanged(_status);
    }

    
    function changeBaseURI(string memory newBaseURI)
        public
        onlyOwner
        notLocked
    {
        require(
            (keccak256(abi.encodePacked((_baseTokenURI))) !=
                keccak256(abi.encodePacked((newBaseURI)))),
            "Base URI cannot be same as previous"
        );
        _baseTokenURI = newBaseURI;
    }

    
    function changeNotRevealedURI(string memory newNotRevealedUri)
        public
        onlyOwner
        notRevealed
    {
        require(
            (keccak256(abi.encodePacked((newNotRevealedUri))) !=
                keccak256(abi.encodePacked((_notRevealedUri)))),
            "Base URI cannot be same as previous"
        );
        _notRevealedUri = newNotRevealedUri;
    }

   
    function reveal() public onlyOwner notRevealed {
        revealed = true;
    }

   
    function lockMetadata() public onlyOwner notLocked Revealed {
        locked = true;
    }

    
    function changeMerkleRoot(bytes32 merkleRoot) public onlyOwner {
        require(
            merkleRoot != _merkleRoot,
            "Merkle root cannot be same as previous"
        );
        _merkleRoot = merkleRoot;
    }

    
    function isAllowedFreeMint(
        bytes32[] memory _proof,
        uint32 freeTokensAmount,
        uint32 tier
    ) internal view returns (bool) {
        require(
            MerkleProof.verify(
                _proof,
                _merkleRoot,
                keccak256(abi.encodePacked(msg.sender, freeTokensAmount, tier))
            ),
            "Caller is not Allowed free mints"
        );
        return true;
    }

    
    function currentSupply() external view returns (uint256) {
        return _tokenIds.current();
    }

    
    function changeMintBeneficiary(address beneficiary) public onlyOwner {
        require(
            beneficiary != address(0),
            "Minting beneficiary cannot be address 0"
        );
        require(
            beneficiary != _mintingBeneficiary,
            "beneficiary cannot be same as previous"
        );
        _mintingBeneficiary = payable(beneficiary);
    }

    
    function pause() public virtual onlyOwner {
        _pause();
    }

    
    function unpause() public virtual onlyOwner {
        _unpause();
    }

    /// You shouldn't open, you know this, put you can press buttons and I will spit something out


    function getContractState()
        external
        view
        returns (
            BoxStatus boxStatus_,
            uint256 bboxes_,
            uint256 price_,
            uint256 currentTokenId_,
            uint256 boxesLeft_
        )
    {
        return (
            _boxStatus,
            _BBOXES,
            PRICE,
            _tokenIds.current(),
            _BBOXES.sub(_tokenIds.current())
        );
    }

    function hasFreeMint(
        address account,
        bytes32[] memory _proof,
        uint32 freeTokensAmount,
        uint32 tier
    ) external view returns (bool allowedForFreeMint, bool freeTokensClaimed) {
        return (
            isAllowedFreeMint(_proof, freeTokensAmount, tier),
            _claimed[account]
        );
    }

    
    function tokenMinted(uint256 _tokenId) public view returns (bool) {
        return _exists(_tokenId);
    }

    
    function withdrawAll() external payable onlyOwner {
        require(address(this).balance != 0, "Balance is zero");
        (bool success, ) = _mintingBeneficiary.call{
            value: address(this).balance
        }("");
        require(success, "Failed to withdraw funds");
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override(ERC721, ERC721Pausable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }
    /// #thinkoutside #betheutility
}
/// Libraries
function freemint(
    uint32 _amount,
    bytes32[] memory _proof,
    uint32 freeTokensAmount,
    uint32 tier
) private {
    require(
        _boxStatus >= BoxStatus(tier),
        "please wait the next sale phase to mint"
    );

    if (_claimed[msg.sender]) {
        require(
            balanceOf(msg.sender).add(_amount) <= 10 + freeTokensAmount,
            "Limit Exceeded, Ser"
        );

        Enter(msg.sender, _amount);
    } else {
        require(
            balanceOf(msg.sender).add(_amount) <= 10,
            "Limit Exceeded, Ser"
        );

        isAllowedFreeMint(_proof, freeTokensAmount, tier);

        _claimed[msg.sender] = true;

        Enter(msg.sender, uint32(_amount.add(freeTokensAmount)));
    }
}

/// Society + Free Mint + Blacklisted T1/T2/T3