<script setup lang="ts">
import { useTelemetry } from '@/composables/telemetry';
import {
    computed,
    inject,
    onMounted,
    onUnmounted,
    ref,
    shallowRef,
    type ShallowRef,
    toValue,
    triggerRef,
    useTemplateRef,
    watch,
} from 'vue';
import {
    type TelemetryDataItem,
    WEBSOCKET_SYMBOL,
    type WebsocketHandle,
} from '@/composables/websocket';
import { GRAPH_DATA, type GraphData } from '@/graph/graph';
import { AXIS_DATA, type AxisData } from '@/graph/axis';
import ValueLabel from '@/components/ValueLabel.vue';
import { type Point, PointLine } from '@/graph/line';
import TooltipDialog from '@/components/TooltipDialog.vue';
import {
    type DynamicDataType,
    isBooleanType,
    isNumericType,
} from '@/composables/dynamic.ts';

const props = defineProps<{
    data: string;
    minimum_separation?: number;
    class?: string;
}>();

const smoothing_distance_x = 5;
const maximum_minimum_separation_live = 100; // ms
const legend_line_length = 8;
const legend_text_offset = 4;
const marker_radius = 3;

const text_offset = computed(() => 10);

const graph_data = inject<GraphData>(GRAPH_DATA)!;
const axis_data = inject<AxisData>(AXIS_DATA)!;

const data_min_sep = computed(() => {
    return Math.max(
        props.minimum_separation || 0,
        toValue(graph_data.max_temporal_resolution),
    );
});

const live_min_sep = computed(() =>
    Math.min(data_min_sep.value, maximum_minimum_separation_live),
);

const { data: telemetry_data } = useTelemetry(() => props.data);
const websocket = inject<ShallowRef<WebsocketHandle>>(WEBSOCKET_SYMBOL)!;
const value = websocket.value.listen_to_telemetry(
    telemetry_data,
    live_min_sep,
    graph_data.live,
);

const min = ref(Infinity);
const max = ref(-Infinity);

const recompute_points = shallowRef(0);

function trigger_recompute() {
    recompute_points.value = Date.now();
}
function debounced_recompute() {
    if (
        recompute_points.value + toValue(graph_data.max_update_rate) <
        Date.now()
    ) {
        trigger_recompute();
    }
}

const line = ref(Symbol());
const index = computed(() => {
    return graph_data.lines.value.indexOf(line.value);
});
onMounted(() => {
    graph_data.lines.value.push(line.value);
});
onUnmounted(() => {
    graph_data.lines.value = graph_data.lines.value.filter(
        (x) => x != line.value,
    );
});

const memo = shallowRef(new PointLine());
watch([value], ([val]) => {
    const min_x = toValue(graph_data.min_x);
    if (val) {
        const val_t = Date.parse(val.timestamp);
        if (val_t >= min_x) {
            const raw_item_val = val.value[telemetry_data.value!.data_type];
            let item_val = 0;
            if (isNumericType(telemetry_data.value!.data_type)) {
                item_val = raw_item_val as number;
            } else if (isBooleanType(telemetry_data.value!.data_type)) {
                item_val = (raw_item_val as boolean) ? 1 : 0;
            }
            const new_item = {
                x: val_t,
                y: item_val,
                value: raw_item_val,
            } as Point;
            memo.value.insert(new_item, data_min_sep.value);
            if (item_val < min.value) {
                min.value = item_val;
            }
            if (item_val > max.value) {
                max.value = item_val;
            }
            triggerRef(memo);
            debounced_recompute();
        }
    }
});
const recompute_bounds = ref(0);
watch(
    [telemetry_data, () => toValue(graph_data.fetch_history)],
    async ([data]) => {
        if (data) {
            const uuid = data.uuid;
            const type = data.data_type;
            try {
                const min_x = new Date(toValue(graph_data.min_x));
                const max_x = new Date(toValue(graph_data.max_x));
                const res = await fetch(
                    `/api/tlm/history/${uuid}?from=${min_x.toISOString()}&to=${max_x.toISOString()}&resolution=${data_min_sep.value}`,
                );
                const response = (await res.json()) as TelemetryDataItem[];
                for (const data_item of response) {
                    const val_t = Date.parse(data_item.timestamp);
                    const raw_item_val = data_item.value[
                        type
                    ] as DynamicDataType;
                    let item_val = 0;
                    if (isNumericType(type)) {
                        item_val = raw_item_val as number;
                    } else if (isBooleanType(type)) {
                        item_val = (raw_item_val as boolean) ? 1 : 0;
                    }
                    const new_item = {
                        x: val_t,
                        y: item_val,
                        value: raw_item_val,
                    } as Point;
                    memo.value.insert(new_item);
                    if (item_val < min.value) {
                        min.value = item_val;
                    }
                    if (item_val > max.value) {
                        max.value = item_val;
                    }
                }
                memo.value.reduce_to_maximum_separation(data_min_sep.value);
                triggerRef(memo);
                debounced_recompute();
                recompute_bounds.value++;
            } catch (e) {
                // TODO: Response?
                console.error(e);
            }
        }
    },
    {
        immediate: true,
    },
);

