import { BN } from './isomorphic/anchor';
import { MARGIN_PRECISION, ZERO } from './constants/numericConstants';
import { getVariant, isVariant, MarketType } from './types';

export type MarginCategory = 'Initial' | 'Maintenance' | 'Fill';

export type MarginCalculationMode =
	| { type: 'Standard' }
	| { type: 'Liquidation' };

export class MarketIdentifier {
	marketType: MarketType;
	marketIndex: number;

	private constructor(marketType: MarketType, marketIndex: number) {
		this.marketType = marketType;
		this.marketIndex = marketIndex;
	}

	static spot(marketIndex: number): MarketIdentifier {
		return new MarketIdentifier(MarketType.SPOT, marketIndex);
	}

	static perp(marketIndex: number): MarketIdentifier {
		return new MarketIdentifier(MarketType.PERP, marketIndex);
	}

	equals(other: MarketIdentifier | undefined): boolean {
		return (
			!!other &&
			isVariant(this.marketType, getVariant(other.marketType)) &&
			this.marketIndex === other.marketIndex
		);
	}
}

export class MarginContext {
	marginType: MarginCategory;
	mode: MarginCalculationMode;
	strict: boolean;
	ignoreInvalidDepositOracles: boolean;
	isolatedMarginBuffers: Map<number, BN>;
	crossMarginBuffer: BN;

	private constructor(marginType: MarginCategory) {
		this.marginType = marginType;
		this.mode = { type: 'Standard' };
		this.strict = false;
		this.ignoreInvalidDepositOracles = false;
		this.isolatedMarginBuffers = new Map();
	}

	static standard(marginType: MarginCategory): MarginContext {
		return new MarginContext(marginType);
	}

	static liquidation(
		crossMarginBuffer: BN,
		isolatedMarginBuffers: Map<number, BN>
	): MarginContext {
		const ctx = new MarginContext('Maintenance');
		ctx.mode = { type: 'Liquidation' };
		ctx.crossMarginBuffer = crossMarginBuffer;
		ctx.isolatedMarginBuffers = isolatedMarginBuffers;
		return ctx;
	}

	strictMode(strict: boolean): this {
		this.strict = strict;
		return this;
	}

	ignoreInvalidDeposits(ignore: boolean): this {
		this.ignoreInvalidDepositOracles = ignore;
		return this;
	}

	setCrossMarginBuffer(crossMarginBuffer: BN): this {
		this.crossMarginBuffer = crossMarginBuffer;
		return this;
	}
	setIsolatedMarginBuffers(isolatedMarginBuffers: Map<number, BN>): this {
		this.isolatedMarginBuffers = isolatedMarginBuffers;
		return this;
	}
	setIsolatedMarginBuffer(marketIndex: number, isolatedMarginBuffer: BN): this {
		this.isolatedMarginBuffers.set(marketIndex, isolatedMarginBuffer);
		return this;
	}
}

export class IsolatedMarginCalculation {
	marginRequirement: BN;
	totalCollateral: BN; // deposit + pnl
	totalCollateralBuffer: BN;
	marginRequirementPlusBuffer: BN;

	constructor() {
		this.marginRequirement = ZERO;
		this.totalCollateral = ZERO;
		this.totalCollateralBuffer = ZERO;
		this.marginRequirementPlusBuffer = ZERO;
	}

	getTotalCollateralPlusBuffer(): BN {
		return this.totalCollateral.add(this.totalCollateralBuffer);
	}

	meetsMarginRequirement(): boolean {
		return this.totalCollateral.gte(this.marginRequirement);
	}

	meetsMarginRequirementWithBuffer(): boolean {
		return this.getTotalCollateralPlusBuffer().gte(
			this.marginRequirementPlusBuffer
		);
	}

	marginShortage(): BN {
		const shortage = this.marginRequirementPlusBuffer.sub(
			this.getTotalCollateralPlusBuffer()
		);
		return shortage.isNeg() ? ZERO : shortage;
	}
}

export class MarginCalculation {
	context: MarginContext;
	totalCollateral: BN;
	totalCollateralBuffer: BN;
	marginRequirement: BN;
	marginRequirementPlusBuffer: BN;
	isolatedMarginCalculations: Map<number, IsolatedMarginCalculation>;
	totalPerpLiabilityValue: BN;

