contract Project8008 is ERC721, Ownable, ReentrancyGuard {
    using Counters for Counters.Counter;
    using ECDSA for bytes32;

    Counters.Counter private _tokenIdCounter;

    uint256 public constant totalSupply = 2842;
    uint256 public public8008s = 2792; // Reserve 50 8008s for promotions, giveaways, and paying dev/artist, etc.
    uint256 public constant BLKS_TO_PROGRESS = 192000; // 30 days in blocks, using average of 13.5s/block
    uint256 public constant MAX_MINT_PER_ADDRESS = 2;
    bool public saleActive = false;
    string public base8008Uri;

    mapping(uint256 => uint256) public block_starts;
    mapping(uint256 => uint16) public transfers;

    event Minted8008(uint256 tokenId);
    event Transferred8008(uint256 tokenId);

    constructor(string memory baseURI) ERC721("Project 8008", "8008") {
        base8008Uri = baseURI;
    }

    // ACCESS CONTROL MODIFIERS --------------------------------------------------------------------------------
    modifier saleIsActive() {
        require(saleActive, "Sale is not open");
        _;
    }

    // Enforce two mints max per wallet
    modifier canMintToWallet(uint256 numberOfTokens) {
        require(
            balanceOf(msg.sender) + numberOfTokens <= MAX_MINT_PER_ADDRESS,
            "Max 2 mints per address"
        );
        _;
    }

    modifier hasSupplyPublic(uint256 numberOfTokens) {
        require(
            _tokenIdCounter.current() + numberOfTokens <= public8008s,
            "Public mints out of stock"
        );
        _;
    }

    modifier hasSupplyReserved(uint256 numberOfTokens) {
        // Check public mint has concluded
        require(
            _tokenIdCounter.current() >= public8008s,
            "Public mint has not concluded yet"
        );
        require(
            _tokenIdCounter.current() + numberOfTokens <= totalSupply,
            "Not enough remaining to mint"
        );
        _;
    }

    // Owner functions for tweaking params ------------------------------------------------------------------------
    function setBaseURI(string memory newuri) external onlyOwner {
        base8008Uri = newuri;
    }

    /*
     * Release remaining reserved 8008s to the public.
     */
    function cancelReserves() external onlyOwner {
        public8008s = totalSupply;
    }

    /*
     * Pause sale if active, make active if paused.
     */
    function flipSaleState() external onlyOwner {
        saleActive = !saleActive;
    }

    /**
     * Public Mint 8008s
     */
    function claim8008(uint256 numberOfTokens)
        external
        nonReentrant
        saleIsActive
        canMintToWallet(numberOfTokens)
        hasSupplyPublic(numberOfTokens)
    {
        // Minting as an if/else instead of for loop saves around 200 gas when minting 1 and 400 when minting 2.
        // Using block.chainid as an alternative to '1' saves 1 gas.
        if (numberOfTokens == block.chainid) {
            _createCollectible(msg.sender);
        } else if (numberOfTokens == 2) {
            _createCollectible(msg.sender);
            _createCollectible(msg.sender);
        }
    }

    /**
     * Mint reserved 8008s for giveaways, marketing and dev/artist payment purposes.
     */
    function reserve8008s(uint256 numberOfTokens)
        external
        nonReentrant
        onlyOwner
        hasSupplyReserved(numberOfTokens)
    {
        for (uint256 i = 0; i < numberOfTokens; i++) {
            _createCollectible(msg.sender);
        }
    }

    function _createCollectible(address mintAddress) private {
        _tokenIdCounter.increment();
        uint256 mintIndex = _tokenIdCounter.current();
        _safeMint(mintAddress, mintIndex);
        block_starts[mintIndex] = block.number;
        transfers[mintIndex] = 0;
        // Fire minted event for logs
        emit Minted8008(mintIndex);
    }

    function totalMinted() public view returns (uint256) {
        return _tokenIdCounter.current();
    }

    // ERC721 related
    /**
     * @dev See {ERC721-transferFrom}.
     */
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public override {
        super.transferFrom(from, to, tokenId);
        transfers[tokenId] = transfers[tokenId] + 1;
        emit Transferred8008(tokenId);
    }

    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes memory _data
    ) public override {
        super.safeTransferFrom(from, to, tokenId, _data);
        transfers[tokenId] = transfers[tokenId] + 1;
        emit Transferred8008(tokenId);
    }

    /*
     * Public funtion to get the number of progress iterations of a particular tokenId.
     */
    function getProgressNum(uint256 tokenId) internal view returns (uint256) {
        require(_exists(tokenId), "getProgressNum query for nonexistent token");
        uint256 currentAgeBlocks = (block.number - block_starts[tokenId]) /
            BLKS_TO_PROGRESS;
        uint256 currentAge = currentAgeBlocks + transfers[tokenId];
        return currentAge < 3 ? currentAge : 3; // currentAge.min(3);
    }

    /**
     * @dev Converts bytes16 to bytes32 hex
     * See https://stackoverflow.com/questions/67893318/solidity-how-to-represent-bytes32-as-string.
     */
    function toHex16(bytes16 data) internal pure returns (bytes32 result) {
        result =
            (bytes32(data) &
                0xFFFFFFFFFFFFFFFF000000000000000000000000000000000000000000000000) |
            ((bytes32(data) &
                0x0000000000000000FFFFFFFFFFFFFFFF00000000000000000000000000000000) >>
                64);
        result =
            (result &
                0xFFFFFFFF000000000000000000000000FFFFFFFF000000000000000000000000) |
            ((result &
                0x00000000FFFFFFFF000000000000000000000000FFFFFFFF0000000000000000) >>
                32);
        result =
            (result &
                0xFFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000) |
            ((result &
                0x0000FFFF000000000000FFFF000000000000FFFF000000000000FFFF00000000) >>
                16);
        result =
            (result &
                0xFF000000FF000000FF000000FF000000FF000000FF000000FF000000FF000000) |
            ((result &
                0x00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000) >>
                8);
        result =
            ((result &
                0xF000F000F000F000F000F000F000F000F000F000F000F000F000F000F000F000) >>
                4) |
            ((result &
                0x0F000F000F000F000F000F000F000F000F000F000F000F000F000F000F000F00) >>
                8);
        result = bytes32(
            0x3030303030303030303030303030303030303030303030303030303030303030 +
                uint256(result) +
                (((uint256(result) +
                    0x0606060606060606060606060606060606060606060606060606060606060606) >>
                    4) &
                    0x0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F) *
                39
        );
    }

    /**
     * @dev Converts bytes32 to string using toHex16
     * See https://stackoverflow.com/questions/67893318/solidity-how-to-represent-bytes32-as-string.
     */
    function toHex(bytes32 data) internal pure returns (string memory) {
        return
            string(
                abi.encodePacked(
                    "0x",
                    toHex16(bytes16(data)),
                    toHex16(bytes16(data << 128))
                )
            );
    }

    /**
     * @dev See {ERC721Metadata-tokenURI}.
     *
     * Hello devs and sleuthers. You might find something interesting here about our project ;)
     *
     * When this was a paid project, I intended to maybe not verify the contract and see if people figure out tokenURI generation to view the age progressions.
     * However now that this is a free mint, I will be verifying the contract, so this isn't as much of a challenge anymore for you.
     *
     * Keep it to yourself or share it with the community, either way I hope you enjoy the project <3
     */
    function tokenURI(uint256 tokenId)
        public
        view
        override
        returns (string memory)
    {
        require(
            _exists(tokenId),
            "ERC721Metadata: URI query for nonexistent token"
        );
        bytes32 hsh = keccak256(abi.encodePacked(tokenId + 2842));
        for (uint256 i = 0; i < getProgressNum(tokenId); i++) {
            hsh = keccak256(abi.encodePacked(hsh));
        }
        return string(abi.encodePacked(base8008Uri, toHex(hsh), ".json"));
    }

    function _baseURI() internal view override returns (string memory) {
        return base8008Uri;
    }
}
/*  
 Special thanks to the flotys and crypto coven contract which greatly
 helped me figure out how to implement certain features cleanly.
 
 As well as @gakonst's foundry for providing amazing dev tooling 
 https://github.com/gakonst/foundry
 */
function getProgressNum(uint256 tokenId) internal view returns (uint256) {
    require(_exists(tokenId), "getProgressNum query for nonexistent token");
    uint256 currentAgeBlocks = (block.number - block_starts[tokenId]) /
        BLKS_TO_PROGRESS;
    uint256 currentAge = currentAgeBlocks + transfers[tokenId];
    return currentAge < 3 ? currentAge : 3; // currentAge.min(3);
}

/*
 * Public funtion to get the number of progress iterations of a particular tokenId.
 */