'use strict';

Object.defineProperty(exports, "__esModule", {
    value: true
});

exports.default = function (_ref) {
    var t = _ref.types;

    /**
     * Add a helper to take an initial descriptor, apply some decorators to it, and optionally
     * define the property.
     */
    function ensureApplyDecoratedDescriptorHelper(path, state) {
        if (!state.applyDecoratedDescriptor) {
            state.applyDecoratedDescriptor = path.scope.generateUidIdentifier('applyDecoratedDescriptor');
            var helper = buildApplyDecoratedDescriptor({
                NAME: state.applyDecoratedDescriptor
            });
            path.scope.getProgramParent().path.unshiftContainer('body', helper);
        }

        return state.applyDecoratedDescriptor;
    }

    /**
     * Add a helper to call as a replacement for class property definition.
     */
    function ensureInitializerDefineProp(path, state) {
        if (!state.initializerDefineProp) {
            state.initializerDefineProp = path.scope.generateUidIdentifier('initDefineProp');
            var helper = buildInitializerDefineProperty({
                NAME: state.initializerDefineProp
            });
            path.scope.getProgramParent().path.unshiftContainer('body', helper);
        }

        return state.initializerDefineProp;
    }

    /**
     * Add a helper that will throw a useful error if the transform fails to detect the class
     * property assignment, so users know something failed.
     */
    function ensureInitializerWarning(path, state) {
        if (!state.initializerWarningHelper) {
            state.initializerWarningHelper = path.scope.generateUidIdentifier('initializerWarningHelper');
            var helper = buildInitializerWarningHelper({
                NAME: state.initializerWarningHelper
            });
            path.scope.getProgramParent().path.unshiftContainer('body', helper);
        }

        return state.initializerWarningHelper;
    }

    /**
     * If the decorator expressions are non-identifiers, hoist them to before the class so we can be sure
     * that they are evaluated in order.
     */
    function applyEnsureOrdering(path) {
        // TODO: This should probably also hoist computed properties.
        var decorators = (path.isClass() ? [path].concat(path.get('body.body')) : path.get('properties')).reduce(function (acc, prop) {
            return acc.concat(prop.node.decorators || []);
        }, []);

        var identDecorators = decorators.filter(function (decorator) {
            return !t.isIdentifier(decorator.expression);
        });
        if (identDecorators.length === 0) return;

        return t.sequenceExpression(identDecorators.map(function (decorator) {
            var expression = decorator.expression;
            var id = decorator.expression = path.scope.generateDeclaredUidIdentifier('dec');
            return t.assignmentExpression('=', id, expression);
        }).concat([path.node]));
    }

    /**
     * Given a class expression with class-level decorators, create a new expression
     * with the proper decorated behavior.
     */
    function applyClassDecorators(classPath, state) {
        var decorators = classPath.node.decorators || [];
        classPath.node.decorators = null;

        if (decorators.length === 0) return;

        var name = classPath.scope.generateDeclaredUidIdentifier('class');

        return decorators.map(function (dec) {
            return dec.expression;
        }).reverse().reduce(function (acc, decorator) {
            return buildClassDecorator({
                CLASS_REF: name,
                DECORATOR: decorator,
                INNER: acc
            }).expression;
        }, classPath.node);
    }

    /**
     * Given a class expression with method-level decorators, create a new expression
     * with the proper decorated behavior.
     */
    function applyMethodDecorators(path, state) {
        var hasMethodDecorators = path.node.body.body.some(function (node) {
            return (node.decorators || []).length > 0;
        });

        if (!hasMethodDecorators) return;

        return applyTargetDecorators(path, state, path.node.body.body);
    }

    /**
     * Given an object expression with property decorators, create a new expression
     * with the proper decorated behavior.
     */
    function applyObjectDecorators(path, state) {
        var hasMethodDecorators = path.node.properties.some(function (node) {
            return (node.decorators || []).length > 0;
        });

        if (!hasMethodDecorators) return;

        return applyTargetDecorators(path, state, path.node.properties);
    }

    /**
     * A helper to pull out property decorators into a sequence expression.
     */
    function applyTargetDecorators(path, state, decoratedProps) {
        var descName = path.scope.generateDeclaredUidIdentifier('desc');
        var valueTemp = path.scope.generateDeclaredUidIdentifier('value');

        var name = path.scope.generateDeclaredUidIdentifier(path.isClass() ? 'class' : 'obj');

        var exprs = decoratedProps.reduce(function (acc, node) {
            var decorators = node.decorators || [];
            node.decorators = null;

            if (decorators.length === 0) return acc;

            if (node.computed) {
                throw path.buildCodeFrameError('Computed method/property decorators are not yet supported.');
            }

            var property = t.isLiteral(node.key) ? node.key : t.stringLiteral(node.key.name);

            var target = path.isClass() && !node.static ? buildClassPrototype({
                CLASS_REF: name
            }).expression : name;

            if (t.isClassProperty(node, { static: false })) {
                var descriptor = path.scope.generateDeclaredUidIdentifier('descriptor');

                var initializer = node.value ? t.functionExpression(null, [], t.blockStatement([t.returnStatement(node.value)])) : t.nullLiteral();
                node.value = t.callExpression(ensureInitializerWarning(path, state), [descriptor, t.thisExpression()]);

                acc = acc.concat([t.assignmentExpression('=', descriptor, t.callExpression(ensureApplyDecoratedDescriptorHelper(path, state), [target, property, t.arrayExpression(decorators.map(function (dec) {
                    return dec.expression;
                })), t.objectExpression([t.objectProperty(t.identifier('enumerable'), t.booleanLiteral(true)), t.objectProperty(t.identifier('initializer'), initializer)])]))]);
            } else {
                acc = acc.concat(t.callExpression(ensureApplyDecoratedDescriptorHelper(path, state), [target, property, t.arrayExpression(decorators.map(function (dec) {
                    return dec.expression;
                })), t.isObjectProperty(node) || t.isClassProperty(node, { static: true }) ? buildGetObjectInitializer({
                    TEMP: path.scope.generateDeclaredUidIdentifier('init'),
                    TARGET: target,
                    PROPERTY: property
                }).expression : buildGetDescriptor({
                    TARGET: target,
                    PROPERTY: property
                }).expression, target]));
            }

            return acc;
        }, []);

        return t.sequenceExpression([t.assignmentExpression('=', name, path.node), t.sequenceExpression(exprs), name]);
    }

    return {
        inherits: require("babel-plugin-syntax-decorators"),

        visitor: {
            ExportDefaultDeclaration: function ExportDefaultDeclaration(path) {
                if (!path.get("declaration").isClassDeclaration()) return;

                var node = path.node;

                var ref = node.declaration.id || path.scope.generateUidIdentifier("default");
                node.declaration.id = ref;

                // Split the class declaration and the export into two separate statements.
                path.replaceWith(node.declaration);
                path.insertAfter(t.exportNamedDeclaration(null, [t.exportSpecifier(ref, t.identifier('default'))]));
            },
            ClassDeclaration: function ClassDeclaration(path) {
                var node = path.node;

                var ref = node.id || path.scope.generateUidIdentifier("class");

                path.replaceWith(t.variableDeclaration("let", [t.variableDeclarator(ref, t.toExpression(node))]));
            },
            ClassExpression: function ClassExpression(path, state) {
                // Create a replacement for the class node if there is one. We do one pass to replace classes with
                // class decorators, and a second pass to process method decorators.
                var decoratedClass = applyEnsureOrdering(path) || applyClassDecorators(path, state) || applyMethodDecorators(path, state);

                if (decoratedClass) path.replaceWith(decoratedClass);
            },
            ObjectExpression: function ObjectExpression(path, state) {
                var decoratedObject = applyEnsureOrdering(path) || applyObjectDecorators(path, state);

                if (decoratedObject) path.replaceWith(decoratedObject);
            },
            AssignmentExpression: function AssignmentExpression(path, state) {
                if (!state.initializerWarningHelper) return;

                if (!path.get('left').isMemberExpression()) return;
                if (!path.get('left.property').isIdentifier()) return;
                if (!path.get('right').isCallExpression()) return;
                if (!path.get('right.callee').isIdentifier({ name: state.initializerWarningHelper.name })) return;

                path.replaceWith(t.callExpression(ensureInitializerDefineProp(path, state), [path.get('left.object').node, t.stringLiteral(path.get('left.property').node.name), path.get('right.arguments')[0].node, path.get('right.arguments')[1].node]));
            }
        }
    };
};

