// File: @openzeppelin/contracts/token/ERC20/IERC20.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `from` to `to` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) external returns (bool);
}

// File: @openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC721/extensions/IERC721Enumerable.sol)

pragma solidity ^0.8.0;

import "../IERC721.sol";

/**
 * @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
 * @dev See https://eips.ethereum.org/EIPS/eip-721
 */
interface IERC721Enumerable is IERC721 {
    /**
     * @dev Returns the total amount of tokens stored by the contract.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns a token ID owned by `owner` at a given `index` of its token list.
     * Use along with {balanceOf} to enumerate all of ``owner``'s tokens.
     */
    function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256);

    /**
     * @dev Returns a token ID at a given `index` of all the tokens stored by the contract.
     * Use along with {totalSupply} to enumerate all tokens.
     */
    function tokenByIndex(uint256 index) external view returns (uint256);
}

// File: @openzeppelin/contracts/token/ERC721/IERC721.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/IERC721.sol)

pragma solidity ^0.8.0;

import "../../utils/introspection/IERC165.sol";

/**
 * @dev Required interface of an ERC721 compliant contract.
 */
interface IERC721 is IERC165 {
    /**
     * @dev Emitted when `tokenId` token is transferred from `from` to `to`.
     */
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
     */
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
     */
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    /**
     * @dev Returns the number of tokens in ``owner``'s account.
     */
    function balanceOf(address owner) external view returns (uint256 balance);

    /**
     * @dev Returns the owner of the `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function ownerOf(uint256 tokenId) external view returns (address owner);

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
    ) external;

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
     * are aware of the ERC721 protocol to prevent tokens from being forever locked.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    /**
     * @dev Transfers `tokenId` token from `from` to `to`.
     *
     * WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721
     * or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
     * understand this adds an external call which potentially creates a reentrancy vulnerability.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    /**
     * @dev Gives permission to `to` to transfer `tokenId` token to another account.
     * The approval is cleared when the token is transferred.
     *
     * Only a single account can be approved at a time, so approving the zero address clears previous approvals.
     *
     * Requirements:
     *
     * - The caller must own the token or be an approved operator.
     * - `tokenId` must exist.
     *
     * Emits an {Approval} event.
     */
    function approve(address to, uint256 tokenId) external;

    /**
     * @dev Approve or remove `operator` as an operator for the caller.
     * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
     *
     * Requirements:
     *
     * - The `operator` cannot be the caller.
     *
     * Emits an {ApprovalForAll} event.
     */
    function setApprovalForAll(address operator, bool _approved) external;

    /**
     * @dev Returns the account approved for `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function getApproved(uint256 tokenId) external view returns (address operator);

    /**
     * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
     *
     * See {setApprovalForAll}
     */
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

// File: @openzeppelin/contracts/utils/introspection/IERC165.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC165 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-165[EIP].
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others ({ERC165Checker}).
 *
 * For an implementation, see {ERC165}.
 */
interface IERC165 {
    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

// File: contracts/QuitclaimAcceptance.sol



pragma solidity ^0.8.16;

import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/**
* This contract wants to represent the Agreement between the OG PugFrens NFT collection creators
* and the Community (represented by Community Leaders).
*
* For the details of the agreement, refer to the original agreement published on Arweave Network.
**/
contract QuitclaimAcceptance {

    struct Sign {
        uint256 ogTeamSign;
        uint256 communitySign;
    }

    // uint
    uint private arcadeLandId = 4863;
    uint[] private pugFrensIDs = [2243, 2246, 2248, 2249, 2250, 2252, 2253, 2254, 2256, 2258, 2260, 2261, 3252, 4751, 8114];
    uint public assetsReceivedDate;  // timestamp of when assets are moved to the contract

    // boolean
    bool public assetsReceived = false;  // true when OG Team transfers all the assets to the Quitclaim contract

    // bytes32
    bytes32 public AGREEMENT_DOCUMENT_HASH;  // hash of the agreement document PDF published on Arweave

    // string
    string public AGREEMENT_PUBLIC_URL;  //  URL of the PDF agreement on Arweave

    // address
    address private OGTeamWallet;
    address private communityWallet;
    IERC721Enumerable ArcadeLandCollection = IERC721Enumerable(0x4a8C9D751EEAbc5521A68FB080DD7E72E46462aF);
    IERC721Enumerable PugFrensNftCollection = IERC721Enumerable(0xB9E971C00b1257D0f6D67EFf37f4a46e1AB6BF24);
    IERC20 USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);

    // structs
    Sign public agreementSign;

    constructor(address _OGTeamWallet, address _communityWallet) {
        OGTeamWallet = _OGTeamWallet;
        communityWallet = _communityWallet;
    }

    /**
    * Modifiers
    **/

    /**
    * Accepts calls only from allowed signers
    **/
    modifier onlySigners {
        require(msg.sender == OGTeamWallet || msg.sender == communityWallet, "Not a valid signer");
        _;
    }

    /**
    * SIGN SECTION
    **/

    /**
    * @notice sets the agreements Hash and URL
    * @param _agreementHash - sha256 hash of the document PDF
    * @param _agreementUrl - Arweave URL of the agreement public URL
    **/
    function setAgreementHashAndUrl (bytes32 _agreementHash, string memory _agreementUrl) public onlySigners {
        require(AGREEMENT_DOCUMENT_HASH == bytes32(0) && bytes(AGREEMENT_PUBLIC_URL).length == 0, "Details already set");
        AGREEMENT_DOCUMENT_HASH = _agreementHash;
        AGREEMENT_PUBLIC_URL = _agreementUrl;
    }

