// We will be using Solidity version 0.8.14
// OpenZeppelin Contracts v4.4.1 (utils/escrow/RefundEscrow.sol)
pragma solidity 0.8.18;




/**
 * @title RefundEscrowNilli
 * @dev Escrow that holds funds for a beneficiary, deposited from multiple
 * parties.
 * @dev Intended usage: See {Escrow}. Same usage guidelines apply here.
 * @dev The owner account (that is, the contract that instantiates this
 * contract) may deposit, close the deposit period, and allow for either
 * withdrawal by the beneficiary, or refunds to the depositors. All interactions
 * with `RefundEscrow` will be made through the owner contract.
 * @dev This contract is slightly modified from OZ to also include depositing, refunding,
 * and withdrawing ERC-20 tokens
 */

contract RefundEscrowNilli is ConditionalEscrow {
    using Address for address payable;

    enum State {
        Locked,
        Unlocked,
        Refunded,
        Delivered
    }

    struct crowdContract {
        string recDisplayName;
        string title;
        string creator;
        string condition1;
        string condition2;
        string condition3;
        bool[3] conditionFlags;
        bool isVerified;
        uint targetAmount;
        uint expirationDate;
        uint qtySigners;
        State curState;
    }

    event StateChange(State _oldState, State _newState);

    crowdContract public _crowdContract;
    
    /**
     * @dev escrowBalance Records the ERC-20 deposits to the contract.
     * Depositor address => ERC-20 address => Depositor amount.
     * Depositor address => ERC-721 token address => ERC-721 tokenID => bool (if in contract).
     */
    mapping(address => mapping(address => uint256)) public escrowBalance;
    mapping(address => mapping(address => mapping(uint256 => bool))) public escrowBalanceNFT;

    /**
     * @dev Constructor.
     */
    constructor(
        string memory recDisplayName,
        string memory title,
        string memory creator,
        string memory condition1,
        string memory condition2,
        string memory condition3,
        uint targetAmount,
        uint expirationDate) 
    {
        _crowdContract = crowdContract({
            recDisplayName : recDisplayName,
            title : title,
            creator : creator,
            condition1 : condition1,
            condition2 : condition2,
            condition3 : condition3,
            conditionFlags : [false, false, false],
            isVerified : false,
            targetAmount : targetAmount,
            expirationDate : expirationDate,
            qtySigners : 0,
            curState : State.Locked
        });
    }

     /**
     * @return The current state of the escrow.
     */
    function state() public view virtual returns (State) {
        State _state = _crowdContract.curState;
        return _state;
    }

    /**
     * @return The beneficiary of the escrow.
     */
    function beneficiary() public view virtual returns (address) {
        return owner();
    }

    /**
     * @return The conditional flags of the escrow.
     */
    function conditionFlags() public view returns (bool[3] memory) {
        return _crowdContract.conditionFlags;
    }

    /**
     * @dev verifies the crowdContract
     */    
    function verify() public onlyOwner {
        require(_crowdContract.isVerified == false, "RefundEscrow: contract is already verified");
        _crowdContract.isVerified = true;
        // TODO: do we need an event here?
    }

    /**
     * @dev adjusts the conditional flags
     */    
    function updateConditionFlags(bool[3] memory updatedFlags) public onlyOwner {
        _crowdContract.conditionFlags = updatedFlags;
        // TODO: do we need an event here?
    }

    /**
     * @dev Stores funds that may later be refunded.
     * @param refundee The address funds will be sent to if a refund occurs.
     */
    function deposit(address refundee) public payable virtual override {
        require(_crowdContract.curState == State.Locked, "RefundEscrow: can only deposit while active");
        super.deposit(refundee);
    }

    /**
     * @dev Allows for the beneficiary to withdraw their funds, rejecting
     * further deposits.
     */
    function stateChangeContract(State newState) public virtual onlyOwner {
        // require(_crowdContract.curState == State.Locked, "RefundEscrow: can only close while active");
        State oldState = _crowdContract.curState;
        _crowdContract.curState = newState;
        emit StateChange(oldState, _crowdContract.curState);
        //TODO: change owner to a beneficiary address
    }

    /**
     * @dev Withdraws the beneficiary's funds.
     */
     // TODO: change this to refund ETH to donator (I believe) - the `withdraw` fx should withdraw ETH to owner
    function withdrawETHOwner(address payable _owner) public virtual onlyOwner {
        require(_crowdContract.curState == State.Unlocked, "RefundEscrow: beneficiary can only withdraw while closed");
        _owner.sendValue(address(this).balance);
    }

    /**
     * @dev Returns whether refundees can withdraw their deposits (be refunded). The overridden function receives a
     * 'payee' argument, but we ignore it here since the condition is global, not per-payee.
     */
    function withdrawalAllowed(address) public view override returns (bool) {
        return _crowdContract.curState == State.Refunded;
    }

    // Customized addition of escrowing ERC-20 and ERC-721 tokens!*********************************************************
    /**
     * @dev Depositor can deposit ERC-20 token.
     * @param token The specific ERC-20 token to deposit to this contract.
     */
    function depositToken(IERC20 token, address depositor, uint256 amount) public onlyOwner {
        require(_crowdContract.curState == State.Locked, "RefundEscrow: can only deposit token while active");
        require(token.transferFrom(depositor, address(this), amount));
        escrowBalance[depositor][address(token)] += amount;
    }

    /**
     * @dev Depositors can withdraw their deposited ERC-20 tokens.
     * @param token The specific ERC-20 token to withdraw to depositor.
     */
    function withdrawToken(IERC20 token, address withdrawer) public onlyOwner {
        require(_crowdContract.curState == State.Refunded, "RefundEscrow: can only refund token when the state is refunding");
        uint256 amount = escrowBalance[withdrawer][address(token)];
        escrowBalance[withdrawer][address(token)] = 0;
        require(token.transfer(withdrawer, amount));
    }

    /**
     * @dev Beneficiary can withdraw a specific ERC-20.
     * @param token The specific ERC-20 token to withdraw to beneficiary.
     */
    function tokenWithdrawOwner(IERC20 token) public onlyOwner {
        require(_crowdContract.curState == State.Unlocked, "RefundEscrow: beneficiary can only withdraw token what closed");
        uint256 amount = token.balanceOf(address(this));
        require(token.transfer(owner(), amount));
    }

    /**
     * @dev Depositor can dump in some ERC-721 tokens here.
     * @param token The specific ERC-721 contract being deposited.
     * @param from The depositor address.
     * @param tokenId The NFT tokenID specific to this NFT collection.
     */
    function nftDepositContract(IERC721 token, address from, uint256 tokenId) public onlyOwner {
        require(_crowdContract.curState == State.Locked, "RefundEscrow: can only deposit NFT while active");
        escrowBalanceNFT[from][address(token)][tokenId] = true;
        token.safeTransferFrom(from, address(this), tokenId);
    }

    /**
     * @dev Depositor can dump in some ERC-721 tokens here.
     * @param token The specific ERC-721 contract being deposited.
     * @param tokenId The NFT tokenID specific to this NFT collection.
     */
    function nftWithdrawOwner(IERC721 token, uint256 tokenId) public onlyOwner {
        require(_crowdContract.curState == State.Unlocked, "RefundEscrow: beneficiary can only withdraw NFT when closed");
        require(token.ownerOf(tokenId) == address(this), "RefundEscrow: this NFT isn't currently owned by this contract");
        // TODO: doesn't currently remove/deduct from ERC721 mapping of recorded NFTs
        token.safeTransferFrom(address(this), owner(), tokenId);
    }

    /**
     * @dev Depositor can dump in some ERC-721 tokens here.
     * @param token The specific ERC-721 contract being deposited.
     * @param depositor The depositor address.
     * @param tokenId The NFT tokenID specific to this NFT collection.
     */
    function nftRefundContract(IERC721 token, address depositor, uint256 tokenId) public onlyOwner {
        require(_crowdContract.curState == State.Refunded, "RefundEscrow: depositors can only withdraw NFT refunds when the state is refunding");
        require(escrowBalanceNFT[depositor][address(token)][tokenId] == true, "RefundEscrow: this wasn't the original NFT owner");
        require(token.ownerOf(tokenId) == address(this), "RefundEscrow: this NFT isn't currently owned by this contract");
        escrowBalanceNFT[depositor][address(token)][tokenId] = false;
        token.safeTransferFrom(address(this), depositor, tokenId);
    }
    // End of customized addition of escrowing ERC-20 and ERC-721 tokens!*********************************************************

}
[{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"endowAddress","type":"address"},{"indexed":false,"internalType":"address","name":"depositorAddress","type":"address"},{"indexed":false,"internalType":"address","name":"depTokAddr","type":"address"},{"indexed":false,"internalType":"uint256","name":"amtDeposited","type":"uint256"},{"indexed":false,"internalType":"string","name":"depType","type":"string"}],"name":"ContractDeposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"endowAddress","type":"address"},{"indexed":false,"internalType":"enum RefundEscrowNilli.State","name":"newState","type":"uint8"}],"name":"ContractStateChange","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"endowAddress","type":"address"},{"indexed":false,"internalType":"address","name":"withdrawerAddress","type":"address"},{"indexed":false,"internalType":"address","name":"withdrawTokAddr","type":"address"},{"indexed":false,"internalType":"string","name":"withdrawType","type":"string"}],"name":"ContractWithdraw","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"contractAddress","type":"address"},{"indexed":false,"internalType":"string","name":"recDisplayName","type":"string"},{"indexed":false,"internalType":"string","name":"title","type":"string"},{"indexed":false,"internalType":"string","name":"creator","type":"string"},{"indexed":false,"internalType":"string","name":"condition1","type":"string"},{"indexed":false,"internalType":"string","name":"condition2","type":"string"},{"indexed":false,"internalType":"string","name":"condition3","type":"string"},{"indexed":false,"internalType":"bool[3]","name":"conditionFlags","type":"bool[3]"},{"indexed":false,"internalType":"uint256","name":"targetAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"expirationDate","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"qtySigners","type":"uint256"},{"indexed":false,"internalType":"enum ParentEscrow.State","name":"curState","type":"uint8"}],"name":"NilliContract","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"inputs":[{"internalType":"contract RefundEscrowNilli","name":"contractAddress","type":"address"},{"internalType":"contract IERC721","name":"token","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"depNFTtoContract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract RefundEscrowNilli","name":"contractAddress","type":"address"},{"internalType":"contract IERC20","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"depTokentoContract","outputs":[{"internalType":"uint256","name":"amt","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract RefundEscrowNilli","name":"contractAddress","type":"address"}],"name":"depositToContract","outputs":[{"internalType":"uint256","name":"amt","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract RefundEscrowNilli","name":"contractAddress","type":"address"}],"name":"renounceContractOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"recDisplayName","type":"string"},{"internalType":"string","name":"title","type":"string"},{"internalType":"string","name":"creator","type":"string"},{"internalType":"string","name":"condition1","type":"string"},{"internalType":"string","name":"condition2","type":"string"},{"internalType":"string","name":"condition3","type":"string"},{"internalType":"uint256","name":"targetAmount","type":"uint256"},{"internalType":"uint256","name":"expirationDate","type":"uint256"}],"name":"startContract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract RefundEscrowNilli","name":"contractAddress","type":"address"},{"internalType":"enum RefundEscrowNilli.State","name":"newState","type":"uint8"}],"name":"stateChangeContract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract RefundEscrowNilli","name":"contractAddress","type":"address"},{"internalType":"bool[3]","name":"condFlags","type":"bool[3]"}],"name":"updateConditionFlags","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract RefundEscrowNilli","name":"contractAddress","type":"address"}],"name":"verifyChild","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract RefundEscrowNilli","name":"contractAddress","type":"address"}],"name":"withdrawETHDepositor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract RefundEscrowNilli","name":"contractAddress","type":"address"},{"internalType":"contract IERC721","name":"token","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"withdrawNFTContract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract RefundEscrowNilli","name":"contractAddress","type":"address"},{"internalType":"contract IERC721","name":"token","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"withdrawNFTOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract RefundEscrowNilli","name":"contractAddress","type":"address"},{"internalType":"contract IERC20","name":"token","type":"address"}],"name":"withdrawTokenContract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract RefundEscrowNilli","name":"contractAddress","type":"address"},{"internalType":"contract IERC20","name":"token","type":"address"}],"name":"withdrawTokenOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract RefundEscrowNilli","name":"contractAddress","type":"address"},{"internalType":"address","name":"newOwner","type":"address"}],"name":"xferContractOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"}]