var parser = require("./parser.js");
var freeFormQuery; // todo: refactor this!

// todo: normalization -> node module?

/* this converts a random boolean expression into a normalized form:
 * A∧B∧… ∨ C∧D∧… ∨ …
 * for example: A∧(B∨C) ⇔ (A∧B)∨(A∧C)
 */
function normalize(query) {
  var normalized_query = {
    logical:"or",
    queries:[]
  };
  function normalize_recursive(rem_query) {
    if (!rem_query.logical) {
      return [{
        logical: "and",
        queries: [rem_query]
      }];
    } else if (rem_query.logical === "and") {
      var c1 = normalize_recursive( rem_query.queries[0] );
      var c2 = normalize_recursive( rem_query.queries[1] );
      // return cross product of c1 and c2
      var c = [];
      for (var i=0; i<c1.length; i++)
        for (var j=0; j<c2.length; j++) {
          c.push({
            logical: "and",
            queries: c1[i].queries.concat(c2[j].queries)
          });
        }
      return c;
    } else if (rem_query.logical === "or") {
      var c1 = normalize_recursive( rem_query.queries[0] );
      var c2 = normalize_recursive( rem_query.queries[1] );
      return [].concat(c1,c2);

    } else {
      console.error("unsupported boolean operator: "+rem_query.logical);
    }
  }
  normalized_query.queries = normalize_recursive(query);
  return normalized_query;
}

