import { isArray, isBoolean, isNumber, isString } from 'vega-util';
import { isAggregateOp, isArgmaxDef, isArgminDef, isCountingAggregateOp } from './aggregate';
import { autoMaxBins, binToString, isBinned, isBinning } from './bin';
import { isScaleChannel, isSecondaryRangeChannel, POSITION_SCALE_CHANNELS, rangeType } from './channel';
import { getMarkConfig } from './compile/common';
import { dateTimeToExpr, isDateTime } from './datetime';
import * as log from './log';
import { isRectBasedMark } from './mark';
import { isSortByChannel } from './sort';
import { isFacetFieldDef } from './spec/facet';
import { getTimeUnitParts, isLocalSingleTimeUnit, normalizeTimeUnit, timeUnitToString } from './timeunit';
import { getFullName, QUANTITATIVE } from './type';
import { contains, flatAccessWithDatum, getFirstDefined, internalField, replacePathInField, titlecase } from './util';
export function isConditionalSelection(c) {
    return c['selection'];
}
export function isRepeatRef(field) {
    return field && !isString(field) && 'repeat' in field;
}
export function toFieldDefBase(fieldDef) {
    const { field, timeUnit, bin, aggregate } = fieldDef;
    return Object.assign(Object.assign(Object.assign(Object.assign({}, (timeUnit ? { timeUnit } : {})), (bin ? { bin } : {})), (aggregate ? { aggregate } : {})), { field });
}
export function isSortableFieldDef(fieldDef) {
    return isTypedFieldDef(fieldDef) && !!fieldDef['sort'];
}
export function getBand(channel, fieldDef, fieldDef2, mark, config, { isMidPoint } = {}) {
    const { timeUnit, bin } = fieldDef;
    if (contains(['x', 'y'], channel)) {
        if (isPositionFieldDef(fieldDef) && fieldDef.band !== undefined) {
            return fieldDef.band;
        }
        else if (timeUnit && !fieldDef2) {
            if (isMidPoint) {
                return getMarkConfig('timeUnitBandPosition', mark, config);
            }
            else {
                return isRectBasedMark(mark.type) ? getMarkConfig('timeUnitBand', mark, config) : 0;
            }
        }
        else if (isBinning(bin)) {
            return isRectBasedMark(mark.type) && !isMidPoint ? 1 : 0.5;
        }
    }
    return undefined;
}
export function hasBand(channel, fieldDef, fieldDef2, mark, config) {
    if (isBinning(fieldDef.bin) || (fieldDef.timeUnit && isTypedFieldDef(fieldDef) && fieldDef.type === 'temporal')) {
        return !!getBand(channel, fieldDef, fieldDef2, mark, config);
    }
    return false;
}
export function isConditionalDef(channelDef) {
    return !!channelDef && !!channelDef['condition'];
}
/**
 * Return if a channelDef is a ConditionalValueDef with ConditionFieldDef
 */
export function hasConditionalFieldDef(channelDef) {
    return !!channelDef && !!channelDef.condition && !isArray(channelDef.condition) && isFieldDef(channelDef.condition);
}
export function hasConditionalValueDef(channelDef) {
    return !!channelDef && !!channelDef.condition && (isArray(channelDef.condition) || isValueDef(channelDef.condition));
}
export function isFieldDef(channelDef) {
    return !!channelDef && (!!channelDef['field'] || channelDef['aggregate'] === 'count');
}
export function isTypedFieldDef(channelDef) {
    return !!channelDef && ((!!channelDef['field'] && !!channelDef['type']) || channelDef['aggregate'] === 'count');
}
export function isStringFieldDef(channelDef) {
    return isFieldDef(channelDef) && isString(channelDef.field);
}
export function isValueDef(channelDef) {
    return channelDef && 'value' in channelDef && channelDef['value'] !== undefined;
}
export function isScaleFieldDef(channelDef) {
    return !!channelDef && (!!channelDef['scale'] || !!channelDef['sort']);
}
export function isPositionFieldDef(channelDef) {
    return (!!channelDef &&
        (!!channelDef['axis'] || !!channelDef['stack'] || !!channelDef['impute'] || channelDef['band'] !== undefined));
}
export function isMarkPropFieldDef(channelDef) {
    return !!channelDef && !!channelDef['legend'];
}
export function isTextFieldDef(channelDef) {
    return !!channelDef && !!channelDef['format'];
}
function isOpFieldDef(fieldDef) {
    return !!fieldDef['op'];
}
/**
 * Get a Vega field reference from a Vega-Lite field def.
 */
