// Copyright (c) 2019 Shellyl_N and Authors
// license: ISC
// https://github.com/shellyln


import { ErrorTypes,
         ErrorMessages,
         TypeAssertionErrorMessageConstraints,
         TypeAssertion,
         RepeatedAssertion,
         SpreadAssertion,
         OptionalAssertion,
         ValidationContext } from '../types';
import { escapeString }      from './escape';
import { nvl }               from './util';


export const errorTypeNames = [
    '',
    'InvalidDefinition',
    'Required',
    'TypeUnmatched',
    'AdditionalPropUnmatched',
    'RepeatQtyUnmatched',
    'SequenceUnmatched',
    'ValueRangeUnmatched',
    'ValuePatternUnmatched',
    'ValueLengthUnmatched',
    'ValueUnmatched',
];


export const defaultMessages: ErrorMessages = {
    invalidDefinition:       '"%{name}" of "%{parentType}" type definition is invalid.',
    required:                '"%{name}" of "%{parentType}" is required.',
    typeUnmatched:           '"%{name}" of "%{parentType}" should be type "%{expectedType}".',
    additionalPropUnmatched: '"%{addtionalProps}" of "%{parentType}" are not matched to additional property patterns.',
    repeatQtyUnmatched:      '"%{name}" of "%{parentType}" should repeat %{repeatQty} times.',
    sequenceUnmatched:       '"%{name}" of "%{parentType}" sequence is not matched',
    valueRangeUnmatched:     '"%{name}" of "%{parentType}" value should be in the range %{minValue} to %{maxValue}.',
    valuePatternUnmatched:   '"%{name}" of "%{parentType}" value should be matched to pattern "%{pattern}"',
    valueLengthUnmatched:    '"%{name}" of "%{parentType}" length should be in the range %{minLength} to %{maxLength}.',
    valueUnmatched:          '"%{name}" of "%{parentType}" value should be "%{expectedValue}".',
};


type TopRepeatable = RepeatedAssertion | SpreadAssertion | OptionalAssertion | null;


interface ReportErrorArguments {
    ctx: ValidationContext;
    substitutions?: [[string, string]]; // addtional or overwritten substitution values
}


function getErrorMessage(errType: ErrorTypes, ...messages: ErrorMessages[]) {
    for (const m of messages) {
        switch (errType) {
        case ErrorTypes.InvalidDefinition:
            if (m.invalidDefinition) {
                return m.invalidDefinition;
            }
            break;
        case ErrorTypes.Required:
            if (m.required) {
                return m.required;
            }
            break;
        case ErrorTypes.TypeUnmatched:
            if (m.typeUnmatched) {
                return m.typeUnmatched;
            }
            break;
        case ErrorTypes.AdditionalPropUnmatched:
            if (m.additionalPropUnmatched) {
                return m.additionalPropUnmatched;
            }
            break;
        case ErrorTypes.RepeatQtyUnmatched:
            if (m.repeatQtyUnmatched) {
                return m.repeatQtyUnmatched;
            }
            break;
        case ErrorTypes.SequenceUnmatched:
            if (m.sequenceUnmatched) {
                return m.sequenceUnmatched;
            }
            break;
        case ErrorTypes.ValueRangeUnmatched:
            if (m.valueRangeUnmatched) {
                return m.valueRangeUnmatched;
            }
            break;
        case ErrorTypes.ValuePatternUnmatched:
            if (m.valuePatternUnmatched) {
                return m.valuePatternUnmatched;
            }
            break;
        case ErrorTypes.ValueLengthUnmatched:
            if (m.valueLengthUnmatched) {
                return m.valueLengthUnmatched;
            }
            break;
        case ErrorTypes.ValueUnmatched:
            if (m.valueUnmatched) {
                return m.valueUnmatched;
            }
            break;
        }
    }
    return '';
}


function findTopRepeatableAssertion(ctx: ValidationContext): TopRepeatable {
    const ret = ctx.typeStack
        .slice()
        .reverse()
        .map(x => Array.isArray(x) ? x[0] : x)
        .find(x => x.kind === 'repeated' || x.kind === 'spread' || x.kind === 'optional'
                ) as RepeatedAssertion | SpreadAssertion | OptionalAssertion || null;
    return ret;
}


function getExpectedType(ty: TypeAssertion): string {
    switch (ty.kind) {
    case 'repeated':
        return `(repeated ${getExpectedType(ty.repeated)})`;
    case 'spread':
        return getExpectedType(ty.spread);
    case 'sequence':
        return '(sequence)';
    case 'primitive':
        return ty.primitiveName;
    case 'primitive-value':
        return `(value ${
            typeof ty.value === 'string' ?
                `'${String(ty.value)}'` :
                String(ty.value)})`;
    case 'optional':
        return getExpectedType(ty.optional);
    case 'one-of':
        return `(one of ${ty.oneOf.map(x => getExpectedType(x)).join(', ')})`;
    case 'never': case 'any': case 'unknown':
        return ty.kind;
    case 'symlink':
        return ty.symlinkTargetName;
    default:
        return ty.typeName ? ty.typeName : '?';
    }
}


