// tslint:disable:no-unnecessary-type-assertion
import { assetDataUtils, orderHashUtils } from '@0xproject/order-utils';
import { AssetProxyId, ERC20AssetData, ERC721AssetData, SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';

export interface OrderHashesByMakerAddress {
    [makerAddress: string]: Set<string>;
}

export interface OrderHashesByERC20ByMakerAddress {
    [makerAddress: string]: {
        [erc20TokenAddress: string]: Set<string>;
    };
}

export interface OrderHashesByERC721AddressByTokenIdByMakerAddress {
    [makerAddress: string]: {
        [erc721TokenAddress: string]: {
            // Ideally erc721TokenId should be a BigNumber, but it's not a valid index type so we just convert it to a string before using it as an index
            [erc721TokenId: string]: Set<string>;
        };
    };
}

/**
 */
export class DependentOrderHashesTracker {
    private readonly _zrxTokenAddress: string;
    // `_orderHashesByMakerAddress` is redundant and could be generated from
    // `_orderHashesByERC20ByMakerAddress` and `_orderHashesByERC721AddressByTokenIdByMakerAddress`
    // on the fly by merging all the entries together but it's more complex and computationally heavy.
    // We might change that in future if we're move memory-constrained.
    private readonly _orderHashesByMakerAddress: OrderHashesByMakerAddress = {};
    private readonly _orderHashesByERC20ByMakerAddress: OrderHashesByERC20ByMakerAddress = {};
    private readonly _orderHashesByERC721AddressByTokenIdByMakerAddress: OrderHashesByERC721AddressByTokenIdByMakerAddress = {};
    constructor(zrxTokenAddress: string) {
        this._zrxTokenAddress = zrxTokenAddress;
    }
    public getDependentOrderHashesByERC721ByMaker(makerAddress: string, tokenAddress: string): string[] {
        const orderHashSets = _.values(
            this._orderHashesByERC721AddressByTokenIdByMakerAddress[makerAddress][tokenAddress],
        );
        const orderHashList = _.reduce(
            orderHashSets,
            (accumulator, orderHashSet) => [...accumulator, ...orderHashSet],
            [] as string[],
        );
        const uniqueOrderHashList = _.uniq(orderHashList);
        return uniqueOrderHashList;
    }
    public getDependentOrderHashesByMaker(makerAddress: string): string[] {
        const dependentOrderHashes = Array.from(this._orderHashesByMakerAddress[makerAddress]);
        return dependentOrderHashes;
    }
    public getDependentOrderHashesByAssetDataByMaker(makerAddress: string, assetData: string): string[] {
        const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData);
        const dependentOrderHashes =
            decodedAssetData.assetProxyId === AssetProxyId.ERC20
                ? this._getDependentOrderHashesByERC20AssetData(makerAddress, assetData)
                : this._getDependentOrderHashesByERC721AssetData(makerAddress, assetData);
        return dependentOrderHashes;
    }
    public addToDependentOrderHashes(signedOrder: SignedOrder): void {
        const decodedMakerAssetData = assetDataUtils.decodeAssetDataOrThrow(signedOrder.makerAssetData);
        if (decodedMakerAssetData.assetProxyId === AssetProxyId.ERC20) {
            this._addToERC20DependentOrderHashes(signedOrder, (decodedMakerAssetData as ERC20AssetData).tokenAddress);
        } else {
            this._addToERC721DependentOrderHashes(
                signedOrder,
                (decodedMakerAssetData as ERC721AssetData).tokenAddress,
                (decodedMakerAssetData as ERC721AssetData).tokenId,
            );
        }
        this._addToERC20DependentOrderHashes(signedOrder, this._zrxTokenAddress);
        this._addToMakerDependentOrderHashes(signedOrder);
    }
    public removeFromDependentOrderHashes(signedOrder: SignedOrder): void {
        const decodedMakerAssetData = assetDataUtils.decodeAssetDataOrThrow(signedOrder.makerAssetData);
        if (decodedMakerAssetData.assetProxyId === AssetProxyId.ERC20) {
            this._removeFromERC20DependentOrderhashes(
                signedOrder,
                (decodedMakerAssetData as ERC20AssetData).tokenAddress,
            );
        } else {
            this._removeFromERC721DependentOrderhashes(
                signedOrder,
                (decodedMakerAssetData as ERC721AssetData).tokenAddress,
                (decodedMakerAssetData as ERC721AssetData).tokenId,
            );
        }
        this._removeFromERC20DependentOrderhashes(signedOrder, this._zrxTokenAddress);
        this._removeFromMakerDependentOrderhashes(signedOrder);
    }
    private _getDependentOrderHashesByERC20AssetData(makerAddress: string, erc20AssetData: string): string[] {
        const tokenAddress = assetDataUtils.decodeERC20AssetData(erc20AssetData).tokenAddress;
        let dependentOrderHashes: string[] = [];
        if (
            !_.isUndefined(this._orderHashesByERC20ByMakerAddress[makerAddress]) &&
            !_.isUndefined(this._orderHashesByERC20ByMakerAddress[makerAddress][tokenAddress])
        ) {
            dependentOrderHashes = Array.from(this._orderHashesByERC20ByMakerAddress[makerAddress][tokenAddress]);
        }
        return dependentOrderHashes;
    }
    private _getDependentOrderHashesByERC721AssetData(makerAddress: string, erc721AssetData: string): string[] {
        const tokenAddress = assetDataUtils.decodeERC721AssetData(erc721AssetData).tokenAddress;
        const tokenId = assetDataUtils.decodeERC721AssetData(erc721AssetData).tokenId;
        let dependentOrderHashes: string[] = [];
        if (
            !_.isUndefined(this._orderHashesByERC721AddressByTokenIdByMakerAddress[makerAddress]) &&
            !_.isUndefined(this._orderHashesByERC721AddressByTokenIdByMakerAddress[makerAddress][tokenAddress]) &&
            !_.isUndefined(
                this._orderHashesByERC721AddressByTokenIdByMakerAddress[makerAddress][tokenAddress][tokenId.toString()],
            )
        ) {
            dependentOrderHashes = Array.from(
                this._orderHashesByERC721AddressByTokenIdByMakerAddress[makerAddress][tokenAddress][tokenId.toString()],
            );
        }
        return dependentOrderHashes;
    }
    private _addToERC20DependentOrderHashes(signedOrder: SignedOrder, erc20TokenAddress: string): void {
        const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
        if (_.isUndefined(this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress])) {
            this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress] = {};
        }
        if (_.isUndefined(this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress][erc20TokenAddress])) {
            this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress][erc20TokenAddress] = new Set();
        }
        this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress][erc20TokenAddress].add(orderHash);
    }
    private _addToERC721DependentOrderHashes(
        signedOrder: SignedOrder,
        erc721TokenAddress: string,
        tokenId: BigNumber,
    ): void {
        const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
        if (_.isUndefined(this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress])) {
            this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress] = {};
        }

        if (
            _.isUndefined(
                this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress],
            )
        ) {
            this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress] = {};
        }

        if (
            _.isUndefined(
                this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress][
                    tokenId.toString()
                ],
            )
        ) {
            this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress][
                tokenId.toString()
            ] = new Set();
        }

        this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress][
            tokenId.toString()
        ].add(orderHash);
    }
    private _addToMakerDependentOrderHashes(signedOrder: SignedOrder): void {
        const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
        if (_.isUndefined(this._orderHashesByMakerAddress[signedOrder.makerAddress])) {
            this._orderHashesByMakerAddress[signedOrder.makerAddress] = new Set();
        }
        this._orderHashesByMakerAddress[signedOrder.makerAddress].add(orderHash);
    }
    private _removeFromERC20DependentOrderhashes(signedOrder: SignedOrder, erc20TokenAddress: string): void {
        const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
        this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress][erc20TokenAddress].delete(orderHash);

        if (_.isEmpty(this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress][erc20TokenAddress])) {
            delete this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress][erc20TokenAddress];
        }

        if (_.isEmpty(this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress])) {
            delete this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress];
        }
    }
    private _removeFromERC721DependentOrderhashes(
        signedOrder: SignedOrder,
        erc721TokenAddress: string,
        tokenId: BigNumber,
    ): void {
        const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
        this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress][
            tokenId.toString()
        ].delete(orderHash);

        if (
            _.isEmpty(
                this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress][
                    tokenId.toString()
                ],
            )
        ) {
            delete this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][
                erc721TokenAddress
            ][tokenId.toString()];
        }

        if (
            _.isEmpty(
                this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress],
            )
        ) {
            delete this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][
                erc721TokenAddress
            ];
        }

        if (_.isEmpty(this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress])) {
            delete this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress];
        }
    }
    private _removeFromMakerDependentOrderhashes(signedOrder: SignedOrder): void {
        const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
        this._orderHashesByMakerAddress[signedOrder.makerAddress].delete(orderHash);

        if (_.isEmpty(this._orderHashesByMakerAddress[signedOrder.makerAddress])) {
            delete this._orderHashesByMakerAddress[signedOrder.makerAddress];
        }
    }
}
