/**
 * Copyright (c) 2017 ~ present NAVER Corp.
 * billboard.js project is licensed under the MIT license
 */
import {
	hierarchy as d3Hierarchy,
	treemap as d3Treemap,
	treemapBinary as d3TreemapBinary,
	treemapDice as d3TreemapDice,
	treemapResquarify as d3TreemapResquarify,
	treemapSlice as d3TreemapSlice,
	treemapSliceDice as d3TreemapSliceDice,
	treemapSquarify as d3TreemapSquarify
} from "d3-hierarchy";
import {select as d3Select} from "d3-selection";
import type {d3Selection} from "../../../types/types";
import {$COMMON, $TREEMAP} from "../../config/classes";
import {getRandom, isFunction} from "../../module/util";
import type {IData, IDataRow, ITreemapData} from "../data/IData";
import {meetsLabelThreshold} from "../internals/text.util";

/**
 * Get treemap elements' position
 * @param {d3Selection} group Root selection
 * @param {object} root Root data
 * @private
 */
function position(group, root): void {
	const $$ = this;
	const {scale: {x, y}, state: {width}} = $$;

	group.selectAll("g")
		.attr("transform", d => (
			`translate(${d === root ? "0,0" : `${x(d.x0)},${y(d.y0)}`})`
		))
		.select("rect")
		.attr("width", d => (
			d === root ? width : x(d.x1) - x(d.x0)
		))
		.attr("height", d => (
			d === root ? 0 : y(d.y1) - y(d.y0)
		));
}

/**
 * Convert data for treemap hierarchy
 * @param {object} data Data object
 * @returns {Array} Array of data for treemap hierarchy
 * @private
 */
function convertDataToTreemapData(data: IData[]): ITreemapData[] {
	const $$ = this;

	return data.map(d => {
		const {id, values} = d;
		const {value} = values[0];

		return {
			name: id,
			id, // needed to keep compatibility on whole code logic
			value,
			ratio: $$.getRatio("treemap", values[0])
		} as ITreemapData;
	});
}

/**
 * Get hierarchy data
 * @param {object} data Data object
 * @returns {Array} Array of hierarchy data
 * @private
 */
function getHierachyData(data) {
	const $$ = this;
	const hierarchyData = d3Hierarchy(data).sum(d => d.value);
	const sortFn = $$.getSortCompareFn(true);

	return [
		$$.treemap(
			sortFn ? hierarchyData.sort(sortFn) : hierarchyData
		)
	];
}

