<template>
<div class="cbbedffa">
	<canvas ref="chartEl"></canvas>
	<div v-if="fetching" class="fetching">
		<MkLoading/>
	</div>
</div>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref, watch, PropType, onUnmounted, shallowRef } from 'vue';
import {
	Chart,
	ArcElement,
	LineElement,
	BarElement,
	PointElement,
	BarController,
	LineController,
	CategoryScale,
	LinearScale,
	TimeScale,
	Legend,
	Title,
	Tooltip,
	SubTitle,
	Filler,
} from 'chart.js';
import 'chartjs-adapter-date-fns';
import { enUS } from 'date-fns/locale';
import zoomPlugin from 'chartjs-plugin-zoom';
import gradient from 'chartjs-plugin-gradient';
import * as os from '@/os';
import { defaultStore } from '@/store';
import MkChartTooltip from '@/components/chart-tooltip.vue';

Chart.register(
	ArcElement,
	LineElement,
	BarElement,
	PointElement,
	BarController,
	LineController,
	CategoryScale,
	LinearScale,
	TimeScale,
	Legend,
	Title,
	Tooltip,
	SubTitle,
	Filler,
	zoomPlugin,
	gradient,
);

const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
const negate = arr => arr.map(x => -x);
const alpha = (hex, a) => {
	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
	const r = parseInt(result[1], 16);
	const g = parseInt(result[2], 16);
	const b = parseInt(result[3], 16);
	return `rgba(${r}, ${g}, ${b}, ${a})`;
};

const colors = {
	blue: '#008FFB',
	green: '#00E396',
	yellow: '#FEB019',
	red: '#FF4560',
	purple: '#e300db',
};
const colorSets = [colors.blue, colors.green, colors.yellow, colors.red, colors.purple];
const getColor = (i) => {
	return colorSets[i % colorSets.length];
};

