import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Pango from 'gi://Pango';
import St from 'gi://St';

import { registerClass } from '../../common/gjs.js';
import { ColorScheme } from '../../misc/theme.js';
import { normalizeIndentation, trim } from './label.js';

var __decorate =
	(this && this.__decorate) ||
	function (decorators, target, key, desc) {
		var c = arguments.length,
			r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc,
			d;
		if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function')
			r = Reflect.decorate(decorators, target, key, desc);
		else
			for (var i = decorators.length - 1; i >= 0; i--)
				if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
		return (c > 3 && r && Object.defineProperty(target, key, r), r);
	};

// https://gitlab.gnome.org/GNOME/gtksourceview/-/blob/master/data/styles/Adwaita-dark.xml
// https://gitlab.gnome.org/GNOME/gtksourceview/-/blob/master/data/styles/Adwaita.xml
const Colors = {
	'blue_1': '#99C1F1',
	'blue_2': '#62A0EA',
	'blue_3': '#3584E4',
	'blue_4': '#1C71D8',
	'blue_5': '#1A5FB4',
	'blue_6': '#1B497E',
	'blue_7': '#193D66',
	'brown_1': '#CDAB8F',
	'brown_2': '#B5835A',
	'brown_3': '#986A44',
	'brown_4': '#865E3C',
	'brown_5': '#63452C',
	'chameleon_3': '#4E9A06',
	'dark_1': '#777777',
	'dark_2': '#5E5E5E',
	'dark_3': '#505050',
	'dark_4': '#3D3D3D',
	'dark_5': '#242424',
	'dark_6': '#121212',
	'dark_7': '#000000',
	'green_1': '#8FF0A4',
	'green_2': '#57E389',
	'green_3': '#33D17A',
	'green_4': '#2EC27E',
	'green_5': '#26A269',
	'green_6': '#1F7F56',
	'green_7': '#1C6849',
	'libadwaita-dark': '#1d1d20',
	'libadwaita-dark-alt': '#242428',
	'light_1': '#FFFFFF',
	'light_2': '#FCFCFC',
	'light_3': '#F6F5F4',
	'light_4': '#DEDDDA',
	'light_5': '#C0BFBC',
	'light_6': '#B0AFAC',
	'light_7': '#9A9996',
	'orange_1': '#FFBE6F',
	'orange_2': '#FFA348',
	'orange_3': '#FF7800',
	'orange_4': '#E66100',
	'orange_5': '#C64600',
	'purple_1': '#DC8ADD',
	'purple_2': '#C061CB',
	'purple_3': '#9141AC',
	'purple_4': '#813D9C',
	'purple_5': '#613583',
	'red_1': '#F66151',
	'red_2': '#ED333B',
	'red_3': '#E01B24',
	'red_4': '#C01C28',
	'red_5': '#A51D2D',
	'teal_1': '#93DDC2',
	'teal_2': '#5BC8AF',
	'teal_3': '#33B2A4',
	'teal_4': '#26A1A2',
	'teal_5': '#218787',
	'violet_2': '#7D8AC7',
	'violet_3': '#6362C8',
	'violet_4': '#4E57BA',
	'yellow_1': '#F9F06B',
	'yellow_2': '#F8E45C',
	'yellow_3': '#F6D32D',
	'yellow_4': '#F5C211',
	'yellow_5': '#E5A50A',
	'yellow_6': '#D38B09',
};

