"use strict";
var __assign = (this && this.__assign) || Object.assign || function(t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
            t[p] = s[p];
    }
    return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
var cli_color_1 = require("cli-color");
var lodash_1 = require("lodash");
var typeOfSchema_1 = require("./typeOfSchema");
var AST_1 = require("./types/AST");
var utils_1 = require("./utils");
function parse(schema, options, rootSchema, keyName, isSchema, processed, usedNames) {
    if (rootSchema === void 0) { rootSchema = schema; }
    if (isSchema === void 0) { isSchema = true; }
    if (processed === void 0) { processed = new Map(); }
    if (usedNames === void 0) { usedNames = new Set(); }
    // If we've seen this node before, return it.
    if (processed.has(schema)) {
        return processed.get(schema);
    }
    var definitions = getDefinitions(rootSchema);
    var keyNameFromDefinition = lodash_1.findKey(definitions, function (_) { return _ === schema; });
    // Cache processed ASTs before they are actually computed, then update
    // them in place using set(). This is to avoid cycles.
    // TODO: Investigate alternative approaches (lazy-computing nodes, etc.)
    var ast = {};
    processed.set(schema, ast);
    var set = function (_ast) { return Object.assign(ast, _ast); };
    return isSchema
        ? parseNonLiteral(schema, options, rootSchema, keyName, keyNameFromDefinition, set, processed, usedNames)
        : parseLiteral(schema, keyName, keyNameFromDefinition, set);
}
exports.parse = parse;
function parseLiteral(schema, keyName, keyNameFromDefinition, set) {
    return set({
        keyName: keyName,
        params: schema,
        standaloneName: keyNameFromDefinition,
        type: 'LITERAL'
    });
}
function parseNonLiteral(schema, options, rootSchema, keyName, keyNameFromDefinition, set, processed, usedNames) {
    utils_1.log(cli_color_1.whiteBright.bgBlue('parser'), schema, '<-' + typeOfSchema_1.typeOfSchema(schema), processed.has(schema) ? '(FROM CACHE)' : '');
    switch (typeOfSchema_1.typeOfSchema(schema)) {
        case 'ALL_OF':
            return set({
                comment: schema.description,
                keyName: keyName,
                params: schema.allOf.map(function (_) { return parse(_, options, rootSchema, undefined, true, processed, usedNames); }),
                standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
                type: 'INTERSECTION'
            });
        case 'ANY':
            return set({
                comment: schema.description,
                keyName: keyName,
                standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
                type: 'ANY'
            });
        case 'ANY_OF':
            return set({
                comment: schema.description,
                keyName: keyName,
                params: schema.anyOf.map(function (_) { return parse(_, options, rootSchema, undefined, true, processed, usedNames); }),
                standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
                type: 'UNION'
            });
        case 'BOOLEAN':
            return set({
                comment: schema.description,
                keyName: keyName,
                standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
                type: 'BOOLEAN'
            });
        case 'NAMED_ENUM':
            return set({
                comment: schema.description,
                keyName: keyName,
                params: schema.enum.map(function (_, n) { return ({
                    ast: parse(_, options, rootSchema, undefined, false, processed, usedNames),
                    keyName: schema.tsEnumNames[n]
                }); }),
                standaloneName: standaloneName(schema, keyName, usedNames),
                type: 'ENUM'
            });
        case 'NAMED_SCHEMA':
            return set(newInterface(schema, options, rootSchema, processed, usedNames, keyName));
        case 'NULL':
            return set({
                comment: schema.description,
                keyName: keyName,
                standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
                type: 'NULL'
            });
        case 'NUMBER':
            return set({
                comment: schema.description,
                keyName: keyName,
                standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
                type: 'NUMBER'
            });
        case 'OBJECT':
            return set({
                comment: schema.description,
                keyName: keyName,
                standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
                type: 'OBJECT'
            });
        case 'ONE_OF':
            return set({
                comment: schema.description,
                keyName: keyName,
                params: schema.oneOf.map(function (_) { return parse(_, options, rootSchema, undefined, true, processed, usedNames); }),
                standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
                type: 'UNION'
            });
        case 'REFERENCE':
            throw utils_1.error('Refs should have been resolved by the resolver!', schema);
        case 'STRING':
            return set({
                comment: schema.description,
                keyName: keyName,
                standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
                type: 'STRING'
            });
        case 'TYPED_ARRAY':
            if (Array.isArray(schema.items)) {
                return set({
                    comment: schema.description,
                    keyName: keyName,
                    params: schema.items.map(function (_) { return parse(_, options, rootSchema, undefined, true, processed, usedNames); }),
                    standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
                    type: 'TUPLE'
                });
            }
            else {
                return set({
                    comment: schema.description,
                    keyName: keyName,
                    params: parse(schema.items, options, rootSchema, undefined, true, processed, usedNames),
                    standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
                    type: 'ARRAY'
                });
            }
        case 'UNION':
            return set({
                comment: schema.description,
                keyName: keyName,
                params: schema.type.map(function (_) { return parse({ type: _ }, options, rootSchema, undefined, true, processed, usedNames); }),
                standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
                type: 'UNION'
            });
        case 'UNNAMED_ENUM':
            return set({
                comment: schema.description,
                keyName: keyName,
                params: schema.enum.map(function (_) { return parse(_, options, rootSchema, undefined, false, processed, usedNames); }),
                standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
                type: 'UNION'
            });
        case 'UNNAMED_SCHEMA':
            return set(newInterface(schema, options, rootSchema, processed, usedNames, keyName, keyNameFromDefinition));
        case 'UNTYPED_ARRAY':
            return set({
                comment: schema.description,
                keyName: keyName,
                params: AST_1.T_ANY,
                standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
                type: 'ARRAY'
            });
    }
}
/**
 * Compute a schema name using a series of fallbacks
 */
