const d3a = require('d3-array');
const { DougalBinaryBundle } = require('@dougal/binary');
const { pool, setSurvey } = require('../db/connection');
const db = require('../db');
const { bundle } = require('../binary/bundle');
const setops = require('../utils/setops');
const { ijRMS, combinations, computeSample } = require('./utils');
const { computePCA } = require('./pca');
const { ERROR, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename);


async function fetchErrors (pid) {

	const client = await setSurvey(pid);

	try {
		const text = `
			SELECT
				fs.line, fs.point,
				ij_error(fs.line::double precision, fs.point::double precision, fs.geometry)::json AS errorfinal
			FROM
				final_shots fs
			ORDER BY fs.line, fs.point;
		`;

		const res = await client.query(text);

		return res.rows.map( row =>
			[row.line, row.point, row.errorfinal.coordinates[0], row.errorfinal.coordinates[1]]
		);
	} catch (err) {
		console.error(err);
	} finally {
		client.release();
	}
}


async function groups () {
	const projects = await db.project.get();
	const groupNames = [
		...projects
			.reduce( (acc, cur) => acc.add(...cur.groups), new Set() )
	].filter( i => !!i );

	return Object.fromEntries(groupNames.map( g => [g, projects.filter( p => p.groups.includes(g) )] ));
}

/*
async function compare (baselineProjectID, monitorProjectID) {
	console.log("Getting baseline", baselineProjectID);
	const baselineData = await db.sequence.get(baselineProjectID);
	console.log("Getting monitor", monitorProjectID);
	const monitorData = await db.sequence.get(monitorProjectID);
	console.log("Comparing");

	const comparison = comparisonGeometricDifferences(baselineData, monitorData);
	return comparison;
}
*/

function geometric_differences (baseline, monitor) {

	if (!baseline || !baseline.length) {
		throw new Error("No baseline data");
	}

	if (!monitor || !monitor.length) {
		throw new Error("No monitor data");
	}

	const comparison = []; // An array of { line, point, εi, εj }; line + point may be repeated

	for (const bp of baseline) {
		const monitor_points = monitor.filter( mp => mp[0] === bp[0] && mp[1] === bp[1] );

		if (!monitor_points.length) {
			// console.log(`No match for L${bp[0]} P${bp[1]}`);
			continue;
		}

		for (const mp of monitor_points) {
			const εi = mp[2] - bp[2], εj = mp[3] - bp[3];
			comparison.push([bp[0], bp[1], εi, εj]);
		}

	}

	return comparison;
}

async function compare (baselineProjectID, monitorProjectID, infoObj) {
	console.log("Getting baseline", baselineProjectID);
	const baselineData = await fetchErrors(baselineProjectID);
	console.log("Getting monitor", monitorProjectID);
	const monitorData = await fetchErrors(monitorProjectID);
	console.log("Comparing");

	const comparison = geometric_differences(baselineData, monitorData);

	if (infoObj instanceof Object) {
		const baselineIJ = baselineData.map(i => i.slice(0,2));
		const monitorIJ = monitorData.map(i => i.slice(0,2));

		infoObj.compared = comparison.length;
		infoObj.baselineLength = baselineData.length;
		infoObj.monitorLength = monitorData.length;
		infoObj.baselineUniqueLength = setops.unique(baselineIJ).length;
		infoObj.monitorUniqueLength = setops.unique(monitorIJ).length;
		infoObj.common = setops.intersection(baselineIJ, monitorIJ).length;
	}

	return comparison;
}


async function save (baselineProjectID, monitorProjectID, bundle, meta) {
	const info = {};
	if (!bundle) {
		const comparison = await compare(baselineProjectID, monitorProjectID, info);
		if (comparison.length) {
			bundle = asBundle(comparison);
		} else {
			console.warn(`No matching points between ${baselineProjectID} and ${monitorProjectID}`);
			return;
		}
	} else if (!(bundle instanceof DougalBinaryBundle)) {
		throw new Error("Illegal data: `bundle` must of null or of type DougalBinaryBundle");
	}

	if (!bundle.byteLength) {
		console.warn(`Empty comparison results between ${baselineProjectID} and ${monitorProjectID}. Refusing to store`);
		return;
	}

	meta = {tstamp: (new Date()), ...info, ...stats(bundle), ...meta};

	console.log("Storing in database");
	const client = await pool.connect();

	try {
		const text = `
			INSERT INTO comparisons.comparisons
				(type, baseline_pid, monitor_pid, data, meta)
			VALUES ('geometric_difference', $1, $2, $3, $4)
			ON CONFLICT (type, baseline_pid, monitor_pid)
			DO UPDATE SET
				data = EXCLUDED.data,
				meta = EXCLUDED.meta;
		`;

		const values = [ baselineProjectID, monitorProjectID, Buffer.from(bundle), meta ];
		const res = await client.query(text, values);
		return res.rowCount;
	} catch (err) {
		console.error(err);
	} finally {
		client.release();
	}
}

