/*
** Copyright (C) 2001-2026 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/


const BAR_GAUGE_BAR_DEFAULT_COLOR = '#000000';
const BAR_GAUGE_BAR_DEFAULT_MIN = 0;
const BAR_GAUGE_BAR_DEFAULT_MAX = 100;
const BAR_GAUGE_BAR_ITEM_WIDTH = 7;

class ZBarGauge extends HTMLElement {

	constructor() {
		super();

		this._refresh_frame = null;

		this._fill = BAR_GAUGE_BAR_DEFAULT_COLOR;
		this._solid = false;
		this._min = 0;
		this._max = 100;
		this._value = 0;

		const shadow = this.attachShadow({mode: 'open'});

		this._canvas = document.createElement('canvas');
		this._canvas.setAttribute('part', 'bar');
		shadow.appendChild(this._canvas);

		this.registerEvents();
	}

	connectedCallback() {
		setTimeout(() => {
			this._events.update();
		});
	}

	disconnectedCallback() {
		this.unregisterEvents();
	}

	static get observedAttributes() {
		return ['fill', 'max', 'min', 'solid', 'value', 'width'];
	}

	attributeChangedCallback(name, old_value, new_value) {
		if (old_value === new_value) {
			return;
		}

		switch (name) {
			case 'fill':
				this._fill = (new_value !== null && /^#([0-9A-F]{6})$/i.test(new_value))
					? new_value
					: BAR_GAUGE_BAR_DEFAULT_COLOR;

				return this._events.update();

			case 'max':
				this._max = new_value !== null && !isNaN(new_value) ? Number(new_value) : BAR_GAUGE_BAR_DEFAULT_MAX;
				break

			case 'min':
				this._min = new_value !== null && !isNaN(new_value) ? Number(new_value) : BAR_GAUGE_BAR_DEFAULT_MIN;
				break;

			case 'solid':
				this._solid = new_value !== null;
				break;

			case 'value':
				if (new_value !== null) {
					this._value = Number(new_value);

					this.dispatchEvent(new Event('change', {bubbles: true}));
				}
				break;

			case 'width':
				if (new_value === null) {
					this.style.width = '';
				}
				else if (!isNaN(new_value)) {
					this.style.width = `${new_value}px`;
				}
				else {
					this.style.width = new_value;
				}
				break;

			default:
				return;
		}

		this._refresh();
	}

	get max() {
		return this.getAttribute('max');
	}

	set max(max) {
		this.setAttribute('max', max);
	}

	get min() {
		return this.getAttribute('min');
	}

	set min(min) {
		this.setAttribute('min', min);
	}

	get value() {
		return this.getAttribute('value');
	}

	set value(value) {
		this.setAttribute('value', value);
	}

	get width() {
		return this.getAttribute('width');
	}

	set width(width) {
		this.setAttribute('width', width);
	}

	addThreshold(value, fill) {
		const threshold = document.createElement('threshold');
		threshold.setAttribute('value', value);
		threshold.setAttribute('fill', fill);

		this.appendChild(threshold);
	}

	_refresh() {
		if (this._refresh_frame !== null || this._thresholds === undefined) {
			return;
		}

		this._refresh_frame = window.requestAnimationFrame(() => {
			this._refresh_frame = null;

			const ctx = this._canvas.getContext("2d");

			const width = this.offsetWidth - 2;
			this._canvas.height = this.offsetHeight - 2;

			const value = Math.max(this._min, Math.min(this._max, this._value));

			if (this._solid) {
				const bar_size = value > this._min
					? Math.max(Math.floor(width / (this._max - this._min) * (value - this._min)), 2)
					: 0;

				this._canvas.width = width + 2;

				this._drawCell(ctx, 1, bar_size, this._getThresholdColorByValue(this._value), 1);
			}
			else {
				const cell_count = Math.floor(width / BAR_GAUGE_BAR_ITEM_WIDTH);
				const cell_interval = (this._max - this._min) / cell_count;

				this._canvas.width = cell_count * BAR_GAUGE_BAR_ITEM_WIDTH + 1;

				for (let i = 0; i < cell_count; i++) {
					const alpha = (value - this._min) / cell_interval > i ? 1 : .25;

					this._drawCell(ctx, i * BAR_GAUGE_BAR_ITEM_WIDTH + 1, BAR_GAUGE_BAR_ITEM_WIDTH - 1,
						this._getThresholdColorByValue(i * cell_interval + this._min), alpha
					);
				}
			}
		});
	}

	_drawCell(ctx, x, width, color, alpha) {
		const rgb = this._hexToRgb(color);
		const rgb_lighten = this._colorLightenDarken(rgb, .5);
		const rgb_darken = this._colorLightenDarken(rgb, -.3);
		const fill = ctx.createLinearGradient(x, 1, x, this._canvas.height - 2);

		fill.addColorStop(0, `rgba(${rgb_lighten.r}, ${rgb_lighten.g}, ${rgb_lighten.b}, ${alpha}`);
		fill.addColorStop(.3, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha}`);
		fill.addColorStop(.8, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha}`);
		fill.addColorStop(1, `rgba(${rgb_darken.r}, ${rgb_darken.g}, ${rgb_darken.b}, ${alpha}`);

		ctx.fillStyle = fill;

		this._roundRect(ctx, x, 1, width, this._canvas.height - 2, 3);
		ctx.fill();
	}

	_roundRect(ctx, x, y, width, height, radius) {
		radius = Math.min(width / 2, radius);

		ctx.beginPath();
		ctx.moveTo(x + radius, y);
		ctx.arcTo(x + width, y, x + width, y + height, radius);
		ctx.arcTo(x + width, y + height, x, y + height, radius);
		ctx.arcTo(x, y + height, x, y, radius);
		ctx.arcTo(x, y, x + width, y, radius);
		ctx.closePath();
	}

	_getThresholdColorByValue(value) {
		let color = this._fill;

		for (const threshold of Object.keys(this._thresholds).sort((a, b) => (a - b))) {
			if (threshold <= value) {
				color = this._thresholds[threshold];
			}
		}

		return color;
	}

	_hexToRgb(hex) {
		const rgb = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);

		return {
			r: rgb ? parseInt(rgb[1], 16) : 0,
			g: rgb ? parseInt(rgb[2], 16) : 0,
			b: rgb ? parseInt(rgb[3], 16) : 0
		};
	}

	_colorLightenDarken(rgb, amount) {
		return {
			r: Math.max(0, Math.min(255, rgb.r + amount * (amount > 0 ? 255 - rgb.r : rgb.r))),
			g: Math.max(0, Math.min(255, rgb.g + amount * (amount > 0 ? 255 - rgb.g : rgb.g))),
			b: Math.max(0, Math.min(255, rgb.b + amount * (amount > 0 ? 255 - rgb.b : rgb.b)))
		};
	}

	registerEvents() {
		this._events = {
			resize: () => {
				this._refresh();
			},

			update: () => {
				this._thresholds = {};

				for (const threshold of this.querySelectorAll('threshold')) {
					if (threshold.hasAttribute('fill') && threshold.hasAttribute('value')) {
						this._thresholds[threshold.getAttribute('value')] = threshold.getAttribute('fill');
					}
				}

				this._refresh();
			}
		}

		this._events_data = {
			resize_observer: new ResizeObserver(this._events.resize),
			mutation_observer: new MutationObserver(this._events.update)
		}

		this._events_data.resize_observer.observe(this);
		this._events_data.mutation_observer.observe(this, {childList: true});
	}

	unregisterEvents() {
		this._events_data.resize_observer.disconnect();
		this._events_data.mutation_observer.disconnect();

		cancelAnimationFrame(this._refresh_frame);
	}
}

customElements.define('z-bar-gauge', ZBarGauge);
