<script setup lang="ts">
import { computed, inject, provide, ref, toValue, watch } from 'vue';
import { AXIS_DATA, type AxisData, AxisType } from '@/graph/axis';
import { GRAPH_DATA, type GraphData, GraphSide } from '@/graph/graph';
import NumericText from '@/components/NumericText.vue';
import { useNow } from '@/composables/ticker';

const props = defineProps<{
    y_limits?: [number, number];
    side?: GraphSide;
    type?: AxisType;
}>();

const minor_tick_length = computed(() => 4);
const major_tick_length = computed(() => 8);
const text_offset = computed(() => 5);
const side = computed(() => props.side || GraphSide.Right);
const type = computed(() => props.type || AxisType.Linear);

const ticker_locations = [Math.log10(1), Math.log10(2), Math.log10(5)];
ticker_locations.reverse();

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

const min_y = ref(Infinity);
const max_y = ref(-Infinity);
const raw_min_y = ref(Infinity);
const raw_max_y = ref(-Infinity);

const axis_update_watch = ref(0);

const axis_update_ticker = useNow(50);

watch([graph_data.min_x, graph_data.max_x, axis_update_ticker], () => {
    axis_update_watch.value++;
    min_y.value = raw_min_y.value;
    max_y.value = raw_max_y.value;
    raw_min_y.value = Infinity;
    raw_max_y.value = -Infinity;
});

function update_min_y(y: number) {
    if (y < min_y.value) {
        min_y.value = y;
    }
    if (y < raw_min_y.value) {
        raw_min_y.value = y;
    }
}
function update_max_y(y: number) {
    if (y > max_y.value) {
        max_y.value = y;
    }
    if (y > raw_max_y.value) {
        raw_max_y.value = y;
    }
}

const min_y_value = computed(() => {
    if (props.y_limits) {
        return props.y_limits[0];
    }
    if (type.value == AxisType.Linear) {
        if (max_y.value > min_y.value) {
            const half_diff_y_value = (max_y.value - min_y.value) / 2.0;
            const average_y_value = min_y.value + half_diff_y_value;
            return average_y_value - half_diff_y_value * 1.05;
        } else {
            return -1.05;
        }
    } else {
        if (max_y.value > min_y.value) {
            const half_diff_y_value =
                (Math.log(max_y.value) - Math.log(min_y.value)) / 2.0;
            const average_y_value = Math.log(min_y.value) + half_diff_y_value;
            return Math.exp(average_y_value - half_diff_y_value * 1.05);
        } else {
            return 0.01;
        }
    }
});
const max_y_value = computed(() => {
    if (props.y_limits) {
        return props.y_limits[1];
    }
    if (type.value == AxisType.Linear) {
        if (max_y.value > min_y.value) {
            const half_diff_y_value = (max_y.value - min_y.value) / 2.0;
            const average_y_value = min_y.value + half_diff_y_value;
            return average_y_value + half_diff_y_value * 1.05;
        } else {
            return 1.05;
        }
    } else {
        if (max_y.value > min_y.value) {
            const half_diff_y_value =
                (Math.log(max_y.value) - Math.log(min_y.value)) / 2.0;
            const average_y_value = Math.log(min_y.value) + half_diff_y_value;
            return Math.exp(average_y_value + half_diff_y_value * 1.05);
        } else {
            return 1.05;
        }
    }
});

const y_map = (y: number) => {
    const height = toValue(graph_data.height);
    const border_top = toValue(graph_data.border_top);
    let max_value = toValue(max_y_value);
    let min_value = toValue(min_y_value);
    let y_value = y;
    if (type.value == AxisType.Logarithmic) {
        max_value = Math.log(max_value);
        min_value = Math.log(min_value);
        y_value = Math.log(y_value);
    }
    const diff_y = max_value - min_value;
    return height * (1 - (y_value - min_value) / diff_y) + border_top;
};

provide<AxisData>(AXIS_DATA, {
    axis_update_watch: axis_update_watch,
    min_y_callback: update_min_y,
    max_y_callback: update_max_y,
    y_map: y_map,
});

const lines = computed(() => {
    const diff_y_val = (max_y_value.value - min_y_value.value) / 2;
    const diff_log10 = Math.log10(diff_y_val);
    const diff_log10_fraction = diff_log10 - Math.floor(diff_log10);
    const ticker_location = ticker_locations.find(
        (location) => location <= diff_log10_fraction,
    )!;
    const grid_spread = Math.pow(10, Math.floor(diff_log10) + ticker_location);
    const major_spread = grid_spread / 2;
    const minor_spread = major_spread / 2;
    const minor_ticks = [];
    const major_ticks = [];
    const grid_lines = [];
    for (
        let i = Math.floor(min_y_value.value / grid_spread);
        i <= Math.ceil(max_y_value.value / grid_spread);
        i++
    ) {
        const y = i * grid_spread;
        grid_lines.push(y);
    }
    for (
        let i = Math.floor(min_y_value.value / major_spread);
        i <= Math.ceil(max_y_value.value / major_spread);
        i++
    ) {
        const y = i * major_spread;
        if (grid_lines.indexOf(y) < 0) {
            major_ticks.push(y);
        }
    }
    for (
        let i = Math.floor(min_y_value.value / minor_spread);
        i <= Math.ceil(max_y_value.value / minor_spread);
        i++
    ) {
        const y = i * minor_spread;
        if (grid_lines.indexOf(y) < 0 && major_ticks.indexOf(y) < 0) {
            minor_ticks.push(y);
        }
    }
    return [minor_ticks, major_ticks, grid_lines];
});
</script>

