import { AbiDecoder } from '@0xproject/utils';
import { ContractAbi, DecodedLogArgs, LogEntry, LogWithDecodedArgs, RawLog } from 'ethereum-types';

const TOKEN_TYPE_COLLISION = `Token can't be marked as ERC20 and ERC721 at the same time`;

/**
 * ERC20 and ERC721 have some events with different args but colliding signature.
 * For exmaple:
 * Transfer(_from address, _to address, _value uint256)
 * Transfer(_from address, _to address, _tokenId uint256)
 * Both have the signature:
 * Transfer(address,address,uint256)
 *
 * In order to correctly decode those events we need to know the token type by address in advance.
 * You can pass it by calling `this.addERC20Token(address)` or `this.addERC721Token(address)`
 */
export class CollisionResistanceAbiDecoder {
    private readonly _erc20AbiDecoder: AbiDecoder;
    private readonly _erc721AbiDecoder: AbiDecoder;
    private readonly _restAbiDecoder: AbiDecoder;
    private readonly _knownERC20Tokens = new Set();
    private readonly _knownERC721Tokens = new Set();
    constructor(erc20Abi: ContractAbi, erc721Abi: ContractAbi, abis: ContractAbi[]) {
        this._erc20AbiDecoder = new AbiDecoder([erc20Abi]);
        this._erc721AbiDecoder = new AbiDecoder([erc721Abi]);
        this._restAbiDecoder = new AbiDecoder(abis);
    }
    public tryToDecodeLogOrNoop<ArgsType extends DecodedLogArgs>(log: LogEntry): LogWithDecodedArgs<ArgsType> | RawLog {
        if (this._knownERC20Tokens.has(log.address)) {
            const maybeDecodedERC20Log = this._erc20AbiDecoder.tryToDecodeLogOrNoop(log);
            return maybeDecodedERC20Log;
        } else if (this._knownERC721Tokens.has(log.address)) {
            const maybeDecodedERC721Log = this._erc721AbiDecoder.tryToDecodeLogOrNoop(log);
            return maybeDecodedERC721Log;
        } else {
            const maybeDecodedLog = this._restAbiDecoder.tryToDecodeLogOrNoop(log);
            return maybeDecodedLog;
        }
    }
    // Hints the ABI decoder that a particular token address is ERC20 and events from it should be decoded as ERC20 events
    public addERC20Token(address: string): void {
        if (this._knownERC721Tokens.has(address)) {
            throw new Error(TOKEN_TYPE_COLLISION);
        }
        this._knownERC20Tokens.add(address);
    }
    // Hints the ABI decoder that a particular token address is ERC721 and events from it should be decoded as ERC721 events
    public addERC721Token(address: string): void {
        if (this._knownERC20Tokens.has(address)) {
            throw new Error(TOKEN_TYPE_COLLISION);
        }
        this._knownERC721Tokens.add(address);
    }
}
