"""This is the core of our couch wrapper.  The CouchBatch class defined here
is what end-users can use to efficiently query and update couchdb.
"""
from couchdbkit.exceptions import ResourceConflict
from couchdbkit.exceptions import ResourceNotFound
from couchdbkit.exceptions import BulkSaveError
from functools             import partial
from pprint                import pprint as pp
import copy
import time

from .exceptions import ActionForbidsDocument
from .exceptions import ActionNeedsDocument
from .exceptions import CreateScheduled
from .exceptions import UpdateScheduled
from .exceptions import OverwriteScheduled
from .exceptions import DeleteScheduled
from .promise    import Promise
from .result     import DbFailure
from .result     import DbValue
from .actions    import ReadAction
from .actions    import CreateAction
from .actions    import OverwriteAction
from .actions    import UpdateAction
from .actions    import DeleteAction
from .view       import ViewResults

class CouchBatch(object):
  """An efficient job batcher for CouchDB.  All the database-access functions
  of this class (.get, .create, .overwrite, .update, and .delete) return a
  promise object without accessing the database.  When that promise object's
  value() method is called, all the accumulated work that the batch has been
  given will ideally be sent to the database in a single operation.  Sometimes
  reads and retries will be necessary, but this still tends to be more
  efficient than doing a ton of reads and writes in the sane ordering that a
  programmer wants to use.

  Since we batch operations for efficiency, actions on the same key may cause
  conflicts.  Rather than coming up with creative and disturbing ways for
  operations on a single key to interact without touching the database, I've
  implemented the more sane optimizations and defined errors for the rest of
  the cases.  The top of this table is the current activity scheduled for the
  key, and the left side is the activity that the user is trying to schedule
  on top of the existing activity.  When these failures occur, it is the
  actual method that raises the exception, rather than a returned promise.
  This should make debugging of complicated batch operations a bit more sane.

  The general reasoning behind these behaviours is the question of whether the
  user could be relying on the failure of the first action as a signal.  If
  the first action can fail, and the second action would mask that failure,
  then the second action is forbidden.

             Current Action
  New Action
             | create        | overwrite     | update        | delete
  -----------+---------------+---------------+---------------+----------------
  create     | Create-       | Overwrite-    | Update-       | DeleteScheduled
             | Scheduled is  | Scheduled is  | Scheduled is  | is immediately
             | immediately   | immediately   | immediately   | raised
             | raised        | raised        | raised        |
             |               |               |               |
  overwrite  | Create-       | New activity  | Update-       | DeleteScheduled
             | Scheduled is  | overrides     | Scheduled is  | is immediately
             | immediately   | existing; both| immediately   | raised
             | raised        | promises tied | raised        |
             |               | to new one    |               |
             |               |               |               |
  update     | Update is     | Update is     | Both updates  | DeleteScheduled
             | applied to new| applied to    | joined into   | is immediately
             | document; both| overwrite doc;| composite;    | raised
             | promises are  | both promises | both promises |
             | dependent on  | dependent on  | tied to update|
             | success of    | update        | success       |
             | creation      | success       |               |
             |               |               |               |
  delete     | Create-       | Scheduled task| Update-       | DeleteScheduled
             | Scheduled is  | becomes delete| Scheduled is  | is immediately
             | immediately   | both promises | immediately   | raised
             | raised        | tied to delete| raised        |
             |               | success       |               |

  this logic is enshrined in _set_action far below...
  """
  def __init__(self, couchkit):
    self.__ck = couchkit
    self.clear_cache()
    self._reset()

  @property
  def ck(self):
    return self.__ck

  def new_batch(self):
    """Create a new batch that shares this batch's couch connection, but
    nothing else.  This may be useful when a function passed to this batch's
    update or create methods needs to do some database work, since using this
    batch from a callback would almost certainly break things.

    @return A new CouchBatch instance, sharing this batch's couch connection
    """
    return CouchBatch(self.__ck)

  def clear_cache(self):
    self.__docCache = {}

  def forget(self, key):
    """Remove a given document from the cache"""
    try:
      del self.__docCache[key]
    except KeyError:
      pass

  def _reset(self):
    """Setup the operations"""
    self._writes   = {}
    self._reads    = {}
    self._stats    = { 'read' : 0, 'write' : 0, 'fromcache' : 0 }

  def do_reads(self):
    """Fulfill all of the promises outstanding on ".get" requests."""
    keys    = list(self._reads)
    results = self.__ck.all_docs(keys=keys, include_docs=True)
    self._stats['read'] += 1

    for row in results:
      key = row['key']
      if row.get('doc'):
        _fulfill(self._reads, key, DbValue(row))
        self.__docCache[key] = row
      elif (row.get('error') == 'not_found') or (
          row.get('value', {}).get('deleted')):
        _fulfill(self._reads, key, DbFailure(ResourceNotFound(row)))
      else:
        raise RuntimeError("Unknown couch error type: %s" % row)

  def get(self, *keys, **kwargs):
    """Get the desired documents from couch.  This will return a dictionary of
    key -> Promise.

    Notice that the promises' values will be the actual couch response
    dictionaries, so the typical keys are 'rev', 'doc', etc.  The data you're
    looking for is probably under the key of 'doc', unless you want the doc's
    revision or something else meta about the document.

    @param keys   The keys for the documents we want to retreive
    @param cached A boolean keyword argument indicating whether we are allowed
                  to return cached values.  Defaults to True.
    """
    keys   = set(keys)
    result = {}

    if set(['cached']).union(kwargs) != set(['cached']):
      unexpected = list(set(kwargs).difference(['cached']))[0]
      raise TypeError(
          "get() got an unexpected keyword argument '%s'" % unexpected)

    if kwargs.get('cached', True):
      for key in keys:
        try:
          cached = self.__docCache[key]
          promise = Promise(key, lambda: None)
          promise._fulfill(DbValue(cached))
          result[key] = promise
          self._stats['fromcache'] += 1
        except KeyError:
          pass

    notcached = keys - set(result)
    for key in notcached:
      if key in self._reads:
        result[key] = self._reads[key].promise
      else:
        promise = Promise(key, self.do_reads)
	result[key] = promise
	action  = ReadAction(key, promise)
	self._reads[key] = action

    return result

  def view(self, vname, **kwargs):
    """Run a view.  It can be useful to run a view through the couch batch
    rather than directly through the couchkit object because when include_docs
    is given, the docs get added to the batch's cache.

    @param vname  The name of the view to query
    @param kwargs CouchDB view arguments
    @return The result of the view
    """
    rows = self.__ck.view(vname, **kwargs)
    self._stats['read'] += 1
    if kwargs.get('include_docs'):
      return ViewResults(rows, self.__docCache)
    else:
      return rows

  def all(self, **kwargs):
    """Return all the docs in couch.  If include_docs is set, all the docs
    returned from this function will be added to this batch's cache.  This is
    pretty much just a special view, so all the kwargs that a view can take,
    this can take as well.

    @param kwargs CouchDB view arguments
    @return The result of the view
    """
    rows = self.__ck.all_docs(**kwargs)
    self._stats['read'] += 1
    if kwargs.get('include_docs'):
      return ViewResults(rows, self.__docCache)
    else:
      return rows

  def do_writes(self, timelimit=5):
    """Run the current batch of write operations.  This will fulfill all the
    promises we have outstanding on create, overwrite, update, and delete
    operations.

    @param timelimit  How many seconds we have to complete; any commit attempts
                      made after timelimit will be marked as failures
    """
    writes       = self._writes
    self._writes = {}
    start        = time.time()
    promises     = [action.promise for action in writes.values()]

    while True:
      if not writes:
        break
      if time.time() > start + timelimit:
        break

      bulk_write  = {}
      needcurrent = []

      for action in writes.values():
        try:
          if action.docid in self.__docCache:
            doc = action.doc(self.__docCache[action.docid])
          else:
            doc = action.doc()
        except ActionNeedsDocument:
          current = self.get(action.docid)[action.docid]
          needcurrent.append( (current, action) )
          continue
        except ActionForbidsDocument:
          action.promise._fulfill(DbFailure(_make_conflict(action.docid)))
          continue
        except Exception, e:
          action.promise._fulfill(DbFailure(e))
          continue

        if doc:
          bulk_write[action.docid] = doc
        else:
          action.promise._fulfill(DbValue(None))
          del writes[doc.docid]

      for current, action in needcurrent:
        try:
          value = current.value()
          doc = action.doc(value)
        except ActionForbidsDocument:
          action.promise._fulfill(DbFailure(_make_conflict(action.docid)))
          continue
        except Exception, e:
          action.promise._fulfill(DbFailure(e))
          continue

        if doc:
          if doc['_id'] != action.docid:
            assert isinstance(action, CreateAction)
            del writes[action.docid]
            if doc['_id'] in writes:
              action.promise._fulfill(DbFailure(_make_conflict(action.docid)))
              continue
            action = CreateAction(doc['_id'], doc,
                action.promise, action.resolver)
            writes[doc['_id']] = action
          bulk_write[action.docid] = doc
        else:
          action.promise._fulfill(DbValue(None))
          del writes[action.docid]

      try:
        self._stats['write'] += 1
        results = self.__ck.bulk_save(bulk_write.values())
      except BulkSaveError, e:
        results = e.results

      retries = []
      for result in results:
        key = result['id']
        action = writes[key]
        if 'error' in result:
          if action.can_retry:
            retries.append(action)