contract Outlaw is
    ERC721,
    ERC721Enumerable,
    ERC721URIStorage,
    Pausable,
    Ownable,
    AccessControl,
    ERC721Burnable,
    ReentrancyGuard
{
    using Counters for Counters.Counter;

    uint96 private constant _ROYALTIES_PERCENTAGE_BASIS_POINTS = 777; // 100 is 1%
    Counters.Counter private _tokenIdCounter;
    address payable private _royaltiesReceipientAddress;
    mapping(uint256 => uint256) private _tokenIdsByImageId;
    mapping(uint256 => uint256) private _imageIdsByTokenId;

    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant WHITELIST_ROLE = keccak256("WHITELIST_ROLE");
    string public constant LICENSE = "https://olw.outlawsnft.io/license";
    uint256 public constant MAX_BATCH_MINT = 10;
    uint256 public constant MAX_SUPPLY = 7777;
    uint256 public constant MAX_WHITELIST_SUPPLY = 777;
    uint256 public constant PUBLIC_COST = 0.077 ether;
    uint256 public constant REMINT_COST = 0.00 ether;
    uint256 public constant WHITELIST_COST = 0.05 ether;

    bool public publicSale = false;
    uint256 public whitelistMaxMint = 0;

    constructor() ERC721("Outlaws", "OLW") ReentrancyGuard() {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MINTER_ROLE, msg.sender);
        _royaltiesReceipientAddress = payable(msg.sender);
        _tokenIdCounter.increment(); // start at 1
    }

    /// @notice mint an nft
    /// @param uris ipfs uris of the metadata
    function mint(string[] memory uris, uint256[] memory imageIds)
        public
        payable
    {
        require(
            uris.length == imageIds.length,
            "uris and image ids must be the same length"
        );
        uint256 mintAmount = uris.length;
        require(mintAmount > 0, "cannot mint 0 tokens");
        require(mintAmount <= MAX_BATCH_MINT, "minting too many tokens");
        if (hasRole(MINTER_ROLE, msg.sender)) {
            _increMint(msg.sender, uris, imageIds);
            return;
        } else if (_canWhitelist(mintAmount)) {
            require(
                msg.value >= (WHITELIST_COST * mintAmount),
                "not enough ether supplied"
            );
            _increMint(msg.sender, uris, imageIds);
            return;
        }
        require(publicSale, "public sale has not started yet");
        require(
            msg.value >= (PUBLIC_COST * mintAmount),
            "not enough ether supplied"
        );
        _increMint(msg.sender, uris, imageIds);
    }

    function mintCost(uint256 mintAmount) public view returns (uint256) {
        if (hasRole(MINTER_ROLE, msg.sender)) {
            return 0;
        } else if (_canWhitelist(mintAmount)) {
            return WHITELIST_COST * mintAmount;
        }
        return PUBLIC_COST * mintAmount;
    }

    /// @notice remint an nft
    function remint(
        uint256 tokenId,
        string memory uri,
        uint256 imageId
    ) public payable {
        address tokenOwner = ownerOf(tokenId);
        require(
            owner() == msg.sender || msg.sender == tokenOwner,
            "must own the token to remint"
        );
        if (owner() != msg.sender) {
            require(msg.value >= REMINT_COST, "not enough ether supplied");
        }
        require(
            _tokenIdsByImageId[imageId] <= 0 ||
                _tokenIdsByImageId[imageId] == tokenId,
            string("image id already minted")
        );
        _burn(tokenId);
        _safeMint(tokenOwner, tokenId);
        _associateImageIdWithTokenId(imageId, tokenId);
        _setTokenURI(tokenId, uri);
    }

    function tokenIdFromImageId(uint256 imageId) public view returns (uint256) {
        return _tokenIdsByImageId[imageId];
    }

    function imageIdFromTokenId(uint256 tokenId) public view returns (uint256) {
        return _imageIdsByTokenId[tokenId];
    }

    function walletOf(address tokenOwner)
        public
        view
        returns (uint256[] memory)
    {
        uint256 tokenCount = balanceOf(tokenOwner);
        uint256[] memory tokenIds = new uint256[](tokenCount);
        for (uint256 i; i < tokenCount; i++) {
            tokenIds[i] = tokenOfOwnerByIndex(tokenOwner, i);
        }
        return tokenIds;
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721Enumerable, AccessControl)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }

    /// @notice withdraw funds from contract
    function withdraw() external nonReentrant onlyOwner {
        (bool success, ) = payable(msg.sender).call{
            value: address(this).balance
        }("");
        require(success);
    }

    function contractBalance() external view onlyOwner returns (uint256) {
        return address(this).balance;
    }

    function addToWhitelist(address[] calldata addresses) public onlyOwner {
        for (uint256 i = 0; i < addresses.length; i++) {
            _grantRole(WHITELIST_ROLE, addresses[i]);
        }
    }

    function removeFromWhitelist(address[] calldata addresses)
        public
        onlyOwner
    {
        for (uint256 i = 0; i < addresses.length; i++) {
            _revokeRole(WHITELIST_ROLE, addresses[i]);
        }
    }

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

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

    function flipPublicSale() public onlyOwner {
        publicSale = !publicSale;
    }

    function setWhitelistMaxMint(uint256 _whitelistMaxMint) public onlyOwner {
        whitelistMaxMint = _whitelistMaxMint;
    }

    function burn(uint256 tokenId) public override onlyOwner {
        super.burn(tokenId);
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal override(ERC721, ERC721Enumerable) whenNotPaused {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function _increMint(
        address tokenOwner,
        string[] memory uris,
        uint256[] memory imageIds
    ) private {
        uint256 mintAmount = uris.length;
        require(
            (_tokenIdCounter.current() + mintAmount) <= MAX_SUPPLY,
            "all 7777 tokens have been minted"
        );
        for (uint256 i = 0; i < mintAmount; i++) {
            _mintOne(tokenOwner, uris[i], imageIds[i]);
        }
    }

    function _mintOne(
        address tokenOwner,
        string memory uri,
        uint256 imageId
    ) private {
        require(
            (_tokenIdCounter.current() + 1) <= MAX_SUPPLY,
            "all 7777 tokens have been minted"
        );
        require(
            _tokenIdsByImageId[imageId] <= 0,
            string("image id already minted")
        );
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(tokenOwner, tokenId);
        _associateImageIdWithTokenId(imageId, tokenId);
        _setTokenURI(tokenId, uri);
    }

    function _canWhitelist(uint256 mintAmount) private view returns (bool) {
        return
            hasRole(WHITELIST_ROLE, msg.sender) &&
            ((balanceOf(msg.sender) + mintAmount) <= whitelistMaxMint) &&
            ((_tokenIdCounter.current() + mintAmount) <= MAX_WHITELIST_SUPPLY ||
                whitelistMaxMint == 1);
    }

    function _burn(uint256 tokenId)
        internal
        override(ERC721, ERC721URIStorage)
    {
        _tokenIdsByImageId[_imageIdsByTokenId[tokenId]] = 0;
        _imageIdsByTokenId[tokenId] = 0;
        super._burn(tokenId);
    }

    function _baseURI() internal pure override returns (string memory) {
        return "ipfs://";
    }

    function _associateImageIdWithTokenId(uint256 imageId, uint256 tokenId)
        private
    {
        _tokenIdsByImageId[imageId] = tokenId;
        _imageIdsByTokenId[tokenId] = imageId;
    }
}
// @custom:security-contact zach@outlawsnft.io
function remint(
    uint256 tokenId,
    string memory uri,
    uint256 imageId
) public payable {
    address tokenOwner = ownerOf(tokenId);
    require(
        owner() == msg.sender || msg.sender == tokenOwner,
        "must own the token to remint"
    );
    if (owner() != msg.sender) {
        require(msg.value >= REMINT_COST, "not enough ether supplied");
    }
    require(
        _tokenIdsByImageId[imageId] <= 0 ||
            _tokenIdsByImageId[imageId] == tokenId,
        string("image id already minted")
    );
    _burn(tokenId);
    _safeMint(tokenOwner, tokenId);
    _associateImageIdWithTokenId(imageId, tokenId);
    _setTokenURI(tokenId, uri);
}

/// @notice remint an nft