"use strict";
var __extends = (this && this.__extends) || (function () {
    var extendStatics = Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
var fs_extra_1 = require("fs-extra");
var readline = require("readline");
var path_1 = require("path");
var pargv_1 = require("pargv");
var os_1 = require("os");
var glob = require("glob");
var di = require("detect-indent");
var async_1 = require("async");
var lodash_1 = require("lodash");
var events_1 = require("events");
var fm = require("front-matter");
var Mustache = require("mustache");
var defaults = {
    configDir: './mustr',
    outputDir: './src',
    templateExt: '.tpl'
};
var Mustr = (function (_super) {
    __extends(Mustr, _super);
    function Mustr(options) {
        var _this = _super.call(this) || this;
        // Utils
        _this.cwd = process.cwd();
        // Parsed templates.
        _this.templates = {};
        _this.options = lodash_1.extend({}, defaults, options);
        _this.log = pargv_1.logger();
        _this.templatesPath = path_1.resolve(_this.cwd, path_1.join(_this.options.configDir, '/**/*.tpl'));
        // Resolve the base directory.
        _this.outputPath = path_1.resolve(_this.cwd, _this.options.outputDir);
        // Resolve the config path.
        _this.registerPath = path_1.resolve(_this.cwd, _this.options.configDir, 'register.js');
        // Normalized the template extension.
        _this.options.templateExt = _this.normalizedExt(_this.options.templateExt);
        return _this;
    }
    /**
     * Has Matter
     * Tests if string contains front matter.
     *
     * @param str the string to inspect.
     */
    Mustr.prototype.hasMatter = function (str) {
        return str.indexOf('---') > -1;
    };
    /**
     * Has Template
     * Loosly check if string contains template chars.
     *
     * @param str the string to inspect.
     */
    Mustr.prototype.hasTemplate = function (str) {
        return str.indexOf('{{') > -1;
    };
    /**
     * Try Require
     * Tries to require a module logging error if failed.
     *
     * @param path the module path to be required.
     */
    Mustr.prototype.tryRequire = function (path) {
        try {
            return require(path);
        }
        catch (ex) {
            this.log.error("failed to require module at " + path + ".").exit();
        }
    };
    /**
     * Normalize Extension
     * Ensures extension begins with "."
     *
     * @param ext the extension to be normalized.
     */
    Mustr.prototype.normalizedExt = function (ext) {
        return "." + ext.replace(/^\./, '');
    };
    /**
     * Normalize
     * Parses/normalizes a template name, path or static template string.
     * When passing a name it searches loaded template paths and matches
     * the filename to the template name passed.
     *
     * @param template the template name, path or static template string.
     */
    Mustr.prototype.normalize = function (template) {
        var rawTemplate, templatePath, templatePathNormalized, templateName, templateExt;
        var EXT_EXP = new RegExp(this.options.templateExt + "$");
        var parsedPath, parsedNormalized;
        // When has extension readfile.
        if (EXT_EXP.test(template)) {
            // If template doesn't exist error & exit.
            if (!fs_extra_1.existsSync(template))
                this.log.error("failed to resolve template at path " + template + ".").exit();
            // Save the template path.
            templatePath = template;
        }
        else if (!this.hasMatter(template) && !this.hasTemplate(template)) {
            var filtered = this.templatesGlob.filter(function (t) {
                return path_1.parse(t).name === template;
            })[0];
            if (!filtered) {
                this.log
                    .error("failed to resolve template for name " + template + ".")
                    .exit();
            }
            templatePath = filtered;
        }
        // If template path exists normalize it.
        if (templatePath) {
            templatePath = templatePath.toLowerCase();
            // We'll resolve the template later
            // set undefined as this is NOT a
            // static template.
            template = undefined;
            // Remove the template ext.
            templatePathNormalized = templatePath.replace(EXT_EXP, '');
            // Parse paths.
            parsedPath = path_1.parse(templatePath);
            parsedNormalized = path_1.parse(templatePathNormalized);
            // Get the name from the path.
            templateName = parsedPath.name;
            // Template may contain extension in path.
            // So we make sure we used the normalized path
            // which exclusdes the template extension.
            templateExt = parsedNormalized.ext;
            templateName = templateName.toLowerCase();
            templateExt = templateExt.toLowerCase();
            templatePathNormalized = templatePathNormalized.toLowerCase();
        }
        else {
            rawTemplate = template;
        }
        // Get the template's configuration.
        var config = {
            name: templateName,
            path: templatePath,
            ext: templateExt,
            pathNormalized: templatePathNormalized,
            raw: rawTemplate,
            partials: {}
        };
        // Return ITemplate
        return config;
    };
    /**
     * Truncate
     * Nothing special, cheesy truncation...move along.
     *
     * @param str the string to be truncated.
     * @param max the max len before truncating.
     */
    Mustr.prototype.truncate = function (str, max) {
        return str.length > max ? str.substr(0, max - 1) + '...' : str;
    };
    /**
     * Init
     * Initializes Mustr for current project
     *
     * @param force when true forces init.
     */
    Mustr.prototype.init = function (force) {
        var configPath = path_1.resolve(this.cwd, 'mustr.json');
        var blueprintsPath = path_1.resolve(this.cwd, 'mustr');
        // Ensure not already initialized.
        if (!force && (fs_extra_1.existsSync(configPath) || fs_extra_1.existsSync(blueprintsPath)))
            this.log.warn('project already initialized, use mu.init(true) or mu init -f from cli to force.').exit();
        // Write out the mustr.json config.
        fs_extra_1.writeFileSync(configPath, JSON.stringify(defaults, null, 2));
        // Copy the blueprints.
        fs_extra_1.copySync(path_1.resolve(__dirname, 'blueprints'), blueprintsPath);
        this.log.info('successfully initialized Mustr.').exit();
    };
    /**
     * Load
     * Loads the template paths and config.
     */
    Mustr.prototype.load = function () {
        if (!fs_extra_1.existsSync(this.registerPath))
            this.log.error("failed to resolve register configuration at " + this.registerPath + ".").exit();
        // Load template paths..
        this.templatesGlob = glob.sync(this.templatesPath);
        // Require the config
        var config = this.tryRequire(this.registerPath);
        if (!lodash_1.isFunction(config))
            this.log
                .error("failed to load config expected function but got type of " + typeof config + ".")
                .exit();
        // Call/load the configuration.
        config(this);
        return this;
    };
    /**
     * Configure
     * Configures template for output.
     *
     * @param name the name of the template to load.
     * @param output the output name/path to use when rendering.
     * @param options any metadata to merge for rendering.
     */
    Mustr.prototype.configure = function (name, output, metadata) {
        var rendered, ext, parsedOutput, outputName, outputPath;
        // Enable options as second arg.
        if (lodash_1.isPlainObject(output)) {
            metadata = output;
            output = undefined;
        }
        // set output to name if undefined.
        if (!lodash_1.isUndefined(name) && lodash_1.isUndefined(output))
            output = name;
        name = name.toLowerCase();
        output = output.toLowerCase();
        // Ensure we have a template and an output name.
        if (lodash_1.isUndefined(name) || lodash_1.isUndefined(output))
            this.log.error('cannot generate template using template name or output name of undefined.').exit();
        var template = this.templates[name];
        // Ensure the template exists.
        if (!template)
            this.log.error("the template " + name + " could not be found.").exit();
        // If contains path then not static defined
        // we need to read the template file.
        if (template.path)
            template.raw = fs_extra_1.readFileSync(template.path).toString();
        // We need to inspect partials.
        // Since its a partial we just need to
        // read the file.
        var partialKeys = Object.keys(template.partials);
        partialKeys.forEach(function (p) {
            var partial = template.partials[p];
            if (partial.path)
                partial.raw = fs_extra_1.readFileSync(partial.path).toString();
            // Update the partial template.
            template.partials[p] = partial;
        });
        // If we don't have template.raw string to be parsed
        // we can't continue.
        if (lodash_1.isUndefined(template.raw))
            this.log.error('cannot generate template with raw template string of undefined.').exit();
        // Parse the template for body and front matter attributes.
        var matter = fm(template.raw);
        var attrs = matter.attributes;
        var tmpConfig = attrs.config || {};
        delete attrs.config;
        template.metadata = attrs;
        template.body = matter.body;
        // Merge in the parsed config with template config.
        template = lodash_1.extend({}, tmpConfig, template);
        // Parse the output path/name.
        parsedOutput = path_1.parse(output);
        // Get the output name.
        outputName = parsedOutput.name;
        // Ensure ext from output or defined in template config.
        ext = parsedOutput.ext || tmpConfig.ext;
        // Set the component name in metadata.
        template.metadata.component = outputName;
        // Check component casing and suffix.
        if (template.suffix) {
            template.metadata.component += " " + template.suffix;
            outputName += "." + template.suffix;
        }
        // Ensure full template output path.
        outputPath = path_1.resolve(this.outputPath, parsedOutput.dir || '', outputName + ext);
        outputPath = outputPath.toLowerCase();
        // Update the template object with
        // the output name and output path.
        template.outputPath = outputPath;
        // Check if casing is specified.
        if (template.casing || template.suffix) {
            var casing = template.casing ? template.casing : 'title';
            template.metadata.component =
                this.transformCase(template.metadata.component, casing);
        }
        // Merge parsed options with template metadata.
        template.metadata = lodash_1.extend({}, template.metadata, metadata);
        return template;
    };
    /**
     * Register
     * Registers a template with Mustr.
     *
     * @param name to use when registering a template.
     * @param template the template name or path to lookup or a static string.
     */
    Mustr.prototype.register = function (name, template) {
        var _this = this;
        var beforeRender, afterRender, partials, methods;
        var addPartials, getPartial;
        var _template;
        if (lodash_1.isPlainObject(name)) {
            var reg = name;
            name = reg.name;
            template = reg.template;
            partials = reg.partials;
            beforeRender = reg.beforeRender;
            afterRender = reg.afterRender;
        }
        // Parse the supplied template.
        var parsed = this.normalize(template);
        // Register the template with the template collection.
        name = name.toLowerCase();
        _template = this.templates[name] = parsed;
        getPartial = function (partial) {
            var partialName = partial.toLowerCase();
            var existing = _this.templates[partialName];
            if (existing)
                return existing;
            partial = _this.normalize(partial);
            partial.isPartial = true;
            return partial;
        };
        addPartials = function () {
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i] = arguments[_i];
            }
            if (!args.length)
                return methods;
            if (lodash_1.isPlainObject(args[0])) {
                var _partials_1 = args[0];
                Object.keys(_partials_1).forEach(function (p) {
                    var partial = getPartial(_partials_1[p]);
                    partial.name = p.toLowerCase();
                    _template.partials[partial.name] = partial;
                });
            }
            else {
                // Flatten the array.
                args = [].concat.apply([], args);
                // Iterate partials and load.
                args.forEach(function (p) {
                    var partial = getPartial(p);
                    _template.partials[partial.name] = partial;
                });
            }
        };
        beforeRender = function (before) {
            _template.beforeRender = before;
            return methods;
        };
        afterRender = function (after) {
            _template.afterRender = after;
            return methods;
        };
        // Add partials if needed.
        addPartials(partials);
        // Build up methods for extending registration.
        // It's just cleaner this way rather than a long
        // signature with lots of params for .register().
        // pass Register object if you don't like chaining.
        methods = {
            partials: partials,
            beforeRender: beforeRender,
            afterRender: afterRender
        };
        return methods;
    };
    /**
     * Render
     * Renders the requested template.
     *
     * @param name the name of the template to render.
     * @param output the output path/name to render the template as.
     * @param options the metadata options to extend with if any.
     */
    Mustr.prototype.render = function (name, output, metadata, done) {
        var _this = this;
        // Load the template.
        var template = name;
        var isTemplate;
        // Ensure we have some templates loaded.
        if (!Object.keys(this.templates).length)
            this.log.warn('whoops you no templates registed, add some templates in "register.js"').exit();
        if (lodash_1.isFunction(output)) {
            done = output;
            output = undefined;
            metadata = undefined;
        }
        else {
            if (lodash_1.isFunction(metadata)) {
                done = metadata;
                metadata = undefined;
            }
            if (lodash_1.isPlainObject(output)) {
                done = metadata;
                metadata = output;
                output = undefined;
            }
        }
        if (lodash_1.isString(template)) {
            if (!lodash_1.isString(output))
                output = name;
            template = this.configure(name, output, metadata);
        }
        var partials = {};
        // If partials get the raw strings.
        var partialKeys = Object.keys(template.partials);
        partialKeys.forEach(function (p) {
            var partial = template.partials[p];
            partials[p] = partial.raw;
        });
        // Ensure before/after render hooks.
        template.beforeRender = template.beforeRender || function (t, d) { d(); };
        template.afterRender = template.afterRender || function (t) { };
        done = done || function (t) { };
        // Ensure injects array.
        template.injects = template.injects || [];
        // Parse the output path.
        var parsedPath = path_1.parse(template.outputPath);
        // Ensure the directory structure exists.
        fs_extra_1.ensureDirSync(parsedPath.dir);
        var injects = [];
        var success = 0;
        var failed = 0;
        // Iterate the inject
        template.injects.forEach(function (inj) {
            var wrapper = function (cb) {
                _this.inject(inj, function (err) {
                    if (err)
                        failed += 1;
                    else
                        success += 1;
                    // callback to continue.
                    cb();
                });
            };
            injects.push(wrapper);
        });
        // Call before render hook.
        template.beforeRender(template, function () {
            // Render the template.
            template.rendered = Mustache.render(template.body, template.metadata, partials);
            // Write the file.
            fs_extra_1.writeFile(template.outputPath, template.rendered, function (err) {
                if (err)
                    _this.log.error(err).exit();
                // No injects required just call callbacks.
                if (!injects.length) {
                    // Call after render.
                    template.afterRender(template);
                    // Render may be called directly with done callback.
                    done(template);
                }
                else {
                    // Iterate injects.
                    async_1.parallel(injects, function (err, results) {
                        _this.log.info(success + " successfully injected " + failed + " failed.");
                        // Call done/after callbacks.
                        template.afterRender(template);
                        done(template);
                    });
                }
            });
        });
    };
    /**
     * Inject
     * Creates a file stream reading chunks until match.
     * Upon match the provided value is injected.
     * NOTE: lines are auto updated with the correct
     * line ending for returns.
     *
     * @param filename the file name to inject into.
     * @param find the string or expression to match.
     * @param insert the value or callback for injecting upon match.
     * @param strategy insert on before, after, replace, first or last when matched.
     * @param done the callback on file written.
     */
    Mustr.prototype.inject = function (filename, find, strategy, insert, done) {
        var _this = this;
        var data = [];
        var idx = 0;
        var matches = 0;
        var insertLen = 0;
        var lastMatch = 0;
        var exp;
        // Allows done callback as second arg.
        if (lodash_1.isFunction(find))
            done = find;
        if (lodash_1.isPlainObject(filename)) {
            var obj = filename;
            filename = obj.filename;
            find = obj.find;
            insert = obj.insert;
            strategy = obj.strategy;
            done = obj.done || done;
        }
        done = done || function () { };
        // Inserts at a given index.
        function insertAt(arr, index, item) {
            var start = arr.slice(0, index);
            var end = arr.slice(index);
            return [].concat(start).concat(item).concat(end);
        }
        // Sets indentation for inserts.
        function setIndent(from, to) {
            var indent = di(from).indent;
            to.forEach(function (t, i) {
                to[i] = (indent + t);
            });
            return to;
        }
        // Resolve the filename.
        filename = path_1.resolve(this.outputPath, filename);
        if (!fs_extra_1.existsSync(filename))
            return done(new Error("failed to inject in " + filename + ", file not found."));
        exp = find;
        // If is string convert to expression.
        if (lodash_1.isString(exp))
            exp = new RegExp(exp);
        // Normalize the insert
        if (lodash_1.isString(insert))
            insert = [insert];
        insert = insert || [];
        insertLen = insert.length;
        var rl = readline.createInterface({
            input: fs_extra_1.createReadStream(filename)
        });
        rl.on('line', function (line) {
            if (exp.test(line)) {
                matches += 1;
                if (strategy !== 'last')
                    insert = setIndent(line, insert);
                if (strategy === 'before') {
                    data = data.concat(insert);
                    data.push(line);
                    idx += insertLen;
                }
                else if (strategy === 'after') {
                    data.push(line);
                    data = data.concat(insert);
                    idx += insertLen;
                }
                else if (strategy === 'replace') {
                    data = data.concat(insert);
                    idx += (insertLen - 1);
                }
                else if (strategy === 'first' && matches === 1) {
                    data = data.concat(insert);
                    data.push(line);
                    idx += insertLen;
                }
                else {
                    data.push(line);
                }
            }
            else {
                data.push(line);
            }
            // keep track of our index.
            idx += 1;
            if (exp.test(line))
                lastMatch = idx;
        });
        rl.on('close', function () {
            if (strategy === 'last' && matches > 0 && lastMatch >= 0) {
                insert = setIndent(data[lastMatch - 1], insert);
                data = insertAt(data, lastMatch, insert);
            }
            var joined = data.join(os_1.EOL);
            fs_extra_1.writeFile(filename, joined, function (err) {
                var insertStr = insert.join(', ');
                if (err)
                    _this.log.warn(err);
                else
                    _this.log.info("injected: " + _this.truncate(insert, 18) + " in: " + path_1.parse(filename).base + ".");
                // Pass error back for render counter.
                // does not count when manually calling
                // .inject method.
                done(err);
            });
        });
    };
    /**
     * Transform To
     * Transforms a string to the desired casing.
     *
     * @param str the string to be transformed.
     * @param to the casing to be transformed to using lodash.
     */
    Mustr.prototype.transformCase = function (str, to) {
        var caseMap = {
            lower: lodash_1.lowerCase,
            upper: lodash_1.upperCase,
            capitalize: lodash_1.capitalize,
            camel: lodash_1.camelCase,
            lowerCase: lodash_1.lowerCase,
            upperCase: lodash_1.upperCase,
            camelCase: lodash_1.camelCase,
            title: function (str) {
                return lodash_1.startCase(lodash_1.toLower(str));
            }
        };
        caseMap.titleCase = caseMap.title;
        if (!caseMap[to])
            this.log.error("invalid transform case " + to + ".").exit();
        return caseMap[to](str).replace(/\s/g, '');
    };
    return Mustr;
}(events_1.EventEmitter));
exports.Mustr = Mustr;
//# sourceMappingURL=index.js.map