	constructor(context: MarginContext) {
		this.context = context;
		this.totalCollateral = ZERO;
		this.totalCollateralBuffer = ZERO;
		this.marginRequirement = ZERO;
		this.marginRequirementPlusBuffer = ZERO;
		this.isolatedMarginCalculations = new Map();
		this.totalPerpLiabilityValue = ZERO;
	}

	addCrossMarginTotalCollateral(delta: BN): void {
		const crossMarginBuffer = this.context.crossMarginBuffer;
		this.totalCollateral = this.totalCollateral.add(delta);
		if (crossMarginBuffer.gt(ZERO) && delta.isNeg()) {
			this.totalCollateralBuffer = this.totalCollateralBuffer.add(
				delta.mul(crossMarginBuffer).div(MARGIN_PRECISION)
			);
		}
	}

	addCrossMarginRequirement(marginRequirement: BN, liabilityValue: BN): void {
		const crossMarginBuffer = this.context.crossMarginBuffer;
		this.marginRequirement = this.marginRequirement.add(marginRequirement);
		if (crossMarginBuffer.gt(ZERO)) {
			this.marginRequirementPlusBuffer = this.marginRequirementPlusBuffer.add(
				marginRequirement.add(
					liabilityValue.mul(crossMarginBuffer).div(MARGIN_PRECISION)
				)
			);
		}
	}

	addIsolatedMarginCalculation(
		marketIndex: number,
		depositValue: BN,
		pnl: BN,
		liabilityValue: BN,
		marginRequirement: BN
	): void {
		const totalCollateral = depositValue.add(pnl);
		const isolatedMarginBuffer =
			this.context.isolatedMarginBuffers.get(marketIndex) ?? ZERO;

		const totalCollateralBuffer =
			isolatedMarginBuffer.gt(ZERO) && pnl.isNeg()
				? pnl.mul(isolatedMarginBuffer).div(MARGIN_PRECISION)
				: ZERO;

		const marginRequirementPlusBuffer = isolatedMarginBuffer.gt(ZERO)
			? marginRequirement.add(
					liabilityValue.mul(isolatedMarginBuffer).div(MARGIN_PRECISION)
			  )
			: marginRequirement;

		const iso = new IsolatedMarginCalculation();
		iso.marginRequirement = marginRequirement;
		iso.totalCollateral = totalCollateral;
		iso.totalCollateralBuffer = totalCollateralBuffer;
		iso.marginRequirementPlusBuffer = marginRequirementPlusBuffer;
		this.isolatedMarginCalculations.set(marketIndex, iso);
	}

	addPerpLiabilityValue(perpLiabilityValue: BN): void {
		this.totalPerpLiabilityValue =
			this.totalPerpLiabilityValue.add(perpLiabilityValue);
	}

	getCrossTotalCollateralPlusBuffer(): BN {
		return this.totalCollateral.add(this.totalCollateralBuffer);
	}

	meetsCrossMarginRequirement(): boolean {
		return this.totalCollateral.gte(this.marginRequirement);
	}

	meetsCrossMarginRequirementWithBuffer(): boolean {
		return this.getCrossTotalCollateralPlusBuffer().gte(
			this.marginRequirementPlusBuffer
		);
	}

	meetsMarginRequirement(): boolean {
		if (!this.meetsCrossMarginRequirement()) return false;
		for (const [, iso] of this.isolatedMarginCalculations) {
			if (!iso.meetsMarginRequirement()) return false;
		}
		return true;
	}

	meetsMarginRequirementWithBuffer(): boolean {
		if (!this.meetsCrossMarginRequirementWithBuffer()) return false;
		for (const [, iso] of this.isolatedMarginCalculations) {
			if (!iso.meetsMarginRequirementWithBuffer()) return false;
		}
		return true;
	}

	getCrossFreeCollateral(): BN {
		const free = this.totalCollateral.sub(this.marginRequirement);
		return free.isNeg() ? ZERO : free;
	}

	getIsolatedFreeCollateral(marketIndex: number): BN {
		const iso = this.isolatedMarginCalculations.get(marketIndex);
		if (!iso)
			throw new Error('InvalidMarginCalculation: missing isolated calc');
		const free = iso.totalCollateral.sub(iso.marginRequirement);
		return free.isNeg() ? ZERO : free;
	}

	getIsolatedMarginCalculation(
		marketIndex: number
	): IsolatedMarginCalculation | undefined {
		return this.isolatedMarginCalculations.get(marketIndex);
	}

	hasIsolatedMarginCalculation(marketIndex: number): boolean {
		return this.isolatedMarginCalculations.has(marketIndex);
	}
}