async function saveSample (baselineProjectID, monitorProjectID, opts = {}) {
	let sample = opts.sample;
	let populationStats = opts.populationStats;
	let sampleStats = opts.sampleStats;

	if (!sample?.length) {
		const sampleSize = opts.sampleSize ?? 2000;
		const record = await get(baselineProjectID, monitorProjectID);
		let data;

		if (record) {
			data = record.data;
		} else {
			console.log("Full data not found in database");
			data = asBundle(await compare(baselineProjectID, monitorProjectID));
		}

		sample = computeSample(data, opts);

		if (!populationStats) {
			populationStats = stats(data);
		}
	}

	const bundle = asBundle(sample, {type: 0x0c});

	if (!sampleStats) {
		sampleStats = stats(bundle);
	}

	meta = {tstamp: (new Date()), populationStats, sampleStats, ...(opts.meta??{})};

	const client = await pool.connect();

	try {
		const text = `
			INSERT INTO comparisons.comparisons
				(type, baseline_pid, monitor_pid, data, meta)
			VALUES ('geometric_difference_sample', $1, $2, $3, $4)
			ON CONFLICT (type, baseline_pid, monitor_pid)
			DO UPDATE SET
				data = EXCLUDED.data,
				meta = EXCLUDED.meta;
		`;

		const values = [ baselineProjectID, monitorProjectID, Buffer.from(bundle), meta ];
		const res = await client.query(text, values);
		return res.rowCount;
	} catch (err) {
		console.error(err);
	} finally {
		client.release();
	}
}

async function get (baselineProjectID, monitorProjectID, type = 'geometric_difference') {

	const client = await pool.connect();

	try {

		const text = `
			SELECT data, meta
			FROM comparisons.comparisons
			WHERE type = $3 AND baseline_pid = $1 AND monitor_pid = $2;
		`;

		const values = [ baselineProjectID, monitorProjectID, type ];
		const res = await client.query(text, values);
		if (!res.rows.length) {
			console.log("Comparison not found in database");
			return;
		}

		const { data, meta } = res.rows[0];
		return {
			data: DougalBinaryBundle.clone(data),
			meta
		};
	} catch (err) {
		console.error(err);
	} finally {
		client.release();
	}
}


async function getSample (baselineProjectID, monitorProjectID) {
	return await get(baselineProjectID, monitorProjectID, 'geometric_difference_sample');
}


async function remove (baselineProjectID, monitorProjectID) {
	const client = await pool.connect();

	try {
		const text = `
			DELETE
			FROM comparisons.comparisons
			WHERE
				(type = 'geometric_difference' OR type = 'geometric_difference_sample')
				AND baseline_pid = $1
				AND monitor_pid = $2;
		`;

		const values = [ baselineProjectID, monitorProjectID ];

		const res = await client.query(text, values);
		return res.rowCount;
	} catch (err) {
		console.error(err);
	} finally {
		client.release();
	}
}

function stats (comparison) {
	let i, j, δi, δj;

	if (comparison instanceof DougalBinaryBundle) {
		console.log("Computing stats");
		const udv = comparison.chunks()[0]?.udv;

		if (!udv) {
			console.error("Could not determine udv from first chunk");
			console.log(comparison.chunks());
			return;
		}

		let records;

		if (udv == 0xa) {
			records = comparison.records;

			// Transpose the records
			[ i, j, δi, δj ] = Array.from({ length: 4 }, (_, i) => records.map(row => row[i]));
		} else if (udv == 0xc) {
			records = comparison.records;
			let _;
			[ _, _, i, j, δi, δj ] = Array.from({ length: 6 }, (_, i) => records.map(row => row[i]));
		} else {
			throw new Error(`Unrecognised DougalBinaryBundle User Defined Value: ${udv}`);
		}

		return {
			length: records.length,
			μ: [ d3a.mean(δi), d3a.mean(δj) ],
			σ: [ d3a.deviation(δi), d3a.deviation(δj) ],
			//rms: ijRMS(δi, δj),
			...computePCA(records)
		}
	} else if (Array.isArray(comparison)) {
		if (Array.isArray(comparison[0])) {
			return stats(asBundle(comparison, {type: 0xc}));
		} else {
			// Assume object
			return stats(asBundle(comparison));
		}
	}
}


/** Compare two projects' errorfinal quantities.
 *
 * Assumes that the preplots are the same.
 * It is not a terribly efficient way of doing it, but considering
 * that this is, by and large only going to be done once every few
 * hours for an active prospect, and never for inactive ones, I
 * think and hope we can live with that.
 *
 * `baseline` and `monitor` are the result of calling
 * db.sequence.get(projectId) on each of the respective
 * projects.
 */