export function vgField(fieldDef, opt = {}) {
    var _a, _b, _c;
    let field = fieldDef.field;
    const prefix = opt.prefix;
    let suffix = opt.suffix;
    let argAccessor = ''; // for accessing argmin/argmax field at the end without getting escaped
    if (isCount(fieldDef)) {
        field = internalField('count');
    }
    else {
        let fn;
        if (!opt.nofn) {
            if (isOpFieldDef(fieldDef)) {
                fn = fieldDef.op;
            }
            else {
                const { bin, aggregate, timeUnit } = fieldDef;
                if (isBinning(bin)) {
                    fn = binToString(bin);
                    suffix = (_a = opt.binSuffix, (_a !== null && _a !== void 0 ? _a : '')) + (_b = opt.suffix, (_b !== null && _b !== void 0 ? _b : ''));
                }
                else if (aggregate) {
                    if (isArgmaxDef(aggregate)) {
                        argAccessor = `.${field}`;
                        field = `argmax_${aggregate.argmax}`;
                    }
                    else if (isArgminDef(aggregate)) {
                        argAccessor = `.${field}`;
                        field = `argmin_${aggregate.argmin}`;
                    }
                    else {
                        fn = String(aggregate);
                    }
                }
                else if (timeUnit) {
                    fn = timeUnitToString(timeUnit);
                    suffix = ((!contains(['range', 'mid'], opt.binSuffix) && opt.binSuffix) || '') + (_c = opt.suffix, (_c !== null && _c !== void 0 ? _c : ''));
                }
            }
        }
        if (fn) {
            field = field ? `${fn}_${field}` : fn;
        }
    }
    if (suffix) {
        field = `${field}_${suffix}`;
    }
    if (prefix) {
        field = `${prefix}_${field}`;
    }
    if (opt.forAs) {
        return field;
    }
    else if (opt.expr) {
        // Expression to access flattened field. No need to escape dots.
        return flatAccessWithDatum(field, opt.expr) + argAccessor;
    }
    else {
        // We flattened all fields so paths should have become dot.
        return replacePathInField(field) + argAccessor;
    }
}
export function isDiscrete(fieldDef) {
    switch (fieldDef.type) {
        case 'nominal':
        case 'ordinal':
        case 'geojson':
            return true;
        case 'quantitative':
            return !!fieldDef.bin;
        case 'temporal':
            return false;
    }
    throw new Error(log.message.invalidFieldType(fieldDef.type));
}
export function isContinuous(fieldDef) {
    return !isDiscrete(fieldDef);
}
export function isCount(fieldDef) {
    return fieldDef.aggregate === 'count';
}
export function verbalTitleFormatter(fieldDef, config) {
    var _a;
    const { field, bin, timeUnit, aggregate } = fieldDef;
    if (aggregate === 'count') {
        return config.countTitle;
    }
    else if (isBinning(bin)) {
        return `${field} (binned)`;
    }
    else if (timeUnit) {
        const unit = (_a = normalizeTimeUnit(timeUnit)) === null || _a === void 0 ? void 0 : _a.unit;
        if (unit) {
            return `${field} (${getTimeUnitParts(unit).join('-')})`;
        }
    }
    else if (aggregate) {
        if (isArgmaxDef(aggregate)) {
            return `${field} for max ${aggregate.argmax}`;
        }
        else if (isArgminDef(aggregate)) {
            return `${field} for min ${aggregate.argmin}`;
        }
        else {
            return `${titlecase(aggregate)} of ${field}`;
        }
    }
    return field;
}
export function functionalTitleFormatter(fieldDef) {
    var _a, _b;
    const { aggregate, bin, timeUnit, field } = fieldDef;
    if (isArgmaxDef(aggregate)) {
        return `${field} for argmax(${aggregate.argmax})`;
    }
    else if (isArgminDef(aggregate)) {
        return `${field} for argmin(${aggregate.argmin})`;
    }
    const timeUnitParams = normalizeTimeUnit(timeUnit);
    const fn = aggregate || ((_a = timeUnitParams) === null || _a === void 0 ? void 0 : _a.unit) || (((_b = timeUnitParams) === null || _b === void 0 ? void 0 : _b.maxbins) && 'timeunit') || (isBinning(bin) && 'bin');
    if (fn) {
        return fn.toUpperCase() + '(' + field + ')';
    }
    else {
        return field;
    }
}
export const defaultTitleFormatter = (fieldDef, config) => {
    switch (config.fieldTitle) {
        case 'plain':
            return fieldDef.field;
        case 'functional':
            return functionalTitleFormatter(fieldDef);
        default:
            return verbalTitleFormatter(fieldDef, config);
    }
};
let titleFormatter = defaultTitleFormatter;
export function setTitleFormatter(formatter) {
    titleFormatter = formatter;
}
export function resetTitleFormatter() {
    setTitleFormatter(defaultTitleFormatter);
}
export function title(fieldDef, config, { allowDisabling, includeDefault = true }) {
    var _a, _b;
    const guide = (_a = getGuide(fieldDef), (_a !== null && _a !== void 0 ? _a : {}));
    const guideTitle = guide.title;
    const def = includeDefault ? defaultTitle(fieldDef, config) : undefined;
    if (allowDisabling) {
        return getFirstDefined(guideTitle, fieldDef.title, def);
    }
    else {
        return _b = (guideTitle !== null && guideTitle !== void 0 ? guideTitle : fieldDef.title), (_b !== null && _b !== void 0 ? _b : def);
    }
}
export function getGuide(fieldDef) {
    if (isPositionFieldDef(fieldDef) && fieldDef.axis) {
        return fieldDef.axis;
    }
    else if (isMarkPropFieldDef(fieldDef) && fieldDef.legend) {
        return fieldDef.legend;
    }
    else if (isFacetFieldDef(fieldDef) && fieldDef.header) {
        return fieldDef.header;
    }
    return undefined;
}
export function defaultTitle(fieldDef, config) {
    return titleFormatter(fieldDef, config);
}
export function format(fieldDef) {
    var _a;
    if (isTextFieldDef(fieldDef) && fieldDef.format) {
        return fieldDef.format;
    }
    else {
        const guide = (_a = getGuide(fieldDef), (_a !== null && _a !== void 0 ? _a : {}));
        return guide.format;
    }
}
export function defaultType(fieldDef, channel) {
    if (fieldDef.timeUnit) {
        return 'temporal';
    }
    if (isBinning(fieldDef.bin)) {
        return 'quantitative';
    }
    switch (rangeType(channel)) {
        case 'continuous':
            return 'quantitative';
        case 'discrete':
            return 'nominal';
        case 'flexible': // color
            return 'nominal';
        default:
            return 'quantitative';
    }
}
/**
 * Returns the fieldDef -- either from the outer channelDef or from the condition of channelDef.
 * @param channelDef
 */
