contract PnG is IPnG, ERC721, Accessable, Pausable, TraitSelector {
    uint256 constant MAX_INT = type(uint256).max;

    struct LastWrite {
        uint64 time;
        uint64 blockNum;
    }

    event GalleonMinted(uint256 indexed tokenId);
    event PirateMinted(uint256 indexed tokenId);

    event GalleonStolen(uint256 indexed tokenId);
    event PirateStolen(uint256 indexed tokenId);
    
    event GalleonBurned(uint256 indexed tokenId);
    event PirateBurned(uint256 indexed tokenId);

    // max number of tokens that can be minted: 50000 in production
    uint256 public maxTokens;
    // number of tokens that can be claimed for a fee: 10,000
    uint256 public PAID_TOKENS;
    // number of tokens have been minted so far
    uint16 public override minted;

    // tokenId -> traits
    mapping(uint256 => GalleonPirate) private tokenTraits;
    // tokenHash -> tokenId
    mapping(uint256 => uint256) public existingCombinations;
    // Tracks the last block and timestamp that a caller has written to state.
    // Disallow some access to functions if they occur while a change is being written.
    mapping(address => LastWrite) private lastWriteAddress;
    mapping(uint256 => LastWrite) private lastWriteToken;

    // reference to the Fleet contract to allow transfers to it without approval
    IFleet public fleet;
    ITraits public traits;

    constructor(uint256 _maxTokens) ERC721("Pirate Game", "PnG") {
        maxTokens = _maxTokens;
        PAID_TOKENS = _maxTokens / 5;
        _pause();
    }

    /** MODIFIERS */

    modifier requireContractsSet() {
        require(address(traits) != address(0) && address(fleet) != address(0), "Contracts not set");
        _;
    }

    modifier blockIfChangingAddress() {
        require(isAdmin(_msgSender()) || lastWriteAddress[tx.origin].blockNum < block.number, "Cannot interact in the current block");
        _;
    }

    modifier blockIfChangingToken(uint256 tokenId) {
        require(isAdmin(_msgSender()) || lastWriteToken[tokenId].blockNum < block.number, "Cannot interact in the current block");
        _;
    }

    function setContracts(address _traits, address _fleet) external onlyOwner {
        traits = ITraits(_traits);
        fleet = IFleet(_fleet);
    }

    /** EXTERNAL */

    function getTokenWriteBlock(uint256 tokenId) external view override
        onlyAdmin
        returns(uint64) 
    {
        return lastWriteToken[tokenId].blockNum;
    }

    /** 
    * Mint a token - any payment / game logic should be handled in the game contract. 
    * This will just generate random traits and mint a token to a designated address.
    */
    function mint(address recipient, uint256 seed) external override 
        whenNotPaused 
        onlyAdmin
    {
        require(minted + 1 <= maxTokens, "All tokens minted");
        minted++;
        generate(minted, seed);
        if(tx.origin != recipient && recipient != address(fleet)) {
            // Stolen!
            if(tokenTraits[minted].isGalleon) {
                emit GalleonStolen(minted);
            }
            else {
                emit PirateStolen(minted);
            }
        }
        _safeMint(recipient, minted);
    }

    /** 
    * Burn a token - any game logic should be handled before this function.
    */
    function burn(uint256 tokenId) external override 
        whenNotPaused 
        onlyAdmin
    {
        require(ownerOf(tokenId) == tx.origin, "Oops you don't own that");
        if(tokenTraits[tokenId].isGalleon) {
            emit GalleonBurned(tokenId);
        }
        else {
            emit PirateBurned(tokenId);
        }
        _burn(tokenId);
    }

    function updateOriginAccess(uint16[] memory tokenIds) external override 
        onlyAdmin
    {
        uint64 blockNum = uint64(block.number);
        uint64 time = uint64(block.timestamp);
        lastWriteAddress[tx.origin] = LastWrite(time, blockNum);
        for (uint256 i = 0; i < tokenIds.length; i++) {
            lastWriteToken[tokenIds[i]] = LastWrite(time, blockNum);
        }
    }

    function transferFrom( address from, address to, uint256 tokenId) 
        public 
        virtual 
        override(ERC721, IERC721) 
        blockIfChangingToken(tokenId) 
    {
        // allow admin contracts to be send without approval
        if(!isAdmin(_msgSender())) {
            require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
        }
        _transfer(from, to, tokenId);
    }

    /** INTERNAL */

    /**
    * generates traits for a specific token, checking to make sure it's unique
    * @param tokenId the id of the token to generate traits for
    * @param seed a pseudorandom 256 bit number to derive traits from
    * @return t - a struct of traits for the given token ID
    */
    function generate(uint256 tokenId, uint256 seed) internal returns (GalleonPirate memory t) {
        t = selectTraits(seed);
        if (existingCombinations[structToHash(t)] == 0) {
            tokenTraits[tokenId] = t;
            existingCombinations[structToHash(t)] = tokenId;

            if(t.isGalleon)
                emit GalleonMinted(tokenId);
            else
                emit PirateMinted(tokenId);

            return t;
        }

        if (seed == MAX_INT)
            return generate(tokenId, 1);
        else
            return generate(tokenId, seed+1);
    }

    /**
    * converts a struct to a 256 bit hash to check for uniqueness
    * @param s the struct to pack into a hash
    * @return the 256 bit hash of the struct
    */
    function structToHash(GalleonPirate memory s) internal pure returns (uint256) {
        if (s.isGalleon) {
            return uint256(keccak256(abi.encodePacked(
                s.isGalleon,
                // Galleon
                s.base,
                s.deck,
                s.sails,
                s.crowsNest,
                s.decor,
                s.flags,
                s.bowsprit
        )));
        } else {
            return uint256(keccak256(abi.encodePacked(
                s.isGalleon,
                s.skin,
                s.clothes,
                s.hair,
                s.earrings,
                s.mouth,
                s.eyes,
                s.weapon,
                s.hat,
                s.alphaIndex
            )));
        }
    }

    /** READ */

    function isGalleon(uint256 tokenId) external view override 
        blockIfChangingToken(tokenId) 
        returns (bool) 
    {
        IPnG.GalleonPirate memory s = tokenTraits[tokenId];
        return s.isGalleon;
    }

    function getMaxTokens() external view override returns (uint256) {
        return maxTokens;
    }

    function getPaidTokens() external view override returns (uint256) {
        return PAID_TOKENS;
    }

    /** ADMIN */

    /**
    * updates the number of tokens for sale
    */
    function _setTokensAmount(uint16 _paidTokens, uint16 _maxTokens) external onlyAdmin {
        PAID_TOKENS = _paidTokens;
        maxTokens = _maxTokens;
    }

    /**
    * enables owner to pause / unpause minting
    */
    function _setPaused(bool _paused) external requireContractsSet onlyAdmin {
        if (_paused) _pause();
        else _unpause();
    }


    /** Traits */

    function getTokenTraits(uint256 tokenId) external view override 
        blockIfChangingAddress 
        blockIfChangingToken(tokenId) 
        returns (GalleonPirate memory) 
    {
        return tokenTraits[tokenId];
    }

    function tokenURI(uint256 tokenId) public view override 
        blockIfChangingAddress 
        blockIfChangingToken(tokenId) 
        returns (string memory) 
    {
        require(_exists(tokenId), "Token ID does not exist");
        return traits.tokenURI(tokenId);
    }


    function totalSupply() public view returns (uint256) {
        return minted;
    }

    
    function balanceOf(address owner) public view virtual override(ERC721, IERC721) 
        blockIfChangingAddress 
        returns (uint256) 
    {
        // checking on this address in the same block it's being modified
        require(
            isAdmin(_msgSender()) || lastWriteAddress[owner].blockNum < block.number, 
            "Cannot interact in the current block"
        );
        return super.balanceOf(owner);
    }


    function ownerOf(uint256 tokenId) public view virtual override(ERC721, IERC721) 
        blockIfChangingAddress 
        blockIfChangingToken(tokenId) 
        returns (address) 
    {
        address addr = super.ownerOf(tokenId);
        // checking on this address in the same block it's being modified
        require(
            isAdmin(_msgSender()) || lastWriteAddress[addr].blockNum < block.number, 
            "Cannot interact in the current block"
        );
        return addr;
    }


    function tokenByIndex(uint256 index) public view returns (uint256) {
        require(
            isAdmin(_msgSender()) || lastWriteToken[index+1].blockNum < block.number, 
            "Cannot interact in the current block"
        );
        return index + 1;
    }


    function approve(address to, uint256 tokenId) public virtual override(ERC721, IERC721) 
        blockIfChangingToken(tokenId) 
    {
        super.approve(to, tokenId);
    }


    function getApproved(uint256 tokenId) public view virtual override(ERC721, IERC721) 
        blockIfChangingToken(tokenId) 
        returns (address) 
    {
        return super.getApproved(tokenId);
    }


    function setApprovalForAll(address operator, bool approved) 
        public virtual override(ERC721, IERC721) 
        blockIfChangingAddress 
    {
        super.setApprovalForAll(operator, approved);
    }


    function isApprovedForAll(address owner, address operator) 
        public view virtual override(ERC721, IERC721) 
        blockIfChangingAddress 
        returns (bool) 
    {
        return super.isApprovedForAll(owner, operator);
    }
    

    function safeTransferFrom( address from, address to, uint256 tokenId) 
        public virtual override(ERC721, IERC721) 
        blockIfChangingToken(tokenId) 
    {
        super.safeTransferFrom(from, to, tokenId);
    }


    function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory _data)
        public virtual override(ERC721, IERC721) 
        blockIfChangingToken(tokenId) 
    {
        super.safeTransferFrom(from, to, tokenId, _data);
    }


    /**
     * allows owner to withdraw funds from minting
     */
    function _withdraw() external onlyTokenClaimer {
        payable(_msgSender()).transfer(address(this).balance);
    }
}
function isGalleon(uint256 tokenId) external view override 
    blockIfChangingToken(tokenId) 
    returns (bool) 
{
    IPnG.GalleonPirate memory s = tokenTraits[tokenId];
    return s.isGalleon;
}

/** READ */