/**
 * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
 * Node module: @schukai/monster
 *
 * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
 * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
 *
 * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
 * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
 * For more information about purchasing a commercial license, please contact Volker Schukai.
 */

import { instanceSymbol } from "../../constants.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import {
	ATTRIBUTE_ERRORMESSAGE,
	ATTRIBUTE_ROLE,
} from "../../dom/constants.mjs";
import {
	CustomElement,
	updaterTransformerMethodsSymbol,
} from "../../dom/customelement.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { findTargetElementFromEvent } from "../../dom/events.mjs";
import { isFunction, isString } from "../../types/is.mjs";
import { MetricGraphStyleSheet } from "./stylesheet/metric-graph.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { AccessibilityStyleSheet } from "../stylesheet/accessibility.mjs";

export { MetricGraph };

/**
 * @private
 * @type {symbol}
 */
export const metricGraphControlElementSymbol = Symbol(
	"metricGraphControlElement",
);

/**
 * A MetricGraph
 *
 * @fragments /fragments/components/data/metric-graph/
 *
 * @example /examples/components/data/metric-graph-simple
 *
 * @since 4.11.0
 * @copyright Volker Schukai
 * @summary A metric graph for showing trend direction and compact historical context around one value.
 */
class MetricGraph extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @returns {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/data/metric-graph@@instance");
	}

	/**
	 * @return {MetricGraph}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		initControlReferences.call(this);
		return this;
	}

	/**
	 * To set the options via the HTML Tag, the attribute `data-monster-options` must be used.
	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
	 *
	 * The individual configuration values can be found in the table.
	 *
	 * @property {Object} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {string} curve The curve of the graph (step, smooth, bubble, bar, area, dot, lollipop, line)
	 * @property {Object} values Value definitions
	 * @property {number} values.value The value of the metric
	 * @property {number} values.change The change of the metric
	 * @property {number} values.secondary The secondary value of the metric
	 * @property {Array} values.points The points of the metric
	 * @property {Object} labels Label definitions
	 * @property {string} labels.title Title of the metric
	 * @property {string} labels.subtext Subtext of the metric
	 * @property {Object} classes CSS classes
	 * @property {string} classes.dot CSS class for the dot
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},

			graphType: "linear",

			values: {
				main: null,
				change: null,
				secondary: null,
				points: [2, 2, 2, -30, 30, 5, 4, 4, 3, 3, 2, 2, 1, 1, 1, 1],
			},
			labels: {
				title: null,
				subtext: null,
			},
			classes: {
				dot: "monster-theme-primary-1",
			},

			aria: {
				description: null,
			},
		});
	}

	/**
	 *
	 * @returns {{tosparkline: ((function(*): (string|string))|*)}}
	 */
	[updaterTransformerMethodsSymbol]() {
		return {
			toGraph: (value) => {
				if (isString(value)) {
					value = value.split(",").map((v) => {
						return parseFloat(v);
					});
				}

				const graphType = this.getOption("graphType");

				if (!Array.isArray(value) || value.length === 0) return "";
				switch (graphType.toLowerCase()) {
					case "step":
						return renderStepGraph.call(this, value);
					case "smooth":
						return renderSmoothGraph.call(this, value);
					case "bubble":
						return renderBubbleGraph.call(this, value);
					case "bar":
						return renderBarGraph.call(this, value);
					case "area":
						return renderAreaGraph.call(this, value);
					case "dot":
						return renderDotGraph.call(this, value);
					case "lollipop":
						return renderLollipopGraph.call(this, value);
					case "line":
					default:
						return renderLineGraph.call(this, value);
				}
			},
		};
	}

	/**
	 * @return {string}
	 */
	static getTag() {
		return "monster-metric-graph";
	}

	/**
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [MetricGraphStyleSheet, AccessibilityStyleSheet];
	}
}

function renderAreaGraph(values, options = {}) {
	const {
		width = 100,
		height = 30,
		stroke = "currentColor",
		strokeWidth = 2,
		fill = "rgba(0, 0, 0, 0.1)",
	} = options;

	if (!Array.isArray(values) || values.length === 0) return "";

	const min = Math.min(...values);
	const max = Math.max(...values);
	const range = max - min || 1;

	const step = width / (values.length - 1);
	const points = values.map((v, i) => {
		const x = i * step;
		const y = height - ((v - min) / range) * height;
		return { x, y };
	});

	let d = `M ${points[0].x},${height} L ${points[0].x},${points[0].y}`;
	for (let i = 1; i < points.length; i++) {
		d += ` L ${points[i].x},${points[i].y}`;
	}
	d += ` L ${points[points.length - 1].x},${height} Z`;

	return `<path d="${d}" stroke="${stroke}" stroke-width="${strokeWidth}" fill="${fill}" stroke-linejoin="round" />`;
}

function renderBubbleGraph(values, options = {}) {
	const {
		width = 100,
		height = 30,
		minRadius = 2,
		maxRadius = 8,
		lightRange = [0.3, 1.0], // fill-opacity von 0.3 bis 1.0
		align = "middle",
	} = options;

	if (!Array.isArray(values) || values.length === 0) return "";

	const min = Math.min(...values);
	const max = Math.max(...values);
	const range = max - min || 1;

	const stepX = width / values.length;

	let centerY;
	switch (align) {
		case "top":
			centerY = maxRadius;
			break;
		case "bottom":
			centerY = height - maxRadius;
			break;
		case "middle":
		default:
			centerY = height / 2;
	}

	return values
		.map((v, i) => {
			const x = i * stepX + stepX / 2;
			const norm = (v - min) / range;
			const r = minRadius + norm * (maxRadius - minRadius);
			const opacity = lightRange[0] + norm * (lightRange[1] - lightRange[0]);

			return `<circle cx="${x.toFixed(2)}" cy="${centerY.toFixed(2)}" r="${r.toFixed(2)}" fill="currentColor" fill-opacity="${opacity.toFixed(2)}" />`;
		})
		.join("");
}

function renderLollipopGraph(values, options = {}) {
	const {
		width = 100,
		height = 30,
		color = "currentColor",
		radius = 2,
		strokeWidth = 1,
	} = options;

	if (!Array.isArray(values) || values.length === 0) return "";

	const min = Math.min(...values);
	const max = Math.max(...values);
	const range = max - min || 1;

	const step = width / (values.length - 1);

	return values
		.map((v, i) => {
			const x = i * step;
			const y = height - ((v - min) / range) * height;
			const line = `<line x1="${x}" y1="${height}" x2="${x}" y2="${y}" stroke="${color}" stroke-width="${strokeWidth}" />`;
			const circle = `<circle cx="${x}" cy="${y}" r="${radius}" fill="${color}" />`;
			return line + circle;
		})
		.join("");
}

function renderDotGraph(values, options = {}) {
	const {
		width = 100,
		height = 30,
		radius = 2,
		color = "currentColor",
	} = options;

	if (!Array.isArray(values) || values.length === 0) return "";

	const min = Math.min(...values);
	const max = Math.max(...values);
	const range = max - min || 1;

	const step = width / (values.length - 1);

	return values
		.map((v, i) => {
			const x = i * step;
			const y = height - ((v - min) / range) * height;
			return `<circle cx="${x.toFixed(2)}" cy="${y.toFixed(2)}" r="${radius}" fill="${color}" />`;
		})
		.join("");
}

function renderBarGraph(values, options = {}) {
	const {
		width = 100,
		height = 30,
		barColor = "currentColor",
		barSpacing = 1,
	} = options;

	if (!Array.isArray(values) || values.length === 0) return "";

	const min = Math.min(...values);
	const max = Math.max(...values);
	const range = max - min || 1;

	const barWidth = width / values.length - barSpacing;

	return values
		.map((v, i) => {
			const x = i * (barWidth + barSpacing);
			const barHeight = ((v - min) / range) * height;
			const y = height - barHeight;
			return `<rect x="${x.toFixed(2)}" y="${y.toFixed(2)}" width="${barWidth.toFixed(2)}" height="${barHeight.toFixed(2)}" fill="${barColor}" />`;
		})
		.join("");
}

function renderLineGraph(values) {
	if (!Array.isArray(values) || values.length === 0) return "";
	const min = Math.min(...values);
	const max = Math.max(...values);
	const range = max - min || 1;

	const step = 100 / (values.length - 1);
	const points = values.map((v, i) => {
		const x = i * step;
		const y = 30 - ((v - min) / range) * 30;
		return `${x},${y}`;
	});

	return `<polyline points="${points.join(" ")}" stroke="currentColor" stroke-width="2" fill="none" />`;
}

function renderSmoothGraph(values, options = {}) {
	const {
		width = 100,
		height = 30,
		stroke = "currentColor",
		strokeWidth = 2,
		fill = "none",
	} = options;

	if (!Array.isArray(values) || values.length === 0) return "";

	const min = Math.min(...values);
	const max = Math.max(...values);
	const range = max - min || 1;

	const stepX = width / (values.length - 1);

	const points = values.map((v, i) => {
		const x = i * stepX;
		const y = height - ((v - min) / range) * height;
		return { x, y };
	});

	// Bézier-Path erzeugen
	let d = `M ${points[0].x},${points[0].y}`;
	for (let i = 1; i < points.length; i++) {
		const prev = points[i - 1];
		const curr = points[i];
		const cx = (prev.x + curr.x) / 2;
		d += ` Q ${prev.x},${prev.y} ${cx},${(prev.y + curr.y) / 2}`;
	}
	d += ` T ${points[points.length - 1].x},${points[points.length - 1].y}`;

	return `<path d="${d}" stroke="${stroke}" stroke-width="${strokeWidth}" fill="${fill}" stroke-linecap="round" stroke-linejoin="round" />`;
}

function renderStepGraph(values, options = {}) {
	const {
		width = 100,
		height = 30,
		stroke = "currentColor",
		strokeWidth = 2,
		fill = "none",
	} = options;

	if (!Array.isArray(values) || values.length === 0) return "";

	const min = Math.min(...values);
	const max = Math.max(...values);
	const range = max - min || 1;

	const stepX = width / (values.length - 1);

	const points = values.map((v, i) => {
		const x = i * stepX;
		const y = height - ((v - min) / range) * height;
		return { x, y };
	});

	let d = `M ${points[0].x},${points[0].y}`;
	for (let i = 1; i < points.length; i++) {
		const prev = points[i - 1];
		const curr = points[i];
		d += ` H ${curr.x} V ${curr.y}`;
	}

	return `<path d="${d}" stroke="${stroke}" stroke-width="${strokeWidth}" fill="${fill}" stroke-linecap="round" stroke-linejoin="round" />`;
}

/**
 * @private
 * @return {void}
 */
