/**
 * grunt-includes
 * https://github.com/vanetix/grunt-includes
 *
 * Copyright (c) 2012-2015 Matt McFarland and Contributors
 * Licensed under the MIT license.
 */

module.exports = function(grunt) {

  /**
   * Dependencies
   */

  var path = require('path');

  // Regex for matching new lines
  var newlineRegexp = /\r?\n/g;

  // Regex for parsing includes
  var defaultRegexp = /^(\s*)include\s+"(\S+)"\s*$/;

  // Regexp to replace with the file inside a template
  var defaultTemplateFileRegexp = /\{\{\s?file\s?\}\}/;

  // Regexp for inerpolating the filename in the template
  var templateFilenameRegexp = /\{\{\s?fileName\s?\}\}/;

  /**
   * Core `grunt-includes` task
   * Iterates over all source files and calls `recurse(path)` on each
   */

  grunt.registerMultiTask('includes', 'Include other files within files.', function() {
    var banner;

    // Default options
    var opts = this.options({
      debug: false,
      banner: '',
      silent: false,
      duplicates: true,
      includeRegexp: defaultRegexp,
      includePath: '',
      filenamePrefix: '',
      filenameSuffix: '',
      template: '',
      templateFileRegexp: defaultTemplateFileRegexp
    });

    if (grunt.util.kindOf(opts.includeRegexp) === 'string') {
      opts.includeRegexp = new RegExp(opts.includeRegexp);
    }

    if (grunt.util.kindOf(opts.templateFileRegexp) === 'string') {
      opts.templateFileRegexp = new RegExp(opts.templateFileRegexp);
    }

    // Render banner
    banner = grunt.template.process(opts.banner);

    this.files.forEach(function(f) {
      var src, cwd = f.cwd;

      src = f.src.filter(function(p) {
        if (cwd) {
          p = path.join(f.cwd, p);
        }

        if (grunt.file.isFile(p)) {
          return true;
        } else {
          grunt.fail.fatal('Source "' + p + '" is not a file');
          return false;
        }
      });

      if (src.length > 1 && isFilename(f.dest)) {
        grunt.fail.fatal('Source file cannot be more than one when dest is a file.');
      }

      src.forEach(function(p) {
        var fileName = f.flatten ? path.basename(p) : p;
        var outFile = isFilename(f.dest) ? f.dest : path.join(f.dest, fileName);

        if (cwd) {
          p = path.join(cwd, p);
        }

        grunt.file.write(outFile, banner + recurse(p, opts));

        if (!opts.silent) {
          grunt.log.oklns('Saved ' + outFile);
        }
      });

    });
  });

  /**
   * Checks if `p` is a filepath, being it has an extension.
   *
   * @param {String} p
   * @return {Boolean}
   */

  function isFilename(p) {
    return !!path.extname(p);
  }

  /**
   * Returns the comment style for file `p`
   *
   * @param {String} p
   * @return {String}
   */

  function commentStyle(p) {
    var comments,
        ext = path.extname(p).slice(1);

    comments = {
      js: "/* %s */",
      css: "/* %s */",
      html: "<!-- %s -->"
    };

    return comments[ext] || '/* %s */';
  }

  /**
   * Returns the new line style for file `p`
   *
   * @param {String} p
   * @return {String}
   */

  function newlineStyle(p) {
    var matches = grunt.file.read(p).match(newlineRegexp);

    return (matches && matches[0]) || grunt.util.linefeed;
  }

  /**
   * Helper for `includes` builds all includes for `p`
   *
   * @param {String} p
   * @return {String}
   */

  function recurse(p, opts, included, indents) {
    var src, next, match, error, comment, content,
        newline, compiled, indent, fileLocation,
        currentTemplate;

    if (!grunt.file.isFile(p)) {
      grunt.fail.warn('Included file "' + p + '" not found.');
      return 'Error including "' + p + '".';
    }

    indents = indents || '';
    comment = commentStyle(p);
    newline = newlineStyle(p);
    included = included || [];

    // If `opts.duplicates` is false and file has been included, error
    if (!opts.duplicates && ~included.indexOf(p)) {
      error = 'Duplicate include: ' + p + ' skipping.';

      if (!opts.silent) {
         grunt.log.error(error);
      }

      if (opts.debug) {
        return comment.replace(/%s/g, error);
      } else {
        return '';
      }
    }

    // At this point the file is considered included
    included.push(p);

    // Split the file on newlines
    src = grunt.file.read(p).split(newline);

    // Loop through the file calling `recurse` if an include is found
    compiled = src.map(function(line) {
      match = line.match(opts.includeRegexp);

      // If the line has an include statement, recurse
      if (match) {
        indent = match[1];
        fileLocation = match[2];

        if (!fileLocation) {
          fileLocation = indent;
          indent = '';
        }

        // If a full filepath + extension is given, use it instead of building
        if (isFilename(fileLocation)) {
          fileLocation = fileLocation;
        } else {
          fileLocation = opts.filenamePrefix + fileLocation + opts.filenameSuffix;
        }

        // Try to locate the file through multiple includePath if array
        if (grunt.util.kindOf(opts.includePath) === 'array') {
          opts.includePath.some(function(p) {
            next = path.join(p, fileLocation);
            return grunt.file.isFile(next);
          });

          if (!next) {
            next = path.join(path.dirname(p), fileLocation);
          }
        } else {
          next = path.join((opts.includePath || path.dirname(p)), fileLocation);
        }

        content = recurse(next, opts, included, indents + indent);

        // Wrap file around in template if `opts.template` has '{{file}}' in it.
        if (opts.template !== '' && opts.template.match(opts.templateFileRegexp)) {
          currentTemplate = opts.template.split(newline).map(function(line) {
            line = line.replace(templateFilenameRegexp, fileLocation);

            if (line.match(opts.templateFileRegexp)) {
              return line;
            } else {
              return indent + indents + line;
            }
          });

          // Safe guard against $ replacements - this can probably be improved
          content = content.replace(/\$/g, '$$$$');
          content = currentTemplate.join(newline).replace(opts.templateFileRegexp, content);
        }

        // Safe guard against $ replacements, change $ to $$
        content = content.replace(/\$/g, '$$$$');
        line = line.replace(opts.includeRegexp, content);

        // Include debug comments if `opts.debug`
        if (opts.debug) {
          line = comment.replace(/%s/g, 'Begin: ' + next) +
                 newline + line + comment.replace(/%s/g, 'End: ' + next);
        }
      }

      // If there are indents and not a match, add them to the line
      return line && indents && !match ? indents + line : line;
    });

    return  compiled.join(newline);
  }
};
