/**
 * Copyright (c) 2017 ~ present NAVER Corp.
 * billboard.js project is licensed under the MIT license
 */
import {brushSelection as d3BrushSelection, brushX as d3BrushX, brushY as d3BrushY} from "d3-brush";
import {select as d3Select} from "d3-selection";
import CLASS from "../../config/classes";
import {SUBCHART_BRUSH_HANDLE_PATH} from "../../config/const";
import {window} from "../../module/browser";
import {
	brushEmpty,
	capitalize,
	emulateEvent,
	getBoundingRect,
	getPointer,
	isArray
} from "../../module/util";
import {
	getMainCoordFromSubchartCoord,
	isContinuousGridFocusEnabled
} from "../internals/subchart.util";

const SUBCHART_TYPES = ["bar", "line", "bubble", "candlestick", "scatter"];
const FOCUS_GRID_STYLE_PROPS = [
	"opacity",
	"stroke",
	"stroke-dasharray",
	"stroke-dashoffset",
	"stroke-linecap",
	"stroke-linejoin",
	"stroke-miterlimit",
	"stroke-opacity",
	"stroke-width"
];

/**
 * Copy main focus grid computed styles to the subchart focus grid line.
 * @param {object} $$ ChartInternal instance
 * @param {object} line Subchart focus grid line selection
 * @private
 */
function syncSubchartGridFocusStyle($$, line): void {
	const source = $$.$el.grid?.main?.select(`line.${CLASS.xgridFocus}`).node();

	if (!source || !window.getComputedStyle) {
		return;
	}

	const style = window.getComputedStyle(source);

	FOCUS_GRID_STYLE_PROPS.forEach(prop => {
		const value = style.getPropertyValue(prop);

		value && line.style(prop, value);
	});
}

/**
 * Convert an SVG attr value to finite number with fallback.
 * @param {string|null} value Attribute value
 * @param {number} fallback Fallback value
 * @returns {number} Numeric value
 * @private
 */
function getFiniteAttr(value: string | null, fallback: number): number {
	const parsed = Number(value);

	return Number.isFinite(parsed) ? parsed : fallback;
}

/**
 * Show a single clipped-free x focus grid line spanning main chart and subchart.
 * @param {object} $$ ChartInternal instance
 * @returns {boolean} Whether continuous focus line was rendered
 * @private
 */
function showContinuousSubchartGridFocus($$): boolean {
	if (!isContinuousGridFocusEnabled($$)) {
		return false;
	}

	const {config, state, $el} = $$;
	const mainLine = $el.grid?.main?.select(
		`line.${CLASS.xgridFocus}:not(.${CLASS.xgridFocusContinuous})`
	);
	const line = $el.main.select(`line.${CLASS.xgridFocusContinuous}`);

	if (!mainLine?.node() || !line.node()) {
		return false;
	}

	const x2 = state.margin2.left - state.margin.left + state.width2;
	const y2 = state.margin2.top - state.margin.top + state.height2;

	syncSubchartGridFocusStyle($$, line);

	config.axis_rotated ?
		line
			.attr("x1", getFiniteAttr(mainLine.attr("x1"), 0))
			.attr("x2", x2)
			.attr("y1", getFiniteAttr(mainLine.attr("y1"), -10))
			.attr("y2", getFiniteAttr(mainLine.attr("y2"), -10)) :
		line
			.attr("x1", getFiniteAttr(mainLine.attr("x1"), -10))
			.attr("x2", getFiniteAttr(mainLine.attr("x2"), -10))
			.attr("y1", getFiniteAttr(mainLine.attr("y1"), 0))
			.attr("y2", y2);

	line.style("visibility", null);
	mainLine.style("visibility", "hidden");

	return true;
}

/**
 * Hide continuous subchart focus grid line.
 * @param {object} $$ ChartInternal instance
 * @private
 */
function hideContinuousSubchartGridFocus($$): void {
	$$.$el.main
		?.select(`line.${CLASS.xgridFocusContinuous}`)
		.style("visibility", "hidden");
}

/**
 * Get main chart pointer coordinates that correspond to a subchart pointer event.
 * @param {object} $$ ChartInternal instance
 * @param {Event} event Input event
 * @param {SVGElement} context Subchart event rect
 * @returns {Array|null} Main chart local coordinates
 * @private
 */
function getMainPointFromSubchartEvent($$, event, context): [number, number] | null {
	const {config, state} = $$;
	const [x, y] = getPointer(event, context);
	const mainCoord = getMainCoordFromSubchartCoord($$, config.axis_rotated ? y : x);

	if (mainCoord === null) {
		return null;
	}

	return config.axis_rotated ? [state.width / 2, mainCoord] : [mainCoord, state.height / 2];
}

