contract Treasure is ReentrancyGuard, Ownable, ITreasure {
    using SafeMath for uint256;
    using Address for address;

    uint8 public constant MIN_BLOCK_DELAY = 20; // Min block delay to accept an answer
    uint8 public BLOCK_DELAY = 20; // Current block delay to accept an answer
    
    // Keeps track of all the transfers sent to fillTreasure() - Incremental only
    uint256 public historicalTreasure;
    
    // Stores the hash of the quest answers. questId: answerHash
    mapping (uint8 => bytes32) public questAnswers;

    // Stores answers precommits - FrontRunning mitigation
    struct Precommit {
        bytes32 commitAnswer;   
        uint256 block;
    }
    mapping (address => Precommit) precommitRecords;

    // Stores data of a quest winner.
    struct Winner {
        bool prizeClaimed;
        uint256 timestamp;
        uint256 nftId;
        uint256 paid;
        string answer;
    }
    mapping (uint8 => Winner) public prizeWinners; // questId: Winner

    address public _hashquestsContract;

    event treasureFilled(uint256 deposit, uint256 treasureBalance, uint256 historicalTreasure);
    event completedQuest(uint256 NFTId, string answer, address recipient, uint8 questId);
    event winnerCollectedPrize(uint256 valueCollected, uint256 NFTId, uint8 questId, address recipient);
    
    constructor(address hashquestsContract) ReentrancyGuard() {
        _hashquestsContract = hashquestsContract;

        // Correct answers for each quest
        questAnswers[1] = 0xd2cd94afe63a799d251a5209b25585cb10fe3ecf43c6c9e5250f477d11369c6f;
        questAnswers[2] = 0xec3026cfc4cea5f2c36c346ba91203504fcf60aaafd72614e785e1a31581e716;
        questAnswers[3] = 0xb32513bf27f6874ff2196e84f72fa973f5a2260668ae4faf37d37e409e3b19ab;
        questAnswers[4] = 0x7f4f10fdcb2a95fef25e212ee74d27f4477e198b7bb4c9e18281e4f55b499f19;
        questAnswers[5] = 0xd81f76c5cdeca64bb85acac6cada800772e07d0014059a5da5a0f3f584c3a365;
    }

    modifier validateToken(uint256 tokenId) {
        require(tokenId <= IERC721Enumerable(_hashquestsContract).totalSupply(), "Invalid NFT ID.");
        require(verifyNFTOwnership(msg.sender, tokenId), "Sender must be the owner of the NFT passed as the argument.");
        _;
    }

    modifier ongoingQuest(uint256 tokenId) {
        uint8 questId = getQuestId(tokenId);
        require(!prizeWinners[questId].prizeClaimed, "This quest is completed.");
        _;
    }

    /**
     * @dev Changes the BLOCK_DELAY to value > MIN_BLOCK_DELAY.
     * Meant to allow for increasing the block delay in case of clogged transactions.
     */
    function changeBlockDelay(uint8 newBlockDelay) onlyOwner public {
        require(newBlockDelay >= MIN_BLOCK_DELAY, "Block delay can't be smaller than the MIN_BLOCK_DELAY.");
        BLOCK_DELAY = newBlockDelay;
    }

    /**
     * @dev Payable method meant to fill the contract's balance.
     */
    function fillTreasure() public override payable returns (bool) {
        historicalTreasure = historicalTreasure.add(msg.value);
        emit treasureFilled(msg.value, address(this).balance, historicalTreasure);
        return true;
    }

    /**
     * @dev Commits an answer to mitigate front running. 
     * Hash must be a keccak256(tokenId, answer, msg.sender)
     */
    function precommitAnswer(bytes32 hashedMessage, uint256 tokenId) public validateToken(tokenId) ongoingQuest(tokenId) {
        precommitRecords[msg.sender] = (Precommit({
            block: block.number,
            commitAnswer: hashedMessage
        }));
    }

    /**
     * @dev Check for correct answer and pays the prize if correct
     */
    function answerQuest(string memory answer, uint256 tokenId) public validateToken(tokenId) ongoingQuest(tokenId) {
        Precommit memory commit = precommitRecords[msg.sender];
        require(block.number >= commit.block.add(BLOCK_DELAY), "Wait the block delay before submitting the answer.");

        // Validates commit
        bytes32 commitHash = keccak256(abi.encode(tokenId, answer, msg.sender));
        require(commit.commitAnswer == commitHash, "Your commited answer doesn't match your current answer.");

        // Validates answer
        bytes32 answerHash = keccak256(abi.encode(answer));
        uint8 questId = getQuestId(tokenId);
        bytes32 correctAnswer = questAnswers[questId];
        if (answerHash == correctAnswer) {
            prizeWinners[questId] = Winner({
                timestamp: block.timestamp,
                nftId: tokenId,
                paid: 0,
                prizeClaimed: true,
                answer: answer
            });
            emit completedQuest(tokenId, answer, msg.sender, questId);
            // Pays the winner
            claimTreaure(prizeWinners[questId], questId);
            precommitRecords[msg.sender];
        } else {
            revert("Incorrect answer! When changing the answer a new commit is requried");
        }
    }

    /**
     * @dev Check is msg.sender is the owner of the tokenId
     */
    function verifyNFTOwnership(address user, uint256 tokenId) private view returns (bool) {
        // Verify the ownership of NFT
        require(_hashquestsContract != address(0), "Owner haven't defined the Hashquests contract yet.");
        if (IERC721Enumerable(_hashquestsContract).ownerOf(tokenId) == user) {
            return true;
        }
        return false;
    }
    
    /**
     * @dev Returns the questId based on the last digit of tokenId
     */
    function getQuestId(uint256 tokenId) private pure returns (uint8) {
        uint256 lastDigit = tokenId.mod(10);
        if (lastDigit == 0 || lastDigit == 5) {
            return 1;
        } else if (lastDigit == 1 || lastDigit == 6) {
            return 2;
        } else if (lastDigit == 2 || lastDigit == 7) {
            return 3;
        } else if (lastDigit == 3 || lastDigit == 8) {
            return 4;
        } else {
            return 5;
        }
    }

    /**
     * @dev Keeps track of winners shares and pays the winner.
     */
    function claimTreaure(Winner storage winner, uint8 questId) private nonReentrant() {
        uint256 treasureShare = historicalTreasure.div(5);
        uint256 withdrawAmount = treasureShare.sub(winner.paid);
        require(withdrawAmount > 0, "All share of this NFTs treasure has been paid.");

        winner.paid = winner.paid.add(withdrawAmount);
        (bool success, ) = msg.sender.call{value:withdrawAmount}("");
        require(success, "Treasure transfer failed.");

        emit winnerCollectedPrize(withdrawAmount, winner.nftId, questId, msg.sender);
    }
    
    /**
     * @dev Allows a winner to withdraw if there is residual treasure for that quest.
     */
    function withdrawPartialReward(uint256 tokenId) public validateToken(tokenId) {
        uint8 questId = getQuestId(tokenId);
        require(prizeWinners[questId].prizeClaimed, "This quest isn't claimed yet. Use answerQuest method.");
        require(prizeWinners[questId].nftId == tokenId, "This NFT has no claim over the treasure.");
        claimTreaure(prizeWinners[questId], questId);
    }
}
function getQuestId(uint256 tokenId) private pure returns (uint8) {
    uint256 lastDigit = tokenId.mod(10);
    if (lastDigit == 0 || lastDigit == 5) {
        return 1;
    } else if (lastDigit == 1 || lastDigit == 6) {
        return 2;
    } else if (lastDigit == 2 || lastDigit == 7) {
        return 3;
    } else if (lastDigit == 3 || lastDigit == 8) {
        return 4;
    } else {
        return 5;
    }
}

/**
 * @dev Returns the questId based on the last digit of tokenId
 */