<template>
    <template v-if="side == GraphSide.Right">
        <g class="minor_tick" clip-path="url(#y_ticker)">
            <template v-for="tick of lines[0]" :key="tick">
                <polyline
                    :points="`${graph_data.x_map(toValue(graph_data.max_x)) - minor_tick_length},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.max_x))},${y_map(tick)}`"
                ></polyline>
                <text
                    class="right_edge middle_text"
                    :x="
                        graph_data.x_map(toValue(graph_data.max_x)) +
                        text_offset
                    "
                    :y="y_map(tick)"
                    ><NumericText :value="tick" :max_width="7"></NumericText
                ></text>
            </template>
        </g>
        <g class="major_tick" clip-path="url(#y_ticker)">
            <template v-for="tick of lines[1]" :key="tick">
                <polyline
                    :points="`${graph_data.x_map(toValue(graph_data.max_x)) - major_tick_length},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.max_x))},${y_map(tick)}`"
                ></polyline>
                <text
                    class="right_edge middle_text"
                    :x="
                        graph_data.x_map(toValue(graph_data.max_x)) +
                        text_offset
                    "
                    :y="y_map(tick)"
                    ><NumericText :value="tick" :max_width="6"></NumericText
                ></text>
            </template>
        </g>
        <g class="grid_tick" clip-path="url(#y_ticker)">
            <template v-for="tick of lines[2]" :key="tick">
                <polyline
                    :points="`${graph_data.x_map(toValue(graph_data.min_x))},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.max_x))},${y_map(tick)}`"
                ></polyline>
                <text
                    class="right_edge middle_text"
                    :x="
                        graph_data.x_map(toValue(graph_data.max_x)) +
                        text_offset
                    "
                    :y="y_map(tick)"
                >
                    <NumericText :value="tick" :max_width="5"></NumericText>
                </text>
            </template>
        </g>
    </template>
    <template v-if="side == GraphSide.Left">
        <g class="minor_tick" clip-path="url(#y_ticker)">
            <template v-for="tick of lines[0]" :key="tick">
                <polyline
                    :points="`${graph_data.x_map(toValue(graph_data.min_x)) + minor_tick_length},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.min_x))},${y_map(tick)}`"
                ></polyline>
                <text
                    class="left_edge middle_text"
                    :x="
                        graph_data.x_map(toValue(graph_data.min_x)) -
                        text_offset
                    "
                    :y="y_map(tick)"
                    ><NumericText :value="tick" :max_width="7"></NumericText
                ></text>
            </template>
        </g>
        <g class="major_tick" clip-path="url(#y_ticker)">
            <template v-for="tick of lines[1]" :key="tick">
                <polyline
                    :points="`${graph_data.x_map(toValue(graph_data.min_x)) + major_tick_length},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.min_x))},${y_map(tick)}`"
                ></polyline>
                <text
                    class="left_edge middle_text"
                    :x="
                        graph_data.x_map(toValue(graph_data.min_x)) -
                        text_offset
                    "
                    :y="y_map(tick)"
                    ><NumericText :value="tick" :max_width="6"></NumericText
                ></text>
            </template>
        </g>
        <g class="grid_tick" clip-path="url(#y_ticker)">
            <template v-for="tick of lines[2]" :key="tick">
                <polyline
                    :points="`${graph_data.x_map(toValue(graph_data.min_x))},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.max_x))},${y_map(tick)}`"
                ></polyline>
                <text
                    class="left_edge middle_text"
                    :x="
                        graph_data.x_map(toValue(graph_data.min_x)) -
                        text_offset
                    "
                    :y="y_map(tick)"
                    ><NumericText :value="tick" :max_width="5"></NumericText
                ></text>
            </template>
        </g>
    </template>
    <slot></slot>
</template>

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

.right_edge {
    text-anchor: start;
}

.left_edge {
    text-anchor: end;
}

.middle_text {
    dominant-baseline: middle;
    font-family: variables.$text-font;
}

.minor_tick {
    stroke: variables.$minor-tick;
    fill: variables.$minor-tick;
}

.major_tick {
    stroke: variables.$major-tick;
    fill: variables.$major-tick;
}

.grid_tick {
    fill: variables.$grid-line;
    stroke: variables.$grid-line;
}
</style>