export function formatErrorMessage(
        msg: string, data: any, ty: TypeAssertion,
        args: ReportErrorArguments,
        values: {dataPath: string, topRepeatable: TopRepeatable, parentType: string, entryName: string}) {

    let ret = msg;
    // TODO: complex type object members' custom error messages are not displayed?
    // TODO: escapeString() is needed?

    const tr = values.topRepeatable;

    const dict = new Map<string, string>([
        ['expectedType',
            ty.stereotype ?
                ty.stereotype :
            escapeString(getExpectedType(ty))],
        ['type',
            escapeString(typeof data)],
        ['expectedValue',
            escapeString(
                ty.kind === 'primitive-value' ?
                    String(ty.value) :
                ty.kind === 'enum' ?
                    ty.typeName ?
                        `enum member of ${ty.typeName}` :
                        '?' :
                '?')],
        ['value',
            escapeString(String(data))],
        ['repeatQty',
            escapeString(
                tr ?
                    tr.kind !== 'optional' ? `${
                        nvl(tr.min, '')}${
                            (tr.min !== null && tr.min !== void 0) ||
                            (tr.max !== null && tr.max !== void 0) ? '..' : ''}${
                            nvl(tr.max, '')}` :
                        '0..1' :
                    '?')],
        ['minValue',
            escapeString(
                ty.kind === 'primitive' ?
                    `${nvl(ty.minValue, nvl(ty.greaterThanValue, '(smallest)'))}` : '?')],
        ['maxValue',
            escapeString(
                ty.kind === 'primitive' ?
                    `${nvl(ty.maxValue, nvl(ty.lessThanValue, '(biggest)'))}` : '?')],
        ['pattern',
            escapeString(
                ty.kind === 'primitive' ?
                    `${ty.pattern ? `/${ty.pattern.source}/${ty.pattern.flags}` : '(pattern)'}` : '?')],
        ['minLength',
            escapeString(
                ty.kind === 'primitive' ?
                    `${nvl(ty.minLength, '0')}` : '?')],
        ['maxLength',
            escapeString(
                ty.kind === 'primitive' ?
                    `${nvl(ty.maxLength, '(biggest)')}` : '?')],
        ['name',
            escapeString(
                `${ty.kind !== 'repeated' && values.dataPath.endsWith('repeated)') ?
                    'repeated item of ' :
                   ty.kind !== 'sequence' && values.dataPath.endsWith('sequence)') ?
                    'sequence item of ' : ''}${
                values.entryName || '?'}`)],
        ['parentType',
            escapeString(
                values.parentType || '?')],
        ['dataPath',
            values.dataPath],

        ...(args.substitutions || []),
    ]);

    for (const ent of dict.entries()) {
        ret = ret.replace(new RegExp(`%{${ent[0]}}`), ent[1]);
    }

    return ret;
}


interface DataPathEntry {
    name: string;
    kind: 'type' | 'key' | 'index';
}


