import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';

export class RemainingFillableCalculator {
    private _signedOrder: SignedOrder;
    private _isMakerTokenZRX: boolean;
    // Transferrable Amount is the minimum of Approval and Balance
    private _transferrableMakerTokenAmount: BigNumber;
    private _transferrableMakerFeeTokenAmount: BigNumber;
    private _remainingMakerTokenAmount: BigNumber;
    private _remainingMakerFeeAmount: BigNumber;
    constructor(
        signedOrder: SignedOrder,
        isMakerTokenZRX: boolean,
        transferrableMakerTokenAmount: BigNumber,
        transferrableMakerFeeTokenAmount: BigNumber,
        remainingMakerTokenAmount: BigNumber,
    ) {
        this._signedOrder = signedOrder;
        this._isMakerTokenZRX = isMakerTokenZRX;
        this._transferrableMakerTokenAmount = transferrableMakerTokenAmount;
        this._transferrableMakerFeeTokenAmount = transferrableMakerFeeTokenAmount;
        this._remainingMakerTokenAmount = remainingMakerTokenAmount;
        this._remainingMakerFeeAmount = remainingMakerTokenAmount
            .times(signedOrder.makerFee)
            .dividedToIntegerBy(signedOrder.makerTokenAmount);
    }
    public computeRemainingMakerFillable(): BigNumber {
        if (this._hasSufficientFundsForFeeAndTransferAmount()) {
            return this._remainingMakerTokenAmount;
        }
        if (this._signedOrder.makerFee.isZero()) {
            return BigNumber.min(this._remainingMakerTokenAmount, this._transferrableMakerTokenAmount);
        }
        return this._calculatePartiallyFillableMakerTokenAmount();
    }
    public computeRemainingTakerFillable(): BigNumber {
        return this.computeRemainingMakerFillable()
            .times(this._signedOrder.takerTokenAmount)
            .dividedToIntegerBy(this._signedOrder.makerTokenAmount);
    }
    private _hasSufficientFundsForFeeAndTransferAmount(): boolean {
        if (this._isMakerTokenZRX) {
            const totalZRXTransferAmountRequired = this._remainingMakerTokenAmount.plus(this._remainingMakerFeeAmount);
            const hasSufficientFunds = this._transferrableMakerTokenAmount.greaterThanOrEqualTo(
                totalZRXTransferAmountRequired,
            );
            return hasSufficientFunds;
        } else {
            const hasSufficientFundsForTransferAmount = this._transferrableMakerTokenAmount.greaterThanOrEqualTo(
                this._remainingMakerTokenAmount,
            );
            const hasSufficientFundsForFeeAmount = this._transferrableMakerFeeTokenAmount.greaterThanOrEqualTo(
                this._remainingMakerFeeAmount,
            );
            const hasSufficientFunds = hasSufficientFundsForTransferAmount && hasSufficientFundsForFeeAmount;
            return hasSufficientFunds;
        }
    }
    private _calculatePartiallyFillableMakerTokenAmount(): BigNumber {
        // Given an order for 200 wei for 2 ZRXwei fee, find 100 wei for 1 ZRXwei. Order ratio is then 100:1
        const orderToFeeRatio = this._signedOrder.makerTokenAmount.dividedBy(this._signedOrder.makerFee);
        // The number of times the maker can fill the order, if each fill only required the transfer of a single
        // baseUnit of fee tokens.
        // Given 2 ZRXwei, the maximum amount of times Maker can fill this order, in terms of fees, is 2
        const fillableTimesInFeeTokenBaseUnits = BigNumber.min(
            this._transferrableMakerFeeTokenAmount,
            this._remainingMakerFeeAmount,
        );
        // The number of times the Maker can fill the order, given the Maker Token Balance
        // Assuming a balance of 150 wei, and an orderToFeeRatio of 100:1, maker can fill this order 1 time.
        let fillableTimesInMakerTokenUnits = this._transferrableMakerTokenAmount.dividedBy(orderToFeeRatio);
        if (this._isMakerTokenZRX) {
            // If ZRX is the maker token, the Fee and the Maker amount need to be removed from the same pool;
            // 200 ZRXwei for 2ZRXwei fee can only be filled once (need 202 ZRXwei)
            const totalZRXTokenPooled = this._transferrableMakerTokenAmount;
            // The purchasing power here is less as the tokens are taken from the same Pool
            // For every one number of fills, we have to take an extra ZRX out of the pool
            fillableTimesInMakerTokenUnits = totalZRXTokenPooled.dividedBy(orderToFeeRatio.plus(new BigNumber(1)));
        }
        // When Ratio is not fully divisible there can be remainders which cannot be represented, so they are floored.
        // This can result in a RoundingError being thrown by the Exchange Contract.
        const partiallyFillableMakerTokenAmount = fillableTimesInMakerTokenUnits
            .times(this._signedOrder.makerTokenAmount)
            .dividedToIntegerBy(this._signedOrder.makerFee);
        const partiallyFillableFeeTokenAmount = fillableTimesInFeeTokenBaseUnits
            .times(this._signedOrder.makerTokenAmount)
            .dividedToIntegerBy(this._signedOrder.makerFee);
        const partiallyFillableAmount = BigNumber.min(
            partiallyFillableMakerTokenAmount,
            partiallyFillableFeeTokenAmount,
        );
        return partiallyFillableAmount;
    }
}
