/**
 * Copyright (c) 2017 ~ present NAVER Corp.
 * billboard.js project is licensed under the MIT license
 */
import {transition as d3Transition} from "d3-transition";
import {$COMMON, $SELECT, $TEXT} from "../../config/classes";
import {KEY} from "../../module/Cache";
import {generateWait} from "../../module/generator";
import {callFn, capitalize, getOption, isTabVisible, notEmpty} from "../../module/util";

export default {
	redraw(options: any = {}): void {
		const $$ = this;
		const {config, state, $el} = $$;
		const {main, treemap} = $el;

		state.redrawing = true;

		// Increment generation counters for cache invalidation
		state.redrawGeneration++;

		if (state.dirty.data || state.dirty.visibility || options.initializing) {
			state.dataGeneration++;
		}

		// Invalidate per-redraw caches (only when size changed or initializing)
		if (options.initializing || state.dirty.size || state.dirty.data || !state.rendered) {
			$$.cache.remove([KEY.svgLeft]);
		}

		const targetsToShow = $$.filterTargetsToShow($$.data.targets);

		// Cache for reuse by sub-methods within this redraw cycle
		state._targetsToShow = targetsToShow;

		// Capture and reset dirty flags immediately to avoid race conditions
		// (async afterRedraw could wipe flags set by a subsequent redraw)
		const dirtySnapshot = {
			data: state.dirty.data,
			visibility: state.dirty.visibility,
			size: state.dirty.size
		};

		state.dirty.data = false;
		state.dirty.visibility = false;
		state.dirty.size = false;

		const {flow, initializing} = options;
		const wth = $$.getWithOption(options);
		const duration = wth.Transition ? config.transition_duration : 0;
		const durationForExit = wth.TransitionForExit ? duration : 0;
		const durationForAxis = wth.TransitionForAxis ? duration : 0;
		const transitions = $$.axis?.generateTransitions(durationForAxis);
		// Shape DOM updates (D3 data join: enter/update/exit) are only needed when data or
		// visibility changes. Zoom/brush only need redraw*() (position recalculation via
		// getRedrawList), not update*(). New APIs that modify data must set dirty flags.
		const needShapeUpdate = dirtySnapshot.data || dirtySnapshot.visibility || initializing;

		if (state.isCanvasMode) {
			$$.setContainerSize();
			$$.updateHtmlLegend?.();
		}

		$$.updateSizes(initializing);

		if (state.isCanvasMode) {
			const zoomScale = $$.scale.zoom;
			const zoomDomain = zoomScale?.domain();

			zoomScale && ($$.scale.zoom = null);
			$$.updateScales(initializing, wth.UpdateXDomain);

			if (zoomScale) {
				zoomScale.range($$.scale.x.range());
				zoomDomain && zoomScale.domain(zoomDomain);
				$$.scale.zoom = $$.getCustomizedXScale(zoomScale);
				$$.axis.x.scale($$.scale.zoom);
				$$.scale.x.domain(config.zoom_rescale ? zoomDomain : $$.org.xDomain);
				$$.scale.subX.domain($$.org.xDomain);
			}

			state.hasAxis && $$.axis.syncAxisDomains(targetsToShow, wth, flow);
			state.hasAxis && $$.applyCanvasSubchartDomain?.();

			const shape = needShapeUpdate ?
				$$.getDrawShape() :
				(state._cachedDrawShape || $$.getDrawShape());

			if (needShapeUpdate) {
				state._cachedDrawShape = shape;
			}

			$$.updateHtmlLegend?.();
			$$.resizeCanvas?.();

			state.canvasFocusKey = null;
			$$.renderCanvasFrame(shape, null, true);

			initializing && $$.updateTypesElements();
			$$.callPluginHook("$redraw", options, 0);

			state.redrawing = false;
			state._targetsToShow = null;
			state._cachedDrawShape = null;

			$$.mapToIds($$.data.targets).forEach(id => {
				state.withoutFadeIn[id] = true;
			});

			callFn(config.onrendered, $$.api);
			return;
		}

		// update legend and transform each g
		if (wth.Legend && config.legend_show) {
			options.withTransition = !!duration;
			!treemap && $$.updateLegend($$.mapToIds($$.data.targets), options, transitions);
		} else if (wth.Dimension) {
			// need to update dimension (e.g. axis.y.tick.values) because y tick values should change
			// no need to update axis in it because they will be updated in redraw()
			$$.updateDimension(true);
		}

		// Data empty label positioning and text.
		config.data_empty_label_text && main.select(`text.${$TEXT.text}.${$COMMON.empty}`)
			.attr("x", state.width / 2)
			.attr("y", state.height / 2)
			.text(config.data_empty_label_text)
			.style("display", targetsToShow.length ? "none" : null);

		// title - position early so other elements can calculate correct padding
		$$.redrawTitle?.();

		// update axis
		if (state.hasAxis) {
			// @TODO: Make 'init' state to be accessible everywhere not passing as argument.
			$$.axis.redrawAxis(targetsToShow, wth, transitions, flow, initializing);

			// grid (optional module — guarded for tree-shakable grid resolver)
			$$.hasGrid?.() && $$.updateGrid();

			// rect for regions (optional module — updateRegion installed by regions resolver)
			config.regions.length && $$.updateRegion?.();

			["bar", "candlestick", "line", "area"].forEach(v => {
				const name = capitalize(v);

				if ((/^(line|area)$/.test(v) && $$.hasTypeOf(name)) || $$.hasType(v)) {
					if (needShapeUpdate) {
						$$[`update${name}`](wth.TransitionForExit);
					}
				}
			});

			// circles for select
			$el.text && main.selectAll(`.${$SELECT.selectedCircles}`)
				.filter($$.isBarType.bind($$))
				.selectAll("circle")
				.remove();

			// event rects will redrawn when flow called
			if (config.interaction_enabled && !flow && wth.EventRect) {
				$$.redrawEventRect();
				$$.bindZoomEvent?.();
			}
		} else {
			// arc
			$el.arcs && $$.redrawArc(duration, durationForExit, wth.Transform);

			// radar
			$el.radar && $$.redrawRadar();

			// polar
			$el.polar && $$.redrawPolar();

			// funnel
			$el.funnel && $$.redrawFunnel();

			// treemap
			treemap && $$.updateTreemap(durationForExit);
		}

		if (!state.resizing && !treemap && ($$.hasPointType() || state.hasRadar)) {
			if (needShapeUpdate) {
				$$.updateCircle();
			}
		} else if ($$.hasLegendDefsPoint?.()) {
			$$.data.targets.forEach($$.point("create", this));
		}

		// text
		if ($$.hasDataLabel() && !$$.hasArcType(null, ["radar"])) {
			if (needShapeUpdate) {
				$$.updateText();
			}
		}

		initializing && $$.updateTypesElements();

		$$.generateRedrawList(targetsToShow, flow, duration, wth.Subchart, needShapeUpdate);
		$$.updateTooltipOnRedraw();

		$$.callPluginHook("$redraw", options, duration);
	},

	/**
	 * Generate redraw list
	 * @param {object} targets targets data to be shown
	 * @param {object} flow flow object
	 * @param {number} duration duration value
	 * @param {boolean} withSubchart whether or not to show subchart
	 * @param {boolean} needShapeRegen whether to regenerate draw shape
	 * @private
	 */
	generateRedrawList(targets, flow: any, duration: number, withSubchart: boolean,
		needShapeRegen: boolean = true): void {
		const $$ = this;
		const {config, state} = $$;
		const shape = needShapeRegen ?
			$$.getDrawShape() :
			(state._cachedDrawShape || $$.getDrawShape());

		if (needShapeRegen) {
			state._cachedDrawShape = shape;
		}

		if (state.hasAxis) {
			// subchart
			config.subchart_show && $$.redrawSubchart(withSubchart, duration, shape);
		}

		// generate flow
		const flowFn = flow && $$.generateFlow({
			targets,
			flow,
			duration: flow.duration,
			shape,
			xv: $$.xv.bind($$)
		});
		const withTransition = (duration || flowFn) && isTabVisible();

		// redraw list
		const redrawList = $$.getRedrawList(shape, flow, flowFn, withTransition);

		// callback function after redraw ends
		const afterRedraw = () => {
			flowFn && flowFn();

			state.redrawing = false;
			state._targetsToShow = null;
			state._cachedDrawShape = null;

			callFn(config.onrendered, $$.api);
		};

		// Only use transition when current tab is visible.
		if (withTransition && redrawList.length) {
			// Wait for end of transitions for callback
			const waitForDraw = generateWait();

			// transition should be derived from one transition
			d3Transition().duration(duration)
				.each(() => {
					redrawList
						.flatMap(t1 => t1)
						.forEach(t => waitForDraw.add(t));
				})
				.call(waitForDraw, afterRedraw);
		} else if (!state.transiting) {
			afterRedraw();
		}

		// update fadein condition
		$$.mapToIds($$.data.targets).forEach(id => {
			state.withoutFadeIn[id] = true;
		});
	},

	getRedrawList(shape, flow, flowFn, withTransition: boolean): Function[] {
		const $$ = <any>this;
		const {config, state: {hasAxis, hasRadar, hasTreemap}, $el: {grid}} = $$;
		const {cx, cy, xForText, yForText} = shape.pos;
		const list: Function[] = [];

		if (hasAxis) {
			if ($$.redrawGrid && (config.grid_x_lines.length || config.grid_y_lines.length)) {
				list.push($$.redrawGrid(withTransition));
			}

			if ($$.redrawRegion && config.regions.length) {
				list.push($$.redrawRegion(withTransition));
			}

			for (const v in shape.type) {
				const name = capitalize(v);
				const drawFn = shape.type[v];

				if ((/^(area|line)$/.test(v) && $$.hasTypeOf(name)) || $$.hasType(v)) {
					list.push($$[`redraw${name}`](drawFn, withTransition));
				}
			}

			!flow && grid.main && $$.updateGridFocus && list.push($$.updateGridFocus());
		}

		if (!$$.hasArcType() || hasRadar) {
			notEmpty(config.data_labels) && config.data_labels !== false &&
				list.push($$.redrawText(xForText, yForText, flow, withTransition));
		}

		if (($$.hasPointType() || hasRadar) && !$$.isPointFocusOnly()) {
			$$.redrawCircle && list.push($$.redrawCircle(cx, cy, withTransition, flowFn));
		}

		if (hasTreemap) {
			list.push($$.redrawTreemap(withTransition));
		}

		return list;
	},

	updateAndRedraw(options: any = {}): void {
		const $$ = this;
		const {config, state} = $$;
		let transitions;

		// same with redraw
		options.withTransition = getOption(options, "withTransition", true);
		options.withTransform = getOption(options, "withTransform", false);
		options.withLegend = getOption(options, "withLegend", false);

		// NOT same with redraw
		options.withUpdateXDomain = true;
		options.withUpdateOrgXDomain = true;
		options.withTransitionForExit = false;
		options.withTransitionForTransform = getOption(options, "withTransitionForTransform",
			options.withTransition);

		// MEMO: called in updateLegend in redraw if withLegend
		if (!(options.withLegend && config.legend_show)) {
			if (state.hasAxis) {
				transitions = $$.axis.generateTransitions(
					options.withTransitionForAxis ? config.transition_duration : 0
				);
			}

			// Update scales
			$$.updateScales();
			$$.updateSvgSize();

			// Update g positions
			$$.transformAll(options.withTransitionForTransform, transitions);
		}

		// Draw with new sizes & scales
		$$.redraw(options);
	}
};