export function reportError(
        errType: ErrorTypes, data: any, ty: TypeAssertion,
        args: ReportErrorArguments) {

    const messages: ErrorMessages[] = [];
    if (ty.messages) {
        messages.push(ty.messages);
    }
    if (args.ctx.errorMessages) {
        messages.push(args.ctx.errorMessages);
    }
    messages.push(defaultMessages);

    const dataPathEntryArray: DataPathEntry[] = [];

    for (let i = 0; i < args.ctx.typeStack.length; i++) {
        const p = args.ctx.typeStack[i];
        const next = args.ctx.typeStack[i + 1];
        const pt = Array.isArray(p) ? p[0] : p;
        const pi = Array.isArray(next) ? next[1] : void 0;

        let isSet = false;
        if (pt.kind === 'repeated') {
            if (i !== args.ctx.typeStack.length - 1) {
                if (pt.name) {
                    dataPathEntryArray.push({kind: 'key', name: pt.name});
                }
                dataPathEntryArray.push({kind: 'index', name: `(${pi !== void 0 ? `${pi}:` : ''}repeated)`});
                isSet = true;
            }
        } else if (pt.kind === 'sequence') {
            if (i !== args.ctx.typeStack.length - 1) {
                if (pt.name) {
                    dataPathEntryArray.push({kind: 'key', name: pt.name});
                }
                dataPathEntryArray.push({kind: 'index', name: `(${pi !== void 0 ? `${pi}:` : ''}sequence)`});
                isSet = true;
            }
        }
        if (! isSet) {
            if (pt.name) {
                if (i === 0) {
                    if (pt.typeName) {
                        dataPathEntryArray.push({kind: 'type', name: pt.typeName});
                    } else {
                        dataPathEntryArray.push({kind: 'key', name: pt.name});
                    }
                } else {
                    const len = dataPathEntryArray.length;
                    if (len && dataPathEntryArray[len - 1].kind === 'type') {
                        if (pt.kind === 'object' && next && pt.typeName) {
                            dataPathEntryArray.push({kind: 'type', name: pt.typeName});
                        } else {
                            dataPathEntryArray.push({kind: 'key', name: pt.name as string}); // NOTE: type inference failed
                        }
                    } else {
                        if (pt.typeName) {
                            dataPathEntryArray.push({kind: 'type', name: pt.typeName});
                        } else {
                            dataPathEntryArray.push({kind: 'key', name: pt.name});
                        }
                    }
                }
            } else if (pt.typeName) {
                dataPathEntryArray.push({kind: 'type', name: pt.typeName});
            }
        }
    }

    let dataPath = '';
    for (let i = 0; i < dataPathEntryArray.length; i++) {
        const p = dataPathEntryArray[i];
        dataPath += p.name;
        if (i + 1 === dataPathEntryArray.length) {
            break;
        }
        dataPath += p.kind === 'type' ? ':' : '.';
    }

    let parentType = '';
    let entryName = '';
    for (let i = dataPathEntryArray.length - 1; 0 <= i; i--) {
        const p = dataPathEntryArray[i];
        if (p.kind === 'type') {
            if (i !== 0 && i === dataPathEntryArray.length - 1) {
                const q = dataPathEntryArray[i - 1];
                if (q.kind === 'index') {
                    continue; // e.g.: "File:acl.(0:repeated).ACL"
                }
            }                 // else: "File:acl.(0:repeated).ACL:target"
            parentType = p.name;
            for (let j = i + 1; j < dataPathEntryArray.length; j++) {
                const q = dataPathEntryArray[j];
                if (q.kind === 'key') {
                    entryName = q.name;
                    break;
                }
            }
            break;
        }
    }
    if (! parentType) {
        for (let i = args.ctx.typeStack.length - 1; 0 <= i; i--) {
            const p = args.ctx.typeStack[i];
            const pt = Array.isArray(p) ? p[0] : p;
            if (pt.typeName) {
                parentType = pt.typeName;
            }
        }
    }

    const topRepeatable: TopRepeatable = findTopRepeatableAssertion(args.ctx);
    const values = {dataPath, topRepeatable, parentType, entryName};

    const constraints: TypeAssertionErrorMessageConstraints = {};
    const cSrces: TypeAssertionErrorMessageConstraints[] = [ty as any];

    if (errType === ErrorTypes.RepeatQtyUnmatched && topRepeatable) {
        cSrces.unshift(topRepeatable as any);
    }

    for (const cSrc of cSrces) {
        if (nvl(cSrc.minValue, false)) {
            constraints.minValue = cSrc.minValue;
        }
        if (nvl(cSrc.maxValue, false)) {
            constraints.maxValue = cSrc.maxValue;
        }
        if (nvl(cSrc.greaterThanValue, false)) {
            constraints.greaterThanValue = cSrc.greaterThanValue;
        }
        if (nvl(cSrc.lessThanValue, false)) {
            constraints.lessThanValue = cSrc.lessThanValue;
        }
        if (nvl(cSrc.minLength, false)) {
            constraints.minLength = cSrc.minLength;
        }
        if (nvl(cSrc.maxLength, false)) {
            constraints.maxLength = cSrc.maxLength;
        }
        if (nvl(cSrc.pattern, false)) {
            const pat = cSrc.pattern as any as RegExp;
            constraints.pattern = `/${pat.source}/${pat.flags}`;
        }
        if (nvl(cSrc.min, false)) {
            constraints.min = cSrc.min;
        }
        if (nvl(cSrc.max, false)) {
            constraints.max = cSrc.max;
        }
    }

    const val: {value?: any} = {};

    switch (typeof data) {
    case 'number': case 'bigint': case 'string': case 'boolean': case 'undefined':
        val.value = data;
        break;
    case 'object':
        if (data === null) {
            val.value = data;
        }
    }

    if (ty.messageId) {
        args.ctx.errors.push({
            code: `${ty.messageId}-${errorTypeNames[errType]}`,
            message: formatErrorMessage(ty.message ?
                ty.message :
                getErrorMessage(errType, ...messages), data, ty, args, values),
            dataPath,
            constraints,
            ...val,
        });
    } else if (ty.message) {
        args.ctx.errors.push({
            code: `${errorTypeNames[errType]}`,
            message: formatErrorMessage(ty.message, data, ty, args, values),
            dataPath,
            constraints,
            ...val,
        });
    } else {
        args.ctx.errors.push({
            code: `${errorTypeNames[errType]}`,
            message: formatErrorMessage(getErrorMessage(errType, ...messages), data, ty, args, values),
            dataPath,
            constraints,
            ...val,
        });
    }
}


export function reportErrorWithPush(
        errType: ErrorTypes, data: any,
        tyidx: [TypeAssertion, number | string | undefined],
        args: ReportErrorArguments) {

    try {
        args.ctx.typeStack.push(tyidx);
        reportError(errType, data, tyidx[0], args);
    } finally {
        args.ctx.typeStack.pop();
    }
}
