var concatMap = require('concat-map'); var balanced = require('balanced-match');

module.exports = expandTop;

var escSlash = '0SLASH'+Math.random()+'0'; var escOpen = '0OPEN'+Math.random()+'0'; var escClose = '0CLOSE'+Math.random()+'0'; var escComma = '0COMMA'+Math.random()+'0'; var escPeriod = '0PERIOD'+Math.random()+'0';

function numeric(str) {

return parseInt(str, 10) == str
  ? parseInt(str, 10)
  : str.charCodeAt(0);

}

function escapeBraces(str) {

return str.split('\\\\').join(escSlash)
          .split('\\{').join(escOpen)
          .split('\\}').join(escClose)
          .split('\\,').join(escComma)
          .split('\\.').join(escPeriod);

}

function unescapeBraces(str) {

return str.split(escSlash).join('\\')
          .split(escOpen).join('{')
          .split(escClose).join('}')
          .split(escComma).join(',')
          .split(escPeriod).join('.');

}

// Basically just str.split(“,”), but handling cases // where we have nested braced sections, which should be // treated as individual members, like {a,{b,c},d} function parseCommaParts(str) {

if (!str)
  return [''];

var parts = [];
var m = balanced('{', '}', str);

if (!m)
  return str.split(',');

var pre = m.pre;
var body = m.body;
var post = m.post;
var p = pre.split(',');

p[p.length-1] += '{' + body + '}';
var postParts = parseCommaParts(post);
if (post.length) {
  p[p.length-1] += postParts.shift();
  p.push.apply(p, postParts);
}

parts.push.apply(parts, p);

return parts;

}

function expandTop(str) {

if (!str)
  return [];

// I don't know why Bash 4.3 does this, but it does.
// Anything starting with {} will have the first two bytes preserved
// but *only* at the top level, so {},a}b will not expand to anything,
// but a{},b}c will be expanded to [a}c,abc].
// One could argue that this is a bug in Bash, but since the goal of
// this module is to match Bash's rules, we escape a leading {}
if (str.substr(0, 2) === '{}') {
  str = '\\{\\}' + str.substr(2);
}

return expand(escapeBraces(str), true).map(unescapeBraces);

}

function identity(e) {

return e;

}

function embrace(str) {

return '{' + str + '}';

} function isPadded(el) {

return /^-?0\d/.test(el);

}

function lte(i, y) {

return i <= y;

} function gte(i, y) {

return i >= y;

}

function expand(str, isTop) {

var expansions = [];

var m = balanced('{', '}', str);
if (!m || /\$$/.test(m.pre)) return [str];

var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
var isSequence = isNumericSequence || isAlphaSequence;
var isOptions = m.body.indexOf(',') >= 0;
if (!isSequence && !isOptions) {
  // {a},b}
  if (m.post.match(/,.*\}/)) {
    str = m.pre + '{' + m.body + escClose + m.post;
    return expand(str);
  }
  return [str];
}

var n;
if (isSequence) {
  n = m.body.split(/\.\./);
} else {
  n = parseCommaParts(m.body);
  if (n.length === 1) {
    // x{{a,b}}y ==> x{a}y x{b}y
    n = expand(n[0], false).map(embrace);
    if (n.length === 1) {
      var post = m.post.length
        ? expand(m.post, false)
        : [''];
      return post.map(function(p) {
        return m.pre + n[0] + p;
      });
    }
  }
}

// at this point, n is the parts, and we know it's not a comma set
// with a single entry.

// no need to expand pre, since it is guaranteed to be free of brace-sets
var pre = m.pre;
var post = m.post.length
  ? expand(m.post, false)
  : [''];

var N;

if (isSequence) {
  var x = numeric(n[0]);
  var y = numeric(n[1]);
  var width = Math.max(n[0].length, n[1].length)
  var incr = n.length == 3
    ? Math.abs(numeric(n[2]))
    : 1;
  var test = lte;
  var reverse = y < x;
  if (reverse) {
    incr *= -1;
    test = gte;
  }
  var pad = n.some(isPadded);

  N = [];

  for (var i = x; test(i, y); i += incr) {
    var c;
    if (isAlphaSequence) {
      c = String.fromCharCode(i);
      if (c === '\\')
        c = '';
    } else {
      c = String(i);
      if (pad) {
        var need = width - c.length;
        if (need > 0) {
          var z = new Array(need + 1).join('0');
          if (i < 0)
            c = '-' + z + c.slice(1);
          else
            c = z + c;
        }
      }
    }
    N.push(c);
  }
} else {
  N = concatMap(n, function(el) { return expand(el, false) });
}

for (var j = 0; j < N.length; j++) {
  for (var k = 0; k < post.length; k++) {
    var expansion = pre + N[j] + post[k];
    if (!isTop || isSequence || expansion)
      expansions.push(expansion);
  }
}

return expansions;

}