import { isDimensionSelector, isFieldSelector, isMeasureSelector, isPartialDatumSelector, isValueSelector, selector } from "../../../../../dataSelector/index.js";
import { hasMultipleMeasureInSingleView, isBarLikeChart } from "../../../../utils/index.js";
import { isSubset } from "./utils.js";
const STACK_END_FIELD = '__VCHART_STACK_END';
const toArray = (value)=>{
    if (Array.isArray(value)) return value;
    return null == value ? [] : [
        value
    ];
};
const getDifferenceBandFields = (spec)=>toArray('horizontal' === spec.direction ? spec.yField : spec.xField);
const getDifferenceValueField = (spec)=>{
    const valueField = toArray('horizontal' === spec.direction ? spec.xField : spec.yField)[0];
    if (!valueField) throw new Error('annotationDifferenceLine requires a value field in the target chart spec');
    return valueField;
};
const normalizeDifferenceValue = (value, selectorLabel, field)=>{
    const numberValue = Number(value);
    if (Number.isNaN(numberValue)) throw new Error(`annotationDifferenceLine ${selectorLabel} selector resolved to a non-numeric value on field "${field}"`);
    return numberValue;
};
const buildStackGroupDatum = (datum, bandFields)=>Object.fromEntries(bandFields.map((field)=>[
            field,
            datum[field]
        ]));
