#!/usr/bin/python
#
# Copyright (C) 2009 Google Inc.
#
# 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.

"""Support for operations that can be applied to the server.

Contains classes and utilities for creating operations that are to be
applied on the server.
"""

import errors
import random
import util
import sys


PROTOCOL_VERSION = '0.22'

# Operation Types
WAVELET_APPEND_BLIP = 'wavelet.appendBlip'
WAVELET_SET_TITLE = 'wavelet.setTitle'
WAVELET_ADD_PARTICIPANT = 'wavelet.participant.add'
WAVELET_DATADOC_SET = 'wavelet.datadoc.set'
WAVELET_MODIFY_TAG = 'wavelet.modifyTag'
WAVELET_MODIFY_PARTICIPANT_ROLE = 'wavelet.modifyParticipantRole'
BLIP_CONTINUE_THREAD = 'blip.continueThread'
BLIP_CREATE_CHILD = 'blip.createChild'
BLIP_DELETE = 'blip.delete'
DOCUMENT_APPEND_MARKUP = 'document.appendMarkup'
DOCUMENT_INLINE_BLIP_INSERT = 'document.inlineBlip.insert'
DOCUMENT_MODIFY = 'document.modify'
ROBOT_CREATE_WAVELET = 'robot.createWavelet'
ROBOT_FETCH_WAVE = 'robot.fetchWave'
ROBOT_NOTIFY = 'robot.notifyCapabilitiesHash'
ROBOT_SEARCH = 'robot.search'

# Assign always NOTIFY_OP_ID to the notify operation so
# we can easily filter it out later
NOTIFY_OP_ID = '0'

class Operation(object):
  """Represents a generic operation applied on the server.

  This operation class contains data that is filled in depending on the
  operation type.

  It can be used directly, but doing so will not result
  in local, transient reflection of state on the blips. In other words,
  creating a 'delete blip' operation will not remove the blip from the local
  context for the duration of this session. It is better to use the OpBased
  model classes directly instead.
  """

  def __init__(self, method, opid, params):
    """Initializes this operation with contextual data.

    Args:
      method: Method to call or type of operation.
      opid: The id of the operation. Any callbacks will refer to these.
      params: An operation type dependent dictionary
    """
    self.method = method
    self.id = opid
    self.params = params

  def __str__(self):
    return '%s[%s]%s' % (self.method, self.id, str(self.params))

  def set_param(self, param, value):
    self.params[param] = value
    return self

  def serialize(self, method_prefix=''):
    """Serialize the operation.

    Args:
      method_prefix: prefixed for each method name to allow for specifying
      a namespace.

    Returns:
      a dict representation of the operation.
    """
    if method_prefix and not method_prefix.endswith('.'):
      method_prefix += '.'
    return {'method': method_prefix + self.method,
            'id': self.id,
            'params': util.serialize(self.params)}

  def set_optional(self, param, value):
    """Sets an optional parameter.

    If value is None or "", this is a no op. Otherwise it calls
    set_param.
    """
    if value == '' or value is None:
      return self
    else:
      return self.set_param(param, value)


