// Internal Imports
import { Component } from '../component';
import { DOMUtils } from '../../services';
import {
	Roles,
	Events,
	GaugeTypes,
	ArrowDirections,
	ColorClassNameTypes,
	Alignments,
} from '../../interfaces';
import { Tools } from '../../tools';

// D3 Imports
import { select } from 'd3-selection';
import { arc } from 'd3-shape';

// arrow paths for delta
const ARROW_UP_PATH_STRING = '4,10 8,6 12,10';
const ARROW_DOWN_PATH_STRING = '12,6 8,10 4,6';

export class Gauge extends Component {
	type = 'gauge';

	// We need to store our arcs so that addEventListeners() can access them
	arc: any;
	backgroundArc: any;

	init() {
		const eventsFragment = this.services.events;
	}

	getValue(): number {
		const data = this.model.getData();
		const value = data.find((d) => d.group === 'value')?.value ?? null;
		return value;
	}

	getValueRatio(): number {
		const value = Tools.clamp(this.getValue(), 0, 100);
		return value / 100;
	}

	getDelta(): number {
		const data = this.model.getData();
		const delta = data.find((d) => d.group === 'delta')?.value ?? null;
		return delta;
	}

	getArcRatio(): number {
		const options = this.getOptions();
		const type = Tools.getProperty(options, 'gauge', 'type');
		const arcRatio = type === GaugeTypes.FULL ? 1 : 0.5;
		return arcRatio;
	}

	getArcSize(): number {
		return this.getArcRatio() * Math.PI * 2;
	}

	getStartAngle(): number {
		const arcSize = this.getArcSize();
		if (arcSize === 2 * Math.PI) {
			return 0;
		}
		return -arcSize / 2;
	}

	// use provided arrow direction or default to using the delta
	getArrow(delta): string {
		const options = this.getOptions();
		const arrowDirection = Tools.getProperty(
			options,
			'gauge',
			'deltaArrow',
			'direction'
		);

		switch (arrowDirection) {
			case ArrowDirections.UP:
				return ARROW_UP_PATH_STRING;
			case ArrowDirections.DOWN:
				return ARROW_DOWN_PATH_STRING;
			default:
				return delta > 0
					? ARROW_UP_PATH_STRING
					: ARROW_DOWN_PATH_STRING;
		}
	}

	render(animate = true) {
		const self = this;
		const svg = this.getContainerSVG();
		const options = this.getOptions();
		const { groupMapsTo } = options.data;

		const value = this.getValue();
		const valueRatio = this.getValueRatio();
		const arcSize = this.getArcSize();

		// angles for drawing the gauge
		const startAngle = this.getStartAngle();
		const rotationAngle = valueRatio * arcSize;
		const currentAngle = startAngle + rotationAngle;
		const endAngle = startAngle + arcSize;

		// Compute the outer radius needed
		const radius = this.computeRadius();
		const innerRadius = this.getInnerRadius();

		// draw the container and arc
		this.backgroundArc = arc()
			.innerRadius(innerRadius)
			.outerRadius(radius)
			.startAngle(currentAngle)
			.endAngle(endAngle);

		this.arc = arc()
			.innerRadius(innerRadius)
			.outerRadius(radius)
			.startAngle(startAngle)
			.endAngle(currentAngle);

		// draw the container
		DOMUtils.appendOrSelect(svg, 'path.arc-background')
			.attr('d', this.backgroundArc)
			.attr('role', Roles.GROUP);

		// Add data arc
		const arcValue = svg.selectAll('path.arc-foreground').data([value]);
		const arcEnter = arcValue.enter().append('path');

		arcEnter
			.merge(arcValue)
			.attr(
				'class',
				this.model.getColorClassName({
					classNameTypes: [ColorClassNameTypes.FILL],
					dataGroupName: 'value',
					originalClassName: 'arc-foreground',
				})
			)
			.style('fill', (d) =>
				Tools.getProperty(this.getOptions(), 'color', 'scale', 'value')
			)
			.attr('d', this.arc)
			// a11y
			.attr('role', Roles.GRAPHICS_SYMBOL)
			.attr('aria-roledescription', 'value')
			.attr('aria-label', (d) => d.value);

		// draw the value and delta to the center
		this.drawValueNumber();
		this.drawDelta();

		arcValue.exit().remove();

		const alignment = Tools.getProperty(options, 'gauge', 'alignment');

		const { width } = DOMUtils.getSVGElementSize(this.getParent(), {
			useAttr: true,
		});

		// Position gauge
		let gaugeTranslateX = radius;
		if (alignment === Alignments.CENTER) {
			gaugeTranslateX = width / 2;
		} else if (alignment === Alignments.RIGHT) {
			gaugeTranslateX = width - radius;
		}
		svg.attr('transform', `translate(${gaugeTranslateX}, ${radius})`);

		// Add event listeners
		this.addEventListeners();
	}