function standaloneName(schema, keyNameFromDefinition, usedNames) {
    var name = schema.title || schema.id || keyNameFromDefinition;
    if (name) {
        return utils_1.generateName(name, usedNames);
    }
}
function newInterface(schema, options, rootSchema, processed, usedNames, keyName, keyNameFromDefinition) {
    var name = standaloneName(schema, keyNameFromDefinition, usedNames);
    return {
        comment: schema.description,
        keyName: keyName,
        params: parseSchema(schema, options, rootSchema, processed, usedNames, name),
        standaloneName: name,
        superTypes: parseSuperTypes(schema, options, processed, usedNames),
        type: 'INTERFACE'
    };
}
function parseSuperTypes(schema, options, processed, usedNames) {
    // Type assertion needed because of dereferencing step
    // TODO: Type it upstream
    var superTypes = schema.extends;
    if (!superTypes) {
        return [];
    }
    if (Array.isArray(superTypes)) {
        return superTypes.map(function (_) { return newNamedInterface(_, options, _, processed, usedNames); });
    }
    return [newNamedInterface(superTypes, options, superTypes, processed, usedNames)];
}
function newNamedInterface(schema, options, rootSchema, processed, usedNames) {
    var namedInterface = newInterface(schema, options, rootSchema, processed, usedNames);
    if (AST_1.hasStandaloneName(namedInterface)) {
        return namedInterface;
    }
    // TODO: Generate name if it doesn't have one
    throw utils_1.error('Supertype must have standalone name!', namedInterface);
}
/**
 * Helper to parse schema properties into params on the parent schema's type
 */
function parseSchema(schema, options, rootSchema, processed, usedNames, parentSchemaName) {
    var asts = lodash_1.map(schema.properties, function (value, key) { return ({
        ast: parse(value, options, rootSchema, key, true, processed, usedNames),
        isPatternProperty: false,
        isRequired: lodash_1.includes(schema.required || [], key),
        isUnreachableDefinition: false,
        keyName: key
    }); });
    var singlePatternProperty = false;
    if (schema.patternProperties) {
        // partially support patternProperties. in the case that
        // additionalProperties is not set, and there is only a single
        // value definition, we can validate against that.
        singlePatternProperty = (!schema.additionalProperties && Object.keys(schema.patternProperties).length === 1);
        asts = asts.concat(lodash_1.map(schema.patternProperties, function (value, key) {
            var ast = parse(value, options, rootSchema, key, true, processed, usedNames);
            var comment = "This interface was referenced by `" + parentSchemaName + "`'s JSON-Schema definition\nvia the `patternProperty` \"" + key + "\".";
            ast.comment = ast.comment ? ast.comment + "\n\n" + comment : comment;
            return ({
                ast: ast,
                isPatternProperty: !singlePatternProperty,
                isRequired: singlePatternProperty || lodash_1.includes(schema.required || [], key),
                isUnreachableDefinition: false,
                keyName: singlePatternProperty ? '[k: string]' : key
            });
        }));
    }
    if (options.unreachableDefinitions) {
        asts = asts.concat(lodash_1.map(schema.definitions, function (value, key) {
            var ast = parse(value, options, rootSchema, key, true, processed, usedNames);
            var comment = "This interface was referenced by `" + parentSchemaName + "`'s JSON-Schema\nvia the `definition` \"" + key + "\".";
            ast.comment = ast.comment ? ast.comment + "\n\n" + comment : comment;
            return {
                ast: ast,
                isPatternProperty: false,
                isRequired: lodash_1.includes(schema.required || [], key),
                isUnreachableDefinition: true,
                keyName: key
            };
        }));
    }
    // handle additionalProperties
    switch (schema.additionalProperties) {
        case undefined:
        case true:
            if (singlePatternProperty) {
                return asts;
            }
            return asts.concat({
                ast: AST_1.T_ANY_ADDITIONAL_PROPERTIES,
                isPatternProperty: false,
                isRequired: true,
                isUnreachableDefinition: false,
                keyName: '[k: string]'
            });
        case false:
            return asts;
        // pass "true" as the last param because in TS, properties
        // defined via index signatures are already optional
        default:
            return asts.concat({
                ast: parse(schema.additionalProperties, options, rootSchema, '[k: string]', true, processed, usedNames),
                isPatternProperty: false,
                isRequired: true,
                isUnreachableDefinition: false,
                keyName: '[k: string]'
            });
    }
}
/**
 * TODO: Memoize
 */
function getDefinitions(schema, isSchema, processed) {
    if (isSchema === void 0) { isSchema = true; }
    if (processed === void 0) { processed = new Set(); }
    if (processed.has(schema)) {
        return {};
    }
    processed.add(schema);
    if (Array.isArray(schema)) {
        return schema.reduce(function (prev, cur) { return (__assign({}, prev, getDefinitions(cur, false, processed))); }, {});
    }
    if (lodash_1.isPlainObject(schema)) {
        return __assign({}, (isSchema && hasDefinitions(schema) ? schema.definitions : {}), Object.keys(schema).reduce(function (prev, cur) { return (__assign({}, prev, getDefinitions(schema[cur], false, processed))); }, {}));
    }
    return {};
}
/**
 * TODO: Reduce rate of false positives
 */
function hasDefinitions(schema) {
    return 'definitions' in schema;
}
//# sourceMappingURL=parser.js.map