function initControlReferences() {
	this[metricGraphControlElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="control"]`,
	);
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control"
             part="control"
             role="group"
             aria-labelledby="metric-title"
             aria-describedby="metric-value metric-subtext metric-graph-desc">

            <div class="metric-card" part="card">
                <div class="metric-header" part="header">
                    <span data-monster-attributes="class path:classes.dot | prefix:metric-icon\\ :"></span>
                    <span id="metric-title" class="metric-title"
                          data-monster-replace="path:labels.title | ??:—"></span>
                </div>

                <div id="metric-value" class="metric-value"
                     data-monster-replace="path:values.main"
                     part="metric-value"
                     aria-live="polite">—</div>

                <div id="metric-subtext" class="metric-subtext" part="metric-subtext">
                    <span data-monster-replace="path:labels.subtext | ??:— ">—</span><br>
                    <span class="metric-subtext-value">
						<strong data-monster-replace="path:values.secondary | ??:—">—</strong>
					</span>
                </div>

                <div part="metric-graph" class="metric-graph">
                    <svg viewBox="0 0 100 30"
                         preserveAspectRatio="none"
                         role="img"
                         aria-labelledby="metric-graph-desc"
                         focusable="false"
                         xmlns="http://www.w3.org/2000/svg"
                         data-monster-replace="path:values.points | call:toGraph">
                    </svg>
                    <span id="metric-graph-desc" class="visually-hidden"
                          data-monster-replace="path:aria.graph | ??:Graphische Darstellung der Kennzahl">
						Graphische Darstellung
					</span>
                </div>
            </div>

            <span class="visually-hidden" data-monster-replace="path:aria.description"></span>
        </div>`;
}

registerCustomElement(MetricGraph);
