"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
 * extractor.ts contains tools that run static analysis on the codebase to pull
 * out data from decorators.
 */
const ast_1 = require("./ast");
const ts = require("typescript");
const internal_1 = require("@mcph/miix-std/dist/internal");
const error_1 = require("./error");
function isAccessFor(name, node) {
    return new RegExp(`(^|\\.)${name}$`).test(node.getText());
}
/**
 * The DecoratorExtract is a class that gathers metadata from decorators in
 * the controls. They're called by the walker as it transverses the source
 * trees.
 */
class DecoratorExtractor {
    /**
     * Called whenever the walker encounters a decorator.
     */
    guardedParse(decorator) {
        if (!ts.isCallExpression(decorator.expression)) {
            return;
        }
        const call = decorator.expression;
        if (!isAccessFor(this.name(), call.expression)) {
            return;
        }
        if (call.arguments.length === 0) {
            this.parse({}, decorator);
            return;
        }
        this.assert(call.arguments.length === 1, call, 'Expected to be called with at most one argument', error_1.ErrorCode.InvalidArgumentCount);
        const [arg] = call.arguments;
        this.assert(arg.kind === ts.SyntaxKind.ObjectLiteralExpression, call, 'Expected to be called with an object literal', error_1.ErrorCode.InvalidArgumentType);
        this.parse(this.parseObjectLiteral(arg), decorator);
        return;
    }
    /**
     * Parses a simple JSON-serializable expression into a JavaScript type.
     * Throws if it's a bad type.
     */
    parseSimpleNode(node) {
        switch (node.kind) {
            case ts.SyntaxKind.TrueKeyword:
                return true;
            case ts.SyntaxKind.FalseKeyword:
                return false;
            case ts.SyntaxKind.NumericLiteral:
                return Number(node.text);
            case ts.SyntaxKind.Identifier:
                return node.text;
            case ts.SyntaxKind.ObjectLiteralExpression:
                return this.parseObjectLiteral(node);
            case ts.SyntaxKind.ArrayLiteralExpression:
                return node.elements.map(n => this.parseSimpleNode(n));
            default:
        }
        if (ts.isLiteralExpression(node)) {
            return node.text;
        }
        this.assert(false, node, 'Only simple, JSON-compatible expressions are allowed here', error_1.ErrorCode.InvalidComplexExpression);
        return ''; // never
    }
    /**
     * parseEnumAccess checks that the node is an enum access or string literal
     * and tries to parse it to the corresponding numeric type.
     */
    parseEnumAccess(node, name, actualEnum) {
        const keys = Object.keys(actualEnum)
            .filter(k => typeof actualEnum[k] === 'number')
            .join(', ');
        if (node.kind === ts.SyntaxKind.StringLiteral) {
            const text = node.text;
            this.assert(typeof actualEnum[text] === 'number', node, `Invalid ${name} value, expected one of ${keys}`, error_1.ErrorCode.InvalidEnumAccess);
            return actualEnum[text];
        }
        const matches = new RegExp(`(^|\\.)${name}\\.(.+)$`).exec(node.getText());
        this.assert(!!matches, node, `Expected to be a lookup on ${name}, or one of ${keys}`, error_1.ErrorCode.InvalidEnumAccess);
        const value = actualEnum[matches[2]];
        this.assert(value, node, `Invalid ${name} value, should be one of ${keys}`, error_1.ErrorCode.InvalidEnumAccess);
        return value;
    }
    /**
     * Parses an object literal key.
     */
    parseLiteralKey(key) {
        return String(this.parseSimpleNode(key));
    }
    /**
     * Parses an object literal value.
     */
    parseLiteralValue(value, _key) {
        return this.parseSimpleNode(value);
    }
    /**
     * Parses an object literal into a plain JavaScript object. Requires that
     * all properties of the object ltieral be simple, JSON-stringify-able
     * types, or throws an error.
     */
    parseObjectLiteral(node) {
        const output = {};
        ts.forEachChild(node, (child) => {
            // disallow `{ shorthand }`
            this.assert(child.kind === ts.SyntaxKind.PropertyAssignment, node, `Only "key": "value" assignments are allowed`, error_1.ErrorCode.InvalidPropertyAssignment);
            const key = this.parseLiteralKey(child.name);
            output[key] = this.parseLiteralValue(child.initializer, key);
        });
        return output;
    }
    assert(value, node, message, code) {
        if (value) {
            return; // all good
        }
        throw new error_1.DeclarationError(code, node, `${message} in @${this.name()}()`);
    }
}
/**
 * ControlExtractor extracts data from @Control decorators.
 */