export default {
	initTreemap(): void {
		const $$ = this;
		const {
			$el,
			state: {
				current: {width, height},
				clip,
				datetimeId
			}
		} = $$;

		clip.id = `${datetimeId}-clip`;

		$$.treemap = d3Treemap()
			.tile($$.getTreemapTile());

		$el.defs
			.append("clipPath")
			.attr("id", clip.id)
			.append("rect")
			.attr("width", width)
			.attr("height", height);

		$el.treemap = $el.main.select(`.${$COMMON.chart}`)
			.attr("clip-path", `url(#${clip.id})`)
			.append("g")
			.classed($TREEMAP.chartTreemaps, true);

		$$.bindTreemapEvent();
	},

	/**
	 * Bind events
	 * @private
	 */
	bindTreemapEvent(): void {
		const $$ = this;
		const {$el, config, state} = $$;
		const getTarget = event => {
			const target = event.isTrusted ? event.target : state.eventReceiver.rect?.node();
			let data;

			if (/^rect$/i.test(target.tagName)) {
				state.event = event;
				data = d3Select(target).datum();
			}

			return data?.data;
		};

		if (config.interaction_enabled) {
			const isTouch = state.inputType === "touch";

			$el.treemap
				.on(isTouch ? "touchstart" : "mouseover mousemove", event => {
					const data = getTarget(event);

					if (data) {
						$$.showTooltip([data], event.currentTarget);
						/^(touchstart|mouseover)$/.test(event.type) && $$.setOverOut(true, data);
					}
				}, isTouch ? {passive: true} : undefined)
				.on(isTouch ? "touchend" : "mouseout", event => {
					const data = getTarget(event);

					if (config.interaction_onout) {
						$$.hideTooltip();
						$$.setOverOut(false, data);
					}
				});
		}
	},

	/**
	 * Get tiling function
	 * @returns {function}
	 * @private
	 */
	getTreemapTile() {
		const $$ = this;
		const {config, state: {current: {width, height}}} = $$;
		const tile = {
			binary: d3TreemapBinary,
			dice: d3TreemapDice,
			slice: d3TreemapSlice,
			sliceDice: d3TreemapSliceDice,
			squarify: d3TreemapSquarify,
			resquarify: d3TreemapResquarify
		}[config.treemap_tile ?? "binary"] ?? d3TreemapBinary;

		return (node, x0, y0, x1, y1) => {
			tile(node, 0, 0, width, height);

			for (const child of node.children) {
				child.x0 = x0 + child.x0 / width * (x1 - x0);
				child.x1 = x0 + child.x1 / width * (x1 - x0);
				child.y0 = y0 + child.y0 / height * (y1 - y0);
				child.y1 = y0 + child.y1 / height * (y1 - y0);
			}
		};
	},

	/**
	 * Get treemap hierarchy data
	 * @param {Array} targets Data targets
	 * @returns {object}
	 * @private
	 */
	getTreemapData(targets: IData[]): ITreemapData {
		const $$ = this;

		return {
			name: "root",
			children: convertDataToTreemapData.bind($$)(
				$$.filterTargetsToShow(targets.filter($$.isTreemapType, $$))
			)
		};
	},

	/**
	 * Update treemap data
	 * @param {Array} targets Data targets
	 * @private
	 */
	updateTargetsForTreemap(targets: IData): void {
		const $$ = this;
		const {$el: {treemap}} = $$;
		const treemapData = getHierachyData.call($$, $$.getTreemapData(targets ?? $$.data.targets));

		// using $el.treemap reference can alter data, so select treemap <g> again
		treemap.data($$.filterNullish(treemapData));
	},

	/**
	 * Render treemap
	 * @param {number} durationForExit Duration for exit transition
	 * @private
	 */
	updateTreemap(durationForExit: number): void {
		const $$ = this;
		const {$el, $T} = $$;
		const data = $el.treemap.datum();
		const classChartTreemap = $$.getChartClass("Treemap");
		const classTreemap = $$.getClass("treemap", true);

		const treemap = $el.treemap
			.selectAll("g")
			.data(data.children);

		$T(treemap.exit(), durationForExit)
			.style("opacity", "0")
			.remove();

		treemap.enter()
			.append("g")
			.append("rect");

		$el.treemap.selectAll("g")
			.attr("class", classChartTreemap)
			.select("rect")
			.attr("class", classTreemap)
			.attr("fill", d => $$.color(d.data.name));
	},

	/**
	 * Generate treemap coordinate points data
	 * @returns {Array} Array of coordinate points
	 * @private
	 */
	generateGetTreemapPoints(): (d: IDataRow) => [number, number][] {
		const $$ = this;
		const {$el, scale: {x, y}} = $$;
		const points = {};

		$el.treemap.selectAll("g").each(d => {
			points[d.data.name] = [
				[x(d.x0), y(d.y0)],
				[x(d.x1), y(d.y1)]
			];
		});

		return d => points[d.id];
	},

	/**
	 * Redraw treemap
	 * @param {boolean} withTransition With or without transition
	 * @returns {Array} Selections
	 * @private
	 */
	redrawTreemap(withTransition?: boolean): d3Selection[] {
		const $$ = this;
		const {$el, state: {current: {width, height}}} = $$;

		// update defs
		$el.defs.select("rect")
			.attr("width", width)
			.attr("height", height);

		return [
			$$.$T($el.treemap, withTransition, getRandom())
				.call(position.bind($$), $el.treemap.datum())
		];
	},

	/**
	 * Get treemap data label format function
	 * @param {object} d Data object
	 * @returns {function} Label formatter function
	 * @private
	 */
	treemapDataLabelFormat(d: IDataRow): Function {
		const $$ = this;
		const {$el: {treemap}, config, scale: {x, y}} = $$;
		const {id, value} = d;
		const format = config.treemap_label_format;
		const ratio = $$.getRatio("treemap", d);
		const percentValue = (ratio * 100).toFixed(2);
		const meetLabelThreshold =
			config.treemap_label_show && meetsLabelThreshold.call($$, ratio, "treemap") ?
				null :
				"0";

		// Get treemap dimensions for the specific data
		const treemapNode = treemap.selectAll("g")
			.filter(node => node.data.id === id)
			.datum();

		let width = 0;
		let height = 0;

		if (treemapNode) {
			const {x0, x1, y0, y1} = treemapNode;
			width = x(x1) - x(x0);
			height = y(y1) - y(y0);
		}

		return function(node) {
			node.style("opacity", meetLabelThreshold);

			return isFunction(format) ?
				format.bind($$.api)(value, ratio, id, {width, height}) :
				`${id}\n${percentValue}%`;
		};
	}
};