/*
function comparisonGeometricDifferences (baseline, monitor) {

	if (!baseline || !baseline.length) {
		throw new Error("No baseline data");
	}

	if (!monitor || !monitor.length) {
		throw new Error("No monitor data");
	}

	const comparison = []; // An array of { line, point, εi, εj, δts }; line + point may be repeated

	for (const bp of baseline) {
		if (!bp.errorfinal) {
			console.log(`No final data for baseline point L${bp.line} S${bp.sequence} P${bp.point}`);
			continue;
		}

		const monitor_points = monitor.filter( mp => mp.line === bp.line && mp.point === bp.point );
		for (const mp of monitor_points) {
			if (!mp.errorfinal) {
				console.log(`No final data for monitor point L${mp.line} S${mp.sequence} P${mp.point}`);
				continue;
			}

			const line = bp.line;
			const point = bp.point;
			const baseSeq = bp.sequence;
			const monSeq = mp.sequence;
			const baseTStamp = bp.tstamp;
			const monTStamp = mp.tstamp;
			const δi = bp.errorfinal.coordinates[0] - mp.errorfinal.coordinates[0];
			const δj = bp.errorfinal.coordinates[1] - mp.errorfinal.coordinates[1];

			const obj = {line, point, baseSeq, monSeq, baseTStamp, monTStamp, δi, δj};
			comparison.push(obj);
			// console.log(obj);
		}
	}

	return comparison.sort(sortFn);
}

function sortComparison (comparison) {
	return comparison.sort( (a, b) => {
		if (a.line == b.line) {
			if (a.point == b.point) {
				return a.baseTStamp - b.baseTStamp;
			} else {
				return a.point - b.point;
			}
		} else {
			return a.line - b.line;
		}
	})
}
*/

function sortFn (a, b) {
	if (a.line == b.line) {
		if (a.point == b.point) {
			return a.baseTStamp - b.baseTStamp;
		} else {
			return a.point - b.point;
		}
	} else {
		return a.line - b.line;
	}
}

function asBundle (comparison, opts = {type: 0x0a}) {
	return DougalBinaryBundle.clone(bundle(comparison, opts));
}

function fromBundle (bundle) {
	if (!(bundle instanceof DougalBinaryBundle)) {
		bundle = DougalBinaryBundle.clone(bundle);
	}

	const json = [];
	for (const record of bundle) {
		record.shift();
		json.push(record);
	}
	return json;
}

async function saveGroup (group, opts = {}) {
	if (group == null) {
		// Save everything
		const g = await groups();
		for (const group of Object.values(g)) {
			await saveGroup(group)
		}
	} if (typeof group === "string") {
		// This is a group name
		const g = await groups();
		group = groups[g];
	}

	if (Array.isArray(group)) {
		const pids = group.map( i => i.pid ).sort();

		for (const [ baselineProjectID, monitorProjectID ] of combinations(pids, 2)) {
			try {
				const isSaved = await save(baselineProjectID, monitorProjectID);
				if (isSaved) {
					await saveSample(baselineProjectID, monitorProjectID, opts.sampleOpts);
				} else {
					await remove(baselineProjectID, monitorProjectID);
				}
				DEBUG("Saved comparison between %s and %s", baselineProjectID, monitorProjectID);
			} catch (err) {
				console.error(err);
				ERROR("Error saving comparison between %s and %s", baselineProjectID, monitorProjectID);
			}
		}
	}
}

/*
async function getGroup (groupName, opts = {}) {

	const group = (await groups())?.[groupName]?.map( i => i.pid)?.sort();

	if (!group?.length) return;

	const client = await pool.connect();

	try {

		const text = `
			-- SQL query goes here
		`;

		const values = combinations(group, 2);
		const res = await client.query(text, values);
		if (!res.rows.length) {
			console.log("Comparison not found in database");
			return;
		}

		if (opts.returnData) {
			return res.rows.map( row => ({
				data: DougalBinaryBundle.clone(row.data),
				meta: row.meta
			});
		} else {
			return res.rows.map( row => row.meta );
		}
	} catch (err) {
		console.error(err);
	} finally {
		client.release();
	}
}
*/


async function getGroup (groupName, opts = {}) {

	const group = (await groups())?.[groupName]?.map( i => i.pid)?.sort();

	if (!group?.length) return;

	const client = await pool.connect();

	try {

		const pairs = combinations(group, 2);
		const flatValues = pairs.flat();
		const placeholders = [];
		for (let i = 0; i < pairs.length; i++) {
			placeholders.push(`($${i * 2 + 1}, $${i * 2 + 2})`);
		}
		const inClause = placeholders.join(',');
		const selectFields = opts.returnData ? 'data, meta' : 'meta';

		const text = `
			SELECT baseline_pid, monitor_pid, ${selectFields}
			FROM comparisons.comparisons
			WHERE type = 'geometric_difference'
			AND (baseline_pid, monitor_pid) IN (VALUES ${inClause})
			ORDER BY baseline_pid, monitor_pid
		`;

		console.log(text);
		console.log(flatValues);

		const res = await client.query(text, flatValues);
		if (!res.rows.length) {
			console.log("Comparison not found in database");
			return;
		}

		if (opts.returnData) {
			return res.rows.map( row => ({
				...row,
				data: DougalBinaryBundle.clone(row.data),
			}));
		} else {
			return res.rows;
		}
	} catch (err) {
		console.error(err);
	} finally {
		client.release();
	}
}

module.exports = {
	groups,
	fetchErrors,
	compare,
	computeSample,
	get,
	save,
	getSample,
	saveSample,
	saveGroup,
	getGroup,
	remove,
	stats,
	// comparisonGeometricDifferences,
	asBundle,
	fromBundle
};
