var collation = require('./collation')

//
// base type system
//
var base = {}

//
// helper utilities
//

function _valueOf(instance) {
  return instance == null ? instance : instance.valueOf()
}

var _toString = Object.prototype.toString

function _isObject(instance) {
  return instance && _toString.call(instance) === '[object Object]'
}

//
// base typewise compare implementation
//
base.compare = function (a, b) {
  //
  // test for invalid values
  //
  if (base.invalid(a, b))
    return NaN

  //
  // short circuit for identical objects
  //
  if (a === b)
    return 0

  //
  // short circuit for base bound types
  //
  var result = base.bound.compare(a, b)
  if (result !== undefined)
    return result

  //
  // cache typeof and valueOf for both values
  //
  var aTypeOf = typeof a
  var bTypeOf = typeof b
  var aValueOf = _valueOf(a)
  var bValueOf = _valueOf(b)

  //
  // loop over type tags and attempt compare
  //
  var order = base.order
  var sorts = base.sorts
  var sort
  for (var i = 0, length = order.length; i < length; ++i) {
    sort = sorts[order[i]]

    //
    // if first arg is a member of this sort we have an answer
    //
    if (sort.is(a, aTypeOf))
      //
      // if b is the same as a then defer to sort's comparator, else a comes first
      //
      return sort.is(b, bTypeOf) ? sort.compare(aValueOf, bValueOf) : -1

    //
    // if b is this type but not a then b comes first
    //
    if (sort.is(b, bTypeOf))
      return 1
  }

  //
  // values are incomparable as they didn't match against any registered types
  //
  return NaN
}

//
// sort equality test
//
base.equal = function(a, b) {
  return base.compare(a, b) === 0
}

//
// test for top-level incomparability using invalid sort definitions
//
base.invalid = function (a, b) {
  var types = base.invalid
  for (var key in types) {
    var type = types[key]
    if (type && type.is && (type.is(a) || type.is(b)))
      return true
  }
  return false
}

//
// definitions for explicitly invalid/incomparable types
//

base.invalid.NAN = {
  is: function (instance) {
    var valueOf = _valueOf(instance)
    return valueOf !== valueOf
  }
}

base.invalid.ERROR = {
  is: function (instance) {
    return instance && instance instanceof Error
  }
}

//
// definitions for boundary types, unserializable as values
//

function BoundedKey(bound, upper, prefix) {
  this.bound = bound
  this.upper = !!upper
  this.prefix = prefix
}

function Boundary(sort) {
  this.sort = sort
}

Boundary.prototype.lower = function (prefix) {
  return new BoundedKey(this, false, prefix)
}

Boundary.prototype.upper = function (prefix) {
  return new BoundedKey(this, true, prefix)
}

Boundary.prototype.is = function (source) {
  return source instanceof BoundedKey && source.sort === this.sort
}

Boundary.add = function (sort) {
  sort.bound = new Boundary(sort)
}

Boundary.add(base)

base.bound.getBoundary = function (source) {
  return source instanceof BoundedKey && source.bound
}

//
// compare a values against top level bounds (assumes first arg is an instance)
//
base.bound.compare = function (a, b) {
  var aBound = base.bound.is(a)
  var bBound = base.bound.is(b)
  if (aBound) {
    if (bBound && !a.upper === !b.upper)
      return 0
    return a.upper ? 1 : -1
  }

  if (bBound)
    return -base.bound.compare(b, a)
}

//
// helper to register fixed (nullary) types
//
function fixed(value) {
  return {
    is: function (instance) {
      return instance === value
    },
    value: value
  }
}

//
// value types defined as ordered map of "sorts"
//
var sorts = base.sorts = {}

sorts.void = fixed(void 0)
sorts.void.compare = collation.inequality

sorts.null = fixed(null)
sorts.null.compare = collation.inequality

var BOOLEAN = sorts.boolean = {}
BOOLEAN.compare = collation.inequality
BOOLEAN.is = function (instance, typeOf) {
  return (typeOf || typeof instance) === 'boolean'
}

BOOLEAN.sorts = {}
BOOLEAN.sorts.true = fixed(true)
BOOLEAN.sorts.false = fixed(false)

Boundary.add(BOOLEAN)


var NUMBER = sorts.number = {}
NUMBER.compare = collation.difference
NUMBER.is = function (instance, typeOf) {
  return (typeOf || typeof instance) === 'number'
}

NUMBER.sorts = {}
NUMBER.sorts.max = fixed(Number.POSITIVE_INFINITY)
NUMBER.sorts.min = fixed(Number.NEGATIVE_INFINITY)

NUMBER.sorts.positive = {}
NUMBER.sorts.positive.is = function (instance) {
  return instance >= 0
}

NUMBER.sorts.negative = {}
NUMBER.sorts.negative.is = function (instance) {
  return instance < 0
}

Boundary.add(NUMBER)


var DATE = sorts.date = {}
DATE.compare = collation.difference
DATE.is = function (instance) {
  return instance instanceof Date && instance.valueOf() === instance.valueOf()
}

DATE.sorts = {}
DATE.sorts.positive = {}
DATE.sorts.positive.is = function (instance) {
  return instance.valueOf() >= 0
}

DATE.sorts.negative = {}
DATE.sorts.negative.is = function (instance) {
  return instance.valueOf() < 0
}

Boundary.add(DATE)


var BINARY = sorts.binary = {}
BINARY.empty = new Buffer([])
BINARY.compare = collation.bitwise
BINARY.is = Buffer.isBuffer

Boundary.add(BINARY)


var STRING = sorts.string = {}
STRING.empty = ''
STRING.compare = collation.inequality
STRING.is = function (instance, typeOf) {
  return (typeOf || typeof instance) === 'string'
}

Boundary.add(STRING)


var ARRAY = sorts.array = {}
ARRAY.empty = []
ARRAY.compare = collation.recursive.elementwise(base.compare)
ARRAY.is = Array.isArray

Boundary.add(ARRAY)


// var OBJECT = sorts.object = {}
// OBJECT.empty = {}
// OBJECT.compare = collation.recursive.fieldwise(base.compare)
// OBJECT.is = _isObject

// Boundary.add(OBJECT)

//
// default order for instance checking in compare operations
//
base.order = []
for (var key in sorts) {
  base.order.push(key)
}

module.exports = base
