var __rest = (this && this.__rest) || function (s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
            if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
                t[p[i]] = s[p[i]];
        }
    return t;
};
import { isObject } from 'vega';
import { array, stringValue } from 'vega-util';
import { STORE, TUPLE, unitName } from '.';
import { LATITUDE, LONGITUDE, X, Y } from '../../channel';
import { warn } from '../../log';
import { hasContinuousDomain } from '../../scale';
import { SELECTION_ID } from '../../selection';
import { keys, vals } from '../../util';
import { isUnitModel } from '../model';
import { assembleInit } from './assemble';
import { TUPLE_FIELDS } from './project';
import scales from './scales';
export const BRUSH = '_brush';
export const SCALE_TRIGGER = '_scale_trigger';
export const GEO_INIT_TICK = 'geo_interval_init_tick'; // Workaround for https://github.com/vega/vega/issues/3481
const INIT = '_init';
const CENTER = '_center';
const interval = {
    defined: selCmpt => selCmpt.type === 'interval',
    parse: (model, selCmpt, selDef) => {
        var _a;
        var _b;
        if (model.hasProjection) {
            const def = Object.assign({}, (isObject(selDef.select) ? selDef.select : {}));
            def.fields = [SELECTION_ID];
            if (!def.encodings) {
                // Remap default x/y projection
                def.encodings = selDef.value ? keys(selDef.value) : [LONGITUDE, LATITUDE];
            }
            selDef.select = Object.assign({ type: 'interval' }, def);
        }
        if (selCmpt.translate && !scales.defined(selCmpt)) {
            const filterExpr = `!event.item || event.item.mark.name !== ${stringValue(selCmpt.name + BRUSH)}`;
            for (const evt of selCmpt.events) {
                if (!evt.between) {
                    warn(`${evt} is not an ordered event stream for interval selections.`);
                    continue;
                }
                const filters = array(((_a = (_b = evt.between[0]).filter) !== null && _a !== void 0 ? _a : (_b.filter = [])));
                if (filters.indexOf(filterExpr) < 0) {
                    filters.push(filterExpr);
                }
            }
        }
    },
    signals: (model, selCmpt, signals) => {
        const name = selCmpt.name;
        const tupleSg = name + TUPLE;
        const channels = vals(selCmpt.project.hasChannel).filter(p => p.channel === X || p.channel === Y);
        const init = selCmpt.init ? selCmpt.init[0] : null;
        signals.push(...channels.reduce((arr, proj) => arr.concat(channelSignals(model, selCmpt, proj, init && init[proj.index])), []));
        if (!model.hasProjection) {
            // Proxy scale reactions to ensure that an infinite loop doesn't occur
            // when an interval selection filter touches the scale.
            if (!scales.defined(selCmpt)) {
                const triggerSg = name + SCALE_TRIGGER;
                const scaleTriggers = channels.map(proj => {
                    const channel = proj.channel;
                    const { data: dname, visual: vname } = proj.signals;
                    const scaleName = stringValue(model.scaleName(channel));
                    const scaleType = model.getScaleComponent(channel).get('type');
                    const toNum = hasContinuousDomain(scaleType) ? '+' : '';
                    return (`(!isArray(${dname}) || ` +
                        `(${toNum}invert(${scaleName}, ${vname})[0] === ${toNum}${dname}[0] && ` +
                        `${toNum}invert(${scaleName}, ${vname})[1] === ${toNum}${dname}[1]))`);
                });
                if (scaleTriggers.length) {
                    signals.push({
                        name: triggerSg,
                        value: {},
                        on: [
                            {
                                events: channels.map(proj => ({ scale: model.scaleName(proj.channel) })),
                                update: scaleTriggers.join(' && ') + ` ? ${triggerSg} : {}`
                            }
                        ]
                    });
                }
            }
            // Only add an interval to the store if it has valid data extents. Data extents
            // are set to null if pixel extents are equal to account for intervals over
            // ordinal/nominal domains which, when inverted, will still produce a valid datum.
            const dataSignals = channels.map(proj => proj.signals.data);
            const update = `unit: ${unitName(model)}, fields: ${name + TUPLE_FIELDS}, values`;
            return signals.concat(Object.assign(Object.assign({ name: tupleSg }, (init ? { init: `{${update}: ${assembleInit(init)}}` } : {})), (dataSignals.length
                ? {
                    on: [
                        {
                            events: [{ signal: dataSignals.join(' || ') }],
                            update: `${dataSignals.join(' && ')} ? {${update}: [${dataSignals}]} : null`
                        }
                    ]
                }
                : {})));
        }
        else {
            const projection = stringValue(model.projectionName());
            const centerSg = model.projectionName() + CENTER;
            const { x, y } = selCmpt.project.hasChannel;
            const xvname = x && x.signals.visual;
            const yvname = y && y.signals.visual;
            const xinit = x ? init && init[x.index] : `${centerSg}[0]`;
            const yinit = y ? init && init[y.index] : `${centerSg}[1]`;
            const sizeSg = (layout) => model.getSizeSignalRef(layout).signal;
            const bbox = `[` +
                `[${xvname ? xvname + '[0]' : '0'}, ${yvname ? yvname + '[0]' : '0'}],` +
                `[${xvname ? xvname + '[1]' : sizeSg('width')}, ` +
                `${yvname ? yvname + '[1]' : sizeSg('height')}]` +
                `]`;
            if (init) {
                signals.unshift({
                    name: name + INIT,
                    init: `[scale(${projection}, [${x ? xinit[0] : xinit}, ${y ? yinit[0] : yinit}]), ` +
                        `scale(${projection}, [${x ? xinit[1] : xinit}, ${y ? yinit[1] : yinit}])]`
                });
                if (!x || !y) {
                    // If initializing a uni-dimensional brush, use the center of the view to determine the other coord
                    const hasCenterSg = signals.find(s => s.name === centerSg);
                    if (!hasCenterSg) {
                        signals.unshift({
                            name: centerSg,
                            update: `invert(${projection}, [${sizeSg('width')}/2, ${sizeSg('height')}/2])`
                        });
                    }
                }
            }
            const intersect = `intersect(${bbox}, {markname: ${stringValue(model.getName('marks'))}}, unit.mark)`;
            const base = `{unit: ${unitName(model)}}`;
            const update = `vlSelectionTuples(${intersect}, ${base})`;
            return signals.concat(Object.assign({ name: tupleSg, update }, (init
                ? {
                    on: [{ events: { signal: GEO_INIT_TICK }, update }]
                }
                : {})));
        }
    },
    topLevelSignals: (model, selCmpt, signals) => {
        if (isUnitModel(model) && model.hasProjection && selCmpt.init) {
            // Workaround for https://github.com/vega/vega/issues/3481
            // The scenegraph isn't populated on the first pulse. So we use a timer signal
            // to re-pulse the dataflow as soon as possible. We return an object to ensure
            // this only occurs once.
            const hasTick = signals.filter(s => s.name === GEO_INIT_TICK);
            if (!hasTick.length) {
                signals.unshift({
                    name: GEO_INIT_TICK,
                    value: null,
                    on: [
                        {
                            events: 'timer{1}',
                            update: `${GEO_INIT_TICK} === null ? {} : ${GEO_INIT_TICK}`
                        }
                    ]
                });
            }
        }
        return signals;
    },
    marks: (model, selCmpt, marks) => {
        const name = selCmpt.name;
        const { x, y } = selCmpt.project.hasChannel;
        const xvname = x === null || x === void 0 ? void 0 : x.signals.visual;
        const yvname = y === null || y === void 0 ? void 0 : y.signals.visual;
        const store = `data(${stringValue(selCmpt.name + STORE)})`;
        // Do not add a brush if we're binding to scales
        // or we don't have a valid interval projection
        if (scales.defined(selCmpt) || (!x && !y)) {
            return marks;
        }
        const update = {
            x: x !== undefined ? { signal: `${xvname}[0]` } : { value: 0 },
            y: y !== undefined ? { signal: `${yvname}[0]` } : { value: 0 },
            x2: x !== undefined ? { signal: `${xvname}[1]` } : { field: { group: 'width' } },
            y2: y !== undefined ? { signal: `${yvname}[1]` } : { field: { group: 'height' } }
        };
        // If the selection is resolved to global, only a single interval is in
        // the store. Wrap brush mark's encodings with a production rule to test
        // this based on the `unit` property. Hide the brush mark if it corresponds
        // to a unit different from the one in the store.
        if (selCmpt.resolve === 'global') {
            for (const key of keys(update)) {
                update[key] = [
                    Object.assign({ test: `${store}.length && ${store}[0].unit === ${unitName(model)}` }, update[key]),
                    { value: 0 }
                ];
            }
        }
        // Two brush marks ensure that fill colors and other aesthetic choices do
        // not interefere with the core marks, but that the brushed region can still
        // be interacted with (e.g., dragging it around).
        const _a = selCmpt.mark, { fill, fillOpacity, cursor } = _a, stroke = __rest(_a, ["fill", "fillOpacity", "cursor"]);
        const vgStroke = keys(stroke).reduce((def, k) => {
            def[k] = [
                {
                    test: [x !== undefined && `${xvname}[0] !== ${xvname}[1]`, y !== undefined && `${yvname}[0] !== ${yvname}[1]`]
                        .filter(t => t)
                        .join(' && '),
                    value: stroke[k]
                },
                { value: null }
            ];
            return def;
        }, {});
        return [
            {
                name: `${name + BRUSH}_bg`,
                type: 'rect',
                clip: true,
                encode: {
                    enter: {
                        fill: { value: fill },
                        fillOpacity: { value: fillOpacity }
                    },
                    update
                }
            },
            ...marks,
            {
                name: name + BRUSH,
                type: 'rect',
                clip: true,
                encode: {
                    enter: Object.assign(Object.assign({}, (cursor ? { cursor: { value: cursor } } : {})), { fill: { value: 'transparent' } }),
                    update: Object.assign(Object.assign({}, update), vgStroke)
                }
            }
        ];
    }
};
export default interval;
/**
 * Returns the visual and data signals for an interval selection.
 */