watch([graph_data.min_x, graph_data.max_x], ([min_x, max_x]) => {
    let memo_changed = false;

    if (min_x) {
        while (
            memo.value.data.length > 2 &&
            memo.value.data[1].x < toValue(min_x)
        ) {
            memo.value.data.shift();
            memo_changed = true;
        }
    }
    if (max_x) {
        while (
            memo.value.data.length > 2 &&
            memo.value.data[memo.value.data.length - 2].x > toValue(max_x)
        ) {
            memo.value.data.pop();
            memo_changed = true;
        }
    }
    if (memo_changed) {
        recompute_bounds.value++;
    }
});
watch([recompute_bounds], () => {
    let min_val = Infinity;
    let max_val = -Infinity;
    for (let i = 1; i < memo.value.data.length; i++) {
        const item_val = memo.value.data[i].y;
        min_val = Math.min(min_val, item_val);
        max_val = Math.max(max_val, item_val);
    }
    triggerRef(memo);
    debounced_recompute();
    max.value = max_val;
    min.value = min_val;
});

watch(
    [min, axis_data.axis_update_watch],
    ([min_val]) => {
        trigger_recompute();
        axis_data.min_y_callback(min_val);
    },
    {
        immediate: true,
    },
);

watch(
    [max, axis_data.axis_update_watch],
    ([max_val]) => {
        trigger_recompute();
        axis_data.max_y_callback(max_val);
    },
    {
        immediate: true,
    },
);

const points = ref('');

const old_max = ref(0);

const group_transform = computed(() => {
    const new_max = toValue(graph_data.max_x);
    const offset = graph_data.x_map(old_max.value) - graph_data.x_map(new_max);
    return `translate(${offset} 0)`;
});

watch([recompute_points], () => {
    let new_points = '';
    if (memo.value.data.length == 0 || telemetry_data.value == null) {
        return '';
    }

    let last_x = graph_data.x_map(
        memo.value.data[memo.value.data.length - 1].x,
    );
    old_max.value = toValue(graph_data.max_x);

    for (let i = memo.value.data.length - 1; i >= 0; i--) {
        const data_item = memo.value.data[i];
        const t = data_item.x;
        const v = data_item.y;
        const x = graph_data.x_map(t);
        const y = axis_data.y_map(v);

        if (last_x - x < smoothing_distance_x) {
            new_points += ` ${x},${y}`;
        } else {
            new_points += ` ${last_x},${y} ${x},${y}`;
        }
        last_x = x;
        if (last_x <= 0.0) {
            break;
        }
    }
    points.value = new_points;
});

const current_data_point = computed(() => {
    if (memo.value.data.length == 0) {
        return undefined;
    }
    const cursor_time = toValue(graph_data.cursor_time);
    const index = Math.max(memo.value.find_index(cursor_time) - 1, 0);
    return memo.value.data[index];
});

const current_data_point_line = computed(() => {
    if (!current_data_point.value) {
        return '';
    }
    const x = current_data_point.value.x;
    const y = axis_data.y_map(current_data_point.value.y);
    return `${graph_data.x_map(x)},${y} ${graph_data.x_map(toValue(graph_data.max_x))},${y}`;
});

const legend_x = computed(() => {
    return (
        toValue(graph_data.legend_x) +
        toValue(graph_data.legend_x_stride) * index.value
    );
});

const legend_y = computed(() => {
    return (
        toValue(graph_data.legend_y) +
        toValue(graph_data.legend_y_stride) * index.value
    );
});

const legend_text = computed(() => {
    const max_chars =
        (toValue(graph_data.legend_width) -
            legend_line_length -
            legend_text_offset * 2) /
        6.5;
    const start_text = props.data;
    if (start_text.length > max_chars) {
        return (
            start_text.substring(0, 3) +
            '...' +
            start_text.substring(start_text.length - max_chars + 6)
        );
    }
    return start_text;
});

const legend_line = computed(() => {
    const x = legend_x.value;
    const y = legend_y.value + 1 + toValue(graph_data.legend_y_stride) / 2;
    return `${x},${y} ${x + legend_line_length},${y}`;
});

const is_selected = ref(false);
const legendRectRef = useTemplateRef<SVGRectElement>('legend-ref');

function onOpenLegend() {
    if (!is_selected.value) {
        setTimeout(() => {
            is_selected.value = true;
        }, 1);
    }
}
function onCloseLegend() {
    if (is_selected.value) {
        setTimeout(() => {
            is_selected.value = false;
        }, 1);
    }
}

const legend_moused_over = ref(false);

function onMouseEnter(event: MouseEvent) {
    if (event.target == event.currentTarget) {
        legend_moused_over.value = true;
        graph_data.should_fade(true);
    }
}

