// import {subscribeOnStream, unsubscribeFromStream,} from './streaming.js';

import {jBars, tvResolutionToPeriodString} from './jBars.js';
import {metadata} from "@/version.js";
import FlexSearch from "flexsearch";
import {useChartOrderStore} from "@/orderbuild.js";
import {useStore} from "@/store/store.js";
import {subOHLC, unsubOHLC} from "@/blockchain/ohlcs.js";
import {socket} from "@/socket.js";

// disable debug messages logging
let console = { log: function() {} }

let feeDropdown = null
let widget = null

// Python code to generate VARIABLE_TICK_SIZE:
// from decimal import Decimal
// K=12  # powers above and below to include
// D=5  # number of significant digits to display
// ' '.join(f'{Decimal(10)**(k-D):f} {Decimal(10)**k:f}' for k in range(-K,K+1))+f' {Decimal(10)**(K+1-D)}'
const VARIABLE_TICK_SIZE = '0.00000000000000001 0.000000000001 0.0000000000000001 0.00000000001 0.000000000000001 0.0000000001 0.00000000000001 0.000000001 0.0000000000001 0.00000001 0.000000000001 0.0000001 0.00000000001 0.000001 0.0000000001 0.00001 0.000000001 0.0001 0.00000001 0.001 0.0000001 0.01 0.000001 0.1 0.00001 1 0.0001 10 0.001 100 0.01 1000 0.1 10000 1 100000 10 1000000 100 10000000 1000 100000000 10000 1000000000 100000 10000000000 1000000 100000000000 10000000 1000000000000 100000000'

export function initFeeDropdown(w) {
	widget = w
	widget.createDropdown(
		{
			title: 'Fees',
			tooltip: 'Choose Fee Tier',
			items: [/*{title: 'Automatic Fee Selection', onSelect: () => {console.log('autofees')}}*/],
			icon: `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28"><g fill="none" stroke="currentColor"><circle cx="10" cy="10" r="2.5"/><circle cx="18" cy="18" r="2.5"/><path stroke-linecap="square" d="M17.5 7.5l-7 13"/></g></svg>`,
		}
	).then(dropdown => {feeDropdown = dropdown; updateFeeDropdown()})
}


function updateFeeDropdown() {
	if (feeDropdown===null) return
	const symbolItem = useChartOrderStore().selectedSymbol
	const feeOpts = {
		items: symbolItem.pools.map((p)=> {
			return {
				title: (p[1]/10000).toFixed(2)+'%',
				onSelect: ()=>selectPool(p),
			}
		})
	}
	feeDropdown.applyOptions(feeOpts)
}


function selectPool(p) {
	const co = useChartOrderStore();
	if ( co.selectedPool === null || co.selectedPool[0] !== p[0] || co.selectedPool[1] !== p[0]) {
		co.selectedPool = p
	}
}


const lastBarsCache = new Map();

// DatafeedConfiguration implementation
const configurationData = {
	// Represents the resolutions for bars supported by your datafeed
	supported_resolutions:
	['1', '3', '5', '10', '15', '30', '60', '120', '240', '480', '720', '1D', '2D', '3D', '1W'],

	// The `exchanges` arguments are used for the `searchSymbols` method if a user selects the exchange
	exchanges: [
		// {
		// 	value: 'UNIv2',
		// 	name: 'Uniswap v2',
		// 	desc: 'Uniswap v2',
		// },
		{
			value: 'UNIv3',
			name: 'Uniswap v3',
			desc: 'Uniswap v3',
			logo: 'https://upload.wikimedia.org/wikipedia/commons/e/e7/Uniswap_Logo.svg',
		},
	],
	// The `symbols_types` arguments are used for the `searchSymbols` method if a user selects this symbol type
	symbols_types: [
		{name: 'swap', value: 'swap',},
	],
};


const tokenMap = {}
const poolMap = {}
let _symbols = null

const indexer = new FlexSearch.Document({
	document: {id: 'id', index: ['fn', 'as[]', 'b', 'q', 'bs', 'qs', 'e', 'd']}, // this must match what is generated for the index object in addSymbol()
	charset: {split: /\W+/},
	tokenize: 'forward',
})

const indexes = {}
const symbolsSeen = {}  // keyed by (base,quote) so we only list one pool per pair even if there are many fee tiers

