/**
 * Copyright (c) 2017 ~ present NAVER Corp.
 * billboard.js project is licensed under the MIT license
 */
import {interpolate as d3Interpolate} from "d3-interpolate";
import {select as d3Select} from "d3-selection";
import {arc as d3Arc, pie as d3Pie} from "d3-shape";
import type {d3Selection} from "../../../types/types";
import {$ARC, $COMMON, $FOCUS, $GAUGE} from "../../config/classes";
import {document} from "../../module/browser";
import {
	callFn,
	endall,
	isDefined,
	isFunction,
	isNumber,
	isObject,
	isUndefined,
	setTextValue,
	tplProcess
} from "../../module/util";
import type {IArcData, IArcDataRow, IData} from "../data/IData";
import {isLabelWithLine, redrawArcLabelLines} from "../internals/text.arc";
import {meetsLabelThreshold, updateTextImage, updateTextImagePos} from "../internals/text.util";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ChartInternalThis = any;

/**
 * Get the first matching arc chart type
 * @param {ChartInternalThis} $$ ChartInternal context
 * @returns {string|undefined} Chart type or undefined
 * @private
 */
function _getArcType($$: ChartInternalThis): string | undefined {
	return ["donut", "pie", "polar", "gauge"].find(type => $$.hasType(type));
}

/**
 * Calculate position for range text or multi-arc gauge labels
 * @param {ChartInternalThis} $$ ChartInternal context
 * @param {IArcData} d Data object
 * @param {IArcData} updated Updated angle data
 * @param {boolean} forRange Whether is for ranged text option
 * @returns {object} Position object {x, y}
 * @private
 */
function _calculateRangeOrGaugePosition(
	$$: ChartInternalThis,
	d: IArcData,
	updated: IArcData,
	forRange: boolean
): {x: number, y: number} {
	const {config, state: {radiusExpanded}} = $$;
	const angle = updated.endAngle - Math.PI / 2;
	const sinAngle = Math.sin(angle);
	const pos = {
		x: Math.cos(angle) * (radiusExpanded + (forRange ? 5 : 25)), // 5: range offset, 25: gauge offset
		y: sinAngle * (radiusExpanded + 15 - Math.abs(sinAngle * 10)) + 3 // 10: y factor, 3: y offset
	};

	if (forRange) {
		const rangeTextPosition = config.arc_rangeText_position;

		if (rangeTextPosition) {
			const rangeValues = config.arc_rangeText_values;
			const position = isFunction(rangeTextPosition) ?
				rangeTextPosition(rangeValues[d.index]) :
				rangeTextPosition;

			pos.x += position?.x ?? 0;
			pos.y += position?.y ?? 0;
		}
	}

	return pos;
}

/**
 * Calculate label ratio for standard arc types
 * @param {ChartInternalThis} $$ ChartInternal context
 * @param {IArcData} d Data object
 * @param {number} outerRadius Outer radius value
 * @param {number} distance Distance from center
 * @returns {number} Calculated ratio
 * @private
 */
function _calculateLabelRatio(
	$$: ChartInternalThis,
	d: IArcData,
	outerRadius: number,
	distance: number
): number {
	const {config} = $$;
	const chartType = _getArcType($$);
	let ratio = chartType ? config[`${chartType}_label_ratio`] : undefined;

	if (ratio) {
		ratio = isFunction(ratio) ? ratio.bind($$.api)(d, outerRadius, distance) : ratio;
	} else {
		// Label positioning constants
		const LABEL_MIN_SIZE = 36; // Minimum space needed for label text (pixels)
		const LABEL_RATIO_THRESHOLD = 0.375; // Threshold ratio (3/8) to determine "small chart"
		const LABEL_RATIO_BASE = 1.175; // Base ratio for dynamic positioning on small charts
		const LABEL_RATIO_LARGE = 0.8; // Fixed ratio for large charts (80% position)

		// Calculate ratio based on chart size
		// For small charts (label space > 37.5% of radius), position labels closer to center
		// For large charts, use fixed 80% position
		const labelSpaceRatio = LABEL_MIN_SIZE / outerRadius;
		const isSmallChart = labelSpaceRatio > LABEL_RATIO_THRESHOLD;

		ratio = outerRadius && distance ?
			(isSmallChart ? LABEL_RATIO_BASE - labelSpaceRatio : LABEL_RATIO_LARGE) * outerRadius /
			distance :
			0;
	}

	return ratio;
}

/**
 * Calculate position for standard arc label (donut, pie, polar)
 * @param {ChartInternalThis} $$ ChartInternal context
 * @param {IArcData} d Data object
 * @param {IArcData} updated Updated angle data
 * @returns {object} Object with pos {x, y} and ratio
 * @private
 */
function _calculateStandardArcPosition(
	$$: ChartInternalThis,
	d: IArcData,
	updated: IArcData
): {pos: {x: number, y: number}, ratio: number} {
	let {outerRadius} = $$.getRadius(d);

	if ($$.hasType("polar")) {
		outerRadius = $$.getPolarOuterRadius(d, outerRadius);
	}

	const [x, y] = $$.svgArc.centroid(updated).map((v: number) => (isNaN(v) ? 0 : v));
	const distance = Math.sqrt(x * x + y * y);
	const ratio = _calculateLabelRatio($$, d, outerRadius, distance);

	return {
		pos: {x, y},
		ratio
	};
}