    /**
    * @notice Sing the agreement
    * @dev By calling this function, the caller accepts the content of the PDF Agreement
    * @param _contractHash the hash of the agreement. Must match AGREEMENT_DOCUMENT_HASH
    **/
    function signAndAccept(bytes32 _contractHash) public onlySigners {
        require(AGREEMENT_DOCUMENT_HASH != bytes32(0) && bytes(AGREEMENT_PUBLIC_URL).length > 0, "Contract details not yet set");
        require(AGREEMENT_DOCUMENT_HASH == _contractHash, "Invalid hash");
        require(assetsReceived, "assets not yet moved to Quitclaim SC");
        if (msg.sender == OGTeamWallet) {
            require(!hasTeamSigned(), "OGTeam already signed");
            agreementSign.ogTeamSign = block.timestamp;
        }
        if (msg.sender == communityWallet) {
            require(!hasCommunitySigned(), "Community already signed");
            agreementSign.communitySign = block.timestamp;
        }
    }

    /**
    * @notice Get if the OG Team has signed the contract
    * @return _signed True if the OG team has signed the contract, false otherwise
    **/
    function hasTeamSigned() public view returns(bool _signed) {
        return agreementSign.ogTeamSign > 0;
    }

    /**
    * @notice Get if the Community Wallet has signed the contract
    * @return _signed True if the Community Wallet has signed the contract, false otherwise
    **/
    function hasCommunitySigned() public view returns(bool _signed) {
        return agreementSign.communitySign > 0;
    }

    /**
    * @notice Get if the contract has been signed by both parties
    * @return _signed True if both parties signed, false otherwise
    **/
    function isAgreementSigned() public view returns(bool _signed) {
        return hasTeamSigned() && hasCommunitySigned();
    }


    /**
    * TRANSFER ASSETS SECTION
    * Manage the transfer of assets and funds between the parties, in the different parts of the agreement
    **/

    /**
    * @notice Move the assets from the OG Team Wallet to this contract
    * @dev Anyone can call this function, but won't execute without allowances
    **/
    function moveAssetsToContract() public {
        require(AGREEMENT_DOCUMENT_HASH != bytes32(0) && bytes(AGREEMENT_PUBLIC_URL).length > 0, "Contract details not yet set");
        require (!assetsReceived, "Already performed");

        // ArcadeLand
        require(ArcadeLandCollection.getApproved(arcadeLandId) == address(this), "Missing transfer approval");
        ArcadeLandCollection.transferFrom(OGTeamWallet, address(this), arcadeLandId);

        // PugFrens
        for (uint idx = 0; idx < pugFrensIDs.length; ++idx) {
            uint id = pugFrensIDs[idx];
            require(PugFrensNftCollection.getApproved(id) == address(this), "Missing transfer approval");
            PugFrensNftCollection.transferFrom(OGTeamWallet, address(this), id);
        }

        // USDC
        require(USDC.allowance(OGTeamWallet, address(this)) >= 45000 * 10**6, "Missing transfer approval");
        USDC.transferFrom(OGTeamWallet, address(this), 45000 * 10**6);

        assetsReceived = true;
        assetsReceivedDate = block.timestamp;
    }

    /**
    * @notice Move all the assets to the Community Wallet, once the agreement has been signed
    **/
    function moveAssetsToCommunity() public {
        require(isAgreementSigned(), "Not signed");
        require(msg.sender == OGTeamWallet, "Only OGTeam wallet can call this function");
        _transferERC721(communityWallet);
        moveUSDC(25000 * (10 ** 6));
    }

    /**
    * @notice Move part of the USDC from this contract to the Community wallet
    * @dev Only OG Team can call this function, as per agreement. 25k USDC per first movement, 20k with a second call to this function
    **/
    function moveUSDC(uint256 amount) public {
        require(assetsReceived, "Assets not received");
        require(msg.sender == OGTeamWallet, "Only OGTeam wallet can call this function");
        require(isAgreementSigned(), "Not signed");

        uint balance = USDC.balanceOf(address(this));
        require(balance > 0, "All USDC transferred");
        require(balance >= amount, "Not enough balance");
        USDC.transfer(communityWallet, amount);
    }

    /**
    * @notice Transfers the ERC721 assets from this contract to the desired receiver
    * @param _to address to send
    **/
    function _transferERC721(address _to) private {
        require(assetsReceived, "Assets not received");

        // ArcadeLand
        if (ArcadeLandCollection.ownerOf(arcadeLandId) == address(this))
            ArcadeLandCollection.transferFrom(address(this), _to, arcadeLandId);

        // PugFrens
        for (uint idx = 0; idx < pugFrensIDs.length; ++idx) {
            uint id = pugFrensIDs[idx];
            if (PugFrensNftCollection.ownerOf(id) == address(this))
                PugFrensNftCollection.transferFrom(address(this), _to, id);
        }
    }

    /**
    * @notice OG Team Wallet can recover the assets if not redeem or used within a year
    * @param _withNFT true if we want to move the NFTs too
    **/
    function recoverLockedAssetsAfterSign(bool _withNFT) public {
        require(msg.sender == OGTeamWallet, "Not OGTeam wallet");
        require(block.timestamp > (assetsReceivedDate + 60*60*24*365), "Minimum timeframe not passed");
        if (_withNFT) _transferERC721(OGTeamWallet);
        USDC.transfer(OGTeamWallet, USDC.balanceOf(address(this)));
    }

    /**
    * @notice OG Team Wallet can recover all the assets if the agreement is not signed by both parties
    **/
    function recoverLockedAssets() public {
        require(!isAgreementSigned(), "Agreement signed");
        _transferERC721(OGTeamWallet);
        USDC.transfer(OGTeamWallet, USDC.balanceOf(address(this)));
    }

}