'use strict';

module.exports = {

copy: copy,
checkDataType: checkDataType,
checkDataTypes: checkDataTypes,
coerceToTypes: coerceToTypes,
toHash: toHash,
getProperty: getProperty,
escapeQuotes: escapeQuotes,
equal: require('fast-deep-equal'),
ucs2length: require('./ucs2length'),
varOccurences: varOccurences,
varReplace: varReplace,
cleanUpCode: cleanUpCode,
finalCleanUpCode: finalCleanUpCode,
schemaHasRules: schemaHasRules,
schemaHasRulesExcept: schemaHasRulesExcept,
schemaUnknownRules: schemaUnknownRules,
toQuotedString: toQuotedString,
getPathExpr: getPathExpr,
getPath: getPath,
getData: getData,
unescapeFragment: unescapeFragment,
unescapeJsonPointer: unescapeJsonPointer,
escapeFragment: escapeFragment,
escapeJsonPointer: escapeJsonPointer

};

function copy(o, to) {

to = to || {};
for (var key in o) to[key] = o[key];
return to;

}

function checkDataType(dataType, data, negate) {

var EQUAL = negate ? ' !== ' : ' === '
  , AND = negate ? ' || ' : ' && '
  , OK = negate ? '!' : ''
  , NOT = negate ? '' : '!';
switch (dataType) {
  case 'null': return data + EQUAL + 'null';
  case 'array': return OK + 'Array.isArray(' + data + ')';
  case 'object': return '(' + OK + data + AND +
                        'typeof ' + data + EQUAL + '"object"' + AND +
                        NOT + 'Array.isArray(' + data + '))';
  case 'integer': return '(typeof ' + data + EQUAL + '"number"' + AND +
                         NOT + '(' + data + ' % 1)' +
                         AND + data + EQUAL + data + ')';
  default: return 'typeof ' + data + EQUAL + '"' + dataType + '"';
}

}

function checkDataTypes(dataTypes, data) {

switch (dataTypes.length) {
  case 1: return checkDataType(dataTypes[0], data, true);
  default:
    var code = '';
    var types = toHash(dataTypes);
    if (types.array && types.object) {
      code = types.null ? '(': '(!' + data + ' || ';
      code += 'typeof ' + data + ' !== "object")';
      delete types.null;
      delete types.array;
      delete types.object;
    }
    if (types.number) delete types.integer;
    for (var t in types)
      code += (code ? ' && ' : '' ) + checkDataType(t, data, true);

    return code;
}

}

var COERCE_TO_TYPES = toHash([ 'string', 'number', 'integer', 'boolean', 'null' ]); function coerceToTypes(optionCoerceTypes, dataTypes) {

if (Array.isArray(dataTypes)) {
  var types = [];
  for (var i=0; i<dataTypes.length; i++) {
    var t = dataTypes[i];
    if (COERCE_TO_TYPES[t]) types[types.length] = t;
    else if (optionCoerceTypes === 'array' && t === 'array') types[types.length] = t;
  }
  if (types.length) return types;
} else if (COERCE_TO_TYPES[dataTypes]) {
  return [dataTypes];
} else if (optionCoerceTypes === 'array' && dataTypes === 'array') {
  return ['array'];
}

}

function toHash(arr) {

var hash = {};
for (var i=0; i<arr.length; i++) hash[arr[i]] = true;
return hash;

}

var IDENTIFIER = /^[a-z$_]*$/i; var SINGLE_QUOTE = /'|\/g; function getProperty(key) {

return typeof key == 'number'
        ? '[' + key + ']'
        : IDENTIFIER.test(key)
          ? '.' + key
          : "['" + escapeQuotes(key) + "']";

}

function escapeQuotes(str) {

return str.replace(SINGLE_QUOTE, '\\$&')
          .replace(/\n/g, '\\n')
          .replace(/\r/g, '\\r')
          .replace(/\f/g, '\\f')
          .replace(/\t/g, '\\t');

}

function varOccurences(str, dataVar) {

dataVar += '[^0-9]';
var matches = str.match(new RegExp(dataVar, 'g'));
return matches ? matches.length : 0;

}

function varReplace(str, dataVar, expr) {

dataVar += '([^0-9])';
expr = expr.replace(/\$/g, '$$$$');
return str.replace(new RegExp(dataVar, 'g'), expr + '$1');

}

var EMPTY_ELSE = /elses*{s*}/g

, EMPTY_IF_NO_ELSE = /if\s*\([^)]+\)\s*\{\s*\}(?!\s*else)/g
, EMPTY_IF_WITH_ELSE = /if\s*\(([^)]+)\)\s*\{\s*\}\s*else(?!\s*if)/g;