/**
 * Dispatch a subchart pointer event through the main chart event rect.
 * @param {object} $$ ChartInternal instance
 * @param {string} type Event type
 * @param {Event} event Input event
 * @param {SVGElement} context Subchart event rect
 * @private
 */
function dispatchSubchartEvent($$, type: string, event, context): void {
	const mainEventRect = $$.$el.eventRect?.node?.();

	if (!mainEventRect) {
		return;
	}

	const point = getMainPointFromSubchartEvent($$, event, context) || [0, 0];
	const rect = getBoundingRect(mainEventRect, true);
	const clientX = rect.left + point[0];
	const clientY = rect.top + point[1];
	const params = {
		bubbles: true,
		cancelable: true,
		screenX: clientX,
		screenY: clientY,
		clientX,
		clientY
	};

	/^touch/.test(type) ?
		emulateEvent.touch(mainEventRect, type, params) :
		emulateEvent.mouse(mainEventRect, type, params);
}

export default {
	/**
	 * Whether subchart brush interaction is enabled.
	 * @returns {boolean}
	 * @private
	 */
	isSubchartBrushEnabled(): boolean {
		const {config} = this;

		return config.subchart_show && config.subchart_brush_enabled !== false;
	},

	/**
	 * Bind event rect handlers for subchart hover interactions.
	 * @param {d3Selection} eventRect Subchart event rect selection
	 * @private
	 */
	bindSubchartEventRect(eventRect): void {
		const $$ = this;
		const {config, state} = $$;

		eventRect.on("mouseover mousemove mouseout touchstart touchmove touchend", null);

		if (!config.interaction_enabled) {
			return;
		}

		if (state.inputType === "mouse") {
			eventRect
				.on("mouseover mousemove", function(event) {
					if ($$.isSubchartBrushEnabled()) {
						return;
					}

					dispatchSubchartEvent($$, event.type, event, this);
				})
				.on("mouseout", function(event) {
					if ($$.isSubchartBrushEnabled()) {
						return;
					}

					dispatchSubchartEvent($$, "mouseout", event, this);
				});
		} else if (state.inputType === "touch") {
			eventRect
				.on("touchstart touchmove touchend", function(event) {
					if ($$.isSubchartBrushEnabled() || event.touches?.length > 1) {
						return;
					}

					dispatchSubchartEvent($$, event.type, event, this);
				});
		}
	},

	/**
	 * Initialize the brush.
	 * @private
	 */
	initBrush(): void {
		const $$ = this;
		const {config, scale, $el: {subchart}, state} = $$;
		const isRotated = config.axis_rotated;
		const height = config.subchart_size_height;
		let lastDomain;
		let lastSelection;
		let timeout;

		// set the brush
		$$.brush = (
			isRotated ? d3BrushY() : d3BrushX()
		).handleSize(5);

		// bind brush event
		$$.brush.on("start brush end", event => {
			const {selection, sourceEvent, target, type} = event;

			if (type === "start") {
				$$.state.inputType === "touch" && $$.hideTooltip();
				lastSelection = sourceEvent ? selection : null;
				// sourceEvent && (state.domain = null);
			}

			// if (type === "brush") {
			if (/(start|brush)/.test(type)) {
				// when brush selection updates happens on one edge, update only chainging edge and
				// is only for adjustment of given domain range to be used to return current domain range.
				type === "brush" && sourceEvent && state.domain &&
					lastSelection?.forEach((v, i) => {
						if (v !== selection[i]) {
							state.domain[i] = scale.x.orgDomain()[i];
						}
					});

				$$.redrawForBrush(type !== "start");
			}

			if (type === "end") {
				lastDomain = scale.x.orgDomain();
			}

			// handle brush's handle position & visibility
			if (target?.handle) {
				if (selection === null) {
					$$.brush.handle.attr("display", "none");
				} else {
					$$.brush.handle.attr("display", null)
						.attr("transform", (d, i) => {
							const pos = [selection[i], height / 2];

							return `translate(${isRotated ? pos.reverse() : pos})`;
						});
				}
			}
		});

		$$.brush.updateResize = function() {
			timeout && clearTimeout(timeout);
			timeout = setTimeout(() => {
				const selection = this.getSelection();

				lastDomain && d3BrushSelection(selection.node()) &&
					this.move(selection, lastDomain.map(scale.subX.orgScale()));
			}, 0);
		};

		$$.brush.update = function() {
			const extent = this.extent()();

			if (extent[1].filter(v => isNaN(v)).length === 0) {
				subchart.main?.select(`.${CLASS.brush}`).call(this);
			}

			return this;
		};

		// set the brush extent
		$$.brush.scale = function(scale) {
			const h = config.subchart_size_height;
			let extent = $$.axis.getExtent();

			if (!extent && scale.range) {
				extent = [[0, 0], [scale.range()[1], h]];
			} else if (isArray(extent)) {
				extent = extent.map((v, i) => [v, i > 0 ? h : i]);
			}

			// [[x0, y0], [x1, y1]], where [x0, y0] is the top-left corner and [x1, y1] is the bottom-right corner
			isRotated && extent[1].reverse();
			this.extent(extent);

			// when extent updates, brush selection also be re-applied
			// https://github.com/d3/d3/issues/2918
			this.update();
		};

		$$.brush.getSelection = () => (
			// @ts-ignore
			subchart.main ? subchart.main.select(`.${CLASS.brush}`) : d3Select([])
		);
	},

	/**
	 * Initialize the subchart.
	 * @private
	 */
	initSubchart(): void {
		const $$ = this;
		const {config, state: {clip, hasAxis}, $el: {defs, svg, subchart, axis}} = $$;

		if (!hasAxis) {
			return;
		}

		const visibility = config.subchart_show ? null : "hidden";
		const clipId = `${clip.id}-subchart`;
		const clipPath = $$.getClipPath(clipId);

		clip.idSubchart = clipId;
		$$.appendClip(defs, clipId);
		$$.initBrush();

		subchart.main = svg.append("g")
			.classed(CLASS.subchart, true)
			.attr("transform", $$.getTranslate("context"));

		const {main} = subchart;

		main.style("visibility", visibility);

		// Define g for chart area
		main.append("g")
			.attr("clip-path", clipPath)
			.attr("class", CLASS.chart);

		$$.withSubchartTypeContext(() => {
			// Define g for chart types area
			SUBCHART_TYPES.forEach(v => {
				const type = capitalize(/^(bubble|scatter)$/.test(v) ? "circle" : v);

				if ($$.hasType(v) || $$.hasTypeOf(type)) {
					const chart = main.select(`.${CLASS.chart}`);
					const chartClassName = CLASS[`chart${type}s`];

					if (chart.select(`.${chartClassName}`).empty()) {
						chart
							.append("g")
							.attr("class", chartClassName);
					}
				}
			});
		});

		// Add extent rect for Brush
		const brush = main.append("g")
			.attr("clip-path", clipPath)
			.attr("class", CLASS.brush)
			.style("pointer-events", $$.isSubchartBrushEnabled() ? null : "none")
			.call($$.brush);

		config.subchart_showHandle && $$.addBrushHandle(brush);

		subchart.eventRect = main.append("g")
			.attr("clip-path", clipPath)
			.attr("class", `${CLASS.eventRects} ${CLASS.eventRects}-subchart`)
			.style("fill-opacity", "0")
			.style("pointer-events", $$.isSubchartBrushEnabled() ? "none" : "all")
			.append("rect")
			.attr("class", `${CLASS.eventRect} ${CLASS.eventRect}-subchart`)
			.attr("width", $$.state.width2)
			.attr("height", $$.state.height2);

		$$.bindSubchartEventRect(subchart.eventRect);

		main.append("g")
			.attr("clip-path", clipPath)
			.attr("class", CLASS.xgridFocus)
			.append("line")
			.attr("class", CLASS.xgridFocus)
			.style("visibility", "hidden");

		// ATTENTION: This must be called AFTER chart added
		// Add Axis
		axis.subX = main.append("g")
			.attr("class", CLASS.axisX)
			.attr("transform", $$.getTranslate("subX"))
			.attr("clip-path", config.axis_rotated ? "" : clip.pathXAxis)
			.style("visibility", config.subchart_axis_x_show ? visibility : "hidden");

		axis.subY = main.append("g")
			.attr("class", CLASS.axisY)
			.attr("transform", $$.getTranslate("subY"))
			.style("visibility", config.subchart_axis_y_show ? visibility : "hidden");

		axis.subY2 = main.append("g")
			.attr("class", CLASS.axisY2)
			.attr("transform", $$.getTranslate("subY2"))
			.style("visibility", config.subchart_axis_y2_show ? visibility : "hidden");
	},

	/**
	 * Add brush handle
	 * Enabled when: subchart.showHandle=true
	 * @param {d3Selection} brush Brush selection
	 * @private
	 */
	addBrushHandle(brush): void {
		const $$ = this;
		const {config} = $$;
		const isRotated = config.axis_rotated;
		const initRange = config.subchart_init_range;
		const customHandleClass = "handle--custom";

		const path = SUBCHART_BRUSH_HANDLE_PATH[isRotated ? "y" : "x"];

		$$.brush.handle = brush.selectAll(`.${customHandleClass}`)
			.data(isRotated ? [{type: "n"}, {type: "s"}] : [{type: "w"}, {type: "e"}])
			.enter()
			.append("path")
			.attr("class", customHandleClass)
			.attr("cursor", `${isRotated ? "ns" : "ew"}-resize`)
			.attr("d", d => path[/[se]/.test(d.type) ? "end" : "start"])
			.attr("display", initRange ? null : "none");
	},

	/**
	 * Update sub chart
	 * @param {object} targets $$.data.targets
	 * @private
	 */
	updateTargetsForSubchart(targets): void {
		const $$ = this;
		const {config, state, $el: {subchart: {main}}} = $$;

		if (config.subchart_show) {
			$$.withSubchartTypeContext(() => {
				SUBCHART_TYPES
					.filter(v => $$.hasType(v) || $$.hasTypeOf(capitalize(v)))
					.forEach(v => {
						const isPointType = /^(bubble|scatter)$/.test(v);
						const name = capitalize(isPointType ? "circle" : v);
						const chartClass = $$.getChartClass(name, true);
						const shapeClass = $$.getClass(isPointType ? "circles" : `${v}s`, true);

						const shapeChart = main.select(`.${CLASS[`chart${`${name}s`}`]}`);

						if (isPointType) {
							const circle = shapeChart
								.selectAll(`.${CLASS.circles}`)
								.data(targets.filter($$[`is${capitalize(v)}Type`].bind($$)))
								.attr("class", shapeClass);

							circle.exit().remove();
							circle.enter().append("g")
								.attr("class", shapeClass);
						} else {
							const shapeUpdate = shapeChart
								.selectAll(`.${CLASS[`chart${name}`]}`)
								.attr("class", chartClass)
								.data(targets.filter($$[`is${name}Type`].bind($$)));

							const shapeEnter = shapeUpdate.enter()
								.append("g")
								.style("opacity", "0")
								.attr("class", chartClass)
								.append("g")
								.attr("class", shapeClass);

							shapeUpdate.exit().remove();

							// Area
							v === "line" && $$.hasTypeOf("Area") &&
								shapeEnter.append("g").attr("class", $$.getClass("areas", true));
						}
					});
			});

			// -- Brush --//
			main.selectAll(`.${CLASS.brush} rect`)
				.attr(config.axis_rotated ? "width" : "height",
					config.axis_rotated ? state.width2 : state.height2);

			$$.$el.subchart.eventRect
				?.attr("width", state.width2)
				.attr("height", state.height2)
				.style("pointer-events", $$.isSubchartBrushEnabled() ? "none" : "all");
		}
	},

	/**
	 * Update subchart y domains using subchart type options.
	 * @param {object} targets Targets to show
	 * @private
	 */
	updateSubchartYDomain(targets): void {
		const $$ = this;
		const {scale} = $$;
		const targetsToShow = targets || $$.filterTargetsToShow($$.data.targets);

		scale.subY?.domain($$.getYDomain(targetsToShow, "y"));
		scale.subY2?.domain($$.getYDomain(targetsToShow, "y2"));
	},

	/**
	 * Redraw subchart.
	 * @private
	 * @param {boolean} withSubchart whether or not to show subchart
	 * @param {number} duration duration
	 */
	redrawSubchart(withSubchart: boolean, duration: number): void {
		const $$ = this;
		const {config, $el: {subchart: {main}}, state} = $$;
		const withTransition = !!duration;

		main.style("visibility", config.subchart_show ? null : "hidden");
		main.select(`.${CLASS.brush}`)
			.style("pointer-events", $$.isSubchartBrushEnabled() ? null : "none");
		$$.$el.subchart.eventRect
			?.style("pointer-events", $$.isSubchartBrushEnabled() ? "none" : "all");
		$$.$el.axis.subY
			?.style("visibility", config.subchart_axis_y_show ? null : "hidden");
		$$.$el.axis.subY2
			?.style("visibility", config.subchart_axis_y2_show ? null : "hidden");

		// subchart
		if (config.subchart_show) {
			// reflect main chart to extent on subchart if zoomed
			if (state.event?.type === "zoom") {
				$$.brush.update();
			}

			// update subchart elements if needed
			if (withSubchart) {
				const initRange = config.subchart_init_range;

				// extent rect
				!brushEmpty($$) && $$.brush.update();

				$$.withSubchartTypeContext(() => {
					const targetsToShow = state._targetsToShow ||
						$$.filterTargetsToShow($$.data.targets);

					$$.updateSubchartYDomain(targetsToShow);

					const subchartShape = $$.getDrawShape();

					Object.keys(subchartShape.type).forEach(v => {
						const name = capitalize(v);
						const drawFn = $$[`generateDraw${name}`](subchartShape.indices[v], true);

						// call shape's update & redraw method
						$$[`update${name}`](withTransition, true);
						$$[`redraw${name}`](drawFn, withTransition, true);
					});

					if ($$.hasType("bubble") || $$.hasType("scatter")) {
						const {cx} = subchartShape.pos;
						const cy = $$.updateCircleY(true);

						$$.updateCircle(true);
						$$.redrawCircle(cx, cy, withTransition, undefined, true);
					}
				});

				if (!state.rendered && initRange) {
					state.domain = initRange;

					$$.brush.move(
						$$.brush.getSelection(),
						initRange.map($$.scale.x)
					);
				}
			}
		}
	},

	/**
	 * Show a focus grid line on subchart following main chart focus.
	 * @param {Array} data Selected data
	 * @private
	 */
	showSubchartGridFocus(data): void {
		const $$ = this;
		const {config, state, $el: {subchart: {main}}} = $$;

		if (
			!main ||
			!config.subchart_show ||
			$$.isSubchartBrushEnabled() ||
			config.grid_focus_show === false ||
			config.subchart_grid_focus === false ||
			!config.tooltip_show ||
			config.axis_tooltip
		) {
			return;
		}

		const focusData = Array.isArray(data) ? data : [data];
		const focus = focusData.find(d => d && $$.getBaseValue(d) != null);

		if (!focus) {
			$$.hideSubchartGridFocus();
			return;
		}

		const pos = $$.scale.subX(focus.x);

		if (!Number.isFinite(pos)) {
			$$.hideSubchartGridFocus();
			return;
		}

		const line = main.select(`g.${CLASS.xgridFocus} line.${CLASS.xgridFocus}`);

		if (showContinuousSubchartGridFocus($$)) {
			line.style("visibility", "hidden");
			return;
		}

		syncSubchartGridFocusStyle($$, line);

		config.axis_rotated ?
			line
				.attr("x1", 0)
				.attr("x2", state.width2)
				.attr("y1", pos)
				.attr("y2", pos) :
			line
				.attr("x1", pos)
				.attr("x2", pos)
				.attr("y1", 0)
				.attr("y2", state.height2);

		line.style("visibility", null);
	},

	/**
	 * Hide subchart focus grid line.
	 * @private
	 */
	hideSubchartGridFocus(): void {
		hideContinuousSubchartGridFocus(this);

		this.$el.subchart.main
			?.select(`g.${CLASS.xgridFocus} line.${CLASS.xgridFocus}`)
			.style("visibility", "hidden");
	},

	/**
	 * Redraw the brush.
	 * @param {boolean} [callCallbck=true] Call 'onbrush' callback or not.
	 * @private
	 */
	redrawForBrush(callCallbck = true): void {
		const $$ = this;
		const {
			config: {
				subchart_onbrush: onBrush,
				zoom_rescale: withY
			},
			scale,
			state
		} = $$;

		$$.redraw({
			withTransition: false,
			withY,
			withSubchart: false,
			withUpdateXDomain: true,
			withDimension: false
		});

		callCallbck && state.rendered &&
			onBrush.bind($$.api)(state.domain ?? scale.x.orgDomain());
	},

	/**
	 * Transform context
	 * @param {boolean} withTransition indicates transition is enabled
	 * @param {object} transitions The return value of the generateTransitions method of Axis.
	 * @private
	 */
	transformContext(withTransition, transitions): void {
		const $$ = this;
		const {$el: {subchart}, $T} = $$;

		const subXAxis = transitions?.axisSubX ?
			transitions.axisSubX :
			$T(subchart.main.select(`.${CLASS.axisX}`), withTransition);
		const subYAxis = transitions?.axisSubY ?
			transitions.axisSubY :
			$T(subchart.main.select(`.${CLASS.axisY}`), withTransition);
		const subY2Axis = transitions?.axisSubY2 ?
			transitions.axisSubY2 :
			$T(subchart.main.select(`.${CLASS.axisY2}`), withTransition);

		subchart.main.attr("transform", $$.getTranslate("context"));
		subXAxis.attr("transform", $$.getTranslate("subX"));
		subYAxis.attr("transform", $$.getTranslate("subY"));
		subY2Axis.attr("transform", $$.getTranslate("subY2"));
	}
};