/**
 * Get radius functions
 * @param {number} expandRate Expand rate number.
 *   - If 0, means for "normal" radius.
 *   - If > 0, means for "expanded" radius.
 * @returns {object} radius functions
 * @private
 */
function _getRadiusFn(expandRate = 0) {
	const $$ = this;
	const {config, state} = $$;
	const hasMultiArcGauge = $$.hasMultiArcGauge();
	const singleArcWidth = state.gaugeArcWidth / $$.filterTargetsToShow($$.data.targets).length;
	const expandWidth = expandRate ?
		(
			Math.min(
				state.radiusExpanded * expandRate - state.radius,
				singleArcWidth * 0.8 - (1 - expandRate) * 100
			)
		) :
		0;

	return {
		/**
		 * Getter of arc innerRadius value
		 * @param {IArcData} d Data object
		 * @returns {number} innerRadius value
		 * @private
		 */
		inner(d: IArcData) {
			const {innerRadius} = $$.getRadius(d);

			return hasMultiArcGauge ?
				state.radius - singleArcWidth * (d.index + 1) :
				(isNumber(innerRadius) ? innerRadius : 0);
		},

		/**
		 * Getter of arc outerRadius value
		 * @param {IArcData} d Data object
		 * @returns {number} outerRadius value
		 * @private
		 */
		outer(d: IArcData) {
			const {outerRadius} = $$.getRadius(d);
			let radius: number;

			if (hasMultiArcGauge) {
				radius = state.radius - singleArcWidth * d.index + expandWidth;
			} else if ($$.hasType("polar") && !expandRate) {
				radius = $$.getPolarOuterRadius(d, outerRadius);
			} else {
				radius = outerRadius;

				if (expandRate) {
					let {radiusExpanded} = state;

					if (state.radius !== outerRadius) {
						radiusExpanded -= Math.abs(state.radius - outerRadius);
					}

					radius = radiusExpanded * expandRate;
				}
			}

			return radius;
		},

		/**
		 * Getter of arc cornerRadius value
		 * @param {IArcData} d Data object
		 * @param {number} outerRadius outer radius value
		 * @returns {number} cornerRadius value
		 * @private
		 */
		corner(d: IArcData, outerRadius): number {
			const {
				arc_cornerRadius_ratio: ratio = 0,
				arc_cornerRadius: cornerRadius = 0
			} = config;
			const {data: {id}, value} = d;
			let corner = 0;

			if (ratio) {
				corner = ratio * outerRadius;
			} else {
				corner = isNumber(cornerRadius) ?
					cornerRadius :
					cornerRadius.call($$.api, id, value, outerRadius);
			}

			return corner;
		}
	};
}

/**
 * Get attrTween function to get interpolated value on transition
 * @param {function} fn Arc function to execute
 * @returns {function} attrTween function
 * @private
 */
function _getAttrTweenFn(fn: (d: IArcData) => string) {
	return function(d: IArcData): (t: number) => string {
		const getAngleKeyValue = ({startAngle = 0, endAngle = 0, padAngle = 0}) => ({
			startAngle,
			endAngle,
			padAngle
		});

		// d3.interpolate interpolates id value, if id is given as color string(ex. gold, silver, etc)
		// to avoid unexpected behavior, interpolate only angle values
		// https://github.com/naver/billboard.js/issues/3321
		const interpolate = d3Interpolate(
			getAngleKeyValue(this._current),
			getAngleKeyValue(d)
		);

		this._current = d;

		return function(t: number): string {
			const interpolated = interpolate(t) as IArcData;
			const {data, index, value} = d;

			return fn({...interpolated, data, index, value});
		};
	};
}

