contract ForgottenSouls is ERC721Enumerable, ReentrancyGuard, Ownable {
    using Strings for uint256;

    struct Flammable {
        uint256 offset;
        uint256 startBlock;
    }
    mapping(address => Flammable) public flammables;
    address[] public flammableAddresses;

    // resolve soulId to source tokenId, caveat is you need to know the
    // flammable offsets
    mapping(uint256 => uint256) public soulIdToFlammableTokenId;

    // keep the order of a given soul's burning
    mapping(uint256 => uint256) public soulIdToBurnIdx;

    // use this to enumerate all souls. nextBurnIdx is your max
    mapping(uint256 => uint256) public burnIdxToSoulId;

    // resolve a flammable token to the soulId. Useful for Book of Lore
    mapping(address => mapping(uint256 => uint256))
        public flammableTokenToSoulId;

    address public bookOfLoreAddress;
    address public infinityVeilAddress;

    string public provenanceHash;

    // the typical base token URI stored here
    string public _baseTokenURI;

    // periodically we'll archive to IPFS and store here
    string public _baseTokenArchiveURI;
    uint256 public _archiveBurnIdx = 0;

    uint256 public nextBurnIdx = 0;
    uint256 public sacredFlameTokenId = 0;

    uint256 public constant MAX_SUPPLY = 1112;

    event SoulBurned(
        address tokenContract,
        uint256 tokenId,
        uint256 soulId,
        uint256 burnIdx
    );

    constructor(
        address _infinityVeilAddress,
        address _bookOfLoreAddress,
        string memory _baseURI
    ) ERC721('ForgottenSouls', 'SOULS') {
        infinityVeilAddress = _infinityVeilAddress;
        bookOfLoreAddress = _bookOfLoreAddress;
        setBaseURI(_baseURI);
    }

    function baseURI() public view returns (string memory) {
        return _baseTokenURI;
    }

    function baseArchiveURI() public view returns (string memory) {
        return _baseTokenArchiveURI;
    }

    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        require(
            _exists(tokenId),
            'ERC721Metadata: URI query for nonexistent token'
        );

        if (
            _archiveBurnIdx > 0 && soulIdToBurnIdx[tokenId] <= _archiveBurnIdx
        ) {
            return
                string(
                    abi.encodePacked(_baseTokenArchiveURI, tokenId.toString())
                );
        } else {
            return string(abi.encodePacked(_baseTokenURI, tokenId.toString()));
        }
    }

    function tokensOfOwner(address _owner)
        external
        view
        returns (uint256[] memory)
    {
        uint256 tokenCount = balanceOf(_owner);
        if (tokenCount == 0) {
            // Return an empty array
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            uint256 index;
            for (index = 0; index < tokenCount; index++) {
                result[index] = tokenOfOwnerByIndex(_owner, index);
            }
            return result;
        }
    }

    function numFlammableAddresses() public view returns (uint256) {
        return flammableAddresses.length;
    }

    // I know you think this function should be called "burn" because you're
    // burning a Wizard and a Flame but that is just confusing to everyone
    function mint(address _tokenContract, uint256 _tokenId)
        public
        nonReentrant
    {
        require(
            (flammables[_tokenContract].startBlock > 0) &&
                (block.number > flammables[_tokenContract].startBlock),
            'NOT_FLAMMABLE'
        );

        require(
            IERC721(_tokenContract).ownerOf(_tokenId) == _msgSender(),
            'NOT_OWNER'
        );

        require(
            IERC1155(infinityVeilAddress).balanceOf(
                _msgSender(),
                sacredFlameTokenId
            ) > 0,
            'NO_FLAMES'
        );

        require(nextBurnIdx < MAX_SUPPLY, 'MAX_SUPPLY');

        uint256 newSoulTokenId = _tokenId + flammables[_tokenContract].offset;

        soulIdToFlammableTokenId[newSoulTokenId] = _tokenId;
        soulIdToBurnIdx[newSoulTokenId] = nextBurnIdx;
        burnIdxToSoulId[nextBurnIdx] = newSoulTokenId;
        flammableTokenToSoulId[_tokenContract][_tokenId] = newSoulTokenId;

        // write the Lore
        if (bookOfLoreAddress != address(0)) {
            IBookOfLore(bookOfLoreAddress).addLoreWithScribe(
                _tokenContract,
                _tokenId,
                0,
                false,
                ''
            );
        }

        // burn the flame
        IERC1155Burnable(infinityVeilAddress).burn(
            _msgSender(),
            sacredFlameTokenId,
            1
        );

        // burn the wizardToken :(
        IERC721Burnable(_tokenContract).burn(_tokenId);

        _safeMint(_msgSender(), newSoulTokenId);

        // emit the event
        emit SoulBurned(_tokenContract, _tokenId, newSoulTokenId, nextBurnIdx);

        // increment the soul id
        nextBurnIdx = nextBurnIdx + 1;
    }

    function burnSoulTokenItselfDestroyingItForever(uint256 tokenId)
        public
        virtual
    {
        require(
            _isApprovedOrOwner(_msgSender(), tokenId),
            'ERC721Burnable: caller is not owner nor approved'
        );
        _burn(tokenId);
    }

    /*
     * Only the owner can do these things
     */
    function addFlammable(
        address contractAddress,
        uint256 offset,
        uint256 startBlock
    ) public onlyOwner {
        flammableAddresses.push(contractAddress);
        setFlammable(contractAddress, offset, startBlock);
    }

    function setFlammable(
        address contractAddress,
        uint256 offset,
        uint256 startBlock
    ) public onlyOwner {
        flammables[contractAddress].offset = offset;
        flammables[contractAddress].startBlock = startBlock;
    }

    function setBaseURI(string memory _newBaseURI) public onlyOwner {
        _baseTokenURI = _newBaseURI;
    }

    function setProvenanceHash(string memory _newProvenanceHash)
        public
        onlyOwner
    {
        provenanceHash = _newProvenanceHash;
    }

    function setInfinityVeilContractAddress(address _infinityVeilAddress)
        public
        onlyOwner
    {
        infinityVeilAddress = _infinityVeilAddress;
    }

    function setBookOfLoreContractAddress(address _bookOfLoreAddress)
        public
        onlyOwner
    {
        bookOfLoreAddress = _bookOfLoreAddress;
    }

    function setFlameTokenId(uint256 _sacredFlameTokenId) public onlyOwner {
        sacredFlameTokenId = _sacredFlameTokenId;
    }

    // emergency exit functions
    function withdrawAll() public payable onlyOwner {
        require(payable(msg.sender).send(address(this).balance));
    }

    function forwardERC20s(IERC20 _token, uint256 _amount) public onlyOwner {
        _token.transfer(msg.sender, _amount);
    }

    function setTokenArchiveURI(
        string memory _newTokenArchiveURI,
        uint256 _newArchiveBurnIdx
    ) public onlyOwner {
        require(_newArchiveBurnIdx < nextBurnIdx, 'NOT_CREATED_YET');
        _baseTokenArchiveURI = _newTokenArchiveURI;
        _archiveBurnIdx = _newArchiveBurnIdx;
    }

    function uploadImages(bytes calldata s) external onlyOwner {}

    function uploadAttributes(bytes calldata s) external onlyOwner {}
}
function addFlammable(
    address contractAddress,
    uint256 offset,
    uint256 startBlock
) public onlyOwner {
    flammableAddresses.push(contractAddress);
    setFlammable(contractAddress, offset, startBlock);
}

/*
 * Only the owner can do these things
 */