export default defineComponent({
	props: {
		src: {
			type: String,
			required: true,
		},
		args: {
			type: Object,
			required: false,
		},
		limit: {
			type: Number,
			required: false,
			default: 90
		},
		span: {
			type: String as PropType<'hour' | 'day'>,
			required: true,
		},
		detailed: {
			type: Boolean,
			required: false,
			default: false
		},
		stacked: {
			type: Boolean,
			required: false,
			default: false
		},
		bar: {
			type: Boolean,
			required: false,
			default: false
		},
		aspectRatio: {
			type: Number,
			required: false,
			default: null
		},
	},

	setup(props) {
		const now = new Date();
		let chartInstance: Chart = null;
		let data: {
			series: {
				name: string;
				type: 'line' | 'area';
				color?: string;
				borderDash?: number[];
				hidden?: boolean;
				data: {
					x: number;
					y: number;
				}[];
			}[];
		} = null;

		const chartEl = ref<HTMLCanvasElement>(null);
		const fetching = ref(true);

		const getDate = (ago: number) => {
			const y = now.getFullYear();
			const m = now.getMonth();
			const d = now.getDate();
			const h = now.getHours();

			return props.span === 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
		};

		const format = (arr) => {
			return arr.map((v, i) => ({
				x: getDate(i).getTime(),
				y: v
			}));
		};

		const tooltipShowing = ref(false);
		const tooltipX = ref(0);
		const tooltipY = ref(0);
		const tooltipTitle = ref(null);
		const tooltipSeries = ref(null);
		let disposeTooltipComponent;

		os.popup(MkChartTooltip, {
			showing: tooltipShowing,
			x: tooltipX,
			y: tooltipY,
			title: tooltipTitle,
			series: tooltipSeries,
		}, {}).then(({ dispose }) => {
			disposeTooltipComponent = dispose;
		});

		function externalTooltipHandler(context) {
			if (context.tooltip.opacity === 0) {
				tooltipShowing.value = false;
				return;
			}

			tooltipTitle.value = context.tooltip.title[0];
			tooltipSeries.value = context.tooltip.body.map((b, i) => ({
				backgroundColor: context.tooltip.labelColors[i].backgroundColor,
				borderColor: context.tooltip.labelColors[i].borderColor,
				text: b.lines[0],
			}));

			const rect = context.chart.canvas.getBoundingClientRect();

			tooltipShowing.value = true;
			tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX;
			tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY;
		}

		const render = () => {
			if (chartInstance) {
				chartInstance.destroy();
			}

			const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
			const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';

			// フォントカラー
			Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');

			const maxes = data.series.map((x, i) => Math.max(...x.data.map(d => d.y)));

			chartInstance = new Chart(chartEl.value, {
				type: props.bar ? 'bar' : 'line',
				data: {
					labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
					datasets: data.series.map((x, i) => ({
						parsing: false,
						label: x.name,
						data: x.data.slice().reverse(),
						tension: 0.3,
						pointRadius: 0,
						borderWidth: 2,
						borderColor: x.color ? x.color : getColor(i),
						borderDash: x.borderDash || [],
						borderJoinStyle: 'round',
						backgroundColor: alpha(x.color ? x.color : getColor(i), 0.1),
						gradient: {
							backgroundColor: {
								axis: 'y',
								colors: {
									0: alpha(x.color ? x.color : getColor(i), 0),
									[maxes[i]]: alpha(x.color ? x.color : getColor(i), 0.1),
								},
							},
						},
						barPercentage: 0.9,
						categoryPercentage: 0.9,
						fill: x.type === 'area',
						clip: 8,
						hidden: !!x.hidden,
					})),
				},
				options: {
					aspectRatio: props.aspectRatio || 2.5,
					layout: {
						padding: {
							left: 0,
							right: 8,
							top: 0,
							bottom: 0,
						},
					},
					scales: {
						x: {
							type: 'time',
							stacked: props.stacked,
							time: {
								stepSize: 1,
								unit: props.span === 'day' ? 'month' : 'day',
							},
							grid: {
								color: gridColor,
								borderColor: 'rgb(0, 0, 0, 0)',
							},
							ticks: {
								display: props.detailed,
								maxRotation: 0,
								autoSkipPadding: 16,
							},
							adapters: {
								date: {
									locale: enUS,
								},
							},
							min: getDate(props.limit).getTime(),
						},
						y: {
							position: 'left',
							stacked: props.stacked,
							grid: {
								color: gridColor,
								borderColor: 'rgb(0, 0, 0, 0)',
							},
							ticks: {
								display: props.detailed,
								//mirror: true,
							},
						},
					},
					interaction: {
						intersect: false,
						mode: 'index',
					},
					elements: {
						point: {
							hoverRadius: 5,
							hoverBorderWidth: 2,
						},
					},
					animation: false,
					plugins: {
						legend: {
							display: props.detailed,
							position: 'bottom',
							labels: {
								boxWidth: 16,
							},
						},
						tooltip: {
							enabled: false,
							mode: 'index',
							animation: {
								duration: 0,
							},
							external: externalTooltipHandler,
						},
						zoom: {
							pan: {
								enabled: true,
							},
							zoom: {
								wheel: {
									enabled: true,
								},
								pinch: {
									enabled: true,
								},
								drag: {
									enabled: false,
								},
								mode: 'x',
							},
							limits: {
								x: {
									min: 'original',
									max: 'original',
								},
								y: {
									min: 'original',
									max: 'original',
								},
							}
						},
						gradient,
					},
				},
				plugins: [{
					id: 'vLine',
					beforeDraw(chart, args, options) {
						if (chart.tooltip._active && chart.tooltip._active.length) {
							const activePoint = chart.tooltip._active[0];
							const ctx = chart.ctx;
							const x = activePoint.element.x;
							const topY = chart.scales.y.top;
							const bottomY = chart.scales.y.bottom;

							ctx.save();
							ctx.beginPath();
							ctx.moveTo(x, bottomY);
							ctx.lineTo(x, topY);
							ctx.lineWidth = 1;
							ctx.strokeStyle = vLineColor;
							ctx.stroke();
							ctx.restore();
						}
					}
				}]
			});
		};

		const exportData = () => {
			// TODO
		};

		const fetchFederationChart = async (): Promise<typeof data> => {
			const raw = await os.api('charts/federation', { limit: props.limit, span: props.span });
			return {
				series: [{
					name: 'Instances total',
					type: 'area',
					data: format(raw.instance.total),
					color: '#888888',
				}, {
					name: 'Instances inc/dec',
					type: 'area',
					data: format(sum(raw.instance.inc, negate(raw.instance.dec))),
					color: colors.purple,
				}, {
					name: 'Inbox instances',
					type: 'area',
					data: format(raw.inboxInstances),
					color: colors.blue,
				}, {
					name: 'Delivered instances',
					type: 'area',
					data: format(raw.deliveredInstances),
					color: colors.green,
				}, {
					name: 'Stalled instances',
					type: 'area',
					data: format(raw.stalled),
					color: colors.red,
				}],
			};
		};

		const fetchApRequestChart = async (): Promise<typeof data> => {
			const raw = await os.api('charts/ap-request', { limit: props.limit, span: props.span });
			return {
				series: [{
					name: 'In',
					type: 'area',
					color: '#008FFB',
					data: format(raw.inboxReceived)
				}, {
					name: 'Out (succ)',
					type: 'area',
					color: '#00E396',
					data: format(raw.deliverSucceeded)
				}, {
					name: 'Out (fail)',
					type: 'area',
					color: '#FEB019',
					data: format(raw.deliverFailed)
				}]
			};
		};

		const fetchNotesChart = async (type: string): Promise<typeof data> => {
			const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
			return {
				series: [{
					name: 'All',
					type: 'line',
					borderDash: [5, 5],
					data: format(type == 'combined'
						? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
						: sum(raw[type].inc, negate(raw[type].dec))
					),
					color: '#888888',
				}, {
					name: 'Renotes',
					type: 'area',
					data: format(type == 'combined'
						? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
						: raw[type].diffs.renote
					),
					color: colors.green,
				}, {
					name: 'Replies',
					type: 'area',
					data: format(type == 'combined'
						? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
						: raw[type].diffs.reply
					),
					color: colors.yellow,
				}, {
					name: 'Normal',
					type: 'area',
					data: format(type == 'combined'
						? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
						: raw[type].diffs.normal
					),
					color: colors.blue,
				}, {
					name: 'With file',
					type: 'area',
					data: format(type == 'combined'
						? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile)
						: raw[type].diffs.withFile
					),
					color: colors.purple,
				}],
			};
		};

		const fetchNotesTotalChart = async (): Promise<typeof data> => {
			const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
			return {
				series: [{
					name: 'Combined',
					type: 'line',
					data: format(sum(raw.local.total, raw.remote.total)),
				}, {
					name: 'Local',
					type: 'area',
					data: format(raw.local.total),
				}, {
					name: 'Remote',
					type: 'area',
					data: format(raw.remote.total),
				}],
			};
		};

		const fetchUsersChart = async (total: boolean): Promise<typeof data> => {
			const raw = await os.api('charts/users', { limit: props.limit, span: props.span });
			return {
				series: [{
					name: 'Combined',
					type: 'line',
					data: format(total
						? sum(raw.local.total, raw.remote.total)
						: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
					),
				}, {
					name: 'Local',
					type: 'area',
					data: format(total
						? raw.local.total
						: sum(raw.local.inc, negate(raw.local.dec))
					),
				}, {
					name: 'Remote',
					type: 'area',
					data: format(total
						? raw.remote.total
						: sum(raw.remote.inc, negate(raw.remote.dec))
					),
				}],
			};
		};

		const fetchActiveUsersChart = async (): Promise<typeof data> => {
			const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span });
			return {
				series: [{
					name: 'Active',
					type: 'area',
					data: format(raw.users),
					color: '#888888',
				}, {
					name: 'Noted',
					type: 'area',
					data: format(raw.notedUsers),
					color: colors.blue,
				}, {
					name: '< Week',
					type: 'area',
					data: format(raw.registeredWithinWeek),
					color: colors.green,
				}, {
					name: '< Month',
					type: 'area',
					data: format(raw.registeredWithinMonth),
					color: colors.yellow,
				}, {
					name: '< Year',
					type: 'area',
					data: format(raw.registeredWithinYear),
					color: colors.red,
				}, {
					name: '> Week',
					type: 'area',
					data: format(raw.registeredOutsideWeek),
					color: colors.yellow,
				}, {
					name: '> Month',
					type: 'area',
					data: format(raw.registeredOutsideMonth),
					color: colors.red,
				}, {
					name: '> Year',
					type: 'area',
					data: format(raw.registeredOutsideYear),
					color: colors.purple,
				}],
			};
		};

		const fetchDriveChart = async (): Promise<typeof data> => {
			const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
			return {
				bytes: true,
				series: [{
					name: 'All',
					type: 'line',
					borderDash: [5, 5],
					data: format(
						sum(
							raw.local.incSize,
							negate(raw.local.decSize),
							raw.remote.incSize,
							negate(raw.remote.decSize)
						)
					),
				}, {
					name: 'Local +',
					type: 'area',
					data: format(raw.local.incSize),
				}, {
					name: 'Local -',
					type: 'area',
					data: format(negate(raw.local.decSize)),
				}, {
					name: 'Remote +',
					type: 'area',
					data: format(raw.remote.incSize),
				}, {
					name: 'Remote -',
					type: 'area',
					data: format(negate(raw.remote.decSize)),
				}],
			};
		};

		const fetchDriveFilesChart = async (): Promise<typeof data> => {
			const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
			return {
				series: [{
					name: 'All',
					type: 'line',
					borderDash: [5, 5],
					data: format(
						sum(
							raw.local.incCount,
							negate(raw.local.decCount),
							raw.remote.incCount,
							negate(raw.remote.decCount)
						)
					),
				}, {
					name: 'Local +',
					type: 'area',
					data: format(raw.local.incCount),
				}, {
					name: 'Local -',
					type: 'area',
					data: format(negate(raw.local.decCount)),
				}, {
					name: 'Remote +',
					type: 'area',
					data: format(raw.remote.incCount),
				}, {
					name: 'Remote -',
					type: 'area',
					data: format(negate(raw.remote.decCount)),
				}],
			};
		};

		const fetchInstanceRequestsChart = async (): Promise<typeof data> => {
			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
			return {
				series: [{
					name: 'In',
					type: 'area',
					color: '#008FFB',
					data: format(raw.requests.received)
				}, {
					name: 'Out (succ)',
					type: 'area',
					color: '#00E396',
					data: format(raw.requests.succeeded)
				}, {
					name: 'Out (fail)',
					type: 'area',
					color: '#FEB019',
					data: format(raw.requests.failed)
				}]
			};
		};

		const fetchInstanceUsersChart = async (total: boolean): Promise<typeof data> => {
			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
			return {
				series: [{
					name: 'Users',
					type: 'area',
					color: '#008FFB',
					data: format(total
						? raw.users.total
						: sum(raw.users.inc, negate(raw.users.dec))
					)
				}]
			};
		};

		const fetchInstanceNotesChart = async (total: boolean): Promise<typeof data> => {
			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
			return {
				series: [{
					name: 'Notes',
					type: 'area',
					color: '#008FFB',
					data: format(total
						? raw.notes.total
						: sum(raw.notes.inc, negate(raw.notes.dec))
					)
				}]
			};
		};

		const fetchInstanceFfChart = async (total: boolean): Promise<typeof data> => {
			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
			return {
				series: [{
					name: 'Following',
					type: 'area',
					color: '#008FFB',
					data: format(total
						? raw.following.total
						: sum(raw.following.inc, negate(raw.following.dec))
					)
				}, {
					name: 'Followers',
					type: 'area',
					color: '#00E396',
					data: format(total
						? raw.followers.total
						: sum(raw.followers.inc, negate(raw.followers.dec))
					)
				}]
			};
		};

		const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof data> => {
			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
			return {
				bytes: true,
				series: [{
					name: 'Drive usage',
					type: 'area',
					color: '#008FFB',
					data: format(total
						? raw.drive.totalUsage
						: sum(raw.drive.incUsage, negate(raw.drive.decUsage))
					)
				}]
			};
		};

		const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof data> => {
			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
			return {
				series: [{
					name: 'Drive files',
					type: 'area',
					color: '#008FFB',
					data: format(total
						? raw.drive.totalFiles
						: sum(raw.drive.incFiles, negate(raw.drive.decFiles))
					)
				}]
			};
		};

		const fetchPerUserNotesChart = async (): Promise<typeof data> => {
			const raw = await os.api('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span });
			return {
				series: [...(props.args.withoutAll ? [] : [{
					name: 'All',
					type: 'line',
					borderDash: [5, 5],
					data: format(sum(raw.inc, negate(raw.dec))),
				}]), {
					name: 'Renotes',
					type: 'area',
					data: format(raw.diffs.renote),
				}, {
					name: 'Replies',
					type: 'area',
					data: format(raw.diffs.reply),
				}, {
					name: 'Normal',
					type: 'area',
					data: format(raw.diffs.normal),
				}],
			};
		};

		const fetchPerUserDriveChart = async (): Promise<typeof data> => {
			const raw = await os.api('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span });
			return {
				series: [{
					name: 'Inc',
					type: 'area',
					data: format(raw.incSize),
				}, {
					name: 'Dec',
					type: 'area',
					data: format(raw.decSize),
				}],
			};
		};

		const fetchAndRender = async () => {
			const fetchData = () => {
				switch (props.src) {
					case 'federation': return fetchFederationChart();
					case 'ap-request': return fetchApRequestChart();
					case 'users': return fetchUsersChart(false);
					case 'users-total': return fetchUsersChart(true);
					case 'active-users': return fetchActiveUsersChart();
					case 'notes': return fetchNotesChart('combined');
					case 'local-notes': return fetchNotesChart('local');
					case 'remote-notes': return fetchNotesChart('remote');
					case 'notes-total': return fetchNotesTotalChart();
					case 'drive': return fetchDriveChart();
					case 'drive-files': return fetchDriveFilesChart();
					
					case 'instance-requests': return fetchInstanceRequestsChart();
					case 'instance-users': return fetchInstanceUsersChart(false);
					case 'instance-users-total': return fetchInstanceUsersChart(true);
					case 'instance-notes': return fetchInstanceNotesChart(false);
					case 'instance-notes-total': return fetchInstanceNotesChart(true);
					case 'instance-ff': return fetchInstanceFfChart(false);
					case 'instance-ff-total': return fetchInstanceFfChart(true);
					case 'instance-drive-usage': return fetchInstanceDriveUsageChart(false);
					case 'instance-drive-usage-total': return fetchInstanceDriveUsageChart(true);
					case 'instance-drive-files': return fetchInstanceDriveFilesChart(false);
					case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true);

					case 'per-user-notes': return fetchPerUserNotesChart();
					case 'per-user-drive': return fetchPerUserDriveChart();
				}
			};
			fetching.value = true;
			data = await fetchData();
			fetching.value = false;
			render();
		};

		watch(() => [props.src, props.span], fetchAndRender);

		onMounted(() => {
			fetchAndRender();
		});

		onUnmounted(() => {
			if (disposeTooltipComponent) disposeTooltipComponent();
		});

		return {
			chartEl,
			fetching,
		};
	},
});
</script>

<style lang="scss" scoped>
.cbbedffa {
	position: relative;

	> .fetching {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		-webkit-backdrop-filter: var(--blur, blur(12px));
		backdrop-filter: var(--blur, blur(12px));
		display: flex;
		justify-content: center;
		align-items: center;
		cursor: wait;
	}
}
</style>