import {
	TableType,
	WorkStepType,
} from "qrc:/js/lib/generated/enum";
import {
	deleteArticles,
} from "qrc:/js/lib/graph_manipulator";
import {
	getTable,
} from "qrc:/js/lib/table_utils";
import {
	assert, assertDebug,
} from "qrc:/js/lib/utils";
import {
	assumeGraphAxioms,
} from "qrc:/js/lib/axioms";
import {
	Costs,
} from "qrc:/js/lib/calc_costs";
import {
	Times,
} from "qrc:/js/lib/calc_times";
import {
	getFutureResult,
} from "qrc:/js/lib/future";

import {
	getSharedDataEntry,
} from "../lib/shared_data";
import {
	nest3Part,
} from "../lib/nest3_utils";

import {
	processRatePerSecond,
	shopOnlyComputeSellingPriceInclGlobalSurcharges,
} from "./export_calc_costs";

interface ShippingCostEntry {
	identifier: string;
	name: string;
	price: number;
}

interface ShippingCost {
	count: number;
	km: number;
	overallPrice: number;
	netWeight: number;
	grossWeight: number;
	packagingCost: ShippingCostEntry;
	transportCost: ShippingCostEntry;
	weights: Array<number>;
}

interface KilometerShippingCost {
	km: number;
	costs: ShippingCost|undefined;
}

interface PackResult {
	package: Packaging;
	result: Nest3ResultBox[];
}

export function packResults(nestingParts: Nest3Part[]): PackResult[] {
	const table = getTable(TableType.packaging);
	const minMass = Math.min(...nestingParts.map(n => n.mass));
	const packageFutures = table.map(row => ({
		package: row,
		future: wsi4.cam.nest3.asyncNest({
			box: {
				lower: {
					entries: [
						0.,
						0.,
						0.,
					],
				},
				upper: {
					entries: [
						row.dimX,
						row.dimY,
						row.dimZ,
					],
				},
			},
			maxWeight: Math.max(0, row.maxWeight - row.packagingWeight),
		}, nestingParts, getSharedDataEntry("prismNestingTimeLimit")),

	}))
		.filter(p => p.package.packagingWeight + minMass <= p.package.maxWeight); // we filter out all packages where the minimal mass + package mass exceeds package maximal weight
	return packageFutures.map(f => ({
		package: f.package,
		result: getFutureResult<"nest3Result">(f.future),
	}));
}

function packResultsOfGraph(): PackResult[] {
	const sources = wsi4.graph.vertices()
		.filter(vertex => wsi4.graph.targets(vertex).length === 0);
	const parts: Nest3Part[] = [];
	for (const s of sources) {
		const n = nest3Part(s);
		if (n === undefined) {
			// error in mass computation
			return [];
		}
		parts.push(n);
	}
	return packResults(parts);
}

interface Package {
	packResult: PackResult;
	price: number;
}

function computePackageTime(numberOfArticles: number, packResult: PackResult): Times {
	if (typeof packResult.package.tep !== "number") {
		return wsi4.throwError("Key \"tep\" missing.");
	}
	if (typeof packResult.package.tea !== "number") {
		return wsi4.throwError("Key \"tea\" missing.");
	}
	if (typeof packResult.package.tr !== "number") {
		return wsi4.throwError("Key \"tr\" missing.");
	}
	let unitTime = 0;
	let setupTime = 0;

	// [s]
	unitTime += packResult.result.length * packResult.package.tep * 60;
	// [s]
	unitTime += numberOfArticles * packResult.package.tea * 60;
	// [s]
	setupTime = packResult.result.length * packResult.package.tr * 60;
	return new Times(setupTime, unitTime);
}

function computePackageCosts(numberOfArticles: number, packResult: PackResult): Package|undefined {
	const times = computePackageTime(numberOfArticles, packResult);

	const ratePerSecond = processRatePerSecond("packagingId");
	if (ratePerSecond === undefined) {
		return undefined;
	}
	const materialCosts = packResult.package.price * packResult.result.length;
	const costs = new Costs(materialCosts, times.setup * ratePerSecond, times.unit * ratePerSecond);
	const sellingPrice = shopOnlyComputeSellingPriceInclGlobalSurcharges(costs);
	if (sellingPrice === undefined) {
		return undefined;
	}
	assert(sellingPrice !== undefined);

	return {
		packResult: packResult,
		price: sellingPrice,
	};
}

function computeTransportCosts(packResult: PackResult, transportCosts: TransportationCosts, distance: number): number|undefined {
	let unitCosts = 0;

	// Add packaging weight costs to unit costs
	unitCosts += packResult.package.packagingWeight * distance * transportCosts.kmKgFactor;

	for (const r of packResult.result) {
		unitCosts += r.weight * distance * transportCosts.kmKgFactor + distance * transportCosts.kmFactor;
	}
	const costs = new Costs(0, transportCosts.fixedCosts * packResult.result.length, unitCosts);

	const price = shopOnlyComputeSellingPriceInclGlobalSurcharges(costs);
	assert(price !== undefined, "Expecting valid price");
	return Math.max(transportCosts.minCosts, price);
}

export function computeShippingCosts(kilometers: Array<number>): Array<KilometerShippingCost> {
	assumeGraphAxioms([ "mergeNodesAreJoining" ]);

	// first we remove all joining
	deleteArticles(wsi4.graph.vertices().filter(v => wsi4.node.workStepType(v) === "joining"));
	assertDebug(() => wsi4.graph.vertices()
		.every(v => wsi4.node.workStepType(v) !== WorkStepType.joining));

	const numberOfArticles = wsi4.graph.vertices()
		.filter(vertex => wsi4.graph.targets(vertex).length === 0).length;
	const packages = packResultsOfGraph()
		.filter(p => p.result.length !== 0)
		.map(p => computePackageCosts(numberOfArticles, p))
		.filter((p): p is Package => p !== undefined);

	const transportationCostsTables = getTable(TableType.transportationCosts);
	return kilometers.map(km => {
		const prices: ShippingCost[] = [];
		for (const pg of packages) {
			const transportOptions = transportationCostsTables.filter(t => t.packagingId === pg.packResult.package.identifier && t.minDistance <= km && km < t.maxDistance);
			if (transportOptions.length === 0) {
				continue;
			}
			for (const transportRow of transportOptions) {
				// if km is 0, it corresponds to delivery ex works, so the transport costs should be 0
				const transportSellingPrice = km === 0. ? 0. : computeTransportCosts(pg.packResult, transportRow, km);
				if (transportSellingPrice === undefined) {
					continue;
				}
				const netWeight = pg.packResult.result.reduce((res, a) => res + a.weight, 0.);
				const grossWeight = pg.packResult.result.reduce((res, a) => res + a.weight + pg.packResult.package.packagingWeight, 0.);
				prices.push({
					count: pg.packResult.result.length,
					km: km,
					packagingCost: {
						identifier: pg.packResult.package.identifier,
						name: pg.packResult.package.name,
						price: pg.price,
					},
					transportCost: {
						identifier: transportRow.identifier,
						name: transportRow.name,
						price: transportSellingPrice,
					},
					overallPrice: pg.price + transportSellingPrice,
					weights: pg.packResult.result.map(r => r.weight),
					netWeight: netWeight,
					grossWeight: grossWeight,
				});
			}

		}
		return {
			km: km,
			costs: prices.length === 0 ? undefined : prices.reduce((a, b) => a.overallPrice < b.overallPrice ? a : b, prices[0]!),
		};
	});
}