	/**
	 * draws the value number associated with the Gauge component in the center
	 */
	drawValueNumber() {
		const svg = this.getContainerSVG();
		const options = this.getOptions();

		const arcType = Tools.getProperty(options, 'gauge', 'type');
		const value = this.getValue();
		const delta = this.getDelta();

		// Sizing and positions relative to the radius
		const radius = this.computeRadius();

		const valueFontSize = Tools.getProperty(
			options,
			'gauge',
			'valueFontSize'
		);
		// if there is a delta, use the size to center the numbers, otherwise center the valueNumber
		const deltaFontSize = Tools.getProperty(
			options,
			'gauge',
			'deltaFontSize'
		);

		const numberSpacing = Tools.getProperty(
			options,
			'gauge',
			'numberSpacing'
		);

		// circular gauge without delta should have valueNumber centered
		let numbersYPosition = 0;
		if (arcType === GaugeTypes.FULL && !delta) {
			numbersYPosition = deltaFontSize(radius);
		} else if (arcType === GaugeTypes.SEMI && delta) {
			// semi circular gauge we want the numbers aligned to the chart container
			numbersYPosition = -(deltaFontSize(radius) + numberSpacing);
		}

		// Add the numbers at the center
		const numbersGroup = DOMUtils.appendOrSelect(
			svg,
			'g.gauge-numbers'
		).attr('transform', `translate(0, ${numbersYPosition})`);

		// Add the big number
		const valueNumberGroup = DOMUtils.appendOrSelect(
			numbersGroup,
			'g.gauge-value-number'
		).attr('transform', 'translate(-10, 0)'); // Optical centering for the presence of the smaller % symbol

		const numberFormatter = Tools.getProperty(
			options,
			'gauge',
			'numberFormatter'
		);
		const valueNumber = valueNumberGroup
			.selectAll('text.gauge-value-number')
			.data([value]);

		valueNumber
			.enter()
			.append('text')
			.attr('class', 'gauge-value-number')
			.merge(valueNumber)
			.style('font-size', `${valueFontSize(radius)}px`)
			.attr('text-anchor', 'middle')
			.text((d) => numberFormatter(d));

		// add the percentage symbol beside the valueNumber
		const {
			width: valueNumberWidth,
		} = DOMUtils.getSVGElementSize(
			DOMUtils.appendOrSelect(svg, 'text.gauge-value-number'),
			{ useBBox: true }
		);

		DOMUtils.appendOrSelect(valueNumberGroup, 'text.gauge-value-symbol')
			.style('font-size', `${valueFontSize(radius) / 2}px`)
			.attr('x', valueNumberWidth / 2)
			.text('%');
	}

