import * as Plot from '@observablehq/plot';
import * as d3 from 'd3';
import {cachedFetchJSON} from './cache';

const tickerSelect = document.querySelector('select#ticker') as HTMLSelectElement;
const charts = document.querySelector('#charts')!;

(async function () {
	const materials: Material[] = await fetch('https://rest.fnar.net/material/allmaterials').then((r) => r.json());
	const selected = document.location.hash.substring(1);
	for (const mat of materials.sort((a, b) => a.Ticker.localeCompare(b.Ticker))) {
		const option = document.createElement('option');
		option.value = mat.Ticker;
		option.textContent = `${mat.Ticker} ${mat.Name}`;
		if (mat.Ticker === selected)
			option.selected = true;
		tickerSelect.appendChild(option);
	}
	if (selected)
		render();
})();

tickerSelect.addEventListener('change', async () => {
	await render();
	document.location.hash = tickerSelect.value;
});
	
async function render() {
	charts.innerHTML = '';
	const ticker = tickerSelect.value;
	const cxpc = await Promise.all([
		getCXPC(ticker, 'NC1'), getCXPC(ticker, 'CI1'),
		getCXPC(ticker, 'IC1'), getCXPC(ticker, 'AI1'),
	]);

	let minDate = null, maxDate = null;
	let maxPrice = 0, maxTraded = 0;
	for (const cxPrices of cxpc)
		for (const p of cxPrices) {
			if (minDate === null || p.DateEpochMs < minDate) minDate = p.DateEpochMs;
			if (maxDate === null || p.DateEpochMs > maxDate) maxDate = p.DateEpochMs;
			if (p.High > maxPrice) maxPrice = p.High;
			if (p.Traded > maxTraded) maxTraded = p.Traded;
		}
	if (minDate === null || maxDate === null)
		throw new Error('no data');
	const dateRange: [Date, Date] = [new Date(minDate), new Date(maxDate)];
	const cxpcRange = {dateRange, maxPrice, maxTraded};

	charts.append(...await Promise.all([
		renderExchange('NC1', ticker, cxpcRange, cxpc[0]), renderExchange('CI1', ticker, cxpcRange, cxpc[1]),
		renderExchange('IC1', ticker, cxpcRange, cxpc[2]), renderExchange('AI1', ticker, cxpcRange, cxpc[3]),
		renderRecipes(ticker),
	]));
}

async function getCXPC(ticker: string, cx: string): Promise<PriceChartPoint[]> {
	const cxpc: PriceChartPoint[] = await cachedFetchJSON(`https://rest.fnar.net/exchange/cxpc/${ticker}.${cx}`);
	const threshold = Date.now() - 100 * 24 * 60 * 60 * 1000; // work around FIO bug that shows old data
	return cxpc.filter((p) => p.Interval === 'HOUR_TWELVE' && p.DateEpochMs > threshold);
}

async function renderExchange(cx: string, ticker: string, cxpcRange: CXPCRange, cxpc: PriceChartPoint[]): Promise<HTMLElement> {
	const div = document.createElement('div');
	div.append(renderPriceChart(cx, cxpcRange, cxpc));
	div.append(renderOrderBook(await cachedFetchJSON(`https://rest.fnar.net/exchange/${ticker}.${cx}`)));
	return div;
}

function renderPriceChart(cx: string, {dateRange, maxPrice, maxTraded}: CXPCRange, cxpc: PriceChartPoint[]):
		SVGSVGElement | HTMLElement {
	const chartsWidth = charts.getBoundingClientRect().width;
	const numFormat = Plot.formatNumber();
	return Plot.plot({
		grid: true,
		width: chartsWidth < 600 ? chartsWidth : chartsWidth / 2 - 10,
		height: 400,
		marginLeft: 26 + 7 * Math.log10(maxPrice),
		marginRight: 26 + 7 * Math.log10(maxTraded),
		x: {domain: dateRange},
		y: {axis: 'left', label: cx, domain: [0, maxPrice * 1.1]},
		marks: [
			Plot.axisY(d3.ticks(0, maxTraded * 1.5, 10), {
				label: 'traded',
				anchor: 'right',
				y: (d) => (d / maxTraded) * maxPrice / 3,
			}),
			Plot.rectY(cxpc, {
				x: (p) => new Date(p.DateEpochMs),
				y: (p) => (p.Traded / maxTraded) * maxPrice / 3, // scale traded to price
				interval: 'day',
				fill: '#272',
				fillOpacity: 0.2,
			}),
			Plot.ruleX(cxpc, {
				x: (p) => new Date(p.DateEpochMs),
				y1: 'Low',
				y2: 'High',
				strokeWidth: 5,
				stroke: '#42a',
			}),
			Plot.dot(cxpc, {
				x: (p) => new Date(p.DateEpochMs),
				y: (p) => p.Volume / p.Traded,
				fill: '#a37',
				r: 2,
			}),
			Plot.crosshairX(cxpc, {
				x: (p) => new Date(p.DateEpochMs),
				y: (p) => p.Volume / p.Traded,
				textStroke: '#111',
			}),
			Plot.text(cxpc, Plot.pointerX({
				px: (p) => new Date(p.DateEpochMs),
				py: (p) => p.Volume / p.Traded,
				dy: -20,
				frameAnchor: "top-right",
				fontVariant: "tabular-nums",
				text: (p) => `${Plot.formatIsoDate(new Date(p.DateEpochMs))}\n` +
					`avg: $${numFormat(p.Volume / p.Traded)}\n` +
					`high: ${numFormat(p.High)}\n` +
					`low: ${numFormat(p.Low)}\n` +
					`traded: ${numFormat(p.Traded)}`
			})),
		]
	});
}

