export type EventsStat = {
	/** application_id */
	a: string;
	// created time
	c: number;
	// id
	i: string;
	// new_status_id
	s?: string;
	/** Type */
	t: number;
};

export type Sides = 'left' | 'right' | 'bot' | 'top';

export type AxisProps = {
	title?: string;
	titleFontSize?: number;
	titlePos?: Sides;
};

export class Axis {
	private _title?: string;
	private _titleFontSize: number;
	private _titlePos: Sides;

	constructor({ title, titleFontSize, titlePos }: AxisProps = {}) {
		this._title = title;
		this._titleFontSize = titleFontSize ?? 15;
		this._titlePos = titlePos ?? 'left';
	}

	getPadding() {
		if (!this._title) return 0;
		return this._titleFontSize + 4;
	}

	apply_title(
		svg: d3.Selection<KnownAny, KnownAny, null, KnownAny>,
		height: number,
		width: number,
		padding: number
	) {
		if (!this._title) return undefined;
		if (this._titlePos === 'left') {
			return (
				svg
					.append('text')
					.attr('text-anchor', 'middle')
					.attr('transform', 'rotate(-90)')
					.attr('font-size', `${this._titleFontSize}px`)
					.attr('font-family', 'Open sans')
					// y becomes x
					.attr('x', -height / 2)
					.attr('y', -padding + this._titleFontSize + 4)
					.text(this._title)
			);
		}
		if (this._titlePos === 'right') {
			return (
				svg
					.append('text')
					.attr('text-anchor', 'middle')
					.attr('transform', 'rotate(90)')
					.attr('font-size', `${this._titleFontSize}px`)
					.attr('font-family', 'Open sans')
					// y becomes x
					.attr('x', height / 2)
					.attr('y', -width - padding + this._titleFontSize + 4)
					.text(this._title)
			);
		}
		if (this._titlePos === 'bot') {
			return svg
				.append('text')
				.attr('text-anchor', 'middle')
				.attr('font-size', `${this._titleFontSize}px`)
				.attr('font-family', 'Open sans')
				.attr('x', width / 2)
				.attr('y', height + padding - 4)
				.text(this._title);
		}
		if (this._titlePos === 'top') {
			return svg
				.append('text')
				.attr('text-anchor', 'middle')
				.attr('font-size', `${this._titleFontSize}px`)
				.attr('font-family', 'Open sans')
				.attr('x', width / 2)
				.attr('y', -padding + this._titleFontSize)
				.text(this._title);
		}

		console.error('Unknown title pos', this.titlePos);
		return undefined;
	}

	//
	// Builder pattern functions
	//
	title(title?: string) {
		this._title = title;
		return this;
	}

	titlePos(pos: Sides) {
		this._titlePos = pos;
		return this;
	}

	titleFontSize(size: number) {
		this._titleFontSize = size;
		return this;
	}

	enforcePos(pos: 'vert' | 'hoz', defaultPos: Sides) {
		if (pos === 'vert') {
			if (this._titlePos !== 'top' && this._titlePos !== 'bot') {
				return this.titlePos(defaultPos);
			}
		} else {
			if (this._titlePos !== 'left' && this._titlePos !== 'right') {
				return this.titlePos(defaultPos);
			}
		}
		return this;
	}
}

export type EnforceSizeType = [number, number];