function addSymbol(p, base, quote, inverted) {
	const symbol = base.s + '/' + quote.s
	const exchange = ['UNIv2', 'UNIv3'][p.e]
	const full_name = exchange + ':' + symbol // + '%' + formatFee(fee)
	let key = `${base.a}${quote.a}`
	console.log('addSymbol', p, base, quote, inverted, key)
	if (symbolsSeen[key]) {
		// add this pool's address to the existing symbol as an additional fee tier
		const symbolInfo = _symbols[key];
		symbolInfo.pools.push([p.a, p.f])
		symbolInfo.pools.sort((a,b)=>a[1]-b[1])
		console.log('integrated symbol', _symbols[key])
		indexes[key].as.push(p.a)
		return
	}
	symbolsSeen[key] = true
	const longExchange = ['Uniswap v2', 'Uniswap v3',][p.e]
	const description = `${base.n} / ${quote.n}`
	const type = 'swap'
	const pools = [[p.a, p.f]]
	const decimals = p.d
	_symbols[key] = {
		ticker: key, full_name, symbol, description,
		exchange, type, inverted, base, quote, pools, decimals, x:p.x
	}
	if (defaultSymbol===null)
		defaultSymbol = _symbols[key]
	console.log('new symbol', key, _symbols[key])
	indexes[key] = {
		// key
		id: key,

		// addresses
		as: [p.a],   // multiple pool addrs for each fee tier
		b: p.b,
		q: p.q,

		// symbols
		fn: full_name,
		bs: base.s,
		qs: quote.s,
		e: exchange + ' ' + longExchange,
		d: description,
	}
}


// function formatFee(fee) {
// 	let str = (fee / 10000).toFixed(2);
// 	if (str.startsWith('0')) // start with the decimal point not a zero
// 		str = str.slice(1)
// 	return str
// }

async function getAllSymbols() {
	if (_symbols===null) {
		const chainId = useStore().chainId;
		const md = metadata[chainId]
		if(!md) {
			console.log('could not get metadata for chain', chainId)
			return []
		}
		_symbols = {}
		for (const t of md.t)
			tokenMap[t.a] = t
		md.p.forEach((p)=>{
			poolMap[p.a] = p
			const base = tokenMap[p.b];
			const quote = tokenMap[p.q];
			if (!base) {
				console.log(`No token ${p.b} found`)
				return
			}
			if (!quote) {
				console.log(`No token ${p.q} found`)
				return
			}
			addSymbol(p, base, quote, false);
			addSymbol(p, quote, base, true);
		})
		console.log('indexes', indexes)
		Object.values(indexes).forEach(indexer.add.bind(indexer))
	}
	console.log('symbols', _symbols)
	return _symbols
}

export function lookupSymbol(key) { // lookup by fullname
	return _symbols[key]
}

export function lookupBaseQuote(baseAddr, quoteAddr) {
	return _symbols[`${baseAddr}${quoteAddr}`]
}

function poolIsInverted() {
	return useChartOrderStore().selectedSymbol.inverted
}

export function maybeInvertBar (bar) {
	if (poolIsInverted()) {
		bar.open = 1/bar.open
		let high = bar.high
		bar.high = 1/bar.low
		bar.low = 1/high
		bar.close = 1/bar.close
		console.log("bar inverted")
	} else {
		console.log("bar NOT inverted")
	}
	return bar
}

function checkBar(bar, msg) {

	// Everything should be positive

	let o_l = bar.open - bar.low
	let c_l = bar.close - bar.low
	let h_o = bar.high - bar.open
	let h_c = bar.high - bar.close
	let h_l = bar.high - bar.low

	if (o_l<0||c_l<0||h_o<0||h_c<0||h_l<0) {
		console.log(msg, "bar.high/low inconsistent:", bar)
		if (o_l<0) console.log("bar inconsistent: open-low:  ", o_l)
		if (c_l<0) console.log("bar inconsistent: close-low: ", c_l)
		if (h_o<0) console.log("bar inconsistent: high-open: ", h_o)
		if (h_c<0) console.log("bar inconsistent: high-close:", h_c)
		if (h_l<0) console.log("bar inconsistent: high-low:  ", h_l)	
	} else {
		console.log(msg, "bar diffs:", bar)
		console.log("bar diff: open-low:  ", o_l)
		console.log("bar diff: close-low: ", c_l)
		console.log("bar diff: high-open: ", h_o)
		console.log("bar diff: high-close:", h_c)
		console.log("bar diff: high-low:  ", h_l)	
	}

}