function renderOrderBook(book: OrderBook): HTMLElement {
	const table = document.createElement('table');
	book.SellingOrders.sort((a, b) => b.ItemCost - a.ItemCost);
	for (const order of book.SellingOrders) {
		if (book.MMSell !== null && order.ItemCost >= book.MMSell && order.ItemCount !== null) continue;
		const row = renderOrder(order);
		row.classList.add('sell');
		table.appendChild(row);
	}
	book.BuyingOrders.sort((a, b) => b.ItemCost - a.ItemCost);
	for (const order of book.BuyingOrders) {
		if (book.MMBuy !== null && order.ItemCost <= book.MMBuy && order.ItemCount !== null) continue;
		const row = renderOrder(order);
		row.classList.add('buy');
		table.appendChild(row);
	}

	const div = document.createElement('div');
	div.classList.add('cxob');
	div.appendChild(table);
	// center the first bid
	const firstBuyRow: HTMLTableRowElement | null = table.querySelector('tr.buy');
	if (firstBuyRow !== null) {
		const centerFirstBuyOrder = () => {
			if (div.isConnected)
				div.scrollTop = Math.max(0, firstBuyRow.offsetTop - div.clientHeight / 2 + 10);
			else
				requestAnimationFrame(centerFirstBuyOrder);
		};
		requestAnimationFrame(centerFirstBuyOrder);
	}
	return div;
}

function renderOrder(order: Order): HTMLTableRowElement {
	const row = document.createElement('tr');
	row.insertCell().textContent = order.CompanyName;
	row.insertCell().textContent = order.CompanyCode;
	row.insertCell().textContent = order.ItemCount === null ? '∞' : order.ItemCount.toString();
	row.insertCell().textContent = `${formatPrice(order.ItemCost)}`;
	return row;
}

const formatPrice = new Intl.NumberFormat(undefined, {minimumSignificantDigits: 3, maximumSignificantDigits: 3}).format;

async function renderRecipes(ticker: string): Promise<HTMLElement> {
	const recipes: Recipe[] = await cachedFetchJSON(`https://rest.fnar.net/recipes/${ticker}`);

	const section = document.createElement('section');
	for (const recipe of recipes)
		section.innerHTML += `<div class="recipe">
			${recipe.Inputs.map((i) => `${i.Amount}×${i.CommodityTicker}`).join(' ')}
			<span class="building">${recipe.BuildingTicker}</span>
			${recipe.Outputs.map((o) => `${o.Amount}×${o.CommodityTicker}`).join(' ')}
		</div>`;
	return section;
}

interface Material {
	Ticker: string;
	Name: string;
}

interface PriceChartPoint {
	Interval: 'MINUTE_FIVE' | 'MINUTE_FIFTEEN' | 'MINUTE_THIRTY' | 'HOUR_ONE' | 'HOUR_TWO' | 'HOUR_FOUR' | 'HOUR_SIX' | 'HOUR_TWELVE' | 'DAY_ONE' | 'DAY_THREE';
	DateEpochMs: number;
	High: number;
	Low: number;
	Volume: number;
	Traded: number;
}

interface CXPCRange {
	dateRange: [Date, Date];
	maxPrice: number;
	maxTraded: number;
}

interface OrderBook {
	BuyingOrders: Order[];
	SellingOrders: Order[];
	MMBuy: number | null;
	MMSell: number | null;
}

interface Order {
	CompanyName: string;
	CompanyCode: string;
	ItemCount: number | null;
	ItemCost: number;
}

interface Recipe {
	BuildingTicker: string;
	Inputs: RecipeMat[];
	Outputs: RecipeMat[];
}

interface RecipeMat {
	CommodityTicker: string;
	Amount: number;
}