// https://highlightjs.readthedocs.io/en/latest/css-classes-reference.html
const Dark = {
	// General purpose
	'keyword': { fgcolor: 'orange_2', weight: 'bold' },
	'built_in': { fgcolor: 'teal_2', weight: 'bold' },
	'type': { fgcolor: 'teal_2', weight: 'bold' },
	'literal': { fgcolor: 'violet_2' },
	'number': { fgcolor: 'violet_2' },
	'operator': {},
	'punctuation': {},
	'property': { fgcolor: 'orange_2', weight: 'bold' },
	'regexp': { fgcolor: 'teal_2' },
	'string': { fgcolor: 'teal_2' },
	'char.escape_': { fgcolor: 'red_1' },
	'subst': { fgcolor: 'orange_4' },
	'symbol': {},
	'class': {},
	'function': {},
	'variable': { fgcolor: 'teal_2', weight: 'bold' },
	'variable.language_': { fgcolor: 'orange_2', weight: 'bold' },
	'variable.constant_': {},
	'title': {},
	'title.class_': {},
	'title.class_.inherited__': {},
	'title.function_': {},
	'title.function_.invoke__': {},
	'params': {},
	'comment': { fgcolor: 'dark_1' },
	'doctag': { fgcolor: 'light_7' },

	// Meta
	'meta': { fgcolor: 'violet_2' }, // orange_3
	'meta.prompt': { fgcolor: 'yellow_4' },

	// Tags, attributes, configs
	'section': { fgcolor: 'teal_3', weight: 'bold' },
	'tag': { fgcolor: 'teal_3' },
	'name': { fgcolor: 'teal_3' },
	'attr': { fgcolor: 'orange_3' },
	'attribute': { fgcolor: 'orange_3' },

	// Text markup
	'bullet': { fgcolor: 'orange_4', weight: 'bold' },
	'code': { fgcolor: 'violet_2' },
	'emphasis': { style: 'italic' },
	'strong': { weight: 'bold' },
	'formula': {},
	'link': { underline: 'single' },
	'quote': { fgcolor: 'dark_1' },

	// Css
	'selector-tag': { fgcolor: 'teal_3' },
	'selector-id': { fgcolor: 'teal_3', weight: 'bold' },
	'selector-class': { fgcolor: 'chameleon_3' },
	'selector-attr': { fgcolor: 'teal_3' },
	'selector-pseudo': { fgcolor: 'violet_2', weight: 'bold' },

	// Templates
	'template-tag': { fgcolor: 'violet_2' },
	'template-variable': { fgcolor: 'violet_2' },

	// Diff
	'addition': { fgcolor: 'teal_3' },
	'deletion': { fgcolor: 'red_1' },
};
const Light = {
	// General purpose
	'keyword': { fgcolor: 'orange_5', weight: 'bold' },
	'built_in': { fgcolor: 'teal_5', weight: 'bold' },
	'type': { fgcolor: 'teal_5', weight: 'bold' },
	'literal': { fgcolor: 'violet_4' },
	'number': { fgcolor: 'violet_4' },
	'operator': {},
	'punctuation': {},
	'property': { fgcolor: 'orange_5', weight: 'bold' },
	'regexp': { fgcolor: 'teal_5' },
	'string': { fgcolor: 'teal_5' },
	'char.escape_': { fgcolor: 'red_2' },
	'subst': { fgcolor: 'orange_5' },
	'symbol': {},
	'class': {},
	'function': {},
	'variable': { fgcolor: 'teal_5', weight: 'bold' },
	'variable.language_': { fgcolor: 'orange_5', weight: 'bold' },
	'variable.constant_': {},
	'title': {},
	'title.class_': {},
	'title.class_.inherited__': {},
	'title.function_': {},
	'title.function_.invoke__': {},
	'params': {},
	'comment': { fgcolor: 'dark_1' },
	'doctag': { fgcolor: 'dark_3' },

	// Meta
	'meta': { fgcolor: 'violet_4' }, // orange_3
	'meta.prompt': { fgcolor: 'yellow_6' },

	// Tags, attributes, configs
	'section': { fgcolor: 'teal_5', weight: 'bold' },
	'tag': { fgcolor: 'teal_5' },
	'name': { fgcolor: 'teal_5' },
	'attr': { fgcolor: 'orange_5' },
	'attribute': { fgcolor: 'orange_5' },

	// Text markup
	'bullet': { fgcolor: 'orange_5', weight: 'bold' },
	'code': { fgcolor: 'violet_4' },
	'emphasis': { style: 'italic' },
	'strong': { weight: 'bold' },
	'formula': {},
	'link': { underline: 'single' },
	'quote': { fgcolor: 'dark_1' },

	// Css
	'selector-tag': { fgcolor: 'teal_5' },
	'selector-id': { fgcolor: 'teal_5', weight: 'bold' },
	'selector-class': { fgcolor: 'chameleon_3' },
	'selector-attr': { fgcolor: 'teal_5' },
	'selector-pseudo': { fgcolor: 'violet_4', weight: 'bold' },

	// Templates
	'template-tag': { fgcolor: 'violet_4' },
	'template-variable': { fgcolor: 'violet_4' },

	// Diff
	'addition': { fgcolor: 'teal_4' },
	'deletion': { fgcolor: 'red_1' },
};

/**
 * Applies a theme to highlight.js output
 * @param colorScheme the color scheme
 * @param highlighted the highlighted highlight.js output
 */
export function applyTheme(colorScheme, highlighted) {
	const theme = colorScheme === ColorScheme.Light ? Light : Dark;
	return highlighted.replace(/class="([^"]*)"/gm, (_, classes) => {
		if (!classes.startsWith('hljs-')) return '';
		const id = classes.substring(5).replace(' ', '.');
		const style = theme[id];
		if (style) {
			return Object.entries(style)
				.map(([key, value]) => {
					value = Colors[value] ?? value;
					return `${key}="${value}"`;
				})
				.join(' ');
		}
		return '';
	});
}

