#
# Copyright (c) 2018 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA. 
#

"""
  Transaction - provides transactional support

  $Id: //eng/vdo-releases/magnesium/src/python/vdo/utils/Transaction.py#1 $

"""

from functools import wraps
import gettext
import sys
import threading

gettext.install('Transaction')

class Transaction(object):
  """Client-visible transaction object.
  """

  # A dictionary mapping thread identifier to a list of the thread's
  # transactions.
  __transactionLists = {}

  ######################################################################
  # Public methods
  ######################################################################
  @classmethod
  def transaction(cls):
    """Returns the current transaction for the current thread.
    """
    return cls._threadTransactionList()[-1]

  ######################################################################
  def addUndoStage(self, undoStage):
    """Adds callable undoStage to transaction stages.

       Arguments:
        undoStage (callable)        - callable to execute as part of
                                      transaction roll-back
    """
    self.__undoStages.append(undoStage)

  ######################################################################
  def setMessage(self, handleMessage, message = None):
    """Sets the message to handle if there is an exception.

       If message is specified and an exception occurs the exception
       will be appended to the message using "; {0}" where "{0}" represents
       the exception.

       If message is not specified, the exception will be coerced to
       a string and passed to handleMessage.

       Specifying handleMessage as None will clear both handleMessage and
       message.

       Arguments:
        handleMessage (callable)  - method to handle message
        message (string)          - message
    """
    raise NotImplementedError

  ######################################################################
  # Overridden methods
  ######################################################################
  def __init__(self):
    super(Transaction, self).__init__()
    self.__undoStages = []

  ######################################################################
  # Protected methods
  ######################################################################
  @classmethod
  def _threadTransactionList(cls):
    """Returns the transaction list for the current thread, creating it
       if need be.

       Returns:
         list of Transactions
    """
    transactionList = None
    threadId = threading.currentThread().ident
    try:
      transactionList = cls.__transactionLists[threadId]
    except KeyError:
      transactionList = []
      cls.__transactionLists[threadId] = transactionList
    return transactionList

  ######################################################################
  def _undoStages(self):
    """Returns the list of undo stages for the transaction.

       Returns:
         list of undo stages
    """
    return self.__undoStages

  ######################################################################
  # Private methods
  ######################################################################

########################################################################
def transactional(func):
  """Method decorator providing transactional capabilities to the method.
  """

  ######################################################################
  class _Transaction(Transaction):
    """Decorator-local transaction object providing actual transaction
       capabilities.
    """

    ####################################################################
    # Public methods
    ####################################################################
    @classmethod
    def addTransaction(cls):
      """Adds, and returns, a transaction to the transaction list for the
         current thread.
      """
      cls._threadTransactionList().append(cls())
      return cls.transaction()

    ####################################################################
    @classmethod
    def removeTransaction(cls):
      """Removes the current transaction for for the current thread.
      """
      cls._threadTransactionList().pop()

    ####################################################################
    def undo(self, exception):
      """Performs the undo processing of the transaction.

         Exceptions from the undo stages are ignored.

         Arguments:
          exception   - the exception which resulted in undo being called
      """
      # Handle any message that was set for the transaction.
      if self.__handleMessage is not None:
        if self.__message is not None:
          self.__handleMessage(_("{0}; {1}").format(self.__message,
                                                     str(exception)))
        else:
          self.__handleMessage(str(exception))

      # Perform the undo processing.
      undoStages = self._undoStages()[:]
      undoStages.reverse()
      for undoStage in undoStages:
        try:
          undoStage()
        except Exception:
          pass

    ####################################################################
    # Overridden methods
    ####################################################################
    def __init__(self):
      super(_Transaction, self).__init__()
      self.__handleMessage = None
      self.__message = None

    ####################################################################
    def setMessage(self, handleMessage, message = None):
      self.__handleMessage = handleMessage
      if self.__handleMessage is None:
        self.__message = None
      else:
        self.__message = message

    ####################################################################
    # Protected methods
    ####################################################################

    ####################################################################
    # Private methods
    ####################################################################

  ######################################################################
  @wraps(func)
  def wrap(*args, **kwargs):
    """Wrapper method providing transactional processing capabilities.
    """
    transaction = _Transaction.addTransaction()

    result = None
    try:
      result = func(*args, **kwargs)
    except Exception as ex:
      transaction.undo(ex)

      # Re-raise the Exception, preserving the stack trace from its origin.
      #pylint: disable=E0710
      raise type(ex), ex, sys.exc_info()[2]
    finally:
      _Transaction.removeTransaction()

    return result
  return wrap