var _babelTemplate = require('babel-template');

var _babelTemplate2 = _interopRequireDefault(_babelTemplate);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var buildClassDecorator = (0, _babelTemplate2.default)('\n  DECORATOR(CLASS_REF = INNER) || CLASS_REF;\n');

var buildClassPrototype = (0, _babelTemplate2.default)('\n  CLASS_REF.prototype;\n');

var buildGetDescriptor = (0, _babelTemplate2.default)('\n    Object.getOwnPropertyDescriptor(TARGET, PROPERTY);\n');

var buildGetObjectInitializer = (0, _babelTemplate2.default)('\n    (TEMP = Object.getOwnPropertyDescriptor(TARGET, PROPERTY), (TEMP = TEMP ? TEMP.value : undefined), {\n        enumerable: true,\n        configurable: true,\n        writable: true,\n        initializer: function(){\n            return TEMP;\n        }\n    })\n');

var buildInitializerWarningHelper = (0, _babelTemplate2.default)('\n    function NAME(descriptor, context){\n        throw new Error(\'Decorating class property failed. Please ensure that transform-class-properties is enabled.\');\n    }\n');

var buildInitializerDefineProperty = (0, _babelTemplate2.default)('\n    function NAME(target, property, descriptor, context){\n        if (!descriptor) return;\n\n        Object.defineProperty(target, property, {\n            enumerable: descriptor.enumerable,\n            configurable: descriptor.configurable,\n            writable: descriptor.writable,\n            value: descriptor.initializer ? descriptor.initializer.call(context) : void 0,\n        });\n    }\n');

var buildApplyDecoratedDescriptor = (0, _babelTemplate2.default)('\n    function NAME(target, property, decorators, descriptor, context){\n        var desc = {};\n        Object[\'ke\' + \'ys\'](descriptor).forEach(function(key){\n            desc[key] = descriptor[key];\n        });\n        desc.enumerable = !!desc.enumerable;\n        desc.configurable = !!desc.configurable;\n        if (\'value\' in desc || desc.initializer){\n            desc.writable = true;\n        }\n\n        desc = decorators.slice().reverse().reduce(function(desc, decorator){\n            return decorator(target, property, desc) || desc;\n        }, desc);\n\n        if (context && desc.initializer !== void 0){\n            desc.value = desc.initializer ? desc.initializer.call(context) : void 0;\n            desc.initializer = undefined;\n        }\n\n        if (desc.initializer === void 0){\n            // This is a hack to avoid this being processed by \'transform-runtime\'.\n            // See issue #9.\n            Object[\'define\' + \'Property\'](target, property, desc);\n            desc = null;\n        }\n\n        return desc;\n    }\n');

;