/*global document window*/
import type GraphContainer from '@gitkraken/gitkraken-components';
import type {
	CssVariables,
	GraphRow,
	GraphZoneType,
	ReadonlyGraphRow,
	SelectCommitsOptions,
} from '@gitkraken/gitkraken-components';
import { consume } from '@lit/context';
import { SignalWatcher } from '@lit-labs/signals';
import { html, LitElement } from 'lit';
import { customElement, query, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import type { GitGraphRowType } from '@gitlens/git/models/graph.js';
import { filterMap } from '@gitlens/utils/array.js';
import { getCssMixedColorValue, getCssOpacityColorValue, getCssVariable } from '@gitlens/utils/color.js';
import { debounce } from '@gitlens/utils/debounce.js';
import type { GraphSelection, RowAction } from '../../../../plus/graph/protocol.js';
import {
	DoubleClickedCommand,
	EnsureRowRequest,
	GetMissingAvatarsCommand,
	GetMissingRefsMetadataCommand,
	GetMoreRowsCommand,
	RowActionCommand,
	UpdateColumnsCommand,
	UpdateRefsVisibilityCommand,
	UpdateSelectionCommand,
} from '../../../../plus/graph/protocol.js';
import type { CustomEventType } from '../../../shared/components/element.js';
import { ipcContext } from '../../../shared/contexts/ipc.js';
import type { TelemetryContext } from '../../../shared/contexts/telemetry.js';
import { telemetryContext } from '../../../shared/contexts/telemetry.js';
import type { Disposable } from '../../../shared/events.js';
import type { ThemeChangeEvent } from '../../../shared/theme.js';
import { onDidChangeTheme } from '../../../shared/theme.js';
import { graphStateContext } from '../context.js';
import type { GlGraph } from './gl-graph.js';
import type { GraphWrapperTheming } from './gl-graph.react.jsx';
import './gl-graph.js';

// These properties in the DOM are auto-generated by VS Code from our `contributes.colors` in package.json
const graphLaneThemeColors = new Map([
	['--vscode-gitlens-graphLane1Color', '#15a0bf'],
	['--vscode-gitlens-graphLane2Color', '#0669f7'],
	['--vscode-gitlens-graphLane3Color', '#8e00c2'],
	['--vscode-gitlens-graphLane4Color', '#c517b6'],
	['--vscode-gitlens-graphLane5Color', '#d90171'],
	['--vscode-gitlens-graphLane6Color', '#cd0101'],
	['--vscode-gitlens-graphLane7Color', '#f25d2e'],
	['--vscode-gitlens-graphLane8Color', '#f2ca33'],
	['--vscode-gitlens-graphLane9Color', '#7bd938'],
	['--vscode-gitlens-graphLane10Color', '#2ece9d'],
]);

declare global {
	// interface HTMLElementTagNameMap {
	// 	'gl-graph-wrapper': GlGraphWrapper;
	// }

	interface GlobalEventHandlersEventMap {
		// passing up event map
		'gl-graph-change-selection': CustomEvent<{ selection: GraphSelection[] }>;
		'gl-graph-change-visible-days': CustomEvent<{ top: number; bottom: number }>;
		'gl-graph-mouse-leave': CustomEvent<void>;
		'gl-graph-row-context-menu': CustomEvent<{ graphZoneType: GraphZoneType; graphRow: GraphRow }>;
		'gl-graph-row-hover': CustomEvent<{
			graphZoneType: GraphZoneType;
			graphRow: GraphRow;
			clientX: number;
			currentTarget: HTMLElement;
		}>;
		'gl-graph-row-unhover': CustomEvent<{
			graphZoneType: GraphZoneType;
			graphRow: GraphRow;
			relatedTarget: EventTarget | null;
		}>;
	}
}

@customElement('gl-graph-wrapper')
export class GlGraphWrapper extends SignalWatcher(LitElement) {
	// use Light DOM
	protected override createRenderRoot(): HTMLElement | DocumentFragment {
		return this;
	}

	private disposables: Disposable[] = [];

	@consume({ context: graphStateContext, subscribe: true })
	private readonly graphState!: typeof graphStateContext.__context__;

	@consume({ context: ipcContext })
	private readonly _ipc!: typeof ipcContext.__context__;

	@consume({ context: telemetryContext as any })
	private readonly _telemetry!: TelemetryContext;

	@query('gl-graph')
	graph!: typeof GlGraph;

	private ref?: GraphContainer;
	private onSetRef = (ref: GraphContainer) => {
		this.ref = ref;
	};

	@state()
	private theming?: GraphWrapperTheming;

	override connectedCallback(): void {
		super.connectedCallback?.();

		this.theming = this.getGraphTheming();
		this.disposables.push(
			onDidChangeTheme(
				debounce((e: ThemeChangeEvent) => {
					this.theming = this.getGraphTheming(e);
				}, 100),
			),
		);
	}

	override disconnectedCallback(): void {
		super.disconnectedCallback?.();

		this.disposables.forEach(d => d.dispose());
		this.disposables = [];
	}

	override render() {
		const { graphState } = this;

		return html`<gl-graph
			.setRef=${this.onSetRef}
			.activeRow=${graphState.activeRow}
			.avatars=${graphState.avatars}
			.columns=${graphState.columns}
			.config=${graphState.config}
			.context=${graphState.context}
			.downstreams=${graphState.downstreams}
			.excludeRefs=${graphState.excludeRefs}
			.excludeTypes=${graphState.excludeTypes}
			.includeOnlyRefs=${graphState.includeOnlyRefs}
			?loading=${graphState.loading}
			nonce=${ifDefined(graphState.nonce)}
			.paging=${graphState.paging}
			.refsMetadata=${graphState.refsMetadata}
			.rows=${graphState.rows}
			.rowsStats=${graphState.rowsStats}
			?rowsStatsLoading=${graphState.rowsStatsLoading}
			.searchMode=${graphState.searchMode}
			.searchResults=${graphState.searchResults}
			.selectedRows=${graphState.selectedRows}
			.theming=${this.theming}
			?windowFocused=${graphState.windowFocused}
			.workingTreeStats=${graphState.workingTreeStats}
			@changecolumns=${this.onColumnsChanged}
			@changerefsvisibility=${this.onRefsVisibilityChanged}
			@changeselection=${this.onSelectionChanged}
			@changevisibledays=${this.onVisibleDaysChanged}
			@missingavatars=${this.onMissingAvatars}
			@missingrefsmetadata=${this.onMissingRefsMetadata}
			@morerows=${this.onGetMoreRows}
			@graphmouseleave=${this.onMouseLeave}
			@refdoubleclick=${this.onRefDoubleClick}
			@rowaction=${this.onRowAction}
			@rowcontextmenu=${this.onRowContextMenu}
			@rowdoubleclick=${this.onRowDoubleClick}
			@rowhover=${this.onRowHover}
			@rowunhover=${this.onRowUnhover}
		></gl-graph>`;
	}

	override focus(): void {
		this.ref?.focus();
	}

	getCommits(shas: string[]): ReadonlyGraphRow[] {
		return this.ref?.getCommits(shas) ?? [];
	}

	selectCommits(shas: string[], options?: SelectCommitsOptions): ReadonlyGraphRow[] {
		return this.ref?.selectCommits(shas, options) ?? [];
	}

	/**
	 * Select a row by SHA, loading it into the graph first if necessary.
	 * The host handles both loading and selecting — the rows notification
	 * carries the updated selection so the graph renders it automatically.
	 */
	ensureAndSelectCommit(sha: string): void {
		// Fast path — row already loaded
		if (this.ref?.getCommits([sha])?.length) {
			this.ref.selectCommits([sha], { ensureVisible: true });
			return;
		}

		// Ask the host to load and select the row; the rows notification
		// will carry the selection so the graph picks it up on render.
		void this._ipc.sendRequest(EnsureRowRequest, { id: sha, select: true });
	}

	private onColumnsChanged(event: CustomEventType<'graph-changecolumns'>) {
		this._ipc.sendCommand(UpdateColumnsCommand, { config: event.detail.settings });
	}

	private onGetMoreRows({ detail: sha }: CustomEventType<'graph-morerows'>) {
		this.graphState.loading = true;
		this._ipc.sendCommand(GetMoreRowsCommand, { id: sha });
	}

	private onMouseLeave() {
		this.dispatchEvent(new CustomEvent('gl-graph-mouse-leave'));
	}

	private onMissingAvatars({ detail: emails }: CustomEventType<'graph-missingavatars'>) {
		this._ipc.sendCommand(GetMissingAvatarsCommand, { emails: emails });
	}

	private onMissingRefsMetadata({ detail: metadata }: CustomEventType<'graph-missingrefsmetadata'>) {
		this._ipc.sendCommand(GetMissingRefsMetadataCommand, { metadata: metadata });
	}

	private onRefDoubleClick({ detail: { ref, metadata } }: CustomEventType<'graph-doubleclickref'>) {
		this._ipc.sendCommand(DoubleClickedCommand, { type: 'ref', ref: ref, metadata: metadata });
	}

	private onRefsVisibilityChanged({ detail }: CustomEventType<'graph-changerefsvisibility'>) {
		this._ipc.sendCommand(UpdateRefsVisibilityCommand, detail);
	}

	private onRowAction({ detail: { action, row } }: CustomEvent<{ action: RowAction; row: GraphRow }>) {
		this._ipc.sendCommand(RowActionCommand, { action: action, row: { id: row.sha, type: row.type } });
	}

	private onRowContextMenu({ detail: { graphRow, graphZoneType } }: CustomEventType<'graph-rowcontextmenu'>) {
		this.dispatchEvent(
			new CustomEvent('gl-graph-row-context-menu', {
				detail: { graphZoneType: graphZoneType, graphRow: graphRow },
			}),
		);
	}

	private onRowDoubleClick({ detail: { row, preserveFocus } }: CustomEventType<'graph-doubleclickrow'>) {
		this._ipc.sendCommand(DoubleClickedCommand, {
			type: 'row',
			row: { id: row.sha, type: row.type },
			preserveFocus: preserveFocus,
		});
	}

	private onRowHover({ detail }: CustomEventType<'graph-graphrowhovered'>) {
		this.dispatchEvent(new CustomEvent('gl-graph-row-hover', { detail: detail }));
	}

	private onRowUnhover({ detail }: CustomEventType<'graph-graphrowunhovered'>) {
		this.dispatchEvent(new CustomEvent('gl-graph-row-unhover', { detail: detail }));
	}

	private onSelectionChanged({ detail: { rows, focusedRow } }: CustomEventType<'graph-changeselection'>) {
		const selection: GraphSelection[] = filterMap(rows, r =>
			r != null
				? ({
						id: r.sha,
						type: r.type as GitGraphRowType,
						active: r === focusedRow,
						hidden: r.hidden,
					} satisfies GraphSelection)
				: undefined,
		);

		const activeKey = focusedRow != null ? `${focusedRow.sha}|${focusedRow.date}` : undefined;
		this.graphState.activeRow = activeKey;
		this.graphState.activeDay = focusedRow?.date;

		this.dispatchEvent(new CustomEvent('gl-graph-change-selection', { detail: { selection: selection } }));
		this._ipc.sendCommand(UpdateSelectionCommand, { selection: selection });
	}

	private onVisibleDaysChanged({ detail }: CustomEventType<'graph-changevisibledays'>) {
		this.dispatchEvent(new CustomEvent('gl-graph-change-visible-days', { detail: detail }));
	}

	private readonly themingDefaults: { cssVariables: CssVariables; themeOpacityFactor: number } = {
		cssVariables: (() => {
			const bgColor = getCssVariableValue('--color-background');
			const mixedGraphColors: CssVariables = {};
			let i = 0;
			let color;
			for (const [colorVar, colorDefault] of graphLaneThemeColors) {
				color = getCssVariableValue(colorVar, { fallbackValue: colorDefault });
				mixedGraphColors[`--graph-color-${i}`] = color;
				for (const mixInt of [15, 25, 45, 50]) {
					mixedGraphColors[`--graph-color-${i}-bg${mixInt}`] = getCssMixedColorValue(bgColor, color, mixInt);
				}
				for (const mixInt of [10, 50]) {
					mixedGraphColors[`--graph-color-${i}-f${mixInt}`] = getCssOpacityColorValue(color, mixInt);
				}
				i++;
			}
			return {
				'--app__bg0': getCssVariableValue('--color-background'),
				'--panel__bg0': getCssVariableValue('--color-graph-background'),
				'--panel__bg1': getCssVariableValue('--color-graph-background2'),
				'--section-border': getCssVariableValue('--color-graph-background2'),
				'--selected-row': getCssVariableValue('--color-graph-selected-row'),
				'--selected-row-border': 'none',
				'--hover-row': getCssVariableValue('--color-graph-hover-row'),
				'--hover-row-border': 'none',
				'--scrollable-scrollbar-thickness': getCssVariableValue('--graph-column-scrollbar-thickness'),
				'--scroll-thumb-bg': getCssVariableValue('--vscode-scrollbarSlider-background'),
				'--scroll-marker-head-color': getCssVariableValue('--color-graph-scroll-marker-head'),
				'--scroll-marker-upstream-color': getCssVariableValue('--color-graph-scroll-marker-upstream'),
				'--scroll-marker-highlights-color': getCssVariableValue('--color-graph-scroll-marker-highlights'),
				'--scroll-marker-local-branches-color': getCssVariableValue(
					'--color-graph-scroll-marker-local-branches',
				),
				'--scroll-marker-remote-branches-color': getCssVariableValue(
					'--color-graph-scroll-marker-remote-branches',
				),
				'--scroll-marker-stashes-color': getCssVariableValue('--color-graph-scroll-marker-stashes'),
				'--scroll-marker-tags-color': getCssVariableValue('--color-graph-scroll-marker-tags'),
				'--scroll-marker-selection-color': getCssVariableValue('--color-graph-scroll-marker-selection'),
				'--scroll-marker-pull-requests-color': getCssVariableValue('--color-graph-scroll-marker-pull-requests'),
				'--stats-added-color': getCssVariableValue('--color-graph-stats-added'),
				'--stats-deleted-color': getCssVariableValue('--color-graph-stats-deleted'),
				'--stats-files-color': getCssVariableValue('--color-graph-stats-files'),
				'--stats-bar-border-radius': getCssVariableValue('--graph-stats-bar-border-radius'),
				'--stats-bar-height': getCssVariableValue('--graph-stats-bar-height'),
				'--text-selected': getCssVariableValue('--color-graph-text-selected'),
				'--text-selected-row': getCssVariableValue('--color-graph-text-selected-row'),
				'--text-hovered': getCssVariableValue('--color-graph-text-hovered'),
				'--text-dimmed-selected': getCssVariableValue('--color-graph-text-dimmed-selected'),
				'--text-dimmed': getCssVariableValue('--color-graph-text-dimmed'),
				'--text-normal': getCssVariableValue('--color-graph-text-normal'),
				'--text-secondary': getCssVariableValue('--color-graph-text-secondary'),
				'--text-disabled': getCssVariableValue('--color-graph-text-disabled'),
				'--text-accent': getCssVariableValue('--color-link-foreground'),
				'--text-inverse': getCssVariableValue('--vscode-input-background'),
				'--text-bright': getCssVariableValue('--vscode-input-background'),
				...mixedGraphColors,
			};
		})(),
		themeOpacityFactor: 1,
	};

	private getGraphTheming(e?: ThemeChangeEvent): GraphWrapperTheming {
		// this will be called on theme updated as well as on config updated since it is dependent on the column colors from config changes and the background color from the theme
		const computedStyle = e?.computedStyle ?? window.getComputedStyle(document.documentElement);
		const bgColor = getCssVariableValue('--color-background', { computedStyle: computedStyle });

		const mixedGraphColors: CssVariables = {};

		let i = 0;
		let color;
		for (const [colorVar, colorDefault] of graphLaneThemeColors) {
			color = getCssVariableValue(colorVar, { computedStyle: computedStyle, fallbackValue: colorDefault });

			mixedGraphColors[`--column-${i}-color`] = getCssVariable(colorVar, computedStyle) || colorDefault;

			for (const mixInt of [15, 25, 45, 50]) {
				mixedGraphColors[`--graph-color-${i}-bg${mixInt}`] = getCssMixedColorValue(bgColor, color, mixInt);
			}

			i++;
		}

		const isHighContrastTheme =
			e?.isHighContrastTheme ??
			(document.body.classList.contains('vscode-high-contrast') ||
				document.body.classList.contains('vscode-high-contrast-light'));

		return {
			cssVariables: {
				...this.themingDefaults.cssVariables,
				'--selected-row-border': isHighContrastTheme
					? `1px solid ${getCssVariableValue('--color-graph-contrast-border', { computedStyle: computedStyle })}`
					: 'none',
				'--hover-row-border': isHighContrastTheme
					? `1px dashed ${getCssVariableValue('--color-graph-contrast-border', { computedStyle: computedStyle })}`
					: 'none',
				...mixedGraphColors,
			},
			themeOpacityFactor:
				parseInt(getCssVariable('--graph-theme-opacity-factor', computedStyle)) ||
				this.themingDefaults.themeOpacityFactor,
		};
	}
}

function getCssVariableValue(
	variable: string,
	options?: { computedStyle?: CSSStyleDeclaration; fallbackValue?: string },
): string {
	const fallbackValue = options?.computedStyle
		? getCssVariable(variable, options?.computedStyle)
		: options?.fallbackValue
			? options.fallbackValue
			: undefined;

	if (fallbackValue) {
		return `var(${variable}, ${fallbackValue})`;
	}
	return `var(${variable})`;
}
