/**
 * Copyright (c) 2017 ~ present NAVER Corp.
 * billboard.js project is licensed under the MIT license
 */
import {
	scaleTime as d3ScaleTime,
	scaleUtc as d3ScaleUtc,
	scaleLinear as d3ScaleLinear,
	scaleLog as d3ScaleLog,
	scaleSymlog as d3ScaleSymlog
} from "d3-scale";
import {isString, isValue, parseDate} from "../../module/util";
import {IDataRow, IGridData} from "../data/IData";


/**
 * Get scale
 * @param {string} [type='linear'] Scale type
 * @param {number} [min] Min range
 * @param {number} [max] Max range
 * @returns {d3.scaleLinear|d3.scaleTime} scale
 * @private
 */
export function getScale(type = "linear", min = 0, max = 1): any {
	const scale = ({
		linear: d3ScaleLinear,
		log: d3ScaleSymlog,
		_log: d3ScaleLog,
		time: d3ScaleTime,
		utc: d3ScaleUtc
	})[type]();

	scale.type = type;
	/_?log/.test(type) && scale.clamp(true);

	return scale.range([min, max]);
}

export default {
	/**
	 * Get x Axis scale function
	 * @param {number} min Min value
	 * @param {number} max Max value
	 * @param {Array} domain Domain value
	 * @param {Function} offset The offset getter to be sum
	 * @returns {Function} scale
	 * @private
	 */
	getXScale(min: number, max: number, domain: number[], offset: Function) {
		const $$ = this;
		const scale = $$.scale.zoom || getScale($$.axis.getAxisType("x"), min, max);

		return $$.getCustomizedScale(
			domain ? scale.domain(domain) : scale,
			offset
		);
	},

	/**
	 * Get y Axis scale function
	 * @param {string} id Axis id: 'y' or 'y2'
	 * @param {number} min Min value
	 * @param {number} max Max value
	 * @param {Array} domain Domain value
	 * @returns {Function} Scale function
	 * @private
	 */
	getYScale(id: "y" | "y2", min: number, max: number, domain: number[]): Function {
		const $$ = this;
		const scale = getScale($$.axis.getAxisType(id), min, max);

		domain && scale.domain(domain);

		return scale;
	},

	/**
	 * Get y Axis scale
	 * @param {string} id Axis id
	 * @param {boolean} isSub Weather is sub Axis
	 * @returns {Function} Scale function
	 * @private
	 */
	getYScaleById(id: string, isSub = false): Function {
		const isY2 = this.axis.getId(id) === "y2";
		const key = isSub ? (isY2 ? "subY2" : "subY") : (isY2 ? "y2" : "y");

		return this.scale[key];
	},

	/**
	 * Get customized scale
	 * @param {d3.scaleLinear|d3.scaleTime} scaleValue Scale function
	 * @param {Function} offsetValue Offset getter to be sum
	 * @returns {Function} Scale function
	 * @private
	 */
	getCustomizedScale(scaleValue: Function | any, offsetValue): Function {
		const $$ = this;
		const offset = offsetValue || (() => $$.axis.x.tickOffset());
		const scale = function(d, raw) {
			const v = scaleValue(d) + offset();

			return raw ? v : Math.ceil(v);
		};

		// copy original scale methods
		for (const key in scaleValue) {
			scale[key] = scaleValue[key];
		}

		scale.orgDomain = () => scaleValue.domain();
		scale.orgScale = () => scaleValue;

		// define custom domain() for categorized axis
		if ($$.axis.isCategorized()) {
			scale.domain = function(domainValue) {
				let domain = domainValue;

				if (!arguments.length) {
					domain = this.orgDomain();

					return [domain[0], domain[1] + 1];
				}

				scaleValue.domain(domain);

				return scale;
			};
		}

		return scale;
	},

	/**
	 * Update scale
	 * @param {boolean} isInit Param is given at the init rendering
	 * @param {boolean} updateXDomain If update x domain
	 * @private
	 */
	updateScales(isInit: boolean, updateXDomain = true): void {
		const $$ = this;
		const {axis, config, format, org, scale,
			state: {width, height, width2, height2, hasAxis}
		} = $$;

		if (hasAxis) {
			const isRotated = config.axis_rotated;
			const resettedPadding = $$.getResettedPadding(1);

			// update edges
			const min = {
				x: isRotated ? resettedPadding : 0,
				y: isRotated ? 0 : height,
				subX: isRotated ? 1 : 0,
				subY: isRotated ? 0 : height2
			};

			const max = {
				x: isRotated ? height : width,
				y: isRotated ? width : resettedPadding,
				subX: isRotated ? height : width,
				subY: isRotated ? width2 : 1
			};

			// update scales
			// x Axis
			const xDomain = updateXDomain && scale.x?.orgDomain();
			const xSubDomain = updateXDomain && org.xDomain;

			scale.x = $$.getXScale(min.x, max.x, xDomain, () => axis.x.tickOffset());
			scale.subX = $$.getXScale(min.x, max.x, xSubDomain, d => (d % 1 ? 0 : axis.subX.tickOffset()));

			format.xAxisTick = axis.getXAxisTickFormat();
			format.subXAxisTick = axis.getXAxisTickFormat(true);

			axis.setAxis("x", scale.x, config.axis_x_tick_outer, isInit);

			if (config.subchart_show) {
				axis.setAxis("subX", scale.subX, config.axis_x_tick_outer, isInit);
			}

			// y Axis
			scale.y = $$.getYScale("y", min.y, max.y, scale.y ? scale.y.domain() : config.axis_y_default);
			scale.subY = $$.getYScale(
				"y", min.subY, max.subY, scale.subY ? scale.subY.domain() : config.axis_y_default);

			axis.setAxis("y", scale.y, config.axis_y_tick_outer, isInit);

			// y2 Axis
			if (config.axis_y2_show) {
				scale.y2 = $$.getYScale("y2", min.y, max.y, scale.y2 ? scale.y2.domain() : config.axis_y2_default);
				scale.subY2 = $$.getYScale(
					"y2", min.subY, max.subY, scale.subY2 ? scale.subY2.domain() : config.axis_y2_default);

				axis.setAxis("y2", scale.y2, config.axis_y2_tick_outer, isInit);
			}
		} else {
			// update for arc
			$$.updateArc?.();
		}
	},

	/**
	 * Get the zoom or unzoomed scaled value
	 * @param {Date|number|object} d Data value
	 * @returns {number|null}
	 * @private
	 */
	xx(d: IDataRow): number | null {
		const $$ = this;
		const {config, scale: {x, zoom}} = $$;
		const fn = config.zoom_enabled && zoom ? zoom : x;

		return d ? fn(isValue(d.x) ? d.x : d) : null;
	},

	xv(d: IGridData): number {
		const $$ = this;
		const {axis, config, scale: {x, zoom}} = $$;
		const fn = config.zoom_enabled && zoom ? zoom : x;
		let value = $$.getBaseValue(d);

		if (axis.isTimeSeries()) {
			value = parseDate.call($$, value);
		} else if (axis.isCategorized() && isString(value)) {
			value = config.axis_x_categories.indexOf(value);
		}

		return Math.ceil(fn(value));
	},

	yv(d: IGridData): number {
		const $$ = this;
		const {scale: {y, y2}} = $$;
		const yScale = d.axis && d.axis === "y2" ? y2 : y;

		return Math.ceil(yScale($$.getBaseValue(d)));
	},

	subxx(d: IDataRow): number | null {
		return d ? this.scale.subX(d.x) : null;
	}
};