class ControlExtractor extends DecoratorExtractor {
    constructor(inputable) {
        super();
        this.inputable = inputable;
        this.controls = Object.create(null);
    }
    name() {
        return 'Control';
    }
    augment(metadata) {
        metadata.controls = this.controls;
    }
    parse(result, decorator) {
        this.assert(!this.controls[result.kind], decorator, `Control type "${result.kind}" declared multiple times`, error_1.ErrorCode.RedeclaredControl);
        result.inputs = result.inputs || [];
        this.inputable.setLastInput(result.inputs);
        this.controls[result.kind] = result;
    }
}
/**
 * SceneExtractor extracts data from @Scene decorators.
 */
class SceneExtractor extends DecoratorExtractor {
    constructor(inputable) {
        super();
        this.inputable = inputable;
        this.scenes = Object.create(null);
    }
    name() {
        return 'Scene';
    }
    augment(metadata) {
        metadata.scenes = this.scenes;
    }
    parse(result, decorator) {
        this.assert(!!(result.default || result.id), decorator, 'Non-default scenes must have a defined ID', error_1.ErrorCode.RequiredSceneID);
        result.inputs = result.inputs || [];
        this.inputable.setLastInput(result.inputs);
        this.scenes[result.id || 'default'] = result;
    }
}
/**
 * SceneExtractor extracts data from @Scene decorators.
 */
class InputExtractor extends DecoratorExtractor {
    name() {
        return 'Input';
    }
    augment() {
        /* noop */
    }
    setLastInput(inputs) {
        this.lastMapping = inputs;
    }
    parse(result, decorator) {
        const lastMapping = this.lastMapping;
        this.assert(!!lastMapping, decorator, "Input on a class that didn't have a @Control or @Scene decorator", error_1.ErrorCode.MissingResourceDecorator);
        const prop = this.getPropertyDeclaration(decorator);
        if (!result.kind) {
            result.kind = this.inferKind(prop);
        }
        if (prop.initializer) {
            result.defaultValue = super.parseLiteralValue(prop.initializer, '');
        }
        lastMapping.push(Object.assign({}, result, { alias: prop.name.getText(), propertyName: prop.name.getText() }));
    }
    parseLiteralValue(value, key) {
        if (key === 'kind') {
            return this.parseEnumAccess(value, 'InputKind', internal_1.InputKind);
        }
        return super.parseLiteralValue(value, key);
    }
    getPropertyDeclaration(decorator) {
        let node = decorator;
        while (node && node.kind !== ts.SyntaxKind.PropertyDeclaration) {
            node = node.parent;
        }
        this.assert(!!node, decorator, 'Expected to be attached to a property declaration', error_1.ErrorCode.InvalidDecoratorTarget);
        return node;
    }
    inferKind(property) {
        this.assert(!!property.type, property, 'You must either explicitly define a type for your property, or explcitly define ' +
            'one by passing something like { kind: Mixer.InputKind.Number }', error_1.ErrorCode.CouldNotInferKind);
        const type = property.type;
        switch (type.kind) {
            case ts.SyntaxKind.NumberKeyword:
                return internal_1.InputKind.Number;
            case ts.SyntaxKind.StringKeyword:
                return internal_1.InputKind.String;
            case ts.SyntaxKind.BooleanKeyword:
                return internal_1.InputKind.Boolean;
            default:
        }
        if (isAccessFor('IDimensions', type)) {
            return internal_1.InputKind.Dimensions;
        }
        this.assert(false, type, `Could not infer the type for ${type.getText()}, please define a kind explicitly`, error_1.ErrorCode.CouldNotInferKind);
        return internal_1.InputKind.Boolean; // never hit
    }
}
/**
 * MetadataExtractor transverses TypeScript sources and generates an IPackageData
 * struct based off decorators in the code.
 */
class MetadataExtractor extends ast_1.TypeScriptWalker {
    constructor() {
        super();
        const inputExtractor = new InputExtractor();
        this.extractors = [
            inputExtractor,
            new ControlExtractor(inputExtractor),
            new SceneExtractor(inputExtractor),
        ];
    }
    /**
     * Returns the package config parsed from the AST, after calls to walker
     * functions.
     */
    gatherResult() {
        const metadata = {};
        this.extractors.forEach(e => {
            e.augment(metadata);
        });
        return metadata;
    }
    visit(node) {
        switch (node.kind) {
            case ts.SyntaxKind.Decorator:
                this.extractors.forEach(e => {
                    e.guardedParse(node);
                });
            default:
        }
        return ts.forEachChild(node, child => this.visit(child));
    }
}
exports.MetadataExtractor = MetadataExtractor;