export function getFieldDef(channelDef) {
    if (isFieldDef(channelDef)) {
        return channelDef;
    }
    else if (hasConditionalFieldDef(channelDef)) {
        return channelDef.condition;
    }
    return undefined;
}
export function getTypedFieldDef(channelDef) {
    if (isFieldDef(channelDef)) {
        return channelDef;
    }
    else if (hasConditionalFieldDef(channelDef)) {
        return channelDef.condition;
    }
    return undefined;
}
/**
 * Convert type to full, lowercase type, or augment the fieldDef with a default type if missing.
 */
export function normalize(channelDef, channel) {
    if (isString(channelDef) || isNumber(channelDef) || isBoolean(channelDef)) {
        const primitiveType = isString(channelDef) ? 'string' : isNumber(channelDef) ? 'number' : 'boolean';
        log.warn(log.message.primitiveChannelDef(channel, primitiveType, channelDef));
        return { value: channelDef };
    }
    // If a fieldDef contains a field, we need type.
    if (isFieldDef(channelDef)) {
        return normalizeFieldDef(channelDef, channel);
    }
    else if (hasConditionalFieldDef(channelDef)) {
        return Object.assign(Object.assign({}, channelDef), { 
            // Need to cast as normalizeFieldDef normally return FieldDef, but here we know that it is definitely Condition<FieldDef>
            condition: normalizeFieldDef(channelDef.condition, channel) });
    }
    return channelDef;
}
export function normalizeFieldDef(fd, channel) {
    const { aggregate, timeUnit, bin, field } = fd;
    const fieldDef = Object.assign({}, fd);
    // Drop invalid aggregate
    if (aggregate && !isAggregateOp(aggregate) && !isArgmaxDef(aggregate) && !isArgminDef(aggregate)) {
        log.warn(log.message.invalidAggregate(aggregate));
        delete fieldDef.aggregate;
    }
    // Normalize Time Unit
    if (timeUnit) {
        fieldDef.timeUnit = normalizeTimeUnit(timeUnit);
    }
    if (field) {
        fieldDef.field = `${field}`;
    }
    // Normalize bin
    if (isBinning(bin)) {
        fieldDef.bin = normalizeBin(bin, channel);
    }
    if (isBinned(bin) && !contains(POSITION_SCALE_CHANNELS, channel)) {
        log.warn(`Channel ${channel} should not be used with "binned" bin`);
    }
    // Normalize Type
    if (isTypedFieldDef(fieldDef)) {
        const { type } = fieldDef;
        const fullType = getFullName(type);
        if (type !== fullType) {
            // convert short type to full type
            fieldDef.type = fullType;
        }
        if (type !== 'quantitative') {
            if (isCountingAggregateOp(aggregate)) {
                log.warn(log.message.invalidFieldTypeForCountAggregate(type, aggregate));
                fieldDef.type = 'quantitative';
            }
        }
    }
    else if (!isSecondaryRangeChannel(channel)) {
        // If type is empty / invalid, then augment with default type
        const newType = defaultType(fieldDef, channel);
        log.warn(log.message.missingFieldType(channel, newType));
        fieldDef['type'] = newType;
    }
    if (isTypedFieldDef(fieldDef)) {
        const { compatible, warning } = channelCompatibility(fieldDef, channel);
        if (!compatible) {
            log.warn(warning);
        }
    }
    if (isSortableFieldDef(fieldDef) && isString(fieldDef.sort)) {
        const { sort } = fieldDef;
        if (isSortByChannel(sort)) {
            return Object.assign(Object.assign({}, fieldDef), { sort: { encoding: sort } });
        }
        const sub = sort.substr(1);
        if (sort.charAt(0) === '-' && isSortByChannel(sub)) {
            return Object.assign(Object.assign({}, fieldDef), { sort: { encoding: sub, order: 'descending' } });
        }
    }
    return fieldDef;
}
export function normalizeBin(bin, channel) {
    if (isBoolean(bin)) {
        return { maxbins: autoMaxBins(channel) };
    }
    else if (bin === 'binned') {
        return {
            binned: true
        };
    }
    else if (!bin.maxbins && !bin.step) {
        return Object.assign(Object.assign({}, bin), { maxbins: autoMaxBins(channel) });
    }
    else {
        return bin;
    }
}
const COMPATIBLE = { compatible: true };
export function channelCompatibility(fieldDef, channel) {
    const type = fieldDef.type;
    if (type === 'geojson' && channel !== 'shape') {
        return {
            compatible: false,
            warning: `Channel ${channel} should not be used with a geojson data.`
        };
    }
    switch (channel) {
        case 'row':
        case 'column':
        case 'facet':
            if (isContinuous(fieldDef)) {
                return {
                    compatible: false,
                    warning: log.message.facetChannelShouldBeDiscrete(channel)
                };
            }
            return COMPATIBLE;
        case 'x':
        case 'y':
        case 'color':
        case 'fill':
        case 'stroke':
        case 'text':
        case 'detail':
        case 'key':
        case 'tooltip':
        case 'href':
        case 'url':
            return COMPATIBLE;
        case 'longitude':
        case 'longitude2':
        case 'latitude':
        case 'latitude2':
            if (type !== QUANTITATIVE) {
                return {
                    compatible: false,
                    warning: `Channel ${channel} should be used with a quantitative field only, not ${fieldDef.type} field.`
                };
            }
            return COMPATIBLE;
        case 'opacity':
        case 'fillOpacity':
        case 'strokeOpacity':
        case 'strokeWidth':
        case 'size':
        case 'x2':
        case 'y2':
            if (type === 'nominal' && !fieldDef['sort']) {
                return {
                    compatible: false,
                    warning: `Channel ${channel} should not be used with an unsorted discrete field.`
                };
            }
            return COMPATIBLE;
        case 'strokeDash':
            if (!contains(['ordinal', 'nominal'], fieldDef.type)) {
                return {
                    compatible: false,
                    warning: 'StrokeDash channel should be used with only discrete data.'
                };
            }
            return COMPATIBLE;
        case 'shape':
            if (!contains(['ordinal', 'nominal', 'geojson'], fieldDef.type)) {
                return {
                    compatible: false,
                    warning: 'Shape channel should be used with only either discrete or geojson data.'
                };
            }
            return COMPATIBLE;
        case 'order':
            if (fieldDef.type === 'nominal' && !('sort' in fieldDef)) {
                return {
                    compatible: false,
                    warning: `Channel order is inappropriate for nominal field, which has no inherent order.`
                };
            }
            return COMPATIBLE;
    }
}
/**
 * Check if the field def uses a time format or does not use any format but is temporal
 * (this does not cover field defs that are temporal but use a number format).
 */
