contract CaptureComposableNFT is Ownable {
    struct Challenge {
        bytes32 answerHash;   // a hash of valid answer
        address winner;   // address(0) if not yet solved
        address from;  // from is an address from which a reward will be transferred
        uint256 tokenId;   // bundles is the address of nft contract
    }

    IERC721 public immutable bundles;
    mapping(uint256 => Challenge) public challenges;

    constructor(address _bundles) {
        bundles = IERC721(_bundles);
    }

    /**
     * @notice only the first player with correct answer gets a reward
     * @param controlHash see the contract docs for info and you can use calculateControlHash function
     * @param answer must be precise
     */
    function solveChallenge(uint256 challengeId, bytes32 controlHash, string memory answer) external {
        bytes32 expectedAnswerHash = challenges[challengeId].answerHash;
        require(expectedAnswerHash != 0x0, 'CaptureComposableNFT: Unknown challenge');
        bytes32 answerHash = keccak256(bytes(answer));
        require(expectedAnswerHash == answerHash, 'CaptureComposableNFT: wrong answer');
        require(keccak256(abi.encodePacked(answerHash, msg.sender)) == controlHash, 'CaptureComposableNFT: wrong controlHash');
        require(challenges[challengeId].winner == address(0), 'CaptureComposableNFT: challenge already solved');

        challenges[challengeId].winner = msg.sender;
        address from = challenges[challengeId].from;
        uint256 tokenId = challenges[challengeId].tokenId;
        bundles.safeTransferFrom(from, msg.sender, tokenId);
    }

    /**
     * @notice a player can check if the answer is correct before sending tx and losing gas
     */
    function checkAnswer(uint256 challengeId, string memory answer) public view returns (bool) {
        return challenges[challengeId].answerHash == keccak256(bytes(answer));
    }

    function isSolved(uint256 challengeId) external view returns (bool) {
        return challenges[challengeId].answerHash != 0x0 && challenges[challengeId].winner != address(0);
    }

    /**
     * @notice a helper function for a player to ease calculating a control hash
     */
    function calculateControlHash(string memory answer, address solver) external pure returns (bytes32) {
        bytes32 answerHash = keccak256(bytes(answer));
        return keccak256(abi.encodePacked(answerHash, solver));
    }

    function newChallenge(uint256 challengeId, bytes32 answerHash, address from, uint256 tokenId) external onlyOwner {
        bytes32 expectedAnswerHash = challenges[challengeId].answerHash;
        require(expectedAnswerHash == 0x0, 'CaptureComposableNFT: challengeId already taken');
        require(bundles.ownerOf(tokenId) == from, 'CaptureComposableNFT: tokenId is not owned by from address');
        require(bundles.isApprovedForAll(from, address(this)), 'CaptureComposableNFT: tokenId is not approved by from address');

        challenges[challengeId].answerHash = answerHash;
        challenges[challengeId].from = from;
        challenges[challengeId].tokenId = tokenId;
    }

    function dropChallenge(uint256 challengeId) external onlyOwner {
        bytes32 expectedAnswerHash = challenges[challengeId].answerHash;
        require(expectedAnswerHash != 0x0, 'CaptureComposableNFT: Unknown challenge');
        require(challenges[challengeId].winner == address(0), 'CaptureComposableNFT: challenge already solved');

        delete challenges[challengeId];
    }

}
/**
 * @notice The contract for capture composable nft challenges
 * controlHash is keccak256(keccak256(answer), msg.sender) and is a simple prevention against front running
 * To win a player gives an answer, keccak256(answer) must agree with stored Challenge.answerHash
 * For every challenge there is one nft as reward, actually it is erc998 composable/bundle
 * @dev Only owner can add or remove challenges, no additional time constraints
 * Do not transfer reward NFTs to this contract !!!
 * Before adding new challenge, current nft owner (aka from) must approveAll this contract
 * There can be only one address of reward nfts (aka bundles)
 * Note, there is no check if tokenId is duplicated, it is responsibility of the owner to not repeat rewards
 */
function checkAnswer(uint256 challengeId, string memory answer) public view returns (bool) {
    return challenges[challengeId].answerHash == keccak256(bytes(answer));
}

/**
 * @notice a player can check if the answer is correct before sending tx and losing gas
 */