'use strict';

var fs = require('fs'),
  lfmt = require('lfmt');

var saveFile = process.cwd() + '/saved.macros';

var macros = {};

var errMsgs = {
  incorrectUsage: '`macro`: you did something wrong.',
  noMacro: 'Can\'t find that!',
  cantBeEmpty: 'Macro can\'t be empty!',
  cantBeRecursive: 'Macro can\'t be recursive!',
  badBrackets: 'Macro has mismatched brackets!',
  noMacros: 'There are no macros.'
};

var loadMacros = function loadMacros() {
  if (Object.keys(macros).length > 0) return;

  try {
    // this scheme does not account for duplicate keys. later records with
    // the same key will simply overwrite the template.
    var rawMacros = fs.readFileSync(saveFile).toString()
      .split('\n')
      .forEach(function(record) {
        if (!record) return;

        // TODO: catch errors for records with no space
        var spaceIndex = record.indexOf(' '),
          name = record.slice(0, spaceIndex),
          template = record.slice(spaceIndex + 1);
        macros[name] = decodeURIComponent(template);

        console.log(lfmt.format('loading macro {{name}} `{{template}}`', {
          name: name,
          template: template
        }));
      });
  } catch (err) {
    // log the error and clean the macros.
    console.error(err);
    macros = {};
  }
};

var addMacro = function addMacro(name, template, cb) {
  macros[name] = template;
  fs.appendFile(saveFile, lfmt.format('{{name}} {{template}}\n', {
    name: name,
    template: encodeURIComponent(template)
  }), function(err) {
    if (err) console.error(err);
    cb && cb();
  });
};

var applyMacro = function applyMacro(name, args, cb) {
  var template = macros[name];
  if (!template) {
    cb && cb(new Error('Macro not found'));
    return;
  }

  var result = template
    .replace(/\$@/g, args.join(' '))
    .replace(/\$(\d+)/g, function(match, p1) {
      return (args[Number(p1)] || '');
    });

  cb && resolveNesting(result, cb);
};

var resolveNesting = function resolveNesting(template, cb) {
  var lastNested = template.lastIndexOf('$(');
  if (lastNested === -1) cb(null, template);
  else {
    var brackets = 1;
    var index = lastNested + 2;

    while (brackets > 0) {
      if (template[index+1] && template[index] === '$' && template[index+1] === '(') {
        brackets += 1;
      } else if (template[index] === ')') {
        brackets -= 1;
      }
      index += 1;
    }

    var nestedMacro = template.substring(lastNested+2, index-1),
      firstSpace = nestedMacro.indexOf(' '),
      name = firstSpace === -1 ? nestedMacro : nestedMacro.substring(0, firstSpace),
      args = firstSpace === -1 ? [] : nestedMacro.substring(firstSpace+1).split(' ');

    applyMacro(name, args, function(err, result) {
      console.log(result);
      resolveNesting(template.slice(0, lastNested)
        + (result || '')
        + template.slice(index),
      cb);
    });
  }
};

var isRecursive = function isRecursive(name, template) {
  var nestedMacros = template.match(/\$\(.*?(\)|\ )/g);

  if (!nestedMacros) return false;

  nestedMacros = nestedMacros.map(function (str) {
    var firstSpace = str.indexOf(' ');
    return str.substring(2, firstSpace === -1 ? (str.length - 1) : firstSpace);
  });

  return nestedMacros.some(function (macro) {
    return macro === name || (macros[macro] && isRecursive(name, macros[macro]));
  });
};

var badBrackets = function badBrackets(template) {
  var index = 0,
    brackets = 0;

  while (template[index]) {
    if (template.substring(index, index + 2) === '$(') {
      brackets += 1;
    } else if (template[index] === ')') {
      brackets -= 1;
    }
    index += 1;
  }
  if (brackets > 0) return true;
  return false;
};

var macro = function macro(argv, message, response, logger) {
  logger.log('macro called with message', message);
  loadMacros();

  if (argv.length < 2) {
    logger.error('`macro` called incorrectly: ', argv.join(' '));
    response.end(errMsgs.incorrectUsage);
    return;
  }

  var subcmd = argv[1];

  if (subcmd === 'set') {
    var name = argv[2];
    var template = argv.slice(3).join(' ');

    if (template.length <= 0) {
      response.end(errMsgs.cantBeEmpty);
      return;
    } else if (badBrackets(template)) {
      response.end(errMsgs.badBrackets);
      return;
    } else if (isRecursive(name, template)) {
      response.end(errMsgs.cantBeRecursive);
      return;
    }

    var oldTemplate = macros[name];
    addMacro(name, template, function(err) {
      if (!err) {
        var respBody = (oldTemplate ?
          lfmt.format('Overwriting old template ```{{oldTemplate}}```\n', {
            oldTemplate: oldTemplate
          }) : '');
        respBody += lfmt.format('Successfully macroed {{name}} to ```{{template}}```', {
          name: name,
          template: template
        })
        response.end(respBody);
      }
    });
  }
  else if (subcmd === 'list') {
    var expand = (argv.indexOf('--expand') !== -1);
    var resBody = Object.keys(macros)
      .map(function(key) {
        if (!expand) return key;
        return lfmt.format('{{name}} - ```{{template}}```', {
          name: key,
          template: macros[key]
        });
      })
      .join(expand ? '\n': ', ');

    resBody = resBody || errMsgs.noMacros;
    response.end(resBody);
  }
  else {
    var name = subcmd;
    applyMacro(name, argv.slice(2), function(err, result) {
      var reply = result || errMsgs.noMacro;
      logger.log(lfmt.format('Sending reply: {{reply}}', {
        reply: reply
      }));
      response.end(reply);
    });
  }
};

macro.metadata = {
  name: 'macro',
  command: 'macro',
  info: {
    description: 'Set a string template macro',
    usage: 'macro [set {template}|{name} {variables...}]'
  }
};

module.exports = macro;

@@ -5615,16 +5615,109 @@
 ,%0A  
-info: %7B%0A
+description: 'Manipulate string template macros',%0A  subcommands: %7B%0A    'set': %7B%0A      name: 'set',%0A  
     
@@ -5764,16 +5764,18 @@
 o',%0A    
+  
 usage: '
@@ -5784,47 +5784,36 @@
 cro 
-%5B
 set %7B
-template%7D%7C%7Bname%7D %7Bvariables...%7D%5D'
+name%7D %7Btemplate%7D'%0A    %7D
 %0A  %7D

Update help metadat