export function isTimeFormatFieldDef(fieldDef) {
    const guide = getGuide(fieldDef);
    const formatType = (guide && guide.formatType) || (isTextFieldDef(fieldDef) && fieldDef.formatType);
    return formatType === 'time' || (!formatType && isTimeFieldDef(fieldDef));
}
/**
 * Check if field def has tye `temporal`. If you want to also cover field defs that use a time format, use `isTimeFormatFieldDef`.
 */
export function isTimeFieldDef(fieldDef) {
    return fieldDef.type === 'temporal' || !!fieldDef.timeUnit;
}
/**
 * Getting a value associated with a fielddef.
 * Convert the value to Vega expression if applicable (for datetime object, or string if the field def is temporal or has timeUnit)
 */
export function valueExpr(v, { timeUnit, type, time, undefinedIfExprNotRequired }) {
    var _a;
    const unit = (_a = normalizeTimeUnit(timeUnit)) === null || _a === void 0 ? void 0 : _a.unit;
    let expr;
    if (isDateTime(v)) {
        expr = dateTimeToExpr(v);
    }
    else if (isString(v) || isNumber(v)) {
        if (unit || type === 'temporal') {
            if (isLocalSingleTimeUnit(unit)) {
                expr = dateTimeToExpr({ [unit]: v });
            }
            else {
                expr = `datetime(${JSON.stringify(v)})`;
            }
        }
    }
    if (expr) {
        return time ? `time(${expr})` : expr;
    }
    // number or boolean or normal string
    return undefinedIfExprNotRequired ? undefined : JSON.stringify(v);
}
/**
 * Standardize value array -- convert each value to Vega expression if applicable
 */
export function valueArray(fieldDef, values) {
    var _a;
    const { type } = fieldDef;
    const timeUnit = (_a = normalizeTimeUnit(fieldDef.timeUnit)) === null || _a === void 0 ? void 0 : _a.unit;
    return values.map(v => {
        const expr = valueExpr(v, { timeUnit, type, undefinedIfExprNotRequired: true });
        // return signal for the expression if we need an expression
        if (expr !== undefined) {
            return { signal: expr };
        }
        // otherwise just return the original value
        return v;
    });
}
/**
 * Checks whether a fieldDef for a particular channel requires a computed bin range.
 */
export function binRequiresRange(fieldDef, channel) {
    if (!isBinning(fieldDef.bin)) {
        console.warn('Only call this method for binned field defs.');
        return false;
    }
    // We need the range only when the user explicitly forces a binned field to be use discrete scale. In this case, bin range is used in axis and legend labels.
    // We could check whether the axis or legend exists (not disabled) but that seems overkill.
    return isScaleChannel(channel) && contains(['ordinal', 'nominal'], fieldDef.type);
}
//# sourceMappingURL=channeldef.js.map