export default {
	initPie(): void {
		const $$ = this;
		const {config} = $$;
		const dataType = config.data_type;
		const padding = config[`${dataType}_padding`];
		const startingAngle = config[`${dataType}_startingAngle`] || 0;
		const padAngle = (
			padding ? padding * 0.01 : config[`${dataType}_padAngle`]
		) || 0;

		$$.pie = d3Pie()
			.startAngle(startingAngle)
			.endAngle(startingAngle + (2 * Math.PI))
			.padAngle(padAngle)
			.value((d: IData | any) => d.values?.reduce((a, b) => a + b.value, 0) ?? d)
			.sort($$.getSortCompareFn.bind($$)(true));
	},

	updateRadius(): void {
		const $$ = this;
		const {config, state} = $$;
		const dataType = config.data_type;
		const padding = config[`${dataType}_padding`];
		const w = config.gauge_width || config.donut_width;
		const gaugeArcWidth = $$.filterTargetsToShow($$.data.targets).length *
			config.gauge_arcs_minWidth;

		// Radius reduction ratio when labels are present
		const LABEL_RADIUS_RATIO = 0.85;

		// Reduce radius for label with lines to make room for external labels
		const labelWithLineRatio = isLabelWithLine.call($$) ? LABEL_RADIUS_RATIO : 1;

		// determine radius
		state.radiusExpanded = Math.min(state.arcWidth, state.arcHeight) / 2 * (
			$$.hasMultiArcGauge() && config.gauge_label_show ?
				LABEL_RADIUS_RATIO :
				labelWithLineRatio
		);

		state.radius = state.radiusExpanded * 0.95;
		state.innerRadiusRatio = w ? (state.radius - w) / state.radius : 0.6;

		state.gaugeArcWidth = w || (
			gaugeArcWidth <= state.radius - state.innerRadius ?
				state.radius - state.innerRadius :
				(gaugeArcWidth <= state.radius ? gaugeArcWidth : state.radius)
		);

		const innerRadius = config.pie_innerRadius || (
			padding ? padding * (state.innerRadiusRatio + 0.1) : 0
		);

		// NOTE: inner/outerRadius can be an object by user setting, only for 'pie' type
		state.outerRadius = config.pie_outerRadius;
		state.innerRadius = $$.hasType("donut") || $$.hasType("gauge") ?
			state.radius * state.innerRadiusRatio :
			innerRadius;
	},

	/**
	 * Get pie's inner & outer radius value
	 * @param {object|undefined} d Data object
	 * @returns {object}
	 * @private
	 */
	getRadius(d: IArcData): {innerRadius: number, outerRadius: number} {
		const $$ = this;
		const data = d?.data;
		let {innerRadius, outerRadius} = $$.state;

		if (!isNumber(innerRadius) && data) {
			innerRadius = innerRadius[data.id] || 0;
		}

		if (isObject(outerRadius) && data && data.id in outerRadius) {
			outerRadius = outerRadius[data.id];
		} else if (!isNumber(outerRadius)) {
			outerRadius = $$.state.radius;
		}

		return {innerRadius, outerRadius};
	},

	updateArc(): void {
		const $$ = this;

		$$.updateRadius();
		$$.svgArc = $$.getSvgArc();
		$$.svgArcExpanded = $$.getSvgArcExpanded();
	},

	getArcLength(): number {
		const $$ = this;
		const {config} = $$;
		const arcLengthInPercent = config.gauge_arcLength * 3.6;
		let len = 2 * (arcLengthInPercent / 360);

		if (arcLengthInPercent < -360) {
			len = -2;
		} else if (arcLengthInPercent > 360) {
			len = 2;
		}

		return len * Math.PI;
	},

	getStartingAngle(): number {
		const $$ = this;
		const {config} = $$;
		const dataType = config.data_type;
		const isFullCircle = $$.hasType("gauge") ? config.gauge_fullCircle : false;
		const defaultStartAngle = -1 * Math.PI / 2;
		const defaultEndAngle = Math.PI / 2;
		let startAngle = config[`${dataType}_startingAngle`] || 0;

		if (!isFullCircle && startAngle <= defaultStartAngle) {
			startAngle = defaultStartAngle;
		} else if (!isFullCircle && startAngle >= defaultEndAngle) {
			startAngle = defaultEndAngle;
		} else if (startAngle > Math.PI || startAngle < -1 * Math.PI) {
			startAngle = Math.PI;
		}

		return startAngle;
	},

	/**
	 * Update angle data
	 * @param {object} dValue Data object
	 * @param {boolean} forRange Weather is for ranged text option(arc.rangeText.values)
	 * @returns {object|null} Updated angle data
	 * @private
	 */
	updateAngle(dValue: IArcData, forRange = false): IArcData | null {
		const $$ = this;
		const {config, state} = $$;
		const hasGauge = forRange && $$.hasType("gauge");

		// to prevent excluding total data sum during the init(when data.hide option is used), use $$.rendered state value
		// const totalSum = $$.getTotalDataSum(state.rendered);
		let {pie} = $$;
		let d = dValue;
		let found = false;

		if (!config) {
			return null;
		}

		const gStart = $$.getStartingAngle();
		const radius = config.gauge_fullCircle || (forRange && !hasGauge) ?
			$$.getArcLength() :
			gStart * -2;

		if (d.data && $$.isGaugeType(d.data) && !$$.hasMultiArcGauge()) {
			const {gauge_min: gMin, gauge_max: gMax} = config;

			// to prevent excluding total data sum during the init(when data.hide option is used), use $$.rendered state value
			const totalSum = $$.getTotalDataSum(state.rendered);

			// https://github.com/naver/billboard.js/issues/2123
			const gEnd = radius * ((totalSum - gMin) / (gMax - gMin));

			pie = pie
				.startAngle(gStart)
				.endAngle(gEnd + gStart);
		}

		if (forRange === false) {
			pie($$.filterTargetsToShow())
				.forEach((t, i) => {
					if (!found && t.data.id === d.data?.id) {
						found = true;
						d = t;
						d.index = i;
					}
				});
		}

		if (isNaN(d.startAngle)) {
			d.startAngle = 0;
		}

		if (isNaN(d.endAngle)) {
			d.endAngle = d.startAngle;
		}

		if (forRange || (d.data && (config.gauge_enforceMinMax || $$.hasMultiArcGauge()))) {
			const {gauge_min: gMin, gauge_max: gMax} = config;
			const max = forRange && !hasGauge ? $$.getTotalDataSum(state.rendered) : gMax;
			const gTic = radius / (max - gMin);
			const value = d.value ?? 0;
			const gValue = value < gMin ? 0 : value < max ? value - gMin : (max - gMin);

			d.startAngle = gStart;
			d.endAngle = gStart + gTic * gValue;
		}

		return found || forRange ? d : null;
	},

	getSvgArc(): Function {
		const $$ = this;
		const {inner, outer, corner} = _getRadiusFn.call($$);

		const arc = d3Arc()
			.innerRadius(inner)
			.outerRadius(outer);

		const newArc = function(d: IArcData, withoutUpdate) {
			let path: string | null = "M 0 0";

			if (d.value || d.data) {
				const data = withoutUpdate ? d : $$.updateAngle(d) ?? null;

				if (data) {
					path = arc.cornerRadius(
						corner(data, outer(data))
					)(data);
				}
			}

			return path;
		};

		// TODO: extends all function
		newArc.centroid = arc.centroid;

		return newArc;
	},

	/**
	 * Get expanded arc path function
	 * @param {number} rate Expand rate
	 * @returns {function} Expanded arc path getter function
	 * @private
	 */
	getSvgArcExpanded(rate = 1): (d: IArcData) => string {
		const $$ = this;
		const {inner, outer, corner} = _getRadiusFn.call($$, rate);

		const arc = d3Arc()
			.innerRadius(inner)
			.outerRadius(outer);

		return (d: IArcData): string => {
			const updated = $$.updateAngle(d);
			const outerR = outer(updated);
			let cornerR = 0;

			if (updated) {
				cornerR = corner(updated, outerR);
			}

			return updated ? <string>arc.cornerRadius(cornerR)(updated) : "M 0 0";
		};
	},

	getArc(d, withoutUpdate: boolean, force?: boolean): string {
		return force || this.isArcType(d.data) ? this.svgArc(d, withoutUpdate) : "M 0 0";
	},

	/**
	 * Render range value text
	 * @private
	 */
	redrawArcRangeText(): void {
		const $$ = this;
		const {config, $el: {arcs}, state, $T} = $$;
		const format = config.arc_rangeText_format;
		const fixed = $$.hasType("gauge") && config.arc_rangeText_fixed;
		let values = config.arc_rangeText_values;

		if (values?.length) {
			const isPercent = config.arc_rangeText_unit === "%";
			const totalSum = $$.getTotalDataSum(state.rendered);

			if (isPercent) {
				values = values.map(v => totalSum / 100 * v);
			}

			const pieData = $$.pie(values).map((d, i) => ((d.index = i), d));
			let rangeText = arcs.selectAll(`.${$ARC.arcRange}`)
				.data(values);

			rangeText.exit();

			rangeText = $T(rangeText.enter()
				.append("text")
				.attr("class", $ARC.arcRange)
				.style("text-anchor", "middle")
				.style("pointer-events", "none")
				.style("opacity", "0")
				.text(v => {
					const range = isPercent ? (v / totalSum * 100) : v;

					return isFunction(format) ? format(range) : (
						`${range}${isPercent ? "%" : ""}`
					);
				})
				.merge(rangeText));

			if ((!state.rendered || (state.rendered && !fixed)) && totalSum > 0) {
				rangeText.attr("transform", function(d, i) {
					return $$.transformForArcLabel(this, pieData[i], true);
				});
			}

			rangeText.style("opacity",
				d => (!fixed && (d > totalSum || totalSum === 0) ? "0" : null));
		}
	},

	/**
	 * Set transform attributes to arc label text
	 * @param {SVGTextElement} textNode Text node element
	 * @param {object} d Data object
	 * @param {boolean} forRange Weather is for ranged text option(arc.rangeText.values)
	 * @returns {string} Translate attribute string
	 * @private
	 */
	transformForArcLabel(textNode: SVGTextElement, d: IArcData, forRange = false): string {
		const $$ = this;
		const updated = $$.updateAngle(d, forRange);

		if (!updated) {
			return "";
		}

		let pos: {x: number, y: number};
		let ratio = 1;

		// Handle range text or multi-arc gauge labels
		if (forRange || $$.hasMultiArcGauge()) {
			pos = _calculateRangeOrGaugePosition($$, d, updated, forRange);
		} // Handle standard arc types (donut, pie, polar)
		else if (!$$.hasType("gauge") || $$.data.targets.length > 1) {
			const result = _calculateStandardArcPosition($$, d, updated);

			pos = result.pos;
			ratio = result.ratio;
		} else {
			return "";
		}

		updateTextImagePos.call($$, textNode, pos);
		return `translate(${pos.x * ratio},${pos.y * ratio})`;
	},

	convertToArcData(d: IArcData | IArcDataRow): object {
		return this.addName({
			id: "data" in d ? d.data.id : d.id,
			value: d.value,
			ratio: this.getRatio("arc", d),
			index: d.index
		});
	},

	textForArcLabel(selection: d3Selection): void {
		const $$ = this;
		const hasGauge = $$.hasType("gauge");
		const chartType = ["donut", "gauge", "pie", "polar"].filter($$.hasType.bind($$))?.[0];

		if ($$.shouldShowArcLabel()) {
			selection
				.style("fill", $$.updateTextColor.bind($$))
				.attr("filter", d =>
					$$.updateTextBGColor.bind($$)(d, $$.config.data_labels_backgroundColors))
				.each(function(d) {
					const node = d3Select(this);
					const updated = $$.updateAngle(d);
					const ratio = $$.getRatio("arc", updated);
					const meetsThreshold = meetsLabelThreshold.call($$, ratio, chartType);

					// Cache calculated values for reuse in redrawArcLabelLines
					d._cache = {updated, ratio, meetsThreshold};

					if (meetsThreshold) {
						const {value} = updated || d;
						const text = (
							$$.getArcLabelConfig("format") || $$.defaultArcValueFormat
						)(value, ratio, d.data.id).toString();

						setTextValue(node, text, [-1, 1], hasGauge);
					} else {
						node.text("");
					}
				});
		}
	},

	expandArc(targetIds: string[]): void {
		const $$ = this;
		const {state: {transiting}, $el} = $$;

		// MEMO: avoid to cancel transition
		if (transiting) {
			const interval = setInterval(() => {
				if (!transiting) {
					clearInterval(interval);

					$el.legend.selectAll(`.${$FOCUS.legendItemFocused}`).size() > 0 &&
						$$.expandArc(targetIds);
				}
			}, 10);

			return;
		}

		const newTargetIds = $$.mapToTargetIds(targetIds);

		$el.svg.selectAll($$.selectorTargets(newTargetIds, `.${$ARC.chartArc}`))
			.each(function(d) {
				if (!$$.shouldExpand(d.data.id)) {
					return;
				}

				const expandDuration = $$.getExpandConfig(d.data.id, "duration");
				const svgArcExpandedSub = $$.getSvgArcExpanded(
					$$.getExpandConfig(d.data.id, "rate")
				);

				d3Select(this).selectAll("path")
					// @ts-ignore
					.transition()
					.duration(expandDuration)
					.attrTween("d", _getAttrTweenFn($$.svgArcExpanded.bind($$)))
					.transition()
					.duration(expandDuration * 2)
					.attrTween("d", _getAttrTweenFn(svgArcExpandedSub.bind($$)));
			});
	},

	unexpandArc(targetIds: string[]): void {
		const $$ = this;
		const {state: {transiting}, $el: {svg}} = $$;

		if (transiting) {
			return;
		}

		const newTargetIds = $$.mapToTargetIds(targetIds);

		svg.selectAll($$.selectorTargets(newTargetIds, `.${$ARC.chartArc}`))
			.selectAll("path")
			.transition()
			.duration(d => $$.getExpandConfig(d.data.id, "duration"))
			.attrTween("d", _getAttrTweenFn($$.svgArc.bind($$)));

		svg.selectAll(`${$ARC.arc}`)
			.style("opacity", null);
	},

	/**
	 * Get expand config value
	 * @param {string} id data ID
	 * @param {string} key config key: 'duration | rate'
	 * @returns {number}
	 * @private
	 */
	getExpandConfig(id: string, key: "duration" | "rate"): number {
		const $$ = this;
		const {config} = $$;
		const def = {
			duration: 50,
			rate: 0.98
		};
		let type;

		if ($$.isDonutType(id)) {
			type = "donut";
		} else if ($$.isGaugeType(id)) {
			type = "gauge";
		} else if ($$.isPieType(id)) {
			type = "pie";
		}

		return type ? config[`${type}_expand_${key}`] : def[key];
	},

	shouldExpand(id: string): boolean {
		const $$ = this;
		const {config} = $$;

		return ($$.isDonutType(id) && config.donut_expand) ||
			($$.isGaugeType(id) && config.gauge_expand) ||
			($$.isPieType(id) && config.pie_expand);
	},

	shouldShowArcLabel(): boolean {
		const $$ = this;
		const {config} = $$;

		return ["donut", "gauge", "pie", "polar"]
			.some(v => $$.hasType(v) && config[`${v}_label_show`]);
	},

	getArcLabelConfig(name = "format"): number | string | Function | object {
		const $$ = this;
		const {config} = $$;
		let fn = v => v;

		["donut", "gauge", "pie", "polar"]
			.filter($$.hasType.bind($$))
			.forEach(v => {
				fn = config[`${v}_label_${name}`];
			});

		if (name === "format") {
			return isFunction(fn) ? fn.bind($$.api) : fn;
		} else {
			return fn;
		}
	},

	updateTargetsForArc(targets: IData): void {
		const $$ = this;
		const {$el} = $$;
		const hasGauge = $$.hasType("gauge");
		const classChartArc = $$.getChartClass("Arc");
		const classArcs = $$.getClass("arcs", true);
		const classFocus = $$.classFocus.bind($$);
		const chartArcs = $el.main.select(`.${$ARC.chartArcs}`);

		const mainPieUpdate = chartArcs
			.selectAll(`.${$ARC.chartArc}`)
			.data($$.pie(targets))
			.attr("class", d => classChartArc(d) + classFocus(d.data));

		const mainPieEnter = mainPieUpdate.enter().append("g")
			.attr("class", classChartArc)
			.call(
				this.setCssRule(false, `.${$ARC.chartArcs} text`, [
					"pointer-events:none",
					"text-anchor:middle"
				])
			);

		mainPieEnter.append("g")
			.attr("class", classArcs)
			.merge(mainPieUpdate);

		mainPieEnter.append("text")
			.attr("dy", hasGauge && !$$.hasMultiTargets() ? "-.1em" : null)
			.style("opacity", "0")
			.style("text-anchor", $$.getStylePropValue("middle"))
			.style("pointer-events", $$.getStylePropValue("none"));

		$el.text = chartArcs.selectAll(`.${$COMMON.target} text`);
		// MEMO: can not keep same color..., but not bad to update color in redraw
		// mainPieUpdate.exit().remove();
	},

	initArc(): void {
		const $$ = this;
		const {$el} = $$;

		$el.arcs = $el.main.select(`.${$COMMON.chart}`)
			.append("g")
			.attr("class", $ARC.chartArcs)
			.attr("transform", $$.getTranslate("arc"));

		$$.setArcTitle();
	},

	/**
	 * Set arc title text
	 * @param {string} str Title text
	 * @private
	 */
	setArcTitle(str?: string) {
		const $$ = this;
		const title = str || $$.getArcTitle();
		const hasGauge = $$.hasType("gauge");

		if (title) {
			const className = hasGauge ? $GAUGE.chartArcsGaugeTitle : $ARC.chartArcsTitle;
			let text = $$.$el.arcs.select(`.${className}`);

			if (text.empty()) {
				text = $$.$el.arcs.append("text")
					.attr("class", className)
					.style("text-anchor", "middle");
			}

			hasGauge && text.attr("dy", "-0.3em");

			setTextValue(text, title, hasGauge ? undefined : [-0.6, 1.35], true);
		}
	},

	/**
	 * Return arc title text
	 * @returns {string} Arc title text
	 * @private
	 */
	getArcTitle(): string {
		const $$ = this;
		const type = ($$.hasType("donut") && "donut") || ($$.hasType("gauge") && "gauge");

		return type ? $$.config[`${type}_title`] : "";
	},

	/**
	 * Get arc title text with needle value
	 * @returns {string|boolean} When title contains needle template string will return processed string, otherwise false
	 * @private
	 */
	getArcTitleWithNeedleValue(): string | false {
		const $$ = this;
		const {config, state} = $$;
		const title = $$.getArcTitle();

		if (title && $$.config.arc_needle_show && /{=[A-Z_]+}/.test(title)) {
			let value = state.current.needle;

			if (!isNumber(value)) {
				value = config.arc_needle_value;
			}

			return tplProcess(title, {
				NEEDLE_VALUE: ~~value
			});
		}

		return false;
	},

	redrawArc(duration: number, durationForExit: number, withTransform?: boolean): void {
		const $$ = this;
		const {config, state, $el: {main}} = $$;
		const hasInteraction = config.interaction_enabled;
		const isSelectable = hasInteraction && config.data_selection_isselectable;

		let mainArc = main.selectAll(`.${$ARC.arcs}`)
			.selectAll(`.${$ARC.arc}`)
			.data($$.arcData.bind($$));

		mainArc.exit()
			.transition()
			.duration(durationForExit)
			.style("opacity", "0")
			.remove();

		mainArc = mainArc.enter()
			.append("path")
			.attr("class", $$.getClass("arc", true))
			.style("fill", d => $$.color(d.data))
			.style("cursor", d => (isSelectable?.bind?.($$.api)(d) ? "pointer" : null))
			.style("opacity", "0")
			.each(function(d) {
				if ($$.isGaugeType(d.data)) {
					d.startAngle = config.gauge_startingAngle;
					d.endAngle = config.gauge_startingAngle;
				}

				this._current = d;
			})
			.merge(mainArc);

		if ($$.hasType("gauge")) {
			$$.updateGaugeMax();
			$$.hasMultiArcGauge() && $$.redrawArcGaugeLine();
		}

		mainArc
			.attr("transform", d => (!$$.isGaugeType(d.data) && withTransform ? "scale(0)" : ""))
			.style("opacity", function(d) {
				return d === this._current ? "0" : null;
			})
			.each(() => {
				state.transiting = true;
			})
			.transition()
			.duration(duration)
			.attrTween("d", function(d) {
				const updated = $$.updateAngle(d);

				if (!updated) {
					return () => "M 0 0";
				}

				if (isNaN(this._current.startAngle)) {
					this._current.startAngle = 0;
				}

				if (isNaN(this._current.endAngle)) {
					this._current.endAngle = this._current.startAngle;
				}

				const interpolate = d3Interpolate(this._current, updated);

				this._current = interpolate(0);

				return function(t) {
					const interpolated = interpolate(t);

					interpolated.data = d.data; // data.id will be updated by interporator

					return $$.getArc(interpolated, true);
				};
			})
			.attr("transform", withTransform ? "scale(1)" : "")
			.style("fill", d => {
				let color;

				if ($$.levelColor) {
					color = $$.levelColor(d.data.values[0].value);

					// update data's color
					config.data_colors[d.data.id] = color;
				} else {
					color = $$.color(d.data);
				}

				return color;
			})
			// Where gauge reading color would receive customization.
			.style("opacity", null)
			.call(endall, function() {
				if ($$.levelColor) {
					const path = d3Select(this);
					const d: any = path.datum(this._current);

					$$.updateLegendItemColor(d.data.id, path.style("fill"));
				}

				state.transiting = false;
				callFn(config.onrendered, $$.api);
			});

		// bind arc events
		hasInteraction && $$.bindArcEvent(mainArc);

		$$.hasType("polar") && $$.redrawPolar();
		$$.hasType("gauge") && $$.redrawBackgroundArcs();

		config.arc_needle_show && $$.redrawNeedle();

		$$.redrawArcText(duration);
		$$.redrawArcRangeText();
	},

	/**
	 * Update needle element
	 * @private
	 */
	redrawNeedle(): void {
		const $$ = this;
		const {$el, config, state: {hiddenTargetIds, radius}} = $$;
		const length = (radius - 1) / 100 * config.arc_needle_length;
		const hasDataToShow = hiddenTargetIds.length !== $$.data.targets.length;
		let needle = $$.$el.arcs.select(`.${$ARC.needle}`);

		// needle options
		const pathFn = config.arc_needle_path;
		const baseWidth = config.arc_needle_bottom_width / 2;
		const topWidth = config.arc_needle_top_width / 2;
		const topRx = config.arc_needle_top_rx;
		const topRy = config.arc_needle_top_ry;
		const bottomLen = config.arc_needle_bottom_len;
		const bottomRx = config.arc_needle_bottom_rx;
		const bottomRy = config.arc_needle_bottom_ry;
		const needleAngle = $$.getNeedleAngle();

		const updateNeedleValue = () => {
			const title = $$.getArcTitleWithNeedleValue();

			title && $$.setArcTitle(title);
		};

		updateNeedleValue();

		if (needle.empty()) {
			needle = $el.arcs
				.append("path")
				.classed($ARC.needle, true);

			$el.needle = needle;

			/**
			 * Function to be exposed as public to facilitate updating needle
			 * @param {number} v Value to be updated
			 * @param {boolean} updateConfig Weather update config's value
			 * @private
			 */
			$el.needle.updateHelper = (v: number, updateConfig = false): void => {
				if ($el.needle.style("display") !== "none") {
					$$.$T($el.needle)
						.style("transform", `rotate(${$$.getNeedleAngle(v)}deg)`)
						.call(endall, () => {
							updateConfig && (config.arc_needle_value = v);
							updateNeedleValue();
						});
				}
			};
		}

		if (hasDataToShow) {
			const path = isFunction(pathFn) ?
				pathFn.call($$, length) :
				`M-${baseWidth} ${bottomLen} A${bottomRx} ${bottomRy} 0 0 0 ${baseWidth} ${bottomLen} L${topWidth} -${length} A${topRx} ${topRy} 0 0 0 -${topWidth} -${length} L-${baseWidth} ${bottomLen} Z`;

			$$.$T(needle)
				.attr("d", path)
				.style("fill", config.arc_needle_color)
				.style("display", null)
				.style("transform", `rotate(${needleAngle}deg)`);
		} else {
			needle.style("display", "none");
		}
	},

	/**
	 * Get needle angle value relative given value
	 * @param {number} v Value to be calculated angle
	 * @returns {number} angle value
	 * @private
	 */
	getNeedleAngle(v?: number): number {
		const $$ = this;
		const {config, state} = $$;
		const arcLength = $$.getArcLength();
		const hasGauge = $$.hasType("gauge");
		const total = $$.getTotalDataSum(true);
		let value = isDefined(v) ? v : config.arc_needle_value;
		let startingAngle = config[`${config.data_type}_startingAngle`] || 0;
		let radian = 0;

		if (!isNumber(value)) {
			value = hasGauge && $$.data.targets.length === 1 ? total : 0;
		}

		state.current.needle = value;

		if (hasGauge) {
			startingAngle = $$.getStartingAngle();

			const radius = config.gauge_fullCircle ? arcLength : startingAngle * -2;
			const {gauge_min: min, gauge_max: max} = config;

			radian = radius * ((value - min) / (max - min));
		} else {
			radian = arcLength * (value / total);
		}

		return (startingAngle + radian) * (180 / Math.PI);
	},

	redrawBackgroundArcs() {
		const $$ = this;
		const {config, state} = $$;
		const hasMultiArcGauge = $$.hasMultiArcGauge();
		const isFullCircle = config.gauge_fullCircle;
		const showEmptyTextLabel = $$.filterTargetsToShow($$.data.targets).length === 0 &&
			!!config.data_empty_label_text;

		const startAngle = $$.getStartingAngle();
		const endAngle = isFullCircle ? startAngle + $$.getArcLength() : startAngle * -1;

		let backgroundArc = $$.$el.arcs.select(
			`${hasMultiArcGauge ? "g" : ""}.${$ARC.chartArcsBackground}`
		);

		if (hasMultiArcGauge) {
			let index = 0;

			backgroundArc = backgroundArc
				.selectAll(`path.${$ARC.chartArcsBackground}`)
				.data($$.data.targets);

			backgroundArc.enter()
				.append("path")
				.attr("class", (d, i) =>
					`${$ARC.chartArcsBackground} ${$ARC.chartArcsBackground}-${i}`)
				.merge(backgroundArc)
				.style("fill", (config.gauge_background) || null)
				.attr("d", ({id}) => {
					if (showEmptyTextLabel || state.hiddenTargetIds.indexOf(id) >= 0) {
						return "M 0 0";
					}

					const d = {
						data: [{value: config.gauge_max}],
						startAngle,
						endAngle,
						index: index++
					};

					return $$.getArc(d, true, true);
				});

			backgroundArc.exit().remove();
		} else {
			backgroundArc.attr("d", showEmptyTextLabel ? "M 0 0" : () => {
				const d = {
					data: [{value: config.gauge_max}],
					startAngle,
					endAngle
				};

				return $$.getArc(d, true, true);
			});
		}
	},

	bindArcEvent(arc): void {
		const $$ = this;
		const {config, state} = $$;
		const isTouch = state.inputType === "touch";
		const isMouse = state.inputType === "mouse";

		// eslint-disable-next-line
		function selectArc(_this, arcData, id) {
			// transitions
			$$.expandArc(id);
			$$.api.focus(id);
			$$.toggleFocusLegend(id, true);
			$$.showTooltip([arcData], _this);
		}

		// eslint-disable-next-line
		function unselectArc(arcData?) {
			const id = arcData?.id || undefined;

			$$.unexpandArc(id);
			$$.api.revert();
			$$.revertLegend();
			$$.hideTooltip();
		}

		arc
			.on("click", function(event, d, i) {
				const updated = $$.updateAngle(d);
				let arcData;

				if (updated) {
					arcData = $$.convertToArcData(updated);

					$$.toggleShape?.(this, arcData, i);
					config.data_onclick.bind($$.api)(arcData, this);
				}
			});

		// mouse events
		if (isMouse) {
			arc
				.on("mouseover", function(event, d) {
					if (state.transiting) { // skip while transiting
						return;
					}

					state.event = event;
					const updated = $$.updateAngle(d);
					const arcData = updated ? $$.convertToArcData(updated) : null;
					const id = arcData?.id || undefined;

					selectArc(this, arcData, id);
					$$.setOverOut(true, arcData);
				})
				.on("mouseout", (event, d) => {
					if (state.transiting || !config.interaction_onout) { // skip while transiting
						return;
					}

					state.event = event;
					const updated = $$.updateAngle(d);
					const arcData = updated ? $$.convertToArcData(updated) : null;

					unselectArc();
					$$.setOverOut(false, arcData);
				})
				.on("mousemove", function(event, d) {
					const updated = $$.updateAngle(d);
					const arcData = updated ? $$.convertToArcData(updated) : null;

					state.event = event;
					$$.showTooltip([arcData], this);
				});
		}

		// touch events
		if (isTouch && $$.hasArcType() && !$$.radars) {
			const getEventArc = event => {
				const {clientX, clientY} = event.changedTouches?.[0] ?? {clientX: 0, clientY: 0};
				const eventArc = d3Select(document.elementFromPoint(clientX, clientY));

				return eventArc;
			};

			$$.$el.svg
				.on("touchstart touchmove", function(event) {
					if (state.transiting) { // skip while transiting
						return;
					}

					state.event = event;

					const eventArc = getEventArc(event);
					const datum: any = eventArc.datum();
					const updated = (datum?.data && datum.data.id) ? $$.updateAngle(datum) : null;
					const arcData = updated ? $$.convertToArcData(updated) : null;
					const id = arcData?.id || undefined;

					$$.callOverOutForTouch(arcData);

					isUndefined(id) ? unselectArc() : selectArc(this, arcData, id);
				}, {passive: true});
		}
	},

	redrawArcText(duration: number): void {
		const $$ = this;
		const {config, state, $el: {main, arcs}} = $$;
		const hasGauge = $$.hasType("gauge");
		const hasMultiArcGauge = $$.hasMultiArcGauge();
		let text;

		// for gauge type, update text when has no title & multi data
		if (!(hasGauge && $$.data.targets.length === 1 && config.gauge_title)) {
			text = main.selectAll(`.${$ARC.chartArc}`)
				.select("text")
				.style("opacity", "0")
				.attr("class", d => ($$.isGaugeType(d.data) ? $GAUGE.gaugeValue : null))
				.call($$.textForArcLabel.bind($$))
				.style("font-size", d => (
					$$.isGaugeType(d.data) && $$.data.targets.length === 1 && !hasMultiArcGauge ?
						`${Math.round(state.radius / 5)}px` :
						null
				));

			updateTextImage.call($$);

			text
				.attr("transform", function(d) {
					return $$.transformForArcLabel.bind($$)(this, d);
				})
				.transition()
				.duration(duration)
				.style("opacity",
					d => ($$.isTargetToShow(d.data.id) && $$.isArcType(d.data) ? null : "0"));

			hasMultiArcGauge && text.attr("dy", "-.1em");
		}

		main.select(`.${$ARC.chartArcsTitle}`)
			.style("opacity", $$.hasType("donut") || hasGauge ? null : "0");

		if (hasGauge) {
			const isFullCircle = config.gauge_fullCircle;

			isFullCircle &&
				text?.attr("dy", `${hasMultiArcGauge ? 0 : Math.round(state.radius / 14)}`);

			if (config.gauge_label_show) {
				arcs.select(`.${$GAUGE.chartArcsGaugeUnit}`)
					.attr("dy", `${isFullCircle ? 1.5 : 0.75}em`)
					.text(config.gauge_units);

				arcs.select(`.${$GAUGE.chartArcsGaugeMin}`)
					.attr("dx", `${
						-1 *
						(state.innerRadius +
							((state.radius - state.innerRadius) / (isFullCircle ? 1 : 2)))
					}px`)
					.attr("dy", "1.2em")
					.text($$.textForGaugeMinMax(config.gauge_min, false));

				// show max text when isn't fullCircle
				!isFullCircle && arcs.select(`.${$GAUGE.chartArcsGaugeMax}`)
					.attr("dx", `${state.innerRadius + ((state.radius - state.innerRadius) / 2)}px`)
					.attr("dy", "1.2em")
					.text($$.textForGaugeMinMax(config.gauge_max, true));
			}
		}

		// Render connector lines for label with lines
		isLabelWithLine.call($$) && redrawArcLabelLines.call($$, duration);
	},

	/**
	 * Get Arc element by id or index
	 * @param {string|number} value id or index of Arc
	 * @returns {d3Selection} Arc path element
	 * @private
	 */
	getArcElementByIdOrIndex(value: string | number): d3Selection {
		const $$ = this;
		const {$el: {arcs}} = $$;
		const filterFn = isNumber(value) ? d => d.index === value : d => d.data.id === value;

		return arcs?.selectAll(`.${$COMMON.target} path`)
			.filter(filterFn);
	}
};