export function enforceSizeHelper<H extends Record<NamesH, number>, NamesH extends string = ''>(
	width: number,
	height: number,
	enforce: boolean
): EnforceHelper<NamesH, H> {
	const c = document.createElement('canvas');
	const ctx = c.getContext('2d');
	if (!ctx) throw new Error('Failed to get ctx');

	const r = {
		_width: width,
		_height: height,
		_enforce: enforce,
		_w_p: 0,

		_h_indicators: [] as Indicator<NamesH>[]
	} as EnforceHelper<NamesH, H>;

	r.h = (name, h, type, data, font) => {
		const nr = r as EnforceHelper<NamesH | typeof name, Record<NamesH | typeof name, number>>;

		// TODO maybe update the values
		if (nr[name]) {
			return r;
		}

		let size = 0;
		if (typeof h === 'string') {
			if (h.endsWith('rem')) {
				const rem = Number(h.substring(0, h.length - 3));
				if (Number.isNaN(rem)) {
					throw new Error('h is not a valid rem value');
				}
				size = rem * Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
			} else {
				const n = Number(h);
				if (Number.isNaN(n)) {
					throw new Error('h is not a number');
				}
				size = n;
			}
		} else {
			size = h;
		}

		if (size < 0) throw Error('h is negative');

		nr._h_indicators.push({
			name,
			size,
			type: type ?? 'fixed',
			data,
			font
		});

		(nr as KnownAny)[name] = size;

		return nr as KnownAny;
	};

	r.w_p = (h) => {
		let size = 0;
		if (typeof h === 'string') {
			if (h.endsWith('rem')) {
				const rem = Number(h.substring(0, h.length - 3));
				if (Number.isNaN(rem)) {
					throw new Error('h is not a valid rem value');
				}
				size = rem * Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
			} else {
				const n = Number(h);
				if (Number.isNaN(n)) {
					throw new Error('h is not a number');
				}
				size = n;
			}
		} else {
			size = h;
		}
		r._w_p = size * 2;
		return r;
	};

	r.enforce = (inEnforce) => {
		const e = inEnforce ?? r._enforce;
		if (!e) return r;

		let h_sum = 0;
		for (const i of r._h_indicators) {
			h_sum += i.size;
		}

		// TODO handle width
		if (h_sum < r._height && !r._h_indicators.some((i) => i.type === 'left')) return r;

		let fSize = r._h_indicators.reduce((acc, i) => {
			if (i.type !== 'fixed') return acc;
			return acc + i.size;
		}, 0);

		// you are fucked anyway
		if (fSize > r._height) return r;

		const h_leftover = h_sum - fSize;
		const th_leftover = r._height - fSize;

		const pr = r._h_indicators
			.filter((i) => i.type === 'dyanmic' || i.type === 'text')
			.map((i) => {
				return [i, (i.size / h_leftover) * th_leftover] as const;
			});

		for (const i of pr) {
			let s = i[1];
			if (i[0].type === 'text') {
				s = Math.floor(
					getFitSizeForList([i[0].data ?? ''], r._width - r._w_p, i[1], 0.5, ctx, i[0].font) ?? i[1]
				);
			}

			fSize += s;
			(r as KnownAny)[i[0].name] = s;
		}

		const left = r._h_indicators.filter((i) => i.type === 'left');
		const len_left = left.length;

		const rest = r._height - fSize;

		for (const i of left) {
			(r as KnownAny)[i.name] = rest / len_left;
		}

		return r as KnownAny;
	};

	r.toEnfoceSize = (name) => {
		if (!r._enforce) return;
		return [r._width - r._w_p, r[name]];
	};

	return r;
}
enforceSizeHelper.fromEnforceSize = (enforceSize?: EnforceSizeType) => {
	return enforceSizeHelper(enforceSize?.[0] ?? 0, enforceSize?.[1] ?? 0, !!enforceSize);
};

export type PaddingManagerProps = {
	width: number;
	height: number;
	fontSize?: number;
};

export type Paddable = number | Axis | string[] | string | undefined;

export class PaddingManager {
	canvas: HTMLCanvasElement;
	ctx: CanvasRenderingContext2D;

	width: number;
	height: number;

	private _paddingLeft = 0;
	private _paddingRight = 0;
	private _paddingTop = 0;
	private _paddingBot = 0;

	private _fontSize = 15;

	private _enforceSize?: EnforceSizeType;