module.exports = function wizard(search, options) {
  var defaults = {
    comment: true,
    outputMode: "geom", // "recursive", "recursive_meta", out <*> ("geom", "ids", …)
    globalBbox: true,
    //todo: more fine grained controll, e.g. to deactivate "in X"
    timeout: 25,
    maxsize: undefined,
    outputFormat: "json", // "json", "xml"
    aroundRadius: 1000,
    freeFormPresets: undefined
  };

  for (var k in options) {
    defaults[k] = options[k];
  }
  options = defaults;

  // quote strings that are safe to be used within c-style comments
  // replace any comment-ending sequences in these strings that would break the resulting query
  function quote_comment_str(s) {
    return s.replace(/\*\//g,'[…]').replace(/\n/g,'\\n');
  }

  var parsedQuery;

  try {
    parsedQuery = parser.parse(search);
  } catch(e) {
    console.error("couldn't parse wizard input");
    return false;
  }

  var query_parts = [];
  var bounds_part;

  if (options.comment === true) {
    query_parts.push('/*');
    query_parts.push('This has been generated by the overpass-turbo wizard.');
    query_parts.push('The original search was:');
    query_parts.push('“'+quote_comment_str(search)+'”');
    query_parts.push('*/');
  } else if (typeof options.comment === "string") {
    query_parts.push('/*');
    query_parts.push(options.comment);
    query_parts.push('*/');
    comment = true;
  }
  query_parts.push(
    '[out:'+options.outputFormat+']'+
    '[timeout:'+options.timeout+']'+
    (options.maxsize !== undefined ? '[maxsize:'+options.maxsize+']' : '')+
    (options.globalBbox && parsedQuery.bounds === "bbox" ? '[bbox:{{bbox}}]' : '')+
  ';');

  switch(parsedQuery.bounds) {
    case "area":
      if (options.comment)
        query_parts.push('// fetch area “'+parsedQuery.area+'” to search in');
      query_parts.push('{{geocodeArea:'+parsedQuery.area+'}}->.searchArea;');
      bounds_part = '(area.searchArea)';
    break;
    case "around":
      if (options.comment)
        query_parts.push('// adjust the search radius (in meters) here');
      query_parts.push('{{radius='+options.aroundRadius+'}}');
      bounds_part = '(around:{{radius}},{{geocodeCoords:'+parsedQuery.area+'}})';
    break;
    case "bbox":
      bounds_part = options.globalBbox ? '' : '({{bbox}})';
    break;
    case "global":
      bounds_part = undefined;
    break;
    default:
      console.error("unknown bounds condition: "+parsedQuery.bounds);
      return false;
    break;
  }

  function get_query_clause(condition) {
    function escRegexp(str) {
      return str.replace(/([()[{*+.$^\\|?])/g, '\\$1');
    }
    function esc(str) {
      if (typeof str !== "string") return;
      // see http://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#Escaping
      return str.replace(/\\/g,"\\\\").replace(/"/g,"\\\"") // need to escape those
                .replace(/\t/g,"\\t").replace(/\n/g,"\\n"); // also escape newlines an tabs for better readability of the query
    }
    var key = esc(condition.key);
    var val = esc(condition.val);
    // convert substring searches into matching regexp ones
    if (condition.query === "substr") {
      condition.query = "like";
      condition.val={regex:escRegexp(condition.val)};
    }
    // special case for empty values
    // see https://github.com/drolbr/Overpass-API/issues/53
    if (val === '') {
      if (condition.query === "eq") {
        condition.query = "like";
        condition.val={regex:'^$'};
      } else if (condition.query === "neq") {
        condition.query = "notlike";
        condition.val={regex:'^$'};
      }
    }
    // special case for empty keys
    // see https://github.com/drolbr/Overpass-API/issues/53#issuecomment-26325122
    if (key === '') {
      if (condition.query === "key") {
        condition.query = "likelike";
        key='^$';
        condition.val={regex: '.*'};
      } else if (condition.query === "eq") {
        condition.query = "likelike";
        key='^$';
        condition.val={regex: '^'+escRegexp(condition.val)+'$'};
      } else if (condition.query === "like") {
        condition.query = "likelike";
        key='^$';
      }
    }
    // construct the query clause
    switch(condition.query) {
      case "key":
        return '["'+key+'"]';
      case "nokey":
        return '["'+key+'"!~".*"]';
      case "eq":
        return '["'+key+'"="'+val+'"]';
      case "neq":
        return '["'+key+'"!="'+val+'"]';
      case "like":
        return '["'+key+'"~"'+esc(condition.val.regex)+'"'
               +(condition.val.modifier==="i"?',i':'')
               +']';
      case "likelike":
        return '[~"'+key+'"~"'+esc(condition.val.regex)+'"'
               +(condition.val.modifier==="i"?',i':'')
               +']';
      case "notlike":
        return '["'+key+'"!~"'+esc(condition.val.regex)+'"'
               +(condition.val.modifier==="i"?',i':'')
               +']';
      case "meta":
        switch(condition.meta) {
          case "id":
            return '('+val+')';
          case "newer":
            if (condition.val.match(/^-?\d+ ?(seconds?|minutes?|hours?|days?|weeks?|months?|years?)?$/))
              return '(newer:"{{date:'+val+'}}")';
            return '(newer:"'+val+'")';
          case "user":
            return '(user:"'+val+'")';
          case "uid":
            return '(uid:'+val+')';
          default:
            console.error("unknown query type: meta/"+condition.meta);
            return false;
        }
      case "free form":
        // own module, special cased below
      default:
        console.error("unknown query type: "+condition.query);
        return false;
    }
  }
  function get_query_clause_str(condition) {
    function quotes(s) {
      if (s.match(/^[a-zA-Z0-9_]+$/) === null)
        return '"'+s.replace(/"/g,'\\"')+'"';
      return s;
    }
    function quoteRegex(s) {
      if (s.regex.match(/^[a-zA-Z0-9_]+$/) === null || s.modifier)
        return '/'+s.regex.replace(/\//g,'\\/')+'/'+(s.modifier||'');
      return s.regex;
    }
    switch(condition.query) {
      case "key":
        return quote_comment_str(quotes(condition.key)+'=*');
      case "nokey":
        return quote_comment_str(quotes(condition.key)+'!=*');
      case "eq":
        return quote_comment_str(quotes(condition.key)+'='+quotes(condition.val));
      case "neq":
        return quote_comment_str(quotes(condition.key)+'!='+quotes(condition.val));
      case "like":
        return quote_comment_str(quotes(condition.key)+'~'+quoteRegex(condition.val));
      case "likelike":
        return quote_comment_str('~'+quotes(condition.key)+'~'+quoteRegex(condition.val));
      case "notlike":
        return quote_comment_str(quotes(condition.key)+'!~'+quoteRegex(condition.val));
      case "substr":
        return quote_comment_str(quotes(condition.key)+':'+quotes(condition.val));
      case "meta":
        switch(condition.meta) {
          case "id":
            return quote_comment_str('id:'+quotes(condition.val));
          case "newer":
            return quote_comment_str('newer:'+quotes(condition.val));
          case "user":
            return quote_comment_str('user:'+quotes(condition.val));
          case "uid":
            return quote_comment_str('uid:'+quotes(condition.val));
          default:
            return '';
        }
      case "free form":
        return quote_comment_str(quotes(condition.free));
      default:
        return '';
    }
  }

  parsedQuery.query = normalize(parsedQuery.query);

  if (options.comment)
    query_parts.push('// gather results');
  query_parts.push('(');
  for (var i=0; i<parsedQuery.query.queries.length; i++) {
    var and_query = parsedQuery.query.queries[i];

    var types = ['node','way','relation'];
    var clauses = [];
    var clauses_str = [];
    for (var j=0; j<and_query.queries.length; j++) {
      var cond_query = and_query.queries[j];
      // todo: looks like some code duplication here could be reduced by refactoring
      if (cond_query.query === "free form") {
        // eventually load free form query module
        if (!freeFormQuery) freeFormQuery = require('./free')(options.freeFormPresets);
        var ffs_clause = freeFormQuery.get_query_clause(cond_query);
        if (ffs_clause === false) {
          console.error("Couldn't find preset for free form input: "+cond_query.free)
          return false;
        }
        // restrict possible data types
        types = types.filter(function(t) {
          return ffs_clause.types.indexOf(t) != -1;
        });
        // add clauses
        if (options.comment)
          clauses_str.push(get_query_clause_str(cond_query));
        clauses = clauses.concat(ffs_clause.conditions.map(function(condition) {
          return get_query_clause(condition);
        }));
      } else if (cond_query.query === "type") {
        // restrict possible data types
        types = types.indexOf(cond_query.type) != -1 ? [cond_query.type] : [];
      } else {
        // add another query clause
        if (options.comment)
          clauses_str.push(get_query_clause_str(cond_query));
        var clause = get_query_clause(cond_query);
        if (clause === false) return false;
        clauses.push(clause);
      }
    }
    clauses_str = clauses_str.join(' and ');

    // construct query
    if (options.comment)
      query_parts.push('  // query part for: “'+clauses_str+'”')
    for (var t=0; t<types.length; t++) {
      var buffer = '  '+types[t];
      for (var c=0; c<clauses.length; c++)
        buffer += clauses[c];
      if (bounds_part)
        buffer += bounds_part;
      buffer += ';';
      query_parts.push(buffer);
    }
  }
  query_parts.push(');');

  if (options.comment)
    query_parts.push('// print results');
  if (options.outputMode === "recursive") {
    query_parts.push('out body;');
    query_parts.push('>;');
    query_parts.push('out skel qt;');
  } else if (options.outputMode === "recursive_meta") {
    query_parts.push('(._;>;);');
    query_parts.push('out meta;');
  } else {
    query_parts.push('out ' + options.outputMode + ';');
  }

  return query_parts.join('\n');
}

module.exports = (function() {
  "use strict";

  /*
   * Generated by PEG.js 0.9.0.
   *
   * http://pegjs.org/
   */

  function peg$subclass(child, parent) {
    function ctor() { this.constructor = child; }
    ctor.prototype = parent.prototype;
    child.prototype = new ctor();
  }

  function peg$SyntaxError(message, expected, found, location) {
    this.message  = message;
    this.expected = expected;
    this.found    = found;
    this.location = location;
    this.name     = "SyntaxError";

    if (typeof Error.captureStackTrace === "function") {
      Error.captureStackTrace(this, peg$SyntaxError);
    }
  }

  peg$subclass(peg$SyntaxError, Error);

  function peg$parse(input) {
    var options = arguments.length > 1 ? arguments[1] : {},
        parser  = this,

        peg$FAILED = {},

        peg$startRuleIndices = { start: 0 },
        peg$startRuleIndex   = 0,

        peg$consts = [
          function(x) { return x },
          "in bbox",
          { type: "literal", value: "in bbox", description: "\"in bbox\"" },
          "IN BBOX",
          { type: "literal", value: "IN BBOX", description: "\"IN BBOX\"" },
          function(x) { return { bounds:"bbox", query:x } },
          "in",
          { type: "literal", value: "in", description: "\"in\"" },
          "IN",
          { type: "literal", value: "IN", description: "\"IN\"" },
          function(x, y) { return { bounds:"area", query:x, area:y } },
          "around",
          { type: "literal", value: "around", description: "\"around\"" },
          "AROUND",
          { type: "literal", value: "AROUND", description: "\"AROUND\"" },
          function(x, y) { return { bounds:"around", query:x, area:y } },
          "global",
          { type: "literal", value: "global", description: "\"global\"" },
          "GLOBAL",
          { type: "literal", value: "GLOBAL", description: "\"GLOBAL\"" },
          function(x) { return { bounds:"global", query:x } },
          "or",
          { type: "literal", value: "or", description: "\"or\"" },
          "OR",
          { type: "literal", value: "OR", description: "\"OR\"" },
          "||",
          { type: "literal", value: "||", description: "\"||\"" },
          "|",
          { type: "literal", value: "|", description: "\"|\"" },
          function(x, y) { return { logical:"or", queries:[x,y] } },
          "and",
          { type: "literal", value: "and", description: "\"and\"" },
          "AND",
          { type: "literal", value: "AND", description: "\"AND\"" },
          "&&",
          { type: "literal", value: "&&", description: "\"&&\"" },
          "&",
          { type: "literal", value: "&", description: "\"&\"" },
          function(x, y) { return { logical:"and", queries:[x,y] } },
          "(",
          { type: "literal", value: "(", description: "\"(\"" },
          ")",
          { type: "literal", value: ")", description: "\")\"" },
          function(x) { return x; },
          "=",
          { type: "literal", value: "=", description: "\"=\"" },
          "==",
          { type: "literal", value: "==", description: "\"==\"" },
          function(x, y) { return { query:"eq", key:x, val:y } },
          "!=",
          { type: "literal", value: "!=", description: "\"!=\"" },
          "<>",
          { type: "literal", value: "<>", description: "\"<>\"" },
          function(x, y) { return { query:"neq", key:x, val:y } },
          "*",
          { type: "literal", value: "*", description: "\"*\"" },
          function(x) { return { query:"key", key:x } },
          "is",
          { type: "literal", value: "is", description: "\"is\"" },
          "not",
          { type: "literal", value: "not", description: "\"not\"" },
          "null",
          { type: "literal", value: "null", description: "\"null\"" },
          "IS",
          { type: "literal", value: "IS", description: "\"IS\"" },
          "NOT",
          { type: "literal", value: "NOT", description: "\"NOT\"" },
          "NULL",
          { type: "literal", value: "NULL", description: "\"NULL\"" },
          function(x) { return { query:"nokey", key:x } },
          "~=",
          { type: "literal", value: "~=", description: "\"~=\"" },
          "~",
          { type: "literal", value: "~", description: "\"~\"" },
          "=~",
          { type: "literal", value: "=~", description: "\"=~\"" },
          function(x, y) { return { query:"like", key:x, val:y.regex?y:{regex:y} } },
          "like",
          { type: "literal", value: "like", description: "\"like\"" },
          "LIKE",
          { type: "literal", value: "LIKE", description: "\"LIKE\"" },
          function(x, y) { return { query:"likelike", key:x, val:y.regex?y:{regex:y} } },
          "!~",
          { type: "literal", value: "!~", description: "\"!~\"" },
          function(x, y) { return { query:"notlike", key:x, val:y.regex?y:{regex:y} } },
          ":",
          { type: "literal", value: ":", description: "\":\"" },
          function(x, y) { return { query:"substr", key:x, val:y } },
          "type",
          { type: "literal", value: "type", description: "\"type\"" },
          function(x) { return { query:"type", type:x } },
          "user",
          { type: "literal", value: "user", description: "\"user\"" },
          "uid",
          { type: "literal", value: "uid", description: "\"uid\"" },
          "newer",
          { type: "literal", value: "newer", description: "\"newer\"" },
          "id",
          { type: "literal", value: "id", description: "\"id\"" },
          function(x, y) { return { query:"meta", meta:x, val:y } },
          function(x) { return { query:"free form", free:x } },
          { type: "other", description: "Key" },
          /^[a-zA-Z0-9_:\-]/,
          { type: "class", value: "[a-zA-Z0-9_:-]", description: "[a-zA-Z0-9_:-]" },
          function(s) { return s.join(''); },
          "\"",
          { type: "literal", value: "\"", description: "\"\\\"\"" },
          "'",
          { type: "literal", value: "'", description: "\"'\"" },
          function(parts) {
                return parts[1];
              },
          { type: "other", description: "string" },
          /^[^'" ()~=!*\/:<>&|[\]{}#+@$%?\^.,]/,
          { type: "class", value: "[^'\" ()~=!*/:<>&|[\\]{}#+@$%?^.,]", description: "[^'\" ()~=!*/:<>&|[\\]{}#+@$%?^.,]" },
          function(chars) { return chars.join(""); },
          "\\",
          { type: "literal", value: "\\", description: "\"\\\\\"" },
          { type: "any", description: "any character" },
          function(char_) { return char_;     },
          function(sequence) { return sequence;  },
          /^['"\\bfnrtv]/,
          { type: "class", value: "['\"\\\\bfnrtv]", description: "['\"\\\\bfnrtv]" },
          function(char_) {
                return char_
                  .replace("b", "\b")
                  .replace("f", "\f")
                  .replace("n", "\n")
                  .replace("r", "\r")
                  .replace("t", "\t")
                  .replace("v", "\x0B") // IE does not recognize "\v".
              },
          "/",
          { type: "literal", value: "/", description: "\"/\"" },
          "i",
          { type: "literal", value: "i", description: "\"i\"" },
          "",
          function(parts) {
                return { regex: parts[1], modifier: parts[3] };
              },
          "\\/",
          { type: "literal", value: "\\/", description: "\"\\\\/\"" },
          function() { return "/";  },
          { type: "other", description: "whitespace" },
          /^[ \t\n\r]/,
          { type: "class", value: "[ \\t\\n\\r]", description: "[ \\t\\n\\r]" }
        ],

        peg$bytecode = [
          peg$decode("%;;/:#;!/1$;;/($8#: #!!)(#'#(\"'#&'#"),
          peg$decode("%;\"/Y#$;</&#0#*;<&&&#/C$2!\"\"6!7\".) &2#\"\"6#7$/($8#:%#!\")(#'#(\"'#&'#.\u0141 &%;\"/y#$;</&#0#*;<&&&#/c$2&\"\"6&7'.) &2(\"\"6(7)/H$$;</&#0#*;<&&&#/2$;2/)$8%:*%\"$ )(%'#($'#(#'#(\"'#&'#.\
xDB &%;\"/y#$;</&#0#*;<&&&#/c$2+\"\"6+7,.) &2-\"\"6-7./H$$;</&#0#*;<&&&#/2$;2/)$8%:/%\"$ )(%'#($'#(#'#(\"'#&'#.u &%;\"/Y#$;</&#0#*;<&&&#/C$20\"\"6071.) &22\"\"6273/($8#:4#!\")(#'#(\"'#&'#./ &%;\"/' 8!:%!! )"),
          peg$decode("%;#/\x91#$;</&#0#*;<&&&#/{$25\"\"6576.A &27\"\"6778.5 &29\"\"697:.) &2;\"\"6;7</H$$;</&#0#*;<&&&#/2$;\"/)$8%:=%\"$ )(%'#($'#(#'#(\"'#&'#.# &;#"),
          peg$decode("%;$/\x91#$;</&#0#*;<&&&#/{$2>\"\"6>7?.A &2@\"\"6@7A.5 &2B\"\"6B7C.) &2D\"\"6D7E/H$$;</&#0#*;<&&&#/2$;#/)$8%:F%\"$ )(%'#($'#(#'#(\"'#&'#.# &;$"),
          peg$decode(";%.b &%2G\"\"6G7H/R#;;/I$;\"/@$;;/7$2I\"\"6I7J/($8%:K%!\")(%'#($'#(#'#(\"'#&'#"),
          peg$decode(";..Y &;/.S &;&.M &;'.G &;(.A &;).; &;*.5 &;+./ &;,.) &;-.# &;0"),
          peg$decode("%;1/_#;;/V$2L\"\"6L7M.) &2N\"\"6N7O/;$;;/2$;2/)$8%:P%\"$ )(%'#($'#(#'#(\"'#&'#"),
          peg$decode("%;1/_#;;/V$2Q\"\"6Q7R.) &2S\"\"6S7T/;$;;/2$;2/)$8%:U%\"$ )(%'#($'#(#'#(\"'#&'#"),
          peg$decode("%;1/d#;;/[$2L\"\"6L7M.) &2N\"\"6N7O/@$;;/7$2V\"\"6V7W/($8%:X%!$)(%'#($'#(#'#(\"'#&'#.\u010B &%;2/\u0101#$;</&#0#*;<&&&#/\xEB$%2Y\"\"6Y7Z/m#$;</&#0#*;<&&&#/W$2[\"\"6[7
\\/H$$;</&#0#*;<&&&#/2$2]\"\"6]7^/#$+%)(%'#($'#(#'#(\"'#&'#.} &%2_\"\"6_7`/m#$;</&#0#*;<&&&#/W$2a\"\"6a7b/H$$;</&#0#*;<&&&#/2$2c\"\"6c7d/#$+%)(%'#($'#(#'#(\"'#&'#/($8#:X#!\")(#'#(\"'#&'#"),
          peg$decode("%;1/d#;;/[$2Q\"\"6Q7R.) &2S\"\"6S7T/@$;;/7$2V\"\"6V7W/($8%:e%!$)(%'#($'#(#'#(\"'#&'#.\xC1 &%;2/\xB7#$;</&#0#*;<&&&#/\xA1$%2Y
\"\"6Y7Z/H#$;</&#0#*;<&&&#/2$2]\"\"6]7^/#$+#)(#'#(\"'#&'#.X &%2_\"\"6_7`/H#$;</&#0#*;<&&&#/2$2c\"\"6c7d/#$+#)(#'#(\"'#&'#/($8#:e#!\")(#'#(\"'#&'#"),
          peg$decode("%;1/q#;;/h$2f\"\"6f7g.5 &2h\"\"6h7i.) &2j\"\"6j7k/A$;;/8$;2.# &;8/)$8%:l%\"$ )(%'#($'#(#'#(\"'#&'#.
\x89 &%;2/#$;</&#0#*;<&&&#/i$2m\"\"6m7n.) &2o\"\"6o7p/N$$;</&#0#*;<&&&#/8$;2.# &;8/)$8%:l%\"$ )(%'#($'#(#'#(\"'#&'#"),
          peg$decode("%2h\"\"6h7i/\x83#;;/z$;2/q$;;/h$2f\"\"6f7g.5 &2h\"\"6h7i.) &2j\"\"6j7k/A$;;/8$;2.# &;8/)$8':q'\"$ )(''#(&'#(%'#($'#(#'#(\"'#&'#"),
          peg$decode("%;1/Y#;;/P$2r\"\"6r7s/A$;;/8$;2.# &;8/)$8%:t%\"$ )(%'#($'#(#'#(\"'#&'#.\xE7 &%;2/\xDD#$;</&#0#*;<&&&#/\xC7$%2[\"\"6[7\\/H#$;</&#0#*;<&&&#/
2$2m\"\"6m7n/#$+#)(#'#(\"'#&'#.X &%2a\"\"6a7b/H#$;</&#0#*;<&&&#/2$2o\"\"6o7p/#$+#)(#'#(\"'#&'#/N$$;</&#0#*;<&&&#/8$;2.# &;8/)$8%:t%\"$ )(%'#($'#(#'#(\"'#&'#"),
          peg$decode("%;2/S#;;/J$2u\"\"6u7v/;$;;/2$;2/)$8%:w%\"$ )(%'#($'#(#'#(\"'#&'#"),
          peg$decode("%2x\"\"6x7y/R#;;/I$2u\"\"6u7v/:$;;/1$;2/($8%:z%! )(%'#($'#(#'#(\"'#&'#"),
          peg$decode("%2{\"\"6{7|.A &2}\"\"6}7~.5 &2\"\"67\x80.) &2\x81\"\"6\x817\x82/S#;;/J$2u\"\"6u7v/;$;;/2$;2/)$8%:\x83%\"$ )(%'#($'#(#'#(\"'#&'#"),
          peg$decode("%;2/' 8!:\x84!! )"),
          peg$decode("<%$4\x86\"\"5!7\x87/,#0)*4\x86\"\"5!7\x87&&&#/' 8!:\x88!! ).\x85 &%%2\x89\"\"6\x897\x8A/;#;3/2$2\x89\"\"6
\x897\x8A/#$+#)(#'#(\"'#&'#.K &%2\x8B\"\"6\x8B7\x8C/;#;4/2$2\x8B\"\"6\x8B7\x8C/#$+#)(#'#(\"'#&'#/' 8!:\x8D!! )=.\" 7\x85"),
          peg$decode("<%$4\x8F\"\"5!7\x90/,#0)*4\x8F\"\"5!7\x90&&&#/' 8!:\x88!! ).\x85 &%%2\x89\"\"6\x897\x8A/;#;3/2$2\x89\"\"6
\x897\x8A/#$+#)(#'#(\"'#&'#.K &%2\x8B\"\"6\x8B7\x8C/;#;4/2$2\x8B\"\"6\x8B7\x8C/#$+#)(#'#(\"'#&'#/' 8!:\x8D!! )=.\" 7\x8E"),
          peg$decode("%$;50#*;5&/' 8!:\x91!! )"),
          peg$decode("%$;60#*;6&/' 8!:\x91!! )"),
          peg$decode("%%<2\x89\"\"6\x897\x8A.) &2\x92\"\"6\x927\x93=.##&&!&'#/6#1\"\"5!7\x94/($8\":\x95\"! )(\"'#&'#.A &%2\x92\"\"6\x927\x93/1#;7/($8\":\x96\"! )(\"'#&'#"),
          peg$decode("%%<2\x8B\"\"6\x8B7\x8C.) &2\x92\"\"6\x927\x93=.##&&!&'#/6#1\"\"5!7\x94/($8\":\x95\"! )(\"'#&'#.A &%2\x92\"\"6\x927\x93/1#;7/($8\":\x96\"! )(\"'#&'#"),
          peg$decode("%4\x97\"\"5!7\x98/' 8!:\x99!! )"),
          peg$decode("<%%2\x9A\"\"6\x9A7\x9B/U#;9/L$2\x9A\"\"6\x9A7\x9B/=$2\x9C\"\"6\x9C7\x9D.# & \x9E.\" &\"/#$+$)($'#(#'#(\"'#&'#/' 8!:\x9F!! )=.\" 7\x8E"),
          peg$decode("%$;:/&#0#*;:&&&#/' 8!:\x91!! )"),
          peg$decode("%%<2\x9A\"\"6\x9A7\x9B.) &2\xA0\"\"6\xA07\xA1=.##&&!&'#/6#1\"\"5!7\x94/($8\":\x95\"! )(\"'#&'#.4 &%2\xA0\"\"6\xA07\xA1/& 8!:\xA2! )"),
          peg$decode("<$;<0#*;<&=.\" 7\xA3"),
          peg$decode("<4\xA4\"\"5!7\xA5=.\" 7\xA3")
        ],

        peg$currPos          = 0,
        peg$savedPos         = 0,
        peg$posDetailsCache  = [{ line: 1, column: 1, seenCR: false }],
        peg$maxFailPos       = 0,
        peg$maxFailExpected  = [],
        peg$silentFails      = 0,

        peg$result;

    if ("startRule" in options) {
      if (!(options.startRule in peg$startRuleIndices)) {
        throw new Error("Can't start parsing from rule \"" + options.startRule + "\".");
      }

      peg$startRuleIndex = peg$startRuleIndices[options.startRule];
    }

    function text() {
      return input.substring(peg$savedPos, peg$currPos);
    }

    function location() {
      return peg$computeLocation(peg$savedPos, peg$currPos);
    }

    function expected(description) {
      throw peg$buildException(
        null,
        [{ type: "other", description: description }],
        input.substring(peg$savedPos, peg$currPos),
        peg$computeLocation(peg$savedPos, peg$currPos)
      );
    }

    function error(message) {
      throw peg$buildException(
        message,
        null,
        input.substring(peg$savedPos, peg$currPos),
        peg$computeLocation(peg$savedPos, peg$currPos)
      );
    }

    function peg$computePosDetails(pos) {
      var details = peg$posDetailsCache[pos],
          p, ch;

      if (details) {
        return details;
      } else {
        p = pos - 1;
        while (!peg$posDetailsCache[p]) {
          p--;
        }

        details = peg$posDetailsCache[p];
        details = {
          line:   details.line,
          column: details.column,
          seenCR: details.seenCR
        };

        while (p < pos) {
          ch = input.charAt(p);
          if (ch === "\n") {
            if (!details.seenCR) { details.line++; }
            details.column = 1;
            details.seenCR = false;
          } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") {
            details.line++;
            details.column = 1;
            details.seenCR = true;
          } else {
            details.column++;
            details.seenCR = false;
          }

          p++;
        }

        peg$posDetailsCache[pos] = details;
        return details;
      }
    }

    function peg$computeLocation(startPos, endPos) {
      var startPosDetails = peg$computePosDetails(startPos),
          endPosDetails   = peg$computePosDetails(endPos);

      return {
        start: {
          offset: startPos,
          line:   startPosDetails.line,
          column: startPosDetails.column
        },
        end: {
          offset: endPos,
          line:   endPosDetails.line,
          column: endPosDetails.column
        }
      };
    }

    function peg$fail(expected) {
      if (peg$currPos < peg$maxFailPos) { return; }

      if (peg$currPos > peg$maxFailPos) {
        peg$maxFailPos = peg$currPos;
        peg$maxFailExpected = [];
      }

      peg$maxFailExpected.push(expected);
    }

    function peg$buildException(message, expected, found, location) {
      function cleanupExpected(expected) {
        var i = 1;

        expected.sort(function(a, b) {
          if (a.description < b.description) {
            return -1;
          } else if (a.description > b.description) {
            return 1;
          } else {
            return 0;
          }
        });

        while (i < expected.length) {
          if (expected[i - 1] === expected[i]) {
            expected.splice(i, 1);
          } else {
            i++;
          }
        }
      }

      function buildMessage(expected, found) {
        function stringEscape(s) {
          function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); }

          return s
            .replace(/\\/g,   '\\\\')
            .replace(/"/g,    '\\"')
            .replace(/\x08/g, '\\b')
            .replace(/\t/g,   '\\t')
            .replace(/\n/g,   '\\n')
            .replace(/\f/g,   '\\f')
            .replace(/\r/g,   '\\r')
            .replace(/[\x00-\x07\x0B\x0E\x0F]/g, function(ch) { return '\\x0' + hex(ch); })
            .replace(/[\x10-\x1F\x80-\xFF]/g,    function(ch) { return '\\x'  + hex(ch); })
            .replace(/[\u0100-\u0FFF]/g,         function(ch) { return '\\u0' + hex(ch); })
            .replace(/[\u1000-\uFFFF]/g,         function(ch) { return '\\u'  + hex(ch); });
        }

        var expectedDescs = new Array(expected.length),
            expectedDesc, foundDesc, i;

        for (i = 0; i < expected.length; i++) {
          expectedDescs[i] = expected[i].description;
        }

        expectedDesc = expected.length > 1
          ? expectedDescs.slice(0, -1).join(", ")
              + " or "
              + expectedDescs[expected.length - 1]
          : expectedDescs[0];

        foundDesc = found ? "\"" + stringEscape(found) + "\"" : "end of input";

        return "Expected " + expectedDesc + " but " + foundDesc + " found.";
      }

      if (expected !== null) {
        cleanupExpected(expected);
      }

      return new peg$SyntaxError(
        message !== null ? message : buildMessage(expected, found),
        expected,
        found,
        location
      );
    }

    function peg$decode(s) {
      var bc = new Array(s.length), i;

      for (i = 0; i < s.length; i++) {
        bc[i] = s.charCodeAt(i) - 32;
      }

      return bc;
    }

    function peg$parseRule(index) {
      var bc    = peg$bytecode[index],
          ip    = 0,
          ips   = [],
          end   = bc.length,
          ends  = [],
          stack = [],
          params, i;

      while (true) {
        while (ip < end) {
          switch (bc[ip]) {
            case 0:
              stack.push(peg$consts[bc[ip + 1]]);
              ip += 2;
              break;

            case 1:
              stack.push(void 0);
              ip++;
              break;

            case 2:
              stack.push(null);
              ip++;
              break;

            case 3:
              stack.push(peg$FAILED);
              ip++;
              break;

            case 4:
              stack.push([]);
              ip++;
              break;

            case 5:
              stack.push(peg$currPos);
              ip++;
              break;

            case 6:
              stack.pop();
              ip++;
              break;

            case 7:
              peg$currPos = stack.pop();
              ip++;
              break;

            case 8:
              stack.length -= bc[ip + 1];
              ip += 2;
              break;

            case 9:
              stack.splice(-2, 1);
              ip++;
              break;

            case 10:
              stack[stack.length - 2].push(stack.pop());
              ip++;
              break;

            case 11:
              stack.push(stack.splice(stack.length - bc[ip + 1], bc[ip + 1]));
              ip += 2;
              break;

            case 12:
              stack.push(input.substring(stack.pop(), peg$currPos));
              ip++;
              break;

            case 13:
              ends.push(end);
              ips.push(ip + 3 + bc[ip + 1] + bc[ip + 2]);

              if (stack[stack.length - 1]) {
                end = ip + 3 + bc[ip + 1];
                ip += 3;
              } else {
                end = ip + 3 + bc[ip + 1] + bc[ip + 2];
                ip += 3 + bc[ip + 1];
              }

              break;

            case 14:
              ends.push(end);
              ips.push(ip + 3 + bc[ip + 1] + bc[ip + 2]);

              if (stack[stack.length - 1] === peg$FAILED) {
                end = ip + 3 + bc[ip + 1];
                ip += 3;
              } else {
                end = ip + 3 + bc[ip + 1] + bc[ip + 2];
                ip += 3 + bc[ip + 1];
              }

              break;

            case 15:
              ends.push(end);
              ips.push(ip + 3 + bc[ip + 1] + bc[ip + 2]);

              if (stack[stack.length - 1] !== peg$FAILED) {
                end = ip + 3 + bc[ip + 1];
                ip += 3;
              } else {
                end = ip + 3 + bc[ip + 1] + bc[ip + 2];
                ip += 3 + bc[ip + 1];
              }

              break;

            case 16:
              if (stack[stack.length - 1] !== peg$FAILED) {
                ends.push(end);
                ips.push(ip);

                end = ip + 2 + bc[ip + 1];
                ip += 2;
              } else {
                ip += 2 + bc[ip + 1];
              }

              break;

            case 17:
              ends.push(end);
              ips.push(ip + 3 + bc[ip + 1] + bc[ip + 2]);

              if (input.length > peg$currPos) {
                end = ip + 3 + bc[ip + 1];
                ip += 3;
              } else {
                end = ip + 3 + bc[ip + 1] + bc[ip + 2];
                ip += 3 + bc[ip + 1];
              }

              break;

            case 18:
              ends.push(end);
              ips.push(ip + 4 + bc[ip + 2] + bc[ip + 3]);

              if (input.substr(peg$currPos, peg$consts[bc[ip + 1]].length) === peg$consts[bc[ip + 1]]) {
                end = ip + 4 + bc[ip + 2];
                ip += 4;
              } else {
                end = ip + 4 + bc[ip + 2] + bc[ip + 3];
                ip += 4 + bc[ip + 2];
              }

              break;

            case 19:
              ends.push(end);
              ips.push(ip + 4 + bc[ip + 2] + bc[ip + 3]);

              if (input.substr(peg$currPos, peg$consts[bc[ip + 1]].length).toLowerCase() === peg$consts[bc[ip + 1]]) {
                end = ip + 4 + bc[ip + 2];
                ip += 4;
              } else {
                end = ip + 4 + bc[ip + 2] + bc[ip + 3];
                ip += 4 + bc[ip + 2];
              }

              break;

            case 20:
              ends.push(end);
              ips.push(ip + 4 + bc[ip + 2] + bc[ip + 3]);

              if (peg$consts[bc[ip + 1]].test(input.charAt(peg$currPos))) {
                end = ip + 4 + bc[ip + 2];
                ip += 4;
              } else {
                end = ip + 4 + bc[ip + 2] + bc[ip + 3];
                ip += 4 + bc[ip + 2];
              }

              break;

            case 21:
              stack.push(input.substr(peg$currPos, bc[ip + 1]));
              peg$currPos += bc[ip + 1];
              ip += 2;
              break;

            case 22:
              stack.push(peg$consts[bc[ip + 1]]);
              peg$currPos += peg$consts[bc[ip + 1]].length;
              ip += 2;
              break;

            case 23:
              stack.push(peg$FAILED);
              if (peg$silentFails === 0) {
                peg$fail(peg$consts[bc[ip + 1]]);
              }
              ip += 2;
              break;

            case 24:
              peg$savedPos = stack[stack.length - 1 - bc[ip + 1]];
              ip += 2;
              break;

            case 25:
              peg$savedPos = peg$currPos;
              ip++;
              break;

            case 26:
              params = bc.slice(ip + 4, ip + 4 + bc[ip + 3]);
              for (i = 0; i < bc[ip + 3]; i++) {
                params[i] = stack[stack.length - 1 - params[i]];
              }

              stack.splice(
                stack.length - bc[ip + 2],
                bc[ip + 2],
                peg$consts[bc[ip + 1]].apply(null, params)
              );

              ip += 4 + bc[ip + 3];
              break;

            case 27:
              stack.push(peg$parseRule(bc[ip + 1]));
              ip += 2;
              break;

            case 28:
              peg$silentFails++;
              ip++;
              break;

            case 29:
              peg$silentFails--;
              ip++;
              break;

            default:
              throw new Error("Invalid opcode: " + bc[ip] + ".");
          }
        }

        if (ends.length > 0) {
          end = ends.pop();
          ip = ips.pop();
        } else {
          break;
        }
      }

      return stack[0];
    }

    peg$result = peg$parseRule(peg$startRuleIndex);

    if (peg$result !== peg$FAILED && peg$currPos === input.length) {
      return peg$result;
    } else {
      if (peg$result !== peg$FAILED && peg$currPos < input.length) {
        peg$fail({ type: "end", description: "end of input" });
      }

      throw peg$buildException(
        null,
        peg$maxFailExpected,
        peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null,
        peg$maxFailPos < input.length
          ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)
          : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)
      );
    }
  }

  return {
    SyntaxError: peg$SyntaxError,
    parse:       peg$parse
  };
})();

  // ffs/wizard module
module.exports = function(presets) {
  var freeFormQuery = {};
  if (!presets) presets = {};

  if (!presets) {
    // browser: try to use jQuery to load presets now
    if (typeof $ === "undefined") return false;
    var presets_file = "data/iD_presets.json";
    try {
      $.ajax(presets_file,{async:false,dataType:"json"}).success(function(data){
        presets = data;
      }).error(function(){
        throw new Error();
      });
    } catch(e) {
      console.error("failed to load presets file", presets_file, e);
    }
  }
  Object.keys(presets).map(function(key) {
    var preset = presets[key];
    preset.nameCased = preset.name;
    preset.name = preset.name.toLowerCase();
    preset.terms = !preset.terms ? [] : preset.terms.map(function(term) {return term.toLowerCase();});
  });
  // load preset translations
  (function loadPresetTranslations() {
    // node: use external preset supplier? // todo
    // browser: use jQuery
    if (typeof $ === "undefined" || typeof i18n === "undefined") return;
    var language = i18n.getLanguage();
    if (language == "en") return;
    var translation_file = "data/iD_presets_"+language+".json";
    try {
      $.ajax(translation_file,{async:false,dataType:"json"}).success(function(data){
        // load translated names and terms into presets object
        Object.keys(data).map(function(preset) {
          var translation = data[preset];
          preset = presets[preset];
          preset.translated = true;
          // save original preset name under alternative terms
          var oriPresetName = preset.name;
          // save translated preset name
          preset.nameCased = translation.name;
          preset.name = translation.name.toLowerCase();
          // add new terms
          if (translation.terms)
            preset.terms = translation.terms.split(",")
              .map(function(term) { return term.trim().toLowerCase(); })
              .concat(preset.terms);
          // add this to the front to allow exact (english) preset names to match before terms
          preset.terms.unshift(oriPresetName);
        });
      }).error(function(){
        throw new Error();
      });
    } catch(e) {
      console.error("failed to load preset translations file: "+translation_file);
    }
  })();

  freeFormQuery.get_query_clause = function(condition) {
    // search presets for ffs term
    var search = condition.free.toLowerCase();
    var candidates = Object.keys(presets).map(function(key) {
      return presets[key];
    }).filter(function(preset) {
      if (preset.searchable===false) return false;
      if (preset.name === search) return true;
      preset._termsIndex = preset.terms.indexOf(search);
      return preset._termsIndex != -1;
    });
    if (candidates.length === 0)
      return false;
    // sort candidates
    candidates.sort(function(a,b) {
      // prefer exact name matches
      if (a.name === search) return -1;
      if (b.name === search) return  1;
      return a._termsIndex - b._termsIndex;
    });
    var preset = candidates[0];
    var types = [];
    preset.geometry.forEach(function(g) {
      switch (g) {
        case "point":
        case "vertex":
          types.push("node");
          break;
        case "line":
          types.push("way");
          break;
        case "area":
          types.push("way");
          types.push("relation"); // todo: additionally add type=multipolygon?
          break;
        case "relation":
          types.push("relation");
          break;
        default:
          console.error("unknown geometry type "+g+" of preset "+preset.name);
      }
    });
    function onlyUnique(value, index, self) {
      return self.indexOf(value) === index;
    }
    return {
      types: types.filter(onlyUnique),
      conditions: Object.keys(preset.tags).map(function(k) {
        var v = preset.tags[k];
        return {
          query: v==="*" ? "key" : "eq",
          key: k,
          val: v
        };
      })
    };
  }

  freeFormQuery.fuzzy_search = function(condition) {
    // search presets for ffs term
    var search = condition.free.toLowerCase();
    // fuzzyness: max lev.dist allowed to still match
    var fuzzyness = 2+Math.floor(search.length/7);
    function fuzzyMatch(term) {
      return levenshteinDistance(term, search) <= fuzzyness;
    }
    var candidates = Object.keys(presets).map(function(key) {
      return presets[key];
    }).filter(function(preset) {
      if (preset.searchable===false) return false;
      if (fuzzyMatch(preset.name)) return true;
      return preset.terms.some(fuzzyMatch);
    });
    if (candidates.length === 0)
      return false;
    // sort candidates
    function preset_weight(preset) {
      return [preset.name].concat(preset.terms).map(function(term, index) {
        return levenshteinDistance(term,search);
      }).reduce(function min(a, b) {
        return a <= b ? a : b;
      });
    };
    candidates.sort(function(a,b) {
      return preset_weight(a) - preset_weight(b);
    });
    var preset = candidates[0];
    return preset.nameCased;
  }


  return freeFormQuery;
};

/*
 * partial implementation of overpass turbo extended query syntax
 * (http://wiki.openstreetmap.org/wiki/Overpass_turbo/Extended_Overpass_Queries)
 */

var Promise = require('promise'),
    request = require('request-promise');

// converts relative time to ISO time string
function relativeTime(instr, callback) {
  var now = Date.now();
  // very basic differential date
  instr = instr.toLowerCase().match(/(-?[0-9]+) ?(seconds?|minutes?|hours?|days?|weeks?|months?|years?)?/);
  if (instr === null) {
    return Promise.reject(new Error('unable to expand date shortcut.'));
  }
  var count = parseInt(instr[1]);
  var interval;
  switch (instr[2]) {
    case "second":
    case "seconds":
    interval=1; break;
    case "minute":
    case "minutes":
    interval=60; break;
    case "hour":
    case "hours":
    interval=3600; break;
    case "day":
    case "days":
    default:
    interval=86400; break;
    case "week":
    case "weeks":
    interval=604800; break;
    case "month":
    case "months":
    interval=2628000; break;
    case "year":
    case "years":
    interval=31536000; break;
  }
  var date = now - count*interval*1000;
  return Promise.resolve((new Date(date)).toISOString());
}

// Promise wrapper for browser XMLHttpRequest
// from http://www.html5rocks.com/en/tutorials/es6/promises/
function get(url) {
  return new Promise(function(resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      if (req.status == 200) {
        resolve(req.response);
      }
      else {
        reject(Error("XMLHttpRequest Error: "+req.statusText));
      }
    };
    req.onerror = function() {
      reject(Error("Network Error"));
    };
    req.send();
  });
}
// helper function to query nominatim for best fitting result
function nominatimRequest(search,filter) {
  var requestUrl = "https://nominatim.openstreetmap.org/search?format=json&q="+encodeURIComponent(search);
  var _request;
  if (typeof XMLHttpRequest !== "undefined") {
    // browser
    _request = get(requestUrl).then(JSON.parse);
  } else {
    // node
    _request = request({
      url: requestUrl,
      method: "GET",
      headers: {"User-Agent": "overpass-wizard"},
      json: true
    });
  }
  return _request.then(function(data) {
    if (filter)
      data = data.filter(filter);
    if (data.length === 0)
      return Promise.reject(new Error("No result found for geocoding search: "+search));
    else
      return data[0];
  });
}

// geocoding shortcuts
function geocodeArea(instr) {
  function filter(n) {
    return n.osm_type && n.osm_id && n.osm_type!=="node";
  }
  return nominatimRequest(instr,filter).then(function(res) {
    var area_ref = 1*res.osm_id;
    if (res.osm_type == "way")
      area_ref += 2400000000;
    if (res.osm_type == "relation")
      area_ref += 3600000000;
    res = "area("+area_ref+")";
    return res;
  });
}
function geocodeCoords(instr) {
  return nominatimRequest(instr).then(function(res) {
    res = res.lat+','+res.lon;
    return res;
  });
}

var expansions = {
  date: relativeTime,
  geocodeArea: geocodeArea,
  geocodeCoords: geocodeCoords
};

module.exports = function(overpassQuery, bbox, callback) {
  // 1. bbox
  if (bbox) overpassQuery = overpassQuery.replace(/{{bbox}}/g, bbox);
  // 2. constants
  var constantRegexp = "{{([a-zA-Z0-9_]+)=(.+?)}}";
  var constants = overpassQuery.match(new RegExp(constantRegexp, 'g')) || [];
  constants.forEach(function(constant) {
    var constantDefinition = constant.match(new RegExp(constantRegexp)),
        constantShortcut = "{{"+constantDefinition[1]+"}}",
        constantValue = constantDefinition[2];
    while (overpassQuery.indexOf(constantShortcut) >= 0) {
      overpassQuery = overpassQuery.replace(constantShortcut, constantValue);
    }
  });
  // 3. shortcuts
  var shortcutRegexp = "{{(date|geocodeArea|geocodeCoords):([\\s\\S]*?)}}";
  var shortcuts = overpassQuery.match(new RegExp(shortcutRegexp, 'g')) || [];
  return Promise.all(shortcuts.map(function(shortcut) {
    shortcut = shortcut.match(new RegExp(shortcutRegexp));
    return expansions[shortcut[1]](shortcut[2]);
  })).then(function(expansions) {
    expansions.forEach(function(expansion, index) {
      overpassQuery = overpassQuery.replace(shortcuts[index], expansion);
    });
    return overpassQuery;
  }).then(function(overpassQuery) {
    callback(undefined, overpassQuery)
  }).catch(function(err) {
    callback(err);
  });
};