class OperationQueue(object):
  """Wraps the queuing of operations using easily callable functions.

  The operation queue wraps single operations as functions and queues the
  resulting operations in-order. Typically there shouldn't be a need to
  call this directly unless operations are needed on entities outside
  of the scope of the robot. For example, to modify a blip that
  does not exist in the current context, you might specify the wave, wavelet
  and blip id to generate an operation.

  Any calls to this will not be reflected in the robot in any way.
  For example, calling wavelet_append_blip will not result in a new blip
  being added to the robot, only an operation to be applied on the
  server.
  """

  # Some class global counters:
  _next_operation_id = 1

  def __init__(self, proxy_for_id=None):
    self.__pending = []
    self._capability_hash = None
    self._proxy_for_id = proxy_for_id

  def _new_blipdata(self, wave_id, wavelet_id, initial_content='',
                    parent_blip_id=None):
    """Creates JSON of the blip used for this session."""
    temp_blip_id = 'TBD_%s_%s' % (wavelet_id,
                                  hex(random.randint(0, sys.maxint)))
    return {'waveId': wave_id,
            'waveletId': wavelet_id,
            'blipId': temp_blip_id,
            'content': initial_content,
            'parentBlipId': parent_blip_id}

  def _new_waveletdata(self, domain, participants):
    """Creates an ephemeral WaveletData instance used for this session.

    Args:
      domain: the domain to create the data for.
      participants initially on the wavelet
    Returns:
      Blipdata (for the rootblip), WaveletData.
    """
    wave_id = domain + '!TBD_%s' % hex(random.randint(0, sys.maxint))
    wavelet_id = domain + '!conv+root'
    root_blip_data = self._new_blipdata(wave_id, wavelet_id)
    participants = set(participants)
    wavelet_data = {'waveId': wave_id,
                    'waveletId': wavelet_id,
                    'rootBlipId': root_blip_data['blipId'],
                    'participants': participants}
    return root_blip_data, wavelet_data

  def __len__(self):
    return len(self.__pending)

  def __iter__(self):
    return self.__pending.__iter__()

  def clear(self):
    self.__pending = []

  def proxy_for(self, proxy):
    """Return a view of this operation queue with the proxying for set to proxy.

    This method returns a new instance of an operation queue that shares the
    operation list, but has a different proxying_for_id set so the robot using
    this new queue will send out operations with the proxying_for field set.
    """
    res = OperationQueue()
    res.__pending = self.__pending
    res._capability_hash = self._capability_hash
    res._proxy_for_id = proxy
    return res

  def set_capability_hash(self, capability_hash):
    self._capability_hash = capability_hash

  def serialize(self, method_prefix=''):
    first = Operation(ROBOT_NOTIFY,
                      NOTIFY_OP_ID,
                      {'capabilitiesHash': self._capability_hash,
                       'protocolVersion': PROTOCOL_VERSION})
    operations = [first] + self.__pending
    return [op.serialize(method_prefix=method_prefix) for op in operations]
    res = util.serialize(operations)
    return res

  def copy_operations(self, other_queue):
    """Copy the pending operations from other_queue into this one."""
    for op in other_queue:
      self.__pending.append(op)

  def new_operation(self, method, wave_id, wavelet_id, props=None, **kwprops):
    """Creates and adds a new operation to the operation list."""
    if props is None:
      props = {}
    props.update(kwprops)
    if not wave_id is None:
      props['waveId'] = wave_id
    if not wavelet_id is None:
      props['waveletId'] = wavelet_id
    if self._proxy_for_id:
      props['proxyingFor'] = self._proxy_for_id
    operation = Operation(method,
                          'op%s' % OperationQueue._next_operation_id,
                          props)
    self.__pending.append(operation)
    OperationQueue._next_operation_id += 1
    return operation

  def wavelet_append_blip(self, wave_id, wavelet_id, initial_content=''):
    """Appends a blip to a wavelet.

    Args:
      wave_id: The wave id owning the containing wavelet.
      wavelet_id: The wavelet id that this blip should be appended to.
      initial_content: optionally the content to start with

    Returns:
      JSON representing the information of the new blip.
    """
    blip_data = self._new_blipdata(wave_id, wavelet_id, initial_content)
    self.new_operation(WAVELET_APPEND_BLIP, wave_id,
                       wavelet_id, blipData=blip_data)
    return blip_data

  def wavelet_add_participant(self, wave_id, wavelet_id, participant_id):
    """Adds a participant to a wavelet.

    Args:
      wave_id: The wave id owning that this operation is applied to.
      wavelet_id: The wavelet id that this operation is applied to.
      participant_id: Id of the participant to add.

    Returns:
      data for the root_blip, wavelet
    """
    return self.new_operation(WAVELET_ADD_PARTICIPANT, wave_id, wavelet_id,
                              participantId=participant_id)

  def wavelet_datadoc_set(self, wave_id, wavelet_id, name, data):
    """Sets a key/value pair on the data document of a wavelet.

    Args:
      wave_id: The wave id owning that this operation is applied to.
      wavelet_id: The wavelet id that this operation is applied to.
      name: The key name for this data.
      data: The value of the data to set.
    Returns:
      The operation created.
    """
    return self.new_operation(WAVELET_DATADOC_SET, wave_id, wavelet_id,
                              datadocName=name, datadocValue=data)

  def robot_create_wavelet(self, domain, participants=None, message=''):
    """Creates a new wavelet.

    Args:
      domain: the domain to create the wave in
      participants: initial participants on this wavelet or None if none
      message: an optional payload that is returned with the corresponding
          event.

    Returns:
      data for the root_blip, wavelet
    """
    if participants is None:
      participants = []
    blip_data, wavelet_data = self._new_waveletdata(domain, participants)
    op = self.new_operation(ROBOT_CREATE_WAVELET,
                            wave_id=wavelet_data['waveId'],
                            wavelet_id=wavelet_data['waveletId'],
                            waveletData=wavelet_data)
    op.set_optional('message', message)
    return blip_data, wavelet_data

  def robot_search(self, query, index=None, num_results=None):
    """Execute a search request.

    For now this only makes sense in the data API. Wave does not maintain
    an index for robots so no results will be returned in that scenario.

    Args:
      query: what to search for
      index: what index to search from
      num_results: how many results to return
    Returns:
      The operation created.
    """
    op = self.new_operation(
        ROBOT_SEARCH, wave_id=None, wavelet_id=None, query=query)
    if not index is None:
      op.set_param('index', index)
    if not num_results is None:
      op.set_param('numResults', num_results)
    return op

  def robot_fetch_wave(self, wave_id, wavelet_id):
    """Requests a snapshot of the specified wave.

    Args:
      wave_id: The wave id owning that this operation is applied to.
      wavelet_id: The wavelet id that this operation is applied to.
    Returns:
      The operation created.
    """
    return self.new_operation(ROBOT_FETCH_WAVE, wave_id, wavelet_id)

  def wavelet_set_title(self, wave_id, wavelet_id, title):
    """Sets the title of a wavelet.

    Args:
      wave_id: The wave id owning that this operation is applied to.
      wavelet_id: The wavelet id that this operation is applied to.
      title: The title to set.
    Returns:
      The operation created.
    """
    return self.new_operation(WAVELET_SET_TITLE, wave_id, wavelet_id,
                              waveletTitle=title)

  def wavelet_modify_participant_role(
      self, wave_id, wavelet_id, participant_id, role):
    """Modify the role of a participant on a wavelet.

    Args:
      wave_id: The wave id owning that this operation is applied to.
      wavelet_id: The wavelet id that this operation is applied to.
      participant_id: Id of the participant to add.
      role: the new roles

    Returns:
      data for the root_blip, wavelet
    """
    return self.new_operation(WAVELET_MODIFY_PARTICIPANT_ROLE, wave_id,
                              wavelet_id, participantId=participant_id,
                              participantRole=role)

  def wavelet_modify_tag(self, wave_id, wavelet_id, tag, modify_how=None):
    """Modifies a tag in a wavelet.

    Args:
      wave_id: The wave id owning that this operation is applied to.
      wavelet_id: The wavelet id that this operation is applied to.
      tag: The tag (a string).
      modify_how: (optional) how to apply the tag. The default is to add
        the tag. Specify 'remove' to remove. Specify None or 'add' to
        add.
    Returns:
      The operation created.
    """
    return self.new_operation(WAVELET_MODIFY_TAG, wave_id, wavelet_id,
                             name=tag).set_optional("modify_how", modify_how)

  def blip_create_child(self, wave_id, wavelet_id, blip_id):
    """Creates a child blip of another blip.

    Args:
      wave_id: The wave id owning that this operation is applied to.
      wavelet_id: The wavelet id that this operation is applied to.
      blip_id: The blip id that this operation is applied to.

    Returns:
      JSON of blip for which further operations can be applied.
    """
    blip_data = self._new_blipdata(wave_id, wavelet_id, parent_blip_id=blip_id)
    self.new_operation(BLIP_CREATE_CHILD, wave_id, wavelet_id,
                       blipId=blip_id,
                       blipData=blip_data)
    return blip_data

  def blip_continue_thread(self, wave_id, wavelet_id, blip_id):
    """Creates a blip in same thread as specified blip.

    Args:
      wave_id: The wave id owning that this operation is applied to.
      wavelet_id: The wavelet id that this operation is applied to.
      blip_id: The blip id that this operation is applied to.

    Returns:
      JSON of blip for which further operations can be applied.
    """
    blip_data = self._new_blipdata(wave_id, wavelet_id)
    self.new_operation(BLIP_CONTINUE_THREAD, wave_id, wavelet_id,
                       blipId=blip_id,
                       blipData=blip_data)
    return blip_data


  def blip_delete(self, wave_id, wavelet_id, blip_id):
    """Deletes the specified blip.

    Args:
      wave_id: The wave id owning that this operation is applied to.
      wavelet_id: The wavelet id that this operation is applied to.
      blip_id: The blip id that this operation is applied to.
    Returns:
      The operation created.
    """
    return self.new_operation(BLIP_DELETE, wave_id, wavelet_id, blipId=blip_id)

  def document_append_markup(self, wave_id, wavelet_id, blip_id, content):
    """Appends content with markup to a document.

    Args:
      wave_id: The wave id owning that this operation is applied to.
      wavelet_id: The wavelet id that this operation is applied to.
      blip_id: The blip id that this operation is applied to.
      content: The markup content to append.
    Returns:
      The operation created.
    """
    return self.new_operation(DOCUMENT_APPEND_MARKUP, wave_id, wavelet_id,
                              blipId=blip_id, content=content)

  def document_modify(self, wave_id, wavelet_id, blip_id):
    """Creates and queues a document modify operation

    The returned operation still needs to be filled with details before
    it makes sense.

    Args:
      wave_id: The wave id owning that this operation is applied to.
      wavelet_id: The wavelet id that this operation is applied to.
      blip_id: The blip id that this operation is applied to.
    Returns:
      The operation created.
    """
    return self.new_operation(DOCUMENT_MODIFY,
                              wave_id,
                              wavelet_id,
                              blipId=blip_id)

  def document_inline_blip_insert(self, wave_id, wavelet_id, blip_id, position):
    """Inserts an inline blip at a specific location.

    Args:
      wave_id: The wave id owning that this operation is applied to.
      wavelet_id: The wavelet id that this operation is applied to.
      blip_id: The blip id that this operation is applied to.
      position: The position in the document to insert the blip.

    Returns:
      JSON data for the blip that was created for further operations.
    """
    inline_blip_data = self._new_blipdata(wave_id, wavelet_id)
    inline_blip_data['parentBlipId'] = blip_id
    self.new_operation(DOCUMENT_INLINE_BLIP_INSERT, wave_id, wavelet_id,
                       blipId=blip_id,
                       index=position,
                       blipData=inline_blip_data)
    return inline_blip_data