function onMouseExit(event: MouseEvent) {
    if (event.target == event.currentTarget) {
        legend_moused_over.value = false;
        graph_data.should_fade(false);
    }
}
</script>

<template>
    <g
        :class="`indexed-color color-${index} ${legend_moused_over ? 'no-fade' : ''}`"
    >
        <defs>
            <marker
                :id="`dot-${index}`"
                :refX="marker_radius"
                :refY="marker_radius"
                markerUnits="strokeWidth"
                :markerWidth="marker_radius * 2"
                :markerHeight="marker_radius * 2"
            >
                <circle
                    :cx="marker_radius"
                    :cy="marker_radius"
                    :r="marker_radius"
                    :class="`indexed-color color-${index} marker`"
                />
            </marker>
        </defs>
        <g clip-path="url(#content)">
            <polyline
                class="fade_other_selected"
                fill="none"
                :transform="group_transform"
                :points="points"
            ></polyline>
            <polyline
                class="fade_other_selected"
                fill="none"
                :marker-start="`url(#dot-${index})`"
                :points="current_data_point_line"
            >
            </polyline>
        </g>
        <ValueLabel
            v-if="current_data_point"
            class="fade_other_selected label"
            :x="graph_data.x_map(toValue(graph_data.max_x)) + text_offset"
            :y="axis_data.y_map(current_data_point.y)"
            :value="current_data_point.value"
        >
        </ValueLabel>
        <template v-if="toValue(graph_data.legend_enabled)">
            <g @mouseenter="onMouseEnter" @mouseleave="onMouseExit">
                <rect
                    ref="legend-ref"
                    :class="`legend ${is_selected ? 'selected' : ''}`"
                    :x="legend_x - legend_text_offset"
                    :y="legend_y"
                    :width="toValue(graph_data.legend_width)"
                    :height="toValue(graph_data.legend_y_stride)"
                    @click="onOpenLegend"
                >
                </rect>
                <polyline
                    class="legend"
                    fill="none"
                    :points="legend_line"
                    @click="onOpenLegend"
                ></polyline>
                <text
                    class="legend"
                    :x="legend_x + legend_line_length + legend_text_offset"
                    :y="legend_y + 1 + toValue(graph_data.legend_y_stride) / 2"
                    @click="onOpenLegend"
                >
                    {{ legend_text }}
                </text>
            </g>
            <foreignObject height="0" width="0">
                <TooltipDialog
                    :show="is_selected"
                    :element="legendRectRef"
                    @close="onCloseLegend"
                >
                    <div class="column">
                        <div class="row header">
                            <div
                                :class="`indexed-color color-${index} colored dash`"
                            ></div>
                            <span class="large">{{ props.data }}</span>
                        </div>
                        <template v-if="telemetry_data">
                            <div class="row">
                                <span class="small">{{
                                    telemetry_data?.uuid
                                }}</span>
                            </div>
                            <div class="row">
                                <span>{{ telemetry_data?.data_type }}</span>
                            </div>
                            <div class="row">
                                <span v-if="current_data_point !== undefined">{{
                                    current_data_point.value
                                }}</span>
                                <span v-else>Missing Data</span>
                            </div>
                        </template>
                        <template v-else>
                            <div class="row">
                                <span>Unknown Signal</span>
                            </div>
                        </template>
                    </div>
                </TooltipDialog>
            </foreignObject>
        </template>
    </g>
</template>

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

.fade rect.fade_other_selected {
    opacity: 10%;
}

.fade .fade_other_selected {
    opacity: 25%;
}

.fade .fade_other_selected.label {
    opacity: 0;
}

.fade .no-fade .fade_other_selected {
    opacity: 100%;
}
</style>

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

.indexed-color {
    stroke: var(--indexed-color);
    fill: var(--indexed-color);
}

circle.marker {
    stroke: variables.$background-color;
    stroke-width: 1px;
}

polyline {
    stroke-width: 1px;
}

text {
    font-family: variables.$monospace-text-font;
    text-anchor: start;
    stroke: variables.$text-color;
    fill: variables.$text-color;
    dominant-baseline: middle;
    font-size: variables.$small-monospace-text-size;
}

rect.legend {
    stroke: none;
    stroke-width: 0;
    fill: transparent;
}

.legend {
    pointer-events: all;
}

rect.legend.selected,
rect.legend:hover,
rect.legend:has(~ .legend:hover) {
    stroke: variables.$gray-2;
    stroke-width: 1px;
}

.legend {
    cursor: help;
}

div.column,
div.row {
    gap: 0.25em;
}

div.header.row {
    margin-bottom: 1ex;
}

div.colored.dash {
    background-color: var(--indexed-color);
    width: 0.75em;
    height: 2px;
    display: inline;
    margin-right: 0.5em;
}

span.large {
    font-size: variables.$large-text-size;
}

span.small {
    font-size: variables.$small-text-size;
}
</style>