	constructor({ width, height, fontSize }: PaddingManagerProps) {
		this.width = width;
		this.height = height;

		if (fontSize !== undefined) {
			this._fontSize = fontSize;
		}

		// This is used to calculate the size of text
		this.canvas = document.createElement('canvas');
		const ctx = this.canvas.getContext('2d');
		if (!ctx) {
			throw new Error('Failed to create context for the internal canvas');
		}
		this.ctx = ctx;
		this.ctx.font = `${this._fontSize}px Open sans`;
	}

	get paddedHeight() {
		return this.height + this._paddingTop + this._paddingBot;
	}

	get paddedWidth() {
		return this.width + this._paddingLeft + this._paddingRight;
	}

	set fontSize(size: number) {
		this._fontSize = size;
		this.ctx.font = `${this._fontSize}px Open sans`;
	}

	get fontSize() {
		return this._fontSize;
	}

	get left() {
		return this._paddingLeft;
	}

	get right() {
		return this._paddingRight;
	}

	get top() {
		return this._paddingTop;
	}

	get bot() {
		return this._paddingBot;
	}

	get translateString() {
		return `translate(${this.left},${this.top})`;
	}

	//
	// Add padding
	//
	pad(side: Sides, padding: Paddable, angle?: number) {
		let pn = 0;

		if (padding === undefined) {
			return this;
		}
		if (typeof padding === 'number') {
			pn = padding;
		} else if (typeof padding === 'string') {
			let a: number | undefined = undefined;
			if (angle !== undefined) {
				a = angle * (Math.PI / 180);
			}
			pn = this.ctx.measureText(padding).width * Math.sin(a ?? Math.PI / 2);
		} else if (Array.isArray(padding)) {
			pn = padding.reduce((acc, s) => Math.max(this.ctx.measureText(s).width, acc), 0);
		} else {
			pn = padding.getPadding();
		}

		switch (side) {
			case 'left':
				this._paddingLeft += pn;
				return this;
			case 'right':
				this._paddingRight += pn;
				return this;
			case 'top':
				this._paddingTop += pn;
				return this;
			case 'bot':
				this._paddingBot += pn;
				return this;
			default:
				throw new Error(`unknown side: ${side}`);
		}
	}

	padHoz(padding: Paddable) {
		return this.pad('left', padding).pad('right', padding);
	}

	padLeft(padding: Paddable, angle?: number) {
		return this.pad('left', padding, angle);
	}

	padRight(padding: Paddable, angle?: number) {
		return this.pad('right', padding, angle);
	}

	padTop(padding: Paddable, angle?: number) {
		return this.pad('top', padding, angle);
	}

	padBot(padding: Paddable, angle?: number) {
		return this.pad('bot', padding, angle);
	}

	resetPadding(side: Sides) {
		switch (side) {
			case 'left':
				this._paddingLeft = 0;
				return this;
			case 'right':
				this._paddingRight = 0;
				return this;
			case 'top':
				this._paddingTop = 0;
				return this;
			case 'bot':
				this._paddingBot = 0;
				return this;
			default:
				throw new Error(`unknown side: ${side}`);
		}
	}

	padAll(n: number) {
		this._paddingLeft += n;
		this._paddingRight += n;
		this._paddingTop += n;
		this._paddingBot += n;
		return this;
	}

	enforce(inEnforce?: EnforceSizeType) {
		const enforce = this._enforceSize ?? inEnforce;
		if (enforce === undefined) return this;

		if (this.paddedWidth !== enforce[0]) {
			this.width = enforce[0] - this.left - this.right;
		}
		if (this.paddedHeight !== enforce[1]) {
			this.height = enforce[1] - this.top - this.bot;
		}

		return this;
	}
}

export function countReducer(acc: Record<number, number>, a: { c: number | string | Date }) {
	const c = new Date(a.c);
	c.setHours(0);
	c.setMinutes(0);
	c.setSeconds(0);
	c.setMilliseconds(0);
	const ct = c.getTime();

	if (acc[ct]) {
		acc[ct] += 1;
	} else {
		acc[ct] = 1;
	}
	return acc;
}
