import { OmnibusRegistrator } from "@hypersphere/omnibus";
import {
	App,
	MarkdownPostProcessorContext,
	MarkdownRenderChild,
} from "obsidian";
import { Sync } from "../../sync/sync/sync";
import { RendererRegistry, RenderReturn } from "../renderer/rendererRegistry";
import { ParserResult, parseWithDefaults, TableDefinition } from "../parser";
import { SqlSealDatabase } from "../../database/database";
import { displayError, displayNotice } from "../../../utils/ui";
import { transformQuery } from "../sql/sqlTransformer";
import { registerObservers } from "../../../utils/registerObservers";
import { Settings } from "../../settings/Settings";
import { ModernCellParser } from "../../syntaxHighlight/cellParser/ModernCellParser";

export class CodeblockProcessor extends MarkdownRenderChild {
	registrator: OmnibusRegistrator;
	renderer: RenderReturn;
	private flags: ParserResult["flags"];
	private extrasEl: HTMLElement;
	private explainEl: HTMLElement;

	constructor(
		private el: HTMLElement,
		private source: string,
		private ctx: MarkdownPostProcessorContext,
		private rendererRegistry: RendererRegistry,
		private db: Pick<SqlSealDatabase, 'select' | 'explain'>,
		private cellParser: ModernCellParser,
		private settings: Settings,
		private app: App,
		private sync: Sync,
		private tq: typeof transformQuery = transformQuery
	) {
		super(el);

		this.registrator = this.sync.getRegistrator();
	}

	query: string;

	async onload() {
		try {
			const defaults: ParserResult = {
				flags: {
					refresh: this.settings.get("enableDynamicUpdates"),
					explain: false,
				},
				query: "",
				renderer: {
					options: "",
					type: this.settings.get("defaultView").toUpperCase(),
				},
				tables: [],
			};

			const results = parseWithDefaults(
				this.source,
				this.rendererRegistry.getViewDefinitions(),
				defaults,
				this.rendererRegistry.flags,
			);

			if (results.tables) {
				await this.registerTables(results.tables);
				if (!results.query) {
					displayNotice(
						this.el,
						`Creating tables: ${results.tables.map((t) => t.tableAlias).join(", ")}`,
					);
					return;
				}
			}

			this.flags = results.flags;
			let rendererEl = this.el;

			if (this.flags.explain) {
				this.extrasEl = this.el.createDiv({ cls: "sqlseal-extras-container" });
				if (this.flags.explain) {
					this.explainEl = this.extrasEl.createEl("pre", {
						cls: "sqlseal-extras-explain-container",
					});
				}
				rendererEl = this.el.createDiv({ cls: "sqlseal-renderer-container" });
			}

            // IF WE'RE ON CANVAS, LETS ADD BACKGRUND
            if (this.isOnCanvas) {
                rendererEl.classList.add('sqlseal-renderer-on-canvas')
            }

			this.renderer = this.rendererRegistry.prepareRender(
				results.renderer.type.toLowerCase(),
				results.renderer.options,
			)(rendererEl, {
				cellParser: this.cellParser,
				sourcePath: this.sourceKey,
			});

			// FIXME: probably should save the one before transform and perform transform every time we execute it.
			this.query = results.query;
			await this.render();
		} catch (e) {
			displayError(this.el, e.toString());
		}
	}

	onunload() {
		this.registrator.offAll();
		if (this.renderer?.cleanup) {
			this.renderer.cleanup();
		}
	}

	async render() {
		try {
			const registeredTablesForContext =
				await this.sync.getTablesMappingForContext(this.sourceKey);

			// Transforming Query
			const res = this.tq(this.query, registeredTablesForContext);
			const transformedQuery = res.sql;

			if (this.flags.refresh) {
				registerObservers({
					bus: this.registrator,
					callback: () => this.render(),
					fileName: this.sourceKey,
					tables: res.mappedTables,
				});
			}

			let variables = {};
			const file = this.app.vault.getFileByPath(this.sourceKey);
			if (file) {
				const fileCache = this.app.metadataCache.getFileCache(file);
				variables = {
					...(fileCache?.frontmatter ?? {}),
					path: file.path,
					fileName: file.name,
					basename: file.basename,
					parent: file.parent?.path,
					extension: file.extension,
				};
			}

			if (this.flags.explain) {
				// Rendering explain
				const result = await this.db.explain(transformedQuery, variables);
				this.explainEl.textContent = result;
			}

			const { data, columns } = (await this.db.select(
				transformedQuery,
				variables,
			))!; // FIXME: check this
			this.renderer.render({
				data,
				columns,
				flags: this.flags,
				frontmatter: variables,
			});
		} catch (e) {
			this.renderer.error(e.toString());
		}
	}

    private cachedName: string | null = null

    get isOnCanvas() {
        return !this.ctx.sourcePath
    }

	get canvasName() {
        // This is hack to detect what name has current canvas.
        // It's not fool proof but should work for majority of use-cases for now.
        // We need to find proper way of getting it or ask Obsidian devs to expose some info.
        
        if (this.cachedName !== null) {
            return this.cachedName
        }
		const canvasViews = this.app.workspace.getLeavesOfType("canvas");

		for (const leaf of canvasViews) {
            const canvasView = leaf.view as any; // Canvas view has data and file properties not exposed in base View type
            const nodes = JSON.parse(canvasView.data).nodes
            const node = nodes.filter((n: any) => n.text).find((n: any) => n.text.contains(this.source))
            if (node) {
                this.cachedName = canvasView.file.path
                return this.cachedName as string
            }
		}
        this.cachedName = ''
        return ''
	}

	get sourceKey() {
		return this.ctx.sourcePath.trim() ? this.ctx.sourcePath.trim() : this.canvasName;
	}

	async registerTables(tables: TableDefinition[]) {
		await Promise.all(
			tables.map((table) =>
				this.sync.registerTable({
					...table,
					sourceFile: this.sourceKey,
				}),
			),
		);
	}
}