function channelSignals(model, selCmpt, proj, init) {
    const scaledInterval = !model.hasProjection;
    const channel = proj.channel;
    const vname = proj.signals.visual;
    const scaleName = stringValue(scaledInterval ? model.scaleName(channel) : model.projectionName());
    const scaled = (str) => `scale(${scaleName}, ${str})`;
    const size = model.getSizeSignalRef(channel === X ? 'width' : 'height').signal;
    const coord = `${channel}(unit)`;
    const von = selCmpt.events.reduce((def, evt) => {
        return [
            ...def,
            { events: evt.between[0], update: `[${coord}, ${coord}]` },
            { events: evt, update: `[${vname}[0], clamp(${coord}, 0, ${size})]` } // Brush End
        ];
    }, []);
    if (scaledInterval) {
        const dname = proj.signals.data;
        const hasScales = scales.defined(selCmpt);
        const scale = model.getScaleComponent(channel);
        const scaleType = scale ? scale.get('type') : undefined;
        const vinit = init ? { init: assembleInit(init, true, scaled) } : { value: [] };
        // React to pan/zooms of continuous scales. Non-continuous scales
        // (band, point) cannot be pan/zoomed and any other changes
        // to their domains (e.g., filtering) should clear the brushes.
        von.push({
            events: { signal: selCmpt.name + SCALE_TRIGGER },
            update: hasContinuousDomain(scaleType) ? `[${scaled(`${dname}[0]`)}, ${scaled(`${dname}[1]`)}]` : `[0, 0]`
        });
        return hasScales
            ? [{ name: dname, on: [] }]
            : [
                Object.assign(Object.assign({ name: vname }, vinit), { on: von }),
                Object.assign(Object.assign({ name: dname }, (init ? { init: assembleInit(init) } : {})), { on: [
                        {
                            events: { signal: vname },
                            update: `${vname}[0] === ${vname}[1] ? null : invert(${scaleName}, ${vname})`
                        }
                    ] })
            ];
    }
    else {
        const initIdx = channel === X ? 0 : 1;
        const initSg = selCmpt.name + INIT;
        const vinit = init ? { init: `[${initSg}[0][${initIdx}], ${initSg}[1][${initIdx}]]` } : { value: [] };
        return [Object.assign(Object.assign({ name: vname }, vinit), { on: von })];
    }
}
//# sourceMappingURL=interval.js.map