export const DataFeed = {
	onReady: (callback) => {
		console.log('[onReady]: Method call');
		setTimeout(() => callback(configurationData));
	},

	searchSymbols: async (
		userInput,
		exchange,
		symbolType,
		onResultReadyCallback,
	) => {
		console.log('[searchSymbols]: Method call');
		// todo limit results by exchange.  use a separate indexer per exchange?
		const found = indexer.search(userInput, 10)
		console.log('found', found)
		const result = []
		for (const f of found)
			for (const key of f.result)
				result.push(_symbols[key])
		onResultReadyCallback(result);
	},

	resolveSymbol: async (
		symbolName,
		onSymbolResolvedCallback,
		onResolveErrorCallback,
		extension
	) => {
		console.log('[resolveSymbol]: Method call', symbolName);
		const symbols = await getAllSymbols();
		const symbolItem = symbolName === 'default' ? defaultSymbol : symbols[symbolName]
		console.log('symbol resolved?', symbolItem)
		if (!symbolItem) {
			console.log('[resolveSymbol]: Cannot resolve symbol', symbolName);
			onResolveErrorCallback('cannot resolve symbol');
			return;
		}
		const co = useChartOrderStore();
		co.selectedSymbol = symbolItem
		const pool = symbolItem.pools[0]; // choose the first-listed pool. server will adjust metadata accordingly.
		// noinspection JSValidateTypes
		co.selectedPool = pool
		updateFeeDropdown()
		const priceScale = '100'
		// LibrarySymbolInfo
		// https://www.tradingview.com/charting-library-docs/latest/api/interfaces/Charting_Library.LibrarySymbolInfo
		const symbolInfo = {
			ticker: symbolItem.ticker,
			name: symbolItem.symbol,
			pro_name: symbolItem.full_name,
			// we add the fee to the description after the specific pool has been resolved
			description: symbolItem.description + ` ${(pool[1] / 10000).toFixed(2)}%`,
			type: symbolItem.type,
			session: '24x7',
			timezone: 'Etc/UTC',
			exchange: symbolItem.exchange,
			minmov: .00000000000000001,
			pricescale: 1,
			variable_tick_size: VARIABLE_TICK_SIZE,
			has_intraday: true, // Added to allow less than one day to work
			visible_plots_set: 'ohlc',
			has_weekly_and_monthly: true, // Added to allow greater than one day to work
			supported_resolutions: configurationData.supported_resolutions,
			// volume_precision: 2,
			data_status: 'streaming',
		};
		console.log('[resolveSymbol]: Symbol resolved', symbolName);
		onSymbolResolvedCallback(symbolInfo);
	},

	getBars: async (symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) => {
		const { from, to, firstDataRequest } = periodParams;
		console.log('[getBars]: Method call', symbolInfo, resolution, from, to);
		try {
			// todo need to consider the selected fee tier
			let bars, metadata;
			const pool = useChartOrderStore().selectedPool;
			[bars, metadata] = await jBars(lookupSymbol(symbolInfo.ticker), pool[0], from, to, resolution); // This is the one that does all the work
			if (firstDataRequest) {
				lastBarsCache.set(symbolInfo.full_name, {
					...bars[bars.length - 1],
				});
			}
			console.log(`[getBars]: returned ${bars.length} bar(s), and metadata ${metadata}`);
			console.log('[getBars]: bars=', bars);
			onHistoryCallback(bars, metadata);
		} catch (error) {
			console.log('[getBars]: Get error', error);
			onErrorCallback(error);
		}
	},

	subscribeBarsOnRealtimeCallback: null,

	subscribeBars: (
		symbolInfo,
		resolution,
		onRealtimeCallback,
		subscriberUID,
		onResetCacheNeededCallback,
	) => {
		console.log('[subscribeBars]', symbolInfo, resolution, subscriberUID);
		const chainId = useStore().chainId;
		const poolAddr = useChartOrderStore().selectedPool[0];
		const period = tvResolutionToPeriodString(resolution);
		subscriptions[subscriberUID] = [chainId, poolAddr, period, onRealtimeCallback, onResetCacheNeededCallback]
		const key = `${chainId}|${poolAddr}|${period}`
		if (key in subscriptionCallbacks)
			subscriptionCallbacks[key].push(subscriberUID)
		else
			subscriptionCallbacks[key] = [subscriberUID]
		console.log('sub', key)
		subOHLC(chainId, poolAddr, period)
	},

	unsubscribeBars: (subscriberUID) => {
		console.log('[unsubscribeBars]', subscriberUID);
		const [chainId, poolAddr, period] = subscriptions[subscriberUID]
		unsubOHLC(chainId, poolAddr, period)
		delete subscriptions[subscriberUID]
		const key = `${chainId}|${poolAddr}|${period}`;
		console.log('unsub',key)
		const remainingSubs = subscriptionCallbacks[key].filter((v)=>v!==subscriberUID)
		if (remainingSubs.length===0)
			delete subscriptionCallbacks[key]
		else
			subscriptionCallbacks[key] = remainingSubs
	},

	poolCallbackState : {lastBar:{'chain|pool|period':null}},

	poolCallback(chainId, poolPeriod, ohlcs) {
		console.log("poolCallback: chainId, pool, ohlcs:", chainId, poolPeriod, ohlcs)
		if (ohlcs == null) {
			console.log("poolCallback: ohlcs == null, nothing to do.")
			return;
		}
		const key = `${chainId}|${poolPeriod}`;
		const subscriptionUIDs = subscriptionCallbacks[key];
		if (subscriptionUIDs===undefined) {
			console.log('unsubbing abandoned subscription', poolPeriod)
			socket.emit('unsubOHLCs', chainId, [poolPeriod])
			return
		}
		function onRealtimeCallback() {
			for (const subId of subscriptionUIDs) {
				const [_chainId, _poolAddr, _period, _onRealtimeCallback, _onResetCacheNeededCallback] = subscriptions[subId]
				_onRealtimeCallback(...arguments)
			}
		}

		function onResetCacheNeededCallback() {
			for (const subId of subscriptionUIDs) {
				const [_chainId, _poolAddr, _period, _onRealtimeCallback, _onResetCacheNeededCallback] = subscriptions[subId]
				_onResetCacheNeededCallback(...arguments)
			}
		}

		let ohlc = ohlcs.at(-1);
		console.log("poolCallBack ohlc:", new Date(Number(ohlc[0])*1000).toGMTString(), ohlc)
		for (let i = 0; i<ohlc.length; i++) if (ohlc[i]!=null) ohlc[i] = Number(ohlc[i])
		// for (const ohlc of ohlcs) {
			let date = new Date(ohlc[0]*1000)
			let close = ohlc[4] // close
			let bar = {
				time:  date.getTime(),
				open:  ohlc[1] ?? close, // open
				high:  ohlc[2] ?? close, // high
				low:   ohlc[3] ?? close, // low
				close: close,
			}
			checkBar(bar, "poolCallback, before inversion:")
			bar = maybeInvertBar(bar)
			checkBar(bar, "poolCallback, after inversion:")
			console.log('DataFeed.poolCallback', date.toGMTString(), ohlcs, bar)
			let lastBar = DataFeed.poolCallbackState.lastBar[key]
			// No last bar then initialize bar
			if (lastBar===undefined) {
				console.log('DataFeed.poolCallback', new Date(bar.time).toGMTString(), 'lastBar=', bar)
				onRealtimeCallback(bar)
				DataFeed.poolCallbackState.lastBar[key] = bar
			}
			// bar time is less than last bar then ignore
			else if (bar.time < lastBar.time ) {
			}
			// bar time equal to last bar then replace last bar
			else if (bar.time === lastBar.time ) {
				console.log('DataFeed.poolCallback', new Date(bar.time).toGMTString(), 'lastBar=', bar)
				if (bar.high < lastBar.high) console.log("bar.high < lastBar.high (lastbar=)")
				if (bar.low  > lastBar.low)  console.log("bar.low  > lastBar.low  (lastbar=)")
				onRealtimeCallback(bar)
				DataFeed.poolCallbackState.lastBar[key] = bar
			}
			// new bar, then render last and replace last bar
			else {
				console.log('DataFeed.poolCallback', new Date(bar.time).toGMTString(), 'lastBar=', bar)
				onRealtimeCallback(bar)
				DataFeed.poolCallbackState.lastBar[key] = bar
			}
		// }
	}
}

let defaultSymbol = null
const subscriptions = {}
const subscriptionCallbacks = {}