function cleanUpCode(out) {

return out.replace(EMPTY_ELSE, '')
          .replace(EMPTY_IF_NO_ELSE, '')
          .replace(EMPTY_IF_WITH_ELSE, 'if (!($1))');

}

var ERRORS_REGEXP = /[^v.]errors/g

, REMOVE_ERRORS = /var errors = 0;|var vErrors = null;|validate.errors = vErrors;/g
, REMOVE_ERRORS_ASYNC = /var errors = 0;|var vErrors = null;/g
, RETURN_VALID = 'return errors === 0;'
, RETURN_TRUE = 'validate.errors = null; return true;'
, RETURN_ASYNC = /if \(errors === 0\) return data;\s*else throw new ValidationError\(vErrors\);/
, RETURN_DATA_ASYNC = 'return data;'
, ROOTDATA_REGEXP = /[^A-Za-z_$]rootData[^A-Za-z0-9_$]/g
, REMOVE_ROOTDATA = /if \(rootData === undefined\) rootData = data;/;

function finalCleanUpCode(out, async) {

var matches = out.match(ERRORS_REGEXP);
if (matches && matches.length == 2) {
  out = async
        ? out.replace(REMOVE_ERRORS_ASYNC, '')
             .replace(RETURN_ASYNC, RETURN_DATA_ASYNC)
        : out.replace(REMOVE_ERRORS, '')
             .replace(RETURN_VALID, RETURN_TRUE);
}

matches = out.match(ROOTDATA_REGEXP);
if (!matches || matches.length !== 3) return out;
return out.replace(REMOVE_ROOTDATA, '');

}

function schemaHasRules(schema, rules) {

if (typeof schema == 'boolean') return !schema;
for (var key in schema) if (rules[key]) return true;

}

function schemaHasRulesExcept(schema, rules, exceptKeyword) {

if (typeof schema == 'boolean') return !schema && exceptKeyword != 'not';
for (var key in schema) if (key != exceptKeyword && rules[key]) return true;

}

function schemaUnknownRules(schema, rules) {

if (typeof schema == 'boolean') return;
for (var key in schema) if (!rules[key]) return key;

}

function toQuotedString(str) {

return '\'' + escapeQuotes(str) + '\'';

}

function getPathExpr(currentPath, expr, jsonPointers, isNumber) {

var path = jsonPointers // false by default
            ? '\'/\' + ' + expr + (isNumber ? '' : '.replace(/~/g, \'~0\').replace(/\\//g, \'~1\')')
            : (isNumber ? '\'[\' + ' + expr + ' + \']\'' : '\'[\\\'\' + ' + expr + ' + \'\\\']\'');
return joinPaths(currentPath, path);

}

function getPath(currentPath, prop, jsonPointers) {

var path = jsonPointers // false by default
            ? toQuotedString('/' + escapeJsonPointer(prop))
            : toQuotedString(getProperty(prop));
return joinPaths(currentPath, path);

}

var JSON_POINTER = /^/(?:[^~]|~0|~1)*$/; var RELATIVE_JSON_POINTER = /^([0-9]+)(#|/(?:|~0|~1)*)?$/; function getData($data, lvl, paths) {

var up, jsonPointer, data, matches;
if ($data === '') return 'rootData';
if ($data[0] == '/') {
  if (!JSON_POINTER.test($data)) throw new Error('Invalid JSON-pointer: ' + $data);
  jsonPointer = $data;
  data = 'rootData';
} else {
  matches = $data.match(RELATIVE_JSON_POINTER);
  if (!matches) throw new Error('Invalid JSON-pointer: ' + $data);
  up = +matches[1];
  jsonPointer = matches[2];
  if (jsonPointer == '#') {
    if (up >= lvl) throw new Error('Cannot access property/index ' + up + ' levels up, current level is ' + lvl);
    return paths[lvl - up];
  }

  if (up > lvl) throw new Error('Cannot access data ' + up + ' levels up, current level is ' + lvl);
  data = 'data' + ((lvl - up) || '');
  if (!jsonPointer) return data;
}

var expr = data;
var segments = jsonPointer.split('/');
for (var i=0; i<segments.length; i++) {
  var segment = segments[i];
  if (segment) {
    data += getProperty(unescapeJsonPointer(segment));
    expr += ' && ' + data;
  }
}
return expr;

}

function joinPaths (a, b) {

if (a == '""') return b;
return (a + ' + ' + b).replace(/' \+ '/g, '');

}

function unescapeFragment(str) {

return unescapeJsonPointer(decodeURIComponent(str));

}

function escapeFragment(str) {

return encodeURIComponent(escapeJsonPointer(str));

}

function escapeJsonPointer(str) {

return str.replace(/~/g, '~0').replace(/\//g, '~1');

}

function unescapeJsonPointer(str) {

return str.replace(/~1/g, '/').replace(/~0/g, '~');

}