<script setup lang="ts">
import {
    computed,
    onUnmounted,
    onWatcherCleanup,
    provide,
    ref,
    useTemplateRef,
    watch,
} from 'vue';
import { useNow } from '@/composables/ticker';
import { GRAPH_DATA, type GraphData, GraphSide } from '@/graph/graph';
import TimeText from '@/components/TimeText.vue';
import {
    getDateString,
    getDurationString,
    parseDurationString,
} from '@/datetime';

const props = defineProps<{
    initial_duration?: number;
    utc?: boolean;
    left_axis?: boolean;
    right_axis?: boolean;
    hide_time_labels?: boolean;
    hide_time_ticks?: boolean;
    include_controls?: boolean;
    legend?: GraphSide;
    cursor?: boolean;
}>();

const divRef = useTemplateRef<HTMLDivElement>('graph-div');

const width = ref(0);
const height = ref(0);

const controls_height = 32;
const min_time_label_separation = 250;

const resize_observer = new ResizeObserver((elements) => {
    for (const element of elements) {
        if (element.target == divRef.value) {
            width.value = element.contentBoxSize[0].inlineSize;
            height.value =
                element.contentBoxSize[0].blockSize -
                (props.include_controls ? controls_height : 0);
        }
    }
});

watch([divRef], ([divRef]) => {
    if (divRef) {
        resize_observer.observe(divRef);
        onWatcherCleanup(() => {
            resize_observer.unobserve(divRef);
        });
    }
});
onUnmounted(() => {
    resize_observer.disconnect();
});

const update_ms = 33;

const now = useNow(update_ms);
const window_duration = ref(props.initial_duration || 10 * 1000);

const time_lines = [
    1, // 1ms
    2, // 2ms
    5, // 5ms
    10, // 10ms
    20, // 20ms
    50, // 50ms
    100, // 100ms
    200, // 200ms
    500, // 500ms
    1000, // 1s
    2000, // 2s
    5000, // 5s
    10000, // 10s
    30000, // 30s
    60000, // 1m
    150000, // 2.5m
    300000, // 5m
    6000000, // 10m
    18000000, // 30m
    36000000, // 1h
    72000000, // 2h
    144000000, // 4h
    216000000, // 6h
    432000000, // 12h
    864000000, // 1d
    1728000000, // 2d
    6048000000, // 1w
];
// time_lines.reverse();
const text_offset = computed(() => 5);

const legend_width = 160;
const border_left = computed(
    () =>
        (props.left_axis ? 96 : 0) +
        (props.legend == GraphSide.Left ? legend_width : 0),
);
const border_right = computed(
    () =>
        (props.right_axis ? 80 : 0) +
        (props.legend == GraphSide.Right ? legend_width : 0),
);
const border_top = computed(() => 6);
const border_bottom = computed(() => (props.hide_time_labels ? 6 : 24));

const max_x = ref(now.value);
const min_x = computed(() => max_x.value - window_duration.value);

const fetch_history = ref(0);

const diff_x = computed(() => max_x.value - min_x.value);

const live = ref(true);
watch([live], ([live_value]) => {
    if (live_value) {
        fetch_history.value++;
    }
});
const valid_max_x_text = ref(true);
watch([now], ([now_value]) => {
    if (live.value) {
        max_x.value = now_value;
        valid_max_x_text.value = true;
    }
});

const max_x_text = computed({
    // getter
    get() {
        return getDateString(new Date(max_x.value), props.utc, true);
    },
    // setter
    set(newValue) {
        const new_max_x = Date.parse(
            newValue.replace('/', '-').replace('/', '-').replace(' ', 'T'),
        );
        if (!Number.isNaN(new_max_x)) {
            fetch_history.value++;
            max_x.value = new_max_x;
            valid_max_x_text.value = true;
        } else {
            valid_max_x_text.value = false;
        }
    },
});
const duration_text = computed({
    get() {
        return getDurationString(window_duration.value);
    },
    set(newValue) {
        const new_duration = parseDurationString(newValue);
        if (!Number.isNaN(new_duration)) {
            window_duration.value = Math.max(new_duration, 1);
            fetch_history.value++;
        }
    },
});

const x_map = (x: number) => {
    return (
        ((width.value - border_left.value - border_right.value) *
            (x - min_x.value)) /
            diff_x.value +
        border_left.value
    );
};
const inv_x_map = (x: number) => {
    return (
        ((x - border_left.value) * diff_x.value) /
            (width.value - border_left.value - border_right.value) +
        min_x.value
    );
};

const telemetry_lines = ref([]);

const legend_enabled = computed(
    () => props.legend === GraphSide.Left || props.legend === GraphSide.Right,
);
const legend_x = computed(() =>
    props.legend === GraphSide.Left ? 8 : width.value - legend_width + 8,
);
const legend_y = computed(() => border_top.value);
const legend_x_stride = computed(() => 0);
const legend_y_stride = computed(() => 16);
const legend_width_output = computed(() => legend_width - 8);

const line_duration = computed(() => {
    const width_px = width.value;
    const diff_x = max_x.value - min_x.value;
    return time_lines.find((duration) => {
        const line_count = diff_x / duration;
        const width_per_line = width_px / line_count;
        return width_per_line >= min_time_label_separation;
    })!;
});

const lines = computed(() => {
    const result = [];
    for (
        let i = Math.ceil(max_x.value / line_duration.value);
        i >= Math.floor(min_x.value / line_duration.value);
        i--
    ) {
        const x = i * line_duration.value;
        result.push(x);
    }
    return result;
});

