#!/usr/bin/python2.5
#
# Copyright 2009 Roman Nurik
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Defines utility functions used throughout the geocell/GeoModel library."""

__author__ = 'api.roman.public@gmail.com (Roman Nurik)'

import geocell
import geomath
import geotypes

def merge_in_place(*lists, **kwargs):
  """Merges an arbitrary number of pre-sorted lists in-place, into the first
  list, possibly pruning out duplicates.
  
  Args:
    list1: The first, sorted list into which the other lists should be merged.
    list2: A subsequent, sorted list to merge into the first.
    ...
    listn:  "   "
    cmp_fn: An optional binary comparison function that compares objects across
        lists and determines the merged list's sort order.
    dup_fn: An optional binary comparison function that should return True if
        the given objects are equivalent and one of them can be pruned from the
        resulting merged list.
  
  Returns:
    list1, in-placed merged wit the other lists, or an empty list if no lists
    were specified.
  """
  cmp_fn = kwargs.get('cmp_fn') or cmp
  dup_fn = kwargs.get('dup_fn') or None
  
  if not lists:
    return []
  
  reverse_indices = [len(arr) for arr in lists]
  aggregate_reverse_index = sum(reverse_indices)
  
  while aggregate_reverse_index > 0:
    pull_arr_index = None
    pull_val = None
    
    for i in range(len(lists)):
      if reverse_indices[i] == 0:
        # Reached the end of this list.
        continue
      
      if (pull_arr_index is not None and
          dup_fn and dup_fn(lists[i][-reverse_indices[i]], pull_val)):
        # Found a duplicate, advance the index of the list in which the
        # duplicate was found.
        reverse_indices[i] -= 1
        aggregate_reverse_index -= 1
        continue
      
      if (pull_arr_index is None or
          cmp_fn(lists[i][-reverse_indices[i]], pull_val) < 0):
        # Found a lower value.
        pull_arr_index = i
        pull_val = lists[i][-reverse_indices[i]]
    
    if pull_arr_index != 0:
      # Add the lowest found value in place into the first array.
      lists[0].insert(len(lists[0]) - reverse_indices[0], pull_val)
    
    aggregate_reverse_index -= 1
    reverse_indices[pull_arr_index] -= 1
  
  return lists[0]


def distance_sorted_edges(cells, point):
  """Returns the edges of the rectangular region containing all of the
  given geocells, sorted by distance from the given point, along with
  the actual distances from the point to these edges.
  
  Args:
    cells: The cells (should be adjacent) defining the rectangular region
        whose edge distances are requested.
    point: The point that should determine the edge sort order.
  
  Returns:
    A list of (direction, distance) tuples, where direction is the edge
    and distance is the distance from the point to that edge. A direction
    value of (0,-1), for example, corresponds to the South edge of the
    rectangular region containing all of the given geocells.
  """
  # TODO(romannurik): Assert that lat,lon are actually inside the geocell.
  boxes = [geocell.compute_box(cell) for cell in cells]
  
  max_box = geotypes.Box(max([box.north for box in boxes]),
                         max([box.east for box in boxes]),
                         min([box.south for box in boxes]),
                         min([box.west for box in boxes]))
  return zip(*sorted([
      ((0,-1), geomath.distance(geotypes.Point(max_box.south, point.lon),
                                point)),
      ((0,1),  geomath.distance(geotypes.Point(max_box.north, point.lon),
                                point)),
      ((-1,0), geomath.distance(geotypes.Point(point.lat, max_box.west),
                                point)),
      ((1,0),  geomath.distance(geotypes.Point(point.lat, max_box.east),
                                point))],
      lambda x, y: cmp(x[1], y[1])))