# Utilities for db handling
_ = require 'lodash'

compileDocumentSelector = require('./selector').compileDocumentSelector
compileSort = require('./selector').compileSort

# Compile a document selector (query) to a lambda function
exports.compileDocumentSelector = compileDocumentSelector

# Processes a find with sorting and filtering and limiting
exports.processFind = (items, selector, options) ->
  filtered = _.filter(_.values(items), compileDocumentSelector(selector))

  # Handle geospatial operators
  filtered = processNearOperator(selector, filtered)
  filtered = processGeoIntersectsOperator(selector, filtered)

  if options and options.sort
    filtered.sort(compileSort(options.sort))

  if options and options.skip
    filtered = _.drop filtered, options.skip

  if options and options.limit
    filtered = _.take filtered, options.limit

  # Clone to prevent accidental updates, or apply fields if present
  if options and options.fields
    filtered = exports.filterFields(filtered, options.fields)

  return filtered

exports.filterFields = (items, fields={}) ->
  # Handle trivial case
  if _.keys(fields).length == 0
    return items

  # For each item
  return _.map items, (item) ->
    newItem = {}

    if _.first(_.values(fields)) == 1
      # Include fields
      for field in _.keys(fields).concat(["_id"])
        path = field.split(".")

        # Determine if path exists
        obj = item
        for pathElem in path
          if obj
            obj = obj[pathElem]

        if not obj?
          continue

        # Go into path, creating as necessary
        from = item
        to = newItem
        for pathElem in _.initial(path)
          to[pathElem] = to[pathElem] or {}

          # Move inside
          to = to[pathElem]
          from = from[pathElem]

        # Copy value
        to[_.last(path)] = from[_.last(path)]

      return newItem
    else
      # Exclude fields
      for field in _.keys(fields).concat(["_id"])
        path = field.split(".")

        # Go inside path
        obj = item
        for pathElem in _.initial(path)
          if obj
            obj = obj[pathElem]

        # If not there, don't exclude
        if not obj?
          continue

        delete obj[_.last(path)]

      return item


# Creates a unique identifier string
exports.createUid = ->
  'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, (c) ->
    r = Math.random()*16|0
    v = if c == 'x' then r else (r&0x3|0x8)
    return v.toString(16)
   )

processNearOperator = (selector, list) ->
  for key, value of selector
    if value? and value['$near']
      geo = value['$near']['$geometry']
      if geo.type != 'Point'
        break

      list = _.filter list, (doc) ->
        return doc[key] and doc[key].type == 'Point'

      # Get distances
      distances = _.map list, (doc) ->
        return { doc: doc, distance: getDistanceFromLatLngInM(
            geo.coordinates[1], geo.coordinates[0],
            doc[key].coordinates[1], doc[key].coordinates[0])
        }

      # Filter non-points
      distances = _.filter distances, (item) -> item.distance >= 0

      # Sort by distance
      distances = _.sortBy distances, 'distance'

      # Filter by maxDistance
      if value['$near']['$maxDistance']
        distances = _.filter distances, (item) -> item.distance <= value['$near']['$maxDistance']

      # Limit to 100
      distances = _.take distances, 100

      # Extract docs
      list = _.map distances, 'doc'
  return list

# Very simple polygon check. Assumes that is a square
pointInPolygon = (point, polygon) ->
  # Check that first == last
  if not _.isEqual(_.first(polygon.coordinates[0]), _.last(polygon.coordinates[0]))
    throw new Error("First must equal last")

  # Check bounds
  if point.coordinates[0] < Math.min.apply(this,
      _.map(polygon.coordinates[0], (coord) -> coord[0]))
    return false
  if point.coordinates[1] < Math.min.apply(this,
      _.map(polygon.coordinates[0], (coord) -> coord[1]))
    return false
  if point.coordinates[0] > Math.max.apply(this,
      _.map(polygon.coordinates[0], (coord) -> coord[0]))
    return false
  if point.coordinates[1] > Math.max.apply(this,
      _.map(polygon.coordinates[0], (coord) -> coord[1]))
    return false
  return true

# From http://www.movable-type.co.uk/scripts/latlong.html
getDistanceFromLatLngInM = (lat1, lng1, lat2, lng2) ->
  R = 6370986 # Radius of the earth in m
  dLat = deg2rad(lat2 - lat1) # deg2rad below
  dLng = deg2rad(lng2 - lng1)
  a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2)
  c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
  d = R * c # Distance in m
  return d

deg2rad = (deg) ->
  deg * (Math.PI / 180)

processGeoIntersectsOperator = (selector, list) ->
  for key, value of selector
    if value? and value['$geoIntersects']
      geo = value['$geoIntersects']['$geometry']
      if geo.type != 'Polygon'
        break

      # Check within for each
      list = _.filter list, (doc) ->
        # Reject non-points
        if not doc[key] or doc[key].type != 'Point'
          return false

        # Check polygon
        return pointInPolygon(doc[key], geo)

  return list

# Tidy up upsert parameters to always be a list of { doc: <doc>, base: <base> },
# doing basic error checking and making sure that _id is present
# Returns [items, success, error]
exports.regularizeUpsert = (docs, bases, success, error) ->
  # Handle case of bases not present
  if _.isFunction(bases)
    [bases, success, error] = [undefined, bases, success]

  # Handle single upsert
  if not _.isArray(docs)
    docs = [docs]
    bases = [bases]
  else
    bases = bases or []

  # Make into list of { doc: .., base: }
  items = _.map(docs, (doc, i) -> { doc: doc, base: if i < bases.length then bases[i] else undefined})

  # Set _id
  for item in items
    if not item.doc._id?
      throw new Error('All documents in the upsert must have an _id')

  return [items, success, error]