const mouse_x = ref<number | null>(null);

function onMouseMove(event: MouseEvent) {
    if (props.cursor) {
        mouse_x.value =
            event.clientX -
            (event.currentTarget as Element).getBoundingClientRect().left;
    }
}

function onMouseOut() {
    mouse_x.value = null;
}

const mouse_t = computed(() => {
    if (mouse_x.value === null) {
        return null;
    }
    const t = inv_x_map(mouse_x.value);
    if (t < min_x.value || t > max_x.value) {
        return null;
    }
    return t;
});

const show_data_at_time = computed(() => {
    if (mouse_t.value === null) {
        return max_x.value + update_ms;
    } else {
        return mouse_t.value;
    }
});

const should_fade = ref(false);

const max_temporal_resolution = computed(() => {
    const delta_t = window_duration.value;
    return Math.floor(delta_t / 1000); // Aim for a maximum of 1000 data points
});

provide<GraphData>(GRAPH_DATA, {
    border_top: border_top,
    min_x: min_x,
    max_x: max_x,
    max_temporal_resolution: max_temporal_resolution,
    live: live,
    fetch_history: fetch_history,
    width: () =>
        Math.max(width.value - border_left.value - border_right.value, 0),
    height: () =>
        Math.max(height.value - border_top.value - border_bottom.value, 0),
    x_map: x_map,
    lines: telemetry_lines,
    max_update_rate: 1000 / 10,
    legend_enabled: legend_enabled,
    legend_x: legend_x,
    legend_y: legend_y,
    legend_x_stride: legend_x_stride,
    legend_y_stride: legend_y_stride,
    legend_width: legend_width_output,
    cursor_time: show_data_at_time,
    should_fade: (value) => (should_fade.value = value),
});
</script>

<template>
    <div ref="graph-div" class="full-size">
        <div
            v-if="include_controls"
            class="controls-header"
            :style="`height: ${controls_height}px`"
        >
            <div class="grow"></div>
            <div>
                <input
                    type="text"
                    autocomplete="off"
                    :size="15"
                    v-model="duration_text"
                />
            </div>
            <div>
                <input
                    type="text"
                    autocomplete="off"
                    :disabled="live"
                    :size="max_x_text.length"
                    v-model="max_x_text"
                />
            </div>
            <div>
                <input type="checkbox" v-model="live" />
                <label>Live</label>
            </div>
        </div>
        <svg
            ref="svg_graph"
            class="graph"
            :width="width"
            :height="height"
            @mousemove="onMouseMove"
            @mouseleave="onMouseOut"
        >
            <defs>
                <clipPath id="content">
                    <rect
                        :x="border_left"
                        :y="border_top"
                        :width="Math.max(width - border_left - border_right, 0)"
                        :height="
                            Math.max(height - border_top - border_bottom, 0)
                        "
                    ></rect>
                </clipPath>
                <clipPath id="y_ticker">
                    <rect
                        :x="0"
                        :y="border_top"
                        :width="width"
                        :height="
                            Math.max(height - border_top - border_bottom, 0)
                        "
                    ></rect>
                </clipPath>
                <clipPath id="x_ticker">
                    <rect
                        :x="border_left"
                        :y="0"
                        :width="Math.max(width - border_left - border_right, 0)"
                        :height="height"
                    ></rect>
                </clipPath>
            </defs>
            <g class="time_tick" clip-path="url(#x_ticker)">
                <template v-for="tick of lines" :key="tick">
                    <polyline
                        v-if="!hide_time_ticks"
                        :points="`${x_map(tick)},${border_top} ${x_map(tick)},${height - border_bottom}`"
                    ></polyline>
                    <TimeText
                        v-if="!hide_time_labels"
                        class="bottom_edge"
                        :x="x_map(tick)"
                        :y="height - border_bottom + text_offset"
                        :timestamp="tick"
                        :utc="props.utc"
                        :show_millis="line_duration < 1000"
                        :key="tick"
                    ></TimeText>
                </template>
            </g>
            <g :class="`${should_fade ? 'fade' : ''}`">
                <slot></slot>
            </g>
            <g class="cursor_tick" v-if="mouse_t && cursor">
                <rect
                    :x="x_map(mouse_t) - 100"
                    :y="height - border_bottom"
                    width="200"
                    height="20"
                ></rect>
                <polyline
                    :points="`${x_map(mouse_t)},${border_top} ${x_map(mouse_t)},${height - border_bottom}`"
                ></polyline>
                <TimeText
                    v-if="mouse_t"
                    id="cursor_text"
                    class="bottom_edge"
                    :x="x_map(mouse_t)"
                    :y="height - border_bottom + text_offset"
                    :timestamp="mouse_t"
                    :utc="props.utc"
                    show_millis
                ></TimeText>
            </g>
        </svg>
    </div>
</template>

<style scoped lang="scss">
@use '@/assets/variables';

.bottom_edge {
    text-anchor: middle;
    dominant-baseline: hanging;
    font-family: variables.$text-font;
}

.time_tick {
    stroke: variables.$time-tick;
    fill: variables.$time-tick;
}

.cursor_tick {
    stroke: variables.$cursor-tick;
    fill: variables.$cursor-tick;
}

.cursor_tick > rect {
    fill: variables.$background-color;
    opacity: 66%;
}

div.full-size {
    width: 100%;
    height: 100%;
}

div.controls-header {
    display: flex;
    flex-flow: row nowrap;
    justify-content: center;
    align-items: center;
    align-content: stretch;
    gap: 0 1em;
    margin: 0 1em 0 1em;
}
</style>