const resolveBandIndex = (dataset, bandDatum)=>{
    const bandIndex = dataset.findIndex((datum)=>isSubset(bandDatum, datum));
    return bandIndex >= 0 ? bandIndex : void 0;
};
const buildFallbackSelectorDatum = (selectorValue)=>{
    const selectorList = Array.isArray(selectorValue) ? selectorValue : [
        selectorValue
    ];
    const fallbackDatum = {};
    for (const currentSelector of selectorList){
        if (isValueSelector(currentSelector) || isFieldSelector(currentSelector)) return;
        if (isMeasureSelector(currentSelector)) {
            const operator = currentSelector.operator || currentSelector.op;
            const selectorValues = Array.isArray(currentSelector.value) ? currentSelector.value : [
                currentSelector.value
            ];
            if (![
                '=',
                '=='
            ].includes(operator) || 1 !== selectorValues.length) return;
            fallbackDatum[currentSelector.field] = selectorValues[0];
            continue;
        }
        if (isDimensionSelector(currentSelector)) {
            const operator = currentSelector.operator || currentSelector.op;
            const selectorValues = Array.isArray(currentSelector.value) ? currentSelector.value : [
                currentSelector.value
            ];
            if ('in' !== operator || 1 !== selectorValues.length) return;
            fallbackDatum[currentSelector.field] = selectorValues[0];
            continue;
        }
        if (isPartialDatumSelector(currentSelector)) {
            Object.assign(fallbackDatum, currentSelector);
            continue;
        }
        return;
    }
    return Object.keys(fallbackDatum).length > 0 ? fallbackDatum : void 0;
};
const inferFallbackValue = (fallbackDatum, valueField, bandFields)=>{
    if (valueField in fallbackDatum) return fallbackDatum[valueField];
    const numericCandidateKeys = Object.keys(fallbackDatum).filter((key)=>{
        if (key === valueField || bandFields.includes(key)) return false;
        return 'number' == typeof fallbackDatum[key];
    });
    if (1 !== numericCandidateKeys.length) return;
    return fallbackDatum[numericCandidateKeys[0]];
};
const resolveFallbackAnchor = (options)=>{
    const { dataset, selectorLabel, selectorValue, valueField, bandFields } = options;
    const fallbackDatum = buildFallbackSelectorDatum(selectorValue);
    if (!fallbackDatum) return;
    const inferredValue = inferFallbackValue(fallbackDatum, valueField, bandFields);
    if (void 0 === inferredValue) return;
    const coordinateDatum = {
        ...fallbackDatum,
        [valueField]: inferredValue
    };
    const bandDatum = buildStackGroupDatum(coordinateDatum, bandFields);
    try {
        return {
            mode: 'element',
            selectorLabel,
            coordinateDatum,
            matchedDatum: fallbackDatum,
            bandDatum,
            bandIndex: resolveBandIndex(dataset, bandDatum),
            value: normalizeDifferenceValue(inferredValue, selectorLabel, valueField)
        };
    } catch  {
        return;
    }
};
const hasMixedSigns = (values)=>{
    const nonZeroValues = values.filter((value)=>0 !== value);
    return nonZeroValues.some((value)=>value > 0) && nonZeroValues.some((value)=>value < 0);
};
const getDifferenceLineStackResolveMode = (vseed, advancedVSeed)=>{
    const hasMultipleMeasure = hasMultipleMeasureInSingleView(advancedVSeed.reshapeMeasures ?? []);
    if (!hasMultipleMeasure) return 'none';
    if ('column' === vseed.chartType) return 'auto';
    if ('bar' === vseed.chartType) return 'auto';
    return 'none';
};
const usesDifferenceLineElementStackEnd = (vseed, advancedVSeed)=>'area' === vseed.chartType && hasMultipleMeasureInSingleView(advancedVSeed.reshapeMeasures ?? []);
const resolveDifferenceAnchor = (options)=>{
    const { dataset, selectorLabel, selectorValue, spec, stackResolveMode, allowSelectorFallback = true } = options;
    const matches = dataset.filter((datum)=>selector(datum, selectorValue));
    const valueField = getDifferenceValueField(spec);
    const bandFields = getDifferenceBandFields(spec);
    if (0 === matches.length) return 'none' === stackResolveMode && allowSelectorFallback ? resolveFallbackAnchor({
        dataset,
        selectorLabel,
        selectorValue,
        valueField,
        bandFields
    }) : void 0;
    if ('none' === stackResolveMode || 'auto' === stackResolveMode && 1 === matches.length) {
        if (1 !== matches.length) throw new Error(`annotationDifferenceLine ${selectorLabel} selector must resolve to exactly one datum, got ${matches.length}`);
        const bandDatum = buildStackGroupDatum(matches[0], bandFields);
        return {
            mode: 'element',
            selectorLabel,
            coordinateDatum: matches[0],
            matchedDatum: matches[0],
            bandDatum,
            bandIndex: resolveBandIndex(dataset, bandDatum),
            value: normalizeDifferenceValue(matches[0][valueField], selectorLabel, valueField)
        };
    }
    const stackGroups = new Map();
    matches.forEach((datum)=>{
        const stackGroupDatum = buildStackGroupDatum(datum, bandFields);
        stackGroups.set(JSON.stringify(stackGroupDatum), stackGroupDatum);
    });
    if (1 !== stackGroups.size) throw new Error(`annotationDifferenceLine ${selectorLabel} selector must resolve to exactly one stack group, got ${stackGroups.size}`);
    const stackGroupDatum = Array.from(stackGroups.values())[0];
    const groupRows = dataset.filter((datum)=>isSubset(stackGroupDatum, datum));
    if ('auto' === stackResolveMode && groupRows.length !== matches.length) throw new Error(`annotationDifferenceLine ${selectorLabel} selector must resolve to exactly one stack element or one full stack group`);
    const groupValues = groupRows.map((datum)=>normalizeDifferenceValue(datum[valueField], selectorLabel, valueField));
    if (hasMixedSigns(groupValues)) throw new Error('annotationDifferenceLine does not support mixed-sign stack totals in v1');
    return {
        mode: 'stackTotal',
        selectorLabel,
        coordinateDatum: stackGroupDatum,
        matchedDatum: matches[0],
        stackGroupDatum,
        bandDatum: stackGroupDatum,
        bandIndex: resolveBandIndex(dataset, stackGroupDatum),
        value: groupValues.reduce((sum, value)=>sum + value, 0)
    };
};
const getStackRuntimeTotal = (runtimeMatches)=>{
    if (0 === runtimeMatches.length) return;
    const stackEndValues = runtimeMatches.map((datum)=>Number(datum[STACK_END_FIELD]));
    if (stackEndValues.some((value)=>!Number.isFinite(value))) return;
    if (hasMixedSigns(stackEndValues)) throw new Error('annotationDifferenceLine does not support mixed-sign runtime stack totals in v1');
    return stackEndValues.some((value)=>value < 0) ? Math.min(...stackEndValues) : Math.max(...stackEndValues);
};
const buildDifferenceCoordinateDatum = (options)=>{
    const { anchor, seriesData, relativeSeries, useElementStackEnd = false } = options;
    if (!anchor.stackGroupDatum) {
        const runtimeMatches = anchor.matchedDatum ? seriesData.filter((datum)=>isSubset(anchor.matchedDatum, datum)) : [];
        if (!useElementStackEnd) return runtimeMatches[0] ?? anchor.coordinateDatum;
        const runtimeMatch = runtimeMatches[0];
        const runtimeStackEnd = Number(runtimeMatch?.[STACK_END_FIELD]);
        return {
            ...runtimeMatch ?? anchor.coordinateDatum,
            [relativeSeries.getStackValueField()]: Number.isFinite(runtimeStackEnd) ? runtimeStackEnd : anchor.value
        };
    }
    const runtimeMatches = seriesData.filter((datum)=>isSubset(anchor.stackGroupDatum, datum));
    const runtimeStackTotal = getStackRuntimeTotal(runtimeMatches);
    return {
        ...runtimeMatches[0] ?? anchor.coordinateDatum,
        [relativeSeries.getStackValueField()]: runtimeStackTotal ?? anchor.value
    };
};
const getRuntimeDifferenceValue = (options)=>{
    const { anchor, seriesData, useElementStackEnd = false } = options;
    if (anchor.stackGroupDatum) {
        const runtimeMatches = seriesData.filter((datum)=>isSubset(anchor.stackGroupDatum, datum));
        return getStackRuntimeTotal(runtimeMatches) ?? anchor.value;
    }
    if (!useElementStackEnd || !anchor.matchedDatum) return anchor.value;
    const runtimeMatch = seriesData.find((datum)=>isSubset(anchor.matchedDatum, datum));
    const runtimeStackEnd = Number(runtimeMatch?.[STACK_END_FIELD]);
    return Number.isFinite(runtimeStackEnd) ? runtimeStackEnd : anchor.value;
};
const getDifferenceValue = (startValue, endValue, differenceType = 'absolute')=>{
    if ('percent' === differenceType) {
        if (0 === startValue) throw new Error('annotationDifferenceLine percent difference cannot be computed because start value is 0');
        return (endValue - startValue) / startValue;
    }
    return endValue - startValue;
};
const buildDifferenceText = (startValue, endValue, differenceType = 'absolute', formatter)=>{
    const differenceValue = getDifferenceValue(startValue, endValue, differenceType);
    if (formatter) return formatter(differenceValue);
    if ('percent' === differenceType) return `${(100 * differenceValue).toFixed(2)}%`;
    return `${differenceValue}`;
};
const inferDifferenceConnectDirection = (vseed, values)=>{
    const isNegativeSide = values.every((value)=>value < 0);
    if (isBarLikeChart(vseed)) return isNegativeSide ? 'left' : 'right';
    return isNegativeSide ? 'bottom' : 'top';
};
const inferDifferenceBracketDirection = (start, end)=>{
    if (void 0 === start.bandIndex || void 0 === end.bandIndex) return 'right';
    return start.bandIndex <= end.bandIndex ? 'right' : 'left';
};
export { buildDifferenceCoordinateDatum, buildDifferenceText, getDifferenceLineStackResolveMode, getDifferenceValue, getRuntimeDifferenceValue, getStackRuntimeTotal, inferDifferenceBracketDirection, inferDifferenceConnectDirection, resolveDifferenceAnchor, usesDifferenceLineElementStackEnd };

//# sourceMappingURL=annotationDifferenceLineCommon.js.map