let CodeLabel = class CodeLabel extends St.Label {
	ext;
	_colorSchemeChangedId = -1;
	_code = '';
	_language = null;
	_tabWidth = 4;
	_syntaxHighlighting = true;
	_showLineNumbers = true;
	_highlighted = '';

	constructor(ext, props) {
		super({ ...props, min_height: 0, clip_to_allocation: true });
		this.ext = ext;
		this.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
		this.clutter_text.ellipsize = Pango.EllipsizeMode.END;
		const params = {
			...{ code: '', language: '', syntaxHighlighting: true, showLineNumbers: true, tabWidth: 4 },
			...props,
		};
		this._code = params.code;
		this._language = params.language;
		this._syntaxHighlighting = params.syntaxHighlighting;
		this._showLineNumbers = params.showLineNumbers;
		this._tabWidth = params.tabWidth;

		// Update text when global color scheme changes
		if (this.ext.themeManager) {
			this._colorSchemeChangedId = this.ext.themeManager?.connect(
				'notify::color-scheme',
				this.updateText.bind(this),
			);
		}

		// Update text after hljs is loaded
		this.ext.connectHljsInit(this.updateText.bind(this));
		this.updateText();
	}

	destroy() {
		if (this._colorSchemeChangedId >= 0) {
			this.ext.themeManager?.disconnect(this._colorSchemeChangedId);
		}
		super.destroy();
	}

	get code() {
		return this._code;
	}

	set code(code) {
		if (this._code === code) return;
		this._code = code;
		this.updateText();
		this.notify('code');
	}

	get language() {
		return this._language;
	}

	set language(language) {
		if (this._language === language) return;
		this._language = language;
		this.updateText();
		this.notify('language');
	}

	get syntaxHighlighting() {
		return this._syntaxHighlighting;
	}

	set syntaxHighlighting(syntaxHighlighting) {
		if (this._syntaxHighlighting === syntaxHighlighting) return;
		this._syntaxHighlighting = syntaxHighlighting;
		this.updateText();
		this.notify('syntax-highlighting');
	}

	get showLineNumbers() {
		return this._showLineNumbers;
	}

	set showLineNumbers(showLineNumbers) {
		if (this._showLineNumbers === showLineNumbers) return;
		this._showLineNumbers = showLineNumbers;
		this.updateLabel();
		this.notify('show-line-numbers');
	}

	get tabWidth() {
		return this._tabWidth;
	}

	set tabWidth(tabWidth) {
		if (this._tabWidth === tabWidth) return;
		this._tabWidth = tabWidth;
		this.updateText();
		this.notify('tab-width');
	}

	updateText() {
		if (this._code == null) return;

		// Trim indentation before highlighting to prevent empty lines
		let text = normalizeIndentation(trim(this._code), this.tabWidth);
		if (this.syntaxHighlighting && this.ext.hljs != null) {
			const language =
				this.language && this.ext.hljs.getLanguage(this.language.id) != null ? this.language.id : null;
			const result = language ? this.ext.hljs.highlight(text, { language }) : this.ext.hljs.highlightAuto(text);
			text = applyTheme(this.ext.themeManager?.colorScheme, result.value);

			// Store language
			if (!language && result.language) {
				const id = result.language;
				const name = this.ext.hljs.getLanguage(id)?.name ?? id;
				this._language = { id, name: id.length < name.length - 3 ? id.charAt(0) + id.slice(1) : name };
				this.notify('language');
			}
		} else {
			text = GLib.markup_escape_text(text, text.length);
		}
		this._highlighted = text;
		this.updateLabel();
	}

	updateLabel() {
		let text = this._highlighted;
		const lines = this._highlighted.split('\n');
		this.clutter_text.line_wrap = lines.length === 1;
		if (this.showLineNumbers && lines.length > 1) {
			// Add line numbers
			const color = this.ext.themeManager?.colorScheme === ColorScheme.Light ? Colors.dark_7 : Colors.light_1;
			const span = `<span color="${color}" alpha="50%">`;
			text = lines.map((l, i) => `${span}${i.toString().padEnd(2, ' ')}</span> ${l}`).join('\n');
		}

		// Add blank line to fix the first span not being styled
		this.clutter_text.set_markup('\n' + text);
	}

	vfunc_allocate(box) {
		this.set_allocation(box);
		const themeNode = this.get_theme_node();
		const contentBox = themeNode.get_content_box(box);
		const scale = this.get_resource_scale();

		// Shift one line up to account for extra blank line
		const layout = this.clutter_text.get_layout();
		const line = layout.get_line_readonly(0);
		const offset = line ? line.get_height() / Pango.SCALE / scale : 0;
		contentBox.y1 -= offset;

		// Fit label to content box without partial clipping
		let y = contentBox.y1;
		for (const l of layout.get_lines_readonly()) {
			const [, extents] = l.get_extents();
			const height = (extents?.height ?? 0) / Pango.SCALE / scale;
			if (y + height <= contentBox.y2) {
				y += height;
			} else {
				contentBox.y2 = Math.ceil(y);
				break;
			}
		}
		this.clutter_text.allocate(contentBox);
	}
};
CodeLabel = __decorate(
	[
		registerClass({
			Properties: {
				'code': GObject.ParamSpec.string('code', null, null, GObject.ParamFlags.READWRITE, ''),
				'language': GObject.ParamSpec.jsobject('language', null, null, GObject.ParamFlags.READWRITE),
				'syntax-highlighting': GObject.ParamSpec.boolean(
					'syntax-highlighting',
					null,
					null,
					GObject.ParamFlags.READWRITE,
					true,
				),
				'show-line-numbers': GObject.ParamSpec.boolean(
					'show-line-numbers',
					null,
					null,
					GObject.ParamFlags.READWRITE,
					true,
				),
				'tab-width': GObject.ParamSpec.int('tab-width', null, null, GObject.ParamFlags.READWRITE, 1, 8, 4),
			},
		}),
	],
	CodeLabel,
);

export { CodeLabel };
