<template>
    <slot />
</template>

<script>
import BaseInputColor from './BaseInputColor.vue';
import { clamp, getAreaGradient, getChannelColor, getChannelGradient, getInputChannelRange, getInputChannelValue, parseColor } from './colorManager';

const DEFAULT_COLOR = '#ff0000';

export default {
    name: 'InputColor',
    extends: BaseInputColor,
    inheritAttrs: false,
    emits: ['value-change-end'],
    data() {
        return {
            isAreaDragging: false,
            parsedColor: parseColor(DEFAULT_COLOR).toFormat(this.getColorSpaceFormat(this.format))
        };
    },
    watch: {
        d_value: {
            immediate: true,
            handler(value) {
                const resolved = value ?? DEFAULT_COLOR;

                if (this.parsedColor?.toString(this.getOutputFormat()) === resolved) {
                    return;
                }

                let parsed;

                try {
                    parsed = parseColor(resolved);
                } catch {
                    parsed = parseColor(DEFAULT_COLOR);
                }

                this.parsedColor = parsed.toFormat(this.getColorSpaceFormat(this.format));
            }
        },
        format(newFormat) {
            const nextSpace = this.getColorSpaceFormat(newFormat);

            if (this.parsedColor.getFormat() === nextSpace) return;

            this.parsedColor = this.parsedColor.toFormat(nextSpace);
        }
    },
    methods: {
        getOutputFormat() {
            return this.format ?? 'hsba';
        },
        getColorSpaceFormat(format = this.format) {
            switch (format) {
                case 'hex':
                case 'hexa':
                case 'rgb':
                case 'rgba':
                    return 'rgba';

                case 'hsl':
                case 'hsla':
                    return 'hsla';

                case 'oklch':
                case 'oklcha':
                    return 'oklcha';

                case 'hsb':
                case 'hsba':
                default:
                    return 'hsba';
            }
        },
        updateColor(nextColor, originalEvent) {
            this.parsedColor = nextColor;
            this.writeValue(nextColor.toString(this.getOutputFormat()), originalEvent);
        },
        emitValueChangeEnd(originalEvent) {
            this.$emit('value-change-end', {
                originalEvent,
                value: this.parsedColor.toString(this.getOutputFormat()),
                color: this.parsedColor
            });
        },
        updateChannel(channel, value, originalEvent) {
            if (this.disabled) return false;

            let next;

            if (channel === 'hex' || channel === 'css') {
                if (value == null || value === '') {
                    this.writeValue(null, originalEvent);

                    return true;
                }

                try {
                    next = parseColor(value);
                } catch {
                    return false;
                }
            } else {
                const numericValue = parseFloat(value);

                if (Number.isNaN(numericValue)) return false;

                const working = this.toChannelColorSpace(this.getChannelColorValue(channel), channel);

                next = working.withChannelValue(channel, numericValue);
            }

            this.updateColor(next, originalEvent);

            return true;
        },
        toChannelColorSpace(color, channel) {
            if (channel === 'alpha' || color.getChannels().includes(channel)) {
                return color;
            }

            switch (channel) {
                case 'hue':
                case 'saturation':
                case 'brightness':
                    return color.toFormat('hsba');

                case 'lightness':
                    return color.toFormat('hsla');

                case 'red':
                case 'green':
                case 'blue':
                    return color.toFormat('rgba');

                case 'L':
                case 'C':
                case 'H':
                    return color.toFormat('oklcha');

                default:
                    return color;
            }
        },
        getChannelColorValue(channel) {
            if (channel === 'alpha') return this.parsedColor;

            if (this.parsedColor.getChannels().includes(channel)) return this.parsedColor;

            if (this.areaColor.getChannels().includes(channel)) return this.areaColor;

            return this.toChannelColorSpace(this.parsedColor, channel);
        },
        updateAreaValue(x, y, element, event) {
            if (!element || this.disabled) return;

            const rect = element.getBoundingClientRect();
            const nx = clamp((x - rect.left) / rect.width, 0, 1);
            const ny = clamp((y - rect.top) / rect.height, 0, 1);
            const { xChannel, yChannel } = this.areaAxes;
            const xRange = this.areaColor.getChannelRange(xChannel);
            const yRange = this.areaColor.getChannelRange(yChannel);
            const xValue = nx * (xRange.max - xRange.min) + xRange.min;
            const yValue = (1 - ny) * (yRange.max - yRange.min) + yRange.min;
            const next = this.areaColor.withChannelValue(xChannel, xValue).withChannelValue(yChannel, yValue);

            this.updateColor(next, event);
        },
        onAreaPointerDown(event) {
            if (event.button !== 0 || this.disabled) return;

            event.currentTarget.setPointerCapture?.(event.pointerId);
            this.isAreaDragging = true;
            this.updateAreaValue(event.clientX, event.clientY, event.currentTarget, event);
        },
        onAreaPointerMove(event) {
            if (!this.isAreaDragging) return;
            this.updateAreaValue(event.clientX, event.clientY, event.currentTarget, event);
        },
        onAreaPointerUp(event) {
            if (!this.isAreaDragging) return;
            event.currentTarget.releasePointerCapture?.(event.pointerId);
            this.isAreaDragging = false;
            this.emitValueChangeEnd(event);
        },
        onAreaBlur(event) {
            this.emitValueChangeEnd(event);
        },
        onAreaKeyDown(event) {
            if (this.disabled) return;

            const { xChannel, yChannel } = this.areaAxes;
            const xStep = this.areaColor.getChannelRange(xChannel).step;
            const yStep = this.areaColor.getChannelRange(yChannel).step;
            let next = this.areaColor;

            switch (event.key) {
                case 'ArrowRight':
                    next = next.incChannelValue(xChannel, xStep);
                    break;
                case 'ArrowLeft':
                    next = next.decChannelValue(xChannel, xStep);
                    break;
                case 'ArrowUp':
                    next = next.incChannelValue(yChannel, yStep);
                    break;
                case 'ArrowDown':
                    next = next.decChannelValue(yChannel, yStep);
                    break;
                default:
                    return;
            }

            event.preventDefault();
            this.updateColor(next, event);
            this.emitValueChangeEnd(event);
        },
        getInputChannelValue(channel) {
            return getInputChannelValue(this.parsedColor, channel, this.getColorSpaceFormat(this.format));
        },
        getInputChannelRange(channel) {
            return getInputChannelRange(this.parsedColor, channel);
        },
        getChannelGradient(channel, orientation) {
            return getChannelGradient(channel, this.getChannelColorValue(channel), orientation);
        },
        getChannelColor(channel) {
            return getChannelColor(this.getChannelColorValue(channel), channel);
        },
        openEyeDropper(event) {
            if (this.disabled || typeof window === 'undefined' || !window.EyeDropper) return;

            const eyeDropper = new window.EyeDropper();

            eyeDropper
                .open()
                .then((result) => {
                    this.updateColor(parseColor(result.sRGBHex), event);
                    this.emitValueChangeEnd(event);
                })
                .catch(() => {
                    /* user cancelled */
                });
        }
    },
    computed: {
        areaColorSpace() {
            return (this.format ?? 'hsba').startsWith('hsl') ? 'hsla' : 'hsba';
        },
        areaColor() {
            return this.parsedColor.toFormat(this.areaColorSpace);
        },
        areaAxes() {
            const channels = this.areaColor.getChannels();

            return { xChannel: channels[1], yChannel: channels[2] };
        },
        areaStyles() {
            const { xChannel, yChannel } = this.areaAxes;
            const xRange = this.areaColor.getChannelRange(xChannel);
            const yRange = this.areaColor.getChannelRange(yChannel);
            const xValue = this.areaColor.getChannelValue(xChannel);
            const yValue = this.areaColor.getChannelValue(yChannel);
            const base = getAreaGradient(this.areaColor, this.areaAxes);

            return {
                ...base,
                '--px-handle-background': this.areaColor.withChannelValue('alpha', 1).toString('css'),
                '--px-handle-position-left': `${((xValue - xRange.min) / (xRange.max - xRange.min)) * 100}%`,
                '--px-handle-position-top': `${(1 - (yValue - yRange.min) / (yRange.max - yRange.min)) * 100}%`
            };
        },
        swatchStyles() {
            if (!this.hasValue) return {};

            return {
                '--px-swatch-background': this.parsedColor.toString('css')
            };
        },
        hasValue() {
            return this.d_value != null;
        }
    }
};
</script>
