// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
import "./Interfaces/IElasticERC20.sol";
    
struct Transaction {
    bytes32 id;
    
    bytes32[16] commitments; 
    bytes32[16] nullifier_hashes; 
    bytes32[16] recipients; 
    bytes32[16] withdrawals; 
    bytes32 deposit; 
    bytes32[161] proof;
    bytes32[16] aggregation_object;
}

struct BatchPublicInputs {
    bytes32 key_hash;
    bytes32 oracle;
    bytes32 old_root;
    bytes32 new_root;
    Transaction[] transactions; 
}
interface IVerifier {
    function verify(
        bytes calldata,
        bytes32[] calldata
    ) external view returns (bool);
    function getVerificationKeyHash() external pure returns (bytes32);
}
contract TechnicalPreview {
    
    IVerifier public verifier;
    IElasticERC20 public token;
    bytes32[] public validRoots;
    bytes32 public keyHash;
    uint256 public MAX_FIELD_SIZE = 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000000;
    bytes32 public ZERO_VALUE = 0x016a430aa58685aba1311244a973a3bc358859da86784be51094368e8fb6f720;
    mapping(bytes32 => bytes32) public utxoPrevRoots; 
    mapping(bytes32 => bool) public nullifierHashes; 
    mapping(bytes32 => bool) public commitments; 
    mapping(bytes32 => bytes32[]) public utxo;
    uint256 public MAX_ITEMS = 16; 
    mapping(uint256 => Transaction[]) public batch;
    uint256 public batchNumber = 0;    
    bytes32 public merkleRoot = ZERO_VALUE;
    constructor(IVerifier _verifier, address _token, bytes32 _keyHash) {
        verifier = _verifier;
        token = IElasticERC20(_token);
        validRoots.push(merkleRoot);
        keyHash = _keyHash;
    }
    function getLatestAggregationObject() public view returns (bytes32[16] memory) {
        return batch[batchNumber][batch[batchNumber].length - 1].aggregation_object;
    }
    function getSpentNullifiers(bytes32[] calldata _nullifierHashes) external view returns (bool[] memory spent) {
        uint256 nullifierHashesLength = _nullifierHashes.length;
        spent = new bool[](nullifierHashesLength);
        for (uint256 i; i < nullifierHashesLength; i++) {
            if (isSpent(_nullifierHashes[i])) {
                spent[i] = true;
            }
        }
    }
    
    function getCommitment(bytes32 _commitment) public view returns (bool) {
        return commitments[_commitment];
    }
    
    function getUtxoFromRoot(bytes32 _root) public view returns (bytes32[] memory) {
        return utxo[_root];
    }
    function getRootFromUtxo(bytes32 _utxo) public view returns (bytes32) {
        return utxoPrevRoots[_utxo];
    }
    function getValidRoots() public view returns (bytes32[] memory) {
        return validRoots;
    }
    function getCurrentBatch() public view returns (Transaction[] memory) {
        return batch[batchNumber];
    }
    
    
    
    function enqueue(Transaction calldata _tx) public {
        
        require(MAX_ITEMS > batch[batchNumber].length, "queue is full");
        batch[batchNumber].push(_tx);
        
        for (uint256 i = 0; i < 16; i++) {
            if (_tx.commitments[i] != ZERO_VALUE) {
                require(!commitments[_tx.commitments[i]], "commitment exists");
                commitments[_tx.commitments[i]] = true;
            }
            
        } 
    }
    function publish(
        bytes calldata _proof,
        BatchPublicInputs calldata _batch
    ) public payable {
        require(uint256(_batch.old_root) == uint256(merkleRoot), "invalid root");
        BatchPublicInputs memory batchPublicInputs = _batch; 
        
        batchPublicInputs.transactions = batch[batchNumber];
        for (uint256 i = 0; i < batchPublicInputs.transactions.length; i++) {
            for (uint256 j = 0; j < 16; j++) {
                if (batchPublicInputs.transactions[i].nullifier_hashes[j] != ZERO_VALUE) {
                    require(!nullifierHashes[batchPublicInputs.transactions[i].nullifier_hashes[j]], "nullifier spent");
                    nullifierHashes[batchPublicInputs.transactions[i].nullifier_hashes[j]] = true;
                }
                
                if (batchPublicInputs.transactions[i].commitments[j] != ZERO_VALUE) {
                    utxo[batchPublicInputs.old_root].push(batchPublicInputs.transactions[i].commitments[j]);
                    utxoPrevRoots[batchPublicInputs.transactions[i].commitments[j]] = batchPublicInputs.old_root; 
                }
                
                if (batchPublicInputs.transactions[i].recipients[j] != ZERO_VALUE) {
                    token.mint(address(uint160(uint256(batchPublicInputs.transactions[i].recipients[j]))), uint256(batchPublicInputs.transactions[i].withdrawals[j]));
                }
            }
        }
        validRoots.push(batchPublicInputs.new_root); 
        merkleRoot = batchPublicInputs.new_root; 
        batchNumber++; 
        
        require(verifier.verify(_proof, prepareBatchPublicInputs(_batch)), "invalid proof");
    }
    function dropQueue(bytes calldata _preimage) public {
        require(keccak256(_preimage) > 0xff00000000000000000000000000000000000000000000000000000000000000); 
        
        for (uint256 i = 0; i < MAX_ITEMS; i++) {
            for (uint256 j = 0; j < 16; j++) {
                commitments[batch[batchNumber][i].commitments[j]] = false;
            }
        }
        batchNumber++;
    }
    
    function isSpent(bytes32 _nullifierHash) public view returns (bool) {
        return nullifierHashes[_nullifierHash];
    }
    function verifyProof(
        bytes calldata _proof,
        bytes32[] memory _publicInputs
    ) public view returns (bool) {
        return verifier.verify(_proof, _publicInputs);
    }
    
    function prepareBatchPublicInputs(BatchPublicInputs memory input) public view returns (bytes32[] memory) {
        bytes32[] memory flatArray = new bytes32[](36);
        uint256 idx = 0;
        flatArray[idx++] = keyHash;
        for (uint256 i = 0; i < 16; i++) {
            
            if (i < input.transactions.length) {
                flatArray[idx++] = input.transactions[i].id;
            } else {
                flatArray[idx++] = ZERO_VALUE;
            }
            
        }
        flatArray[idx++] = input.old_root;
        flatArray[idx++] = input.new_root;
        flatArray[idx++] = input.oracle;
        for (uint256 i = 0; i < 16; i++ ) {
            
            flatArray[idx++] = input.transactions[(input.transactions.length - 1)].aggregation_object[i];
        }
        
        for (uint256 i = 0; i < flatArray.length; i++) {
            require(uint256(flatArray[i]) < 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000000, "too large!");
        }
        return flatArray;
    }
}