	/**
	 * adds the delta number for the gauge
	 */
	drawDelta() {
		const self = this;
		const svg = this.getContainerSVG();
		const options = this.getOptions();
		const delta = this.getDelta();

		// Sizing and positions relative to the radius
		const radius = this.computeRadius();
		const deltaFontSize = delta
			? Tools.getProperty(options, 'gauge', 'deltaFontSize')
			: () => 0;

		// use numberFormatter here only if there is a delta supplied
		const numberFormatter = delta
			? Tools.getProperty(options, 'gauge', 'numberFormatter')
			: () => null;

		const arrowSize = Tools.getProperty(
			options,
			'gauge',
			'deltaArrow',
			'size'
		);
		const numberSpacing = Tools.getProperty(
			options,
			'gauge',
			'numberSpacing'
		);

		const numbersGroup = DOMUtils.appendOrSelect(svg, 'g.gauge-numbers');

		// Add the smaller number of the delta
		const deltaGroup = DOMUtils.appendOrSelect(
			numbersGroup,
			'g.gauge-delta'
		).attr(
			'transform',
			`translate(0, ${deltaFontSize(radius) + numberSpacing})`
		);

		const deltaNumber = DOMUtils.appendOrSelect(
			deltaGroup,
			'text.gauge-delta-number'
		);

		deltaNumber.data(delta === null ? [] : [delta]);

		deltaNumber
			.enter()
			.append('text')
			.classed('gauge-delta-number', true)
			.merge(deltaNumber)
			.attr('text-anchor', 'middle')
			.style('font-size', `${deltaFontSize(radius)}px`)
			.text((d) => `${numberFormatter(d)}%`);

		// Add the caret for the delta number
		const {
			width: deltaNumberWidth,
		} = DOMUtils.getSVGElementSize(
			DOMUtils.appendOrSelect(svg, '.gauge-delta-number'),
			{ useBBox: true }
		);

		// check if delta arrow is disabled
		const arrowEnabled = Tools.getProperty(
			options,
			'gauge',
			'deltaArrow',
			'enabled'
		);
		const deltaArrow = deltaGroup
			.selectAll('svg.gauge-delta-arrow')
			.data(delta !== null && arrowEnabled ? [delta] : []);

		deltaArrow
			.enter()
			.append('svg')
			.merge(deltaArrow)
			.attr('class', 'gauge-delta-arrow')
			.attr('x', -arrowSize(radius) - deltaNumberWidth / 2)
			.attr('y', -arrowSize(radius) / 2 - deltaFontSize(radius) * 0.35)
			.attr('width', arrowSize(radius))
			.attr('height', arrowSize(radius))
			.attr('viewBox', '0 0 16 16');

		// Needed to correctly size SVG in Firefox
		DOMUtils.appendOrSelect(deltaArrow, 'rect.gauge-delta-arrow-backdrop')
			.attr('width', '16')
			.attr('height', '16')
			.attr('fill', 'none');

		// Draw the arrow with status
		const status = Tools.getProperty(options, 'gauge', 'status');
		DOMUtils.appendOrSelect(deltaArrow, 'polygon.gauge-delta-arrow')
			.attr(
				'class',
				status !== null ? `gauge-delta-arrow status--${status}` : ''
			)
			.attr('points', self.getArrow(delta));

		deltaArrow.exit().remove();
		deltaNumber.exit().remove();
	}

	getInnerRadius() {
		// Compute the outer radius needed
		const radius = this.computeRadius();
		const arcWidth = Tools.getProperty(
			this.getOptions(),
			'gauge',
			'arcWidth'
		);
		return radius - arcWidth;
	}

	addEventListeners() {
		const self = this;
		this.parent
			.selectAll('path.arc-foreground')
			.on('mouseover', function (datum) {
				// Dispatch mouse event
				self.services.events.dispatchEvent(Events.Gauge.ARC_MOUSEOVER, {
					element: select(this),
					datum,
				});
			})
			.on('mousemove', function (datum) {
				const hoveredElement = select(this);

				// Dispatch mouse event
				self.services.events.dispatchEvent(Events.Gauge.ARC_MOUSEMOVE, {
					element: hoveredElement,
					datum,
				});
			})
			.on('click', function (datum) {
				// Dispatch mouse event
				self.services.events.dispatchEvent(Events.Gauge.ARC_CLICK, {
					element: select(this),
					datum,
				});
			})
			.on('mouseout', function (datum) {
				const hoveredElement = select(this);

				// Dispatch mouse event
				self.services.events.dispatchEvent(Events.Gauge.ARC_MOUSEOUT, {
					element: hoveredElement,
					datum,
				});
			});
	}

	// Helper functions
	protected computeRadius() {
		const options = this.getOptions();
		const arcType = Tools.getProperty(options, 'gauge', 'type');

		const { width, height } = DOMUtils.getSVGElementSize(this.parent, {
			useAttrs: true,
		});
		const radius =
			arcType === GaugeTypes.SEMI
				? Math.min(width / 2, height)
				: Math.min(width / 2, height / 2);

		return radius;
	}
}
