define([
  'jquery',
  'lodash',
  'moment',
  'chromath'
],
function($, _, moment) {
  'use strict';

  var kbn = {};

  kbn.get_object_fields = function(obj) {
    var field_array = [];
    obj = kbn.flatten_json(obj._source);
    for (var field in obj) {
      field_array.push(field);
    }
    return field_array.sort();
  };

  kbn.get_all_fields = function(data,flat) {
    return _.uniq(_.without(_.reduce(data,function(memo,hit) {
      return flat ? memo.concat(_.keys(kbn.flatten_json(hit._source))) : memo.concat(_.keys(hit._source));
    },[]),'$$hashkey'));
  };

  kbn.has_field = function(obj,field) {
    var obj_fields = kbn.get_object_fields(obj);
    if (_.inArray(obj_fields,field) < 0) {
      return false;
    } else {
      return true;
    }
  };

  kbn.get_related_fields = function(docs,field) {
    var field_array = [];
    _.each(docs, function(doc) {
      var keys = _.keys(doc);
      if(_.contains(keys,field)) {
        field_array = field_array.concat(keys);
      }
    });
    var counts = _.countBy(_.without(field_array,field),function(field){return field;});
    return _.map(counts, function(num, key){return {name:key,count:num};});
  };

  kbn.recurse_field_dots = function(object,field) {
    var value = null;
    var nested;
    if (typeof object[field] !== 'undefined') {
      value = object[field];
    }
    else if (nested = field.match(/(.*?)\.(.*)/)) {
      if(typeof object[nested[1]] !== 'undefined') {
        value = (typeof object[nested[1]][nested[2]] !== 'undefined') ?
          object[nested[1]][nested[2]] : kbn.recurse_field_dots(
            object[nested[1]],nested[2]);
      }
    }

    return value;
  };

  kbn.top_field_values = function(docs,field,count,grouped) {
    var all_values = _.pluck(docs,field),
      groups = {},
      counts,
      hasArrays;
    // manually grouping into pairs allows us to keep the original value,
    _.each(all_values, function (value) {
      var k;
      if(_.isArray(value)) {
        hasArrays =  true;
      }
      if(_.isArray(value) && !grouped) {
        k = value;
      } else {
        k = _.isUndefined(value) ? '' : [value.toString()];
      }
      _.each(k, function(key) {
        if (_.has(groups, key)) {
          groups[key][1] ++;
        } else {
          groups[key] = [(grouped ? value : key), 1];
        }
      });
    });

    counts = _.values(groups).sort(function(a, b) {
      return a[1] - b[1];
    }).reverse().slice(0,count);

    return {
      counts: counts,
      hasArrays : hasArrays
    };
  };

   /**
     * Calculate a graph interval
     *
     * from::           Date object containing the start time
     * to::             Date object containing the finish time
     * size::           Calculate to approximately this many bars
     * user_interval::  User specified histogram interval
     *
     */
  kbn.calculate_interval = function(from,to,size,user_interval) {
    if(_.isObject(from)) {
      from = from.valueOf();
    }
    if(_.isObject(to)) {
      to = to.valueOf();
    }
    return user_interval === 0 ? kbn.round_interval((to - from)/size) : user_interval;
  };

  kbn.round_interval = function(interval) {
    switch (true) {
    // 0.5s
    case (interval <= 500):
      return 100;       // 0.1s
    // 5s
    case (interval <= 5000):
      return 1000;      // 1s
    // 7.5s
    case (interval <= 7500):
      return 5000;      // 5s
    // 15s
    case (interval <= 15000):
      return 10000;     // 10s
    // 45s
    case (interval <= 45000):
      return 30000;     // 30s
    // 3m
    case (interval <= 180000):
      return 60000;     // 1m
    // 9m
    case (interval <= 450000):
      return 300000;    // 5m
    // 20m
    case (interval <= 1200000):
      return 600000;    // 10m
    // 45m
    case (interval <= 2700000):
      return 1800000;   // 30m
    // 2h
    case (interval <= 7200000):
      return 3600000;   // 1h
    // 6h
    case (interval <= 21600000):
      return 10800000;  // 3h
    // 24h
    case (interval <= 86400000):
      return 43200000;  // 12h
    // 48h
    case (interval <= 172800000):
      return 86400000;  // 24h
    // 1w
    case (interval <= 604800000):
      return 86400000;  // 24h
    // 3w
    case (interval <= 1814400000):
      return 604800000; // 1w
    // 2y
    case (interval < 3628800000):
      return 2592000000; // 30d
    default:
      return 31536000000; // 1y
    }
  };

  kbn.secondsToHms = function(seconds){
    var numyears = Math.floor(seconds / 31536000);
    if(numyears){
      return numyears + 'y';
    }
    var numdays = Math.floor((seconds % 31536000) / 86400);
    if(numdays){
      return numdays + 'd';
    }
    var numhours = Math.floor(((seconds % 31536000) % 86400) / 3600);
    if(numhours){
      return numhours + 'h';
    }
    var numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60);
    if(numminutes){
      return numminutes + 'm';
    }
    var numseconds = (((seconds % 31536000) % 86400) % 3600) % 60;
    if(numseconds){
      return numseconds + 's';
    }
    return 'less then a second'; //'just now' //or other string you like;
  };

  kbn.to_percent = function(number,outof) {
    return Math.floor((number/outof)*10000)/100 + "%";
  };

  kbn.addslashes = function(str) {
    str = str.replace(/\\/g, '\\\\');
    str = str.replace(/\'/g, '\\\'');
    str = str.replace(/\"/g, '\\"');
    str = str.replace(/\0/g, '\\0');
    return str;
  };

  kbn.interval_regex = /(\d+(?:\.\d+)?)([Mwdhmsy])/;

  // histogram & trends
  kbn.intervals_in_seconds = {
    y: 31536000,
    M: 2592000,
    w: 604800,
    d: 86400,
    h: 3600,
    m: 60,
    s: 1
  };

  kbn.describe_interval = function (string) {
    var matches = string.match(kbn.interval_regex);
    if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) {
      throw new Error('Invalid interval string, expecting a number followed by one of "Mwdhmsy"');
    } else {
      return {
        sec: kbn.intervals_in_seconds[matches[2]],
        type: matches[2],
        count: parseFloat(matches[1])
      };
    }
  };

  kbn.interval_to_ms = function(string) {
    var info = kbn.describe_interval(string);
    return Math.ceil(info.sec * 1000 * info.count);
  };

  kbn.interval_to_seconds = function (string) {
    var info = kbn.describe_interval(string);
    return Math.ceil(info.sec * info.count);
  };

  // This should go away, moment.js can do this
  kbn.time_ago = function(string) {
    return new Date(new Date().getTime() - (kbn.interval_to_ms(string)));
  };

  /* This is a simplified version of elasticsearch's date parser */
  kbn.parseDate = function(text) {
    if(_.isDate(text)) {
      return text;
    }
    var time,
      mathString = "",
      index,
      parseString;
    if (text.substring(0,3) === "now") {
      time = new Date();
      mathString = text.substring("now".length);
    } else {
      index = text.indexOf("||");
      parseString;
      if (index === -1) {
        parseString = text;
        mathString = ""; // nothing else
      } else {
        parseString = text.substring(0, index);
        mathString = text.substring(index + 2);
      }
      // We're going to just require ISO8601 timestamps, k?
      time = new Date(parseString);
    }

    if (!mathString.length) {
      return time;
    }

    //return [time,parseString,mathString];
    return kbn.parseDateMath(mathString, time);
  };

  kbn.parseDateMath = function(mathString, time, roundUp) {
    var dateTime = moment(time);
    for (var i = 0; i < mathString.length; ) {
      var c = mathString.charAt(i++),
        type,
        num,
        unit;
      if (c === '/') {
        type = 0;
      } else if (c === '+') {
        type = 1;
      } else if (c === '-') {
        type = 2;
      } else {
        return false;
      }

      if (isNaN(mathString.charAt(i))) {
        num = 1;
      } else {
        var numFrom = i;
        while (!isNaN(mathString.charAt(i))) {
          i++;
        }
        num = parseInt(mathString.substring(numFrom, i),10);
      }
      if (type === 0) {
        // rounding is only allowed on whole numbers
        if (num !== 1) {
          return false;
        }
      }
      unit = mathString.charAt(i++);
      switch (unit) {
      case 'y':
        if (type === 0) {
          roundUp ? dateTime.endOf('year') : dateTime.startOf('year');
        } else if (type === 1) {
          dateTime.add('years',num);
        } else if (type === 2) {
          dateTime.subtract('years',num);
        }
        break;
      case 'M':
        if (type === 0) {
          roundUp ? dateTime.endOf('month') : dateTime.startOf('month');
        } else if (type === 1) {
          dateTime.add('months',num);
        } else if (type === 2) {
          dateTime.subtract('months',num);
        }
        break;
      case 'w':
        if (type === 0) {
          roundUp ? dateTime.endOf('week') : dateTime.startOf('week');
        } else if (type === 1) {
          dateTime.add('weeks',num);
        } else if (type === 2) {
          dateTime.subtract('weeks',num);
        }
        break;
      case 'd':
        if (type === 0) {
          roundUp ? dateTime.endOf('day') : dateTime.startOf('day');
        } else if (type === 1) {
          dateTime.add('days',num);
        } else if (type === 2) {
          dateTime.subtract('days',num);
        }
        break;
      case 'h':
      case 'H':
        if (type === 0) {
          roundUp ? dateTime.endOf('hour') : dateTime.startOf('hour');
        } else if (type === 1) {
          dateTime.add('hours',num);
        } else if (type === 2) {
          dateTime.subtract('hours',num);
        }
        break;
      case 'm':
        if (type === 0) {
          roundUp ? dateTime.endOf('minute') : dateTime.startOf('minute');
        } else if (type === 1) {
          dateTime.add('minutes',num);
        } else if (type === 2) {
          dateTime.subtract('minutes',num);
        }
        break;
      case 's':
        if (type === 0) {
          roundUp ? dateTime.endOf('second') : dateTime.startOf('second');
        } else if (type === 1) {
          dateTime.add('seconds',num);
        } else if (type === 2) {
          dateTime.subtract('seconds',num);
        }
        break;
      default:
        return false;
      }
    }
    return dateTime.toDate();
  };

  // LOL. hahahahaha. DIE.
  kbn.flatten_json = function(object,root,array) {
    if (typeof array === 'undefined') {
      array = {};
    }
    if (typeof root === 'undefined') {
      root = '';
    }
    for(var index in object) {
      var obj = object[index];
      var rootname = root.length === 0 ? index : root + '.' + index;
      if(typeof obj === 'object' ) {
        if(_.isArray(obj)) {
          if(obj.length > 0 && typeof obj[0] === 'object') {
            var strval = '';
            for (var objidx = 0, objlen = obj.length; objidx < objlen; objidx++) {
              if (objidx > 0) {
                strval = strval + ', ';
              }

              strval = strval + JSON.stringify(obj[objidx]);
            }
            array[rootname] = strval;
          } else if(obj.length === 1 && _.isNumber(obj[0])) {
            array[rootname] = parseFloat(obj[0]);
          } else {
            array[rootname] = typeof obj === 'undefined' ? null : obj;
          }
        } else {
          kbn.flatten_json(obj,rootname,array);
        }
      } else {
        array[rootname] = typeof obj === 'undefined' ? null : obj;
      }
    }
    return kbn.sortObj(array);
  };

  kbn.xmlEnt = function(value) {
    if(_.isString(value)) {
      var stg1 = value.replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/\r\n/g, '<br/>')
        .replace(/\r/g, '<br/>')
        .replace(/\n/g, '<br/>')
        .replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;')
        .replace(/  /g, '&nbsp;&nbsp;')
        .replace(/&lt;del&gt;/g, '<del>')
        .replace(/&lt;\/del&gt;/g, '</del>');
      return stg1;
    } else {
      return value;
    }
  };

  kbn.sortObj = function(arr) {
    // Setup Arrays
    var sortedKeys = [];
    var sortedObj = {};
    var i;
    // Separate keys and sort them
    for (i in arr) {
      sortedKeys.push(i);
    }
    sortedKeys.sort();

    // Reconstruct sorted obj based on keys
    for (i in sortedKeys) {
      sortedObj[sortedKeys[i]] = arr[sortedKeys[i]];
    }
    return sortedObj;
  };

  kbn.query_color_dot = function (color, diameter) {
    return '<div class="icon-circle" style="' + [
        'display:inline-block',
        'color:' + color,
        'font-size:' + diameter + 'px',
      ].join(';') + '"></div>';
  };

  kbn.colorSteps = function(col,steps) {

    var _d = steps > 5 ? 1.6/steps : 0.25, // distance between steps
      _p = []; // adjustment percentage

    // Create a range of numbers between -0.8 and 0.8
    for(var i = 1; i<steps+1; i+=1) {
      _p.push(i%2 ? ((i-1)*_d*-1)/2 : i*_d/2);
    }

    // Create the color range
    return _.map(_p.sort(function(a,b){return a-b;}),function(v) {
      return v<0 ? Chromath.darken(col,v*-1).toString() : Chromath.lighten(col,v).toString();
    });
  };

  // Find the smallest missing number in an array
  kbn.smallestMissing = function(arr,start,end) {
    start = start || 0;
    end = end || arr.length-1;

    if(start > end) {
      return end + 1;
    }
    if(start !== arr[start]) {
      return start;
    }
    var middle = Math.floor((start + end) / 2);

    if (arr[middle] > middle) {
      return kbn.smallestMissing(arr, start, middle);
    } else {
      return kbn.smallestMissing(arr, middle + 1, end);
    }
  };

  kbn.byteFormat = function (size, decimals, min_resolution) {
    var ext, steps = 0;

    if (_.isUndefined(decimals)) {
      decimals = 2;
    }

    if (_.isUndefined(min_resolution)) {
      min_resolution = 0;
    }


    while (Math.abs(size) >= 1024) {
      steps++;
      size /= 1024;
      min_resolution /= 1024;
    }

    switch (steps) {
    case 0:
      ext = " B";
      break;
    case 1:
      ext = " KB";
      break;
    case 2:
      ext = " MB";
      break;
    case 3:
      ext = " GB";
      break;
    case 4:
      ext = " TB";
      break;
    case 5:
      ext = " PB";
      break;
    case 6:
      ext = " EB";
      break;
    case 7:
      ext = " ZB";
      break;
    case 8:
      ext = " YB";
      break;
    }

    if (min_resolution) {
      min_resolution *= Math.pow(10, decimals);
      while (min_resolution % 1 !== 0) {
        decimals++;
        min_resolution *= 10;
      }
    }

    if (decimals === 0) {
      decimals = undefined;
    }

    return (size.toFixed(decimals) + ext);
  };

  kbn.shortFormat = function (size, decimals, min_resolution) {
    var ext, steps = 0;

    if (_.isUndefined(decimals)) {
      decimals = 2;
    }
    if (_.isUndefined(min_resolution)) {
      min_resolution = 0;
    }

    while (Math.abs(size) >= 1000) {
      steps++;
      size /= 1000;
      min_resolution /= 1000;
    }

    switch (steps) {
    case 0:
      ext = "";
      break;
    case 1:
      ext = " K";
      break;
    case 2:
      ext = " Mil";
      break;
    case 3:
      ext = " Bil";
      break;
    case 4:
      ext = " Tri";
      break;
    case 5:
      ext = " Quadr";
      break;
    case 6:
      ext = " Quint";
      break;
    case 7:
      ext = " Sext";
      break;
    case 8:
      ext = " Sept";
      break;
    }

    if (min_resolution) {
      min_resolution *= Math.pow(10, decimals);
      while (min_resolution % 1 !== 0) {
        decimals++;
        min_resolution *= 10;
      }
    }

    if (decimals === 0) {
      decimals = undefined;
    }

    return (size.toFixed(decimals) + ext);
  };

  return kbn;
});
