r"""smtrace module is used along with vtkSMTrace to generate Python trace for
ParaView. While this module is primarily designed to be used from the ParaView
GUI, Python scripts can use this module too to generate trace from the script
executed.

Typical usage is as follows::

    from paraview import smtrace
    config = smtracer.start_trace()

    # config is an instance of vtkSMTrace. One can setup properties on this
    # object to control  the generated trace. e.g.
    config.SetFullyTraceSupplementalProxies(True)

    # do the actions to trace.
        ...

    # stop trace. The generated trace is returned.
    txt = smtracer.stop_trace()


=========================
Developer Documentation
=========================

This section describes the module design for developers wanted to extend this
module or use this module for advance tracing/state generation.

The design can be described as follows:

C++ code (either in ServerManager or the GUI layer) should trace actions.
This is done using SM_SCOPED_TRACE() macro provided by vtkSMTrace. When tracing
is enabled, each SM_SCOPED_TRACE() call creates a :class:`.TraceItem`. The TraceItem
instance is scoped, i.e. the object is finalized and destroyed when the scope
exits.

There are various types of TraceItem, ranging from those that trace specific
action such as :class:`.Show`, or those that trace any modified properties
(:class:`.PropertiesModified`). Generic TraceItem types, such as
:class:`.CallMethod` and :class:`.CallFunction` can be used to trace methods
called on vtkObject instances or functions in the global namespace.

TraceItems create or use :class:`.Accessor` instances. Accessors are objects
created for Proxies and Properties in ParaView. Accessor knows how to access
that proxy or property in the Python trace. TraceItems that create new proxies
such as :class:`.RegisterPipelineProxy` and :class:`.RegisterViewProxy`, create
new :class:`.ProxyAccessor` instances. Other such as
:class:`.PropertiesModified` trace item rely on accessors already created.
:class:`.Trace` can provide access to already created accessor as well as create
new accessor for proxies create before the tracing began
(:method:`.Trace.get_accessor`).

Additionally, there are filters such as :class:`.ProxyFilter`,
:class:`.PipelineProxyFilter`, etc. which are used to filter properties that get
traced and where they get traced i.e. in contructor call or right after it.

===============================
Notes about references
===============================

RealProxyAccessor keeps a hard reference to the servermanager.Proxy instance.
This is required. If we don't, then the Python object for vtkSMProxy also gets
garbage collected since there's no reference to it.

"""

import weakref
import paraview.servermanager as sm
import paraview.simple as simple
from paraview.vtk import vtkTimeStamp

class TraceOutput:
  """Internal class used to collect the trace output. Everytime anything is pushed into
  this using the append API, we ensure that the trace is updated. Trace
  doesn't put commands to the trace-output as soon as modifications are noticed
  to try to consolidate the state changes."""
  def __init__(self, data=None):
    self.__data = []
    self.append(data) if data else None

  def append(self, data):
    if isinstance(data, list):
      self.__data += data
      #print "\n".join(data),"\n"
    elif isinstance(data, str):
      self.__data.append(data)
      #print data,"\n"

  def append_separator(self):
    try:
      self.__data.append("") if self.__data[-1] != "" else None
    except IndexError:
      pass

  def append_separated(self, data):
      self.append_separator()
      self.append(data)

  def __str__(self):
    return '\n'.join(self.__data)

  def raw_data(self): return self.__data

  def reset(self): self.__data = []


class Trace(object):
    __REGISTERED_ACCESSORS = {}

    Output = None

    @classmethod
    def reset(cls):
        """Resets the Output and clears all register accessors."""
        cls.__REGISTERED_ACCESSORS.clear()
        cls.Output = TraceOutput()

    @classmethod
    def get_registered_name(cls, proxy, reggroup):
        """Returns the registered name for `proxy` in the given `reggroup`."""
        return proxy.SMProxy.GetSessionProxyManager().GetProxyName(reggroup, proxy.SMProxy)

    @classmethod
    def get_varname(cls, name):
        """returns an unique variable name given a suggested variable name. If
        the suggested variable name is already taken, this method will try to
        find a good suffix that's available."""
        name = sm._make_name_valid(name)
        name = name[0].lower() + name[1:]
        original_name = name
        suffix = 1
        # build a set of existing variable names
        varnameset = set([accessor.Varname for accessor in cls.__REGISTERED_ACCESSORS.values()])
        while name in varnameset:
            name = "%s_%d" % (original_name, suffix)
            suffix += 1
        return name

    @classmethod
    def register_accessor(cls, accessor):
        """Register an instance of an Accessor or subclass"""
        cls.__REGISTERED_ACCESSORS[accessor.get_object()] = accessor

    @classmethod
    def unregister_accessor(cls, accessor):
        del cls.__REGISTERED_ACCESSORS[accessor.get_object()]

    @classmethod
    def get_accessor(cls, obj):
        """Returns an accessor for obj. If none exists, a new one may be
        created, if possible. Currently obj is expected to be a
        :class:`servermanager.Proxy` instance. In future, we may change this to
        be a vtkSMProxy instance instead."""
        if obj is None:
            return None
        assert isinstance(obj, sm.Proxy)
        try:
            return cls.__REGISTERED_ACCESSORS[obj]
        except KeyError:
            # Create accessor if possible else raise
            # "untraceable" exception.
            if cls.create_accessor(obj):
                return cls.__REGISTERED_ACCESSORS[obj]
            #return "<unknown>"
            raise Untraceable(
                    "%s is not 'known' at this point. Hence, we cannot trace "\
                    "it. Skipping this action." % repr(obj))

    @classmethod
    def has_accessor(cls, obj):
        return cls.__REGISTERED_ACCESSORS.has_key(obj)

    @classmethod
    def create_accessor(cls, obj):
        """Create a new accessor for a proxy. This returns True when a
        ProxyAccessor has been created, other returns False. This is needed to
        bring into trace proxies that were either already created when the trace
        was started or were created indirectly and hence not explictly traced."""
        if isinstance(obj, sm.SourceProxy):
            # handle pipeline source/filter proxy.
            pname = obj.SMProxy.GetSessionProxyManager().GetProxyName("sources", obj.SMProxy)
            if pname:
                if obj == simple.GetActiveSource():
                    accessor = ProxyAccessor(cls.get_varname(pname), obj)
                    cls.Output.append_separated([\
                        "# get active source.",
                        "%s = GetActiveSource()" % accessor])
                else:
                    accessor = ProxyAccessor(cls.get_varname(pname), obj)
                    cls.Output.append_separated([\
                        "# find source",
                        "%s = FindSource('%s')" % (accessor, pname)])
                return True
        if obj.SMProxy.IsA("vtkSMViewProxy"):
            # handle view proxy.
            pname = obj.SMProxy.GetSessionProxyManager().GetProxyName("views", obj.SMProxy)
            if pname:
                trace = TraceOutput()
                accessor = ProxyAccessor(cls.get_varname(pname), obj)
                if obj == simple.GetActiveView():
                    trace.append("# get active view")
                    trace.append("%s = GetActiveViewOrCreate('%s')" % (accessor, obj.GetXMLName()))
                else:
                    ctor_args = "'%s', viewtype='%s'" % (pname, obj.GetXMLName())
                    trace.append("# find view")
                    trace.append("%s = FindViewOrCreate(%s)" % (accessor, ctor_args))
                # trace view size, if present. We trace this commented out so
                # that the playback in the GUI doesn't cause issues.
                viewSizeAccessor = accessor.get_property("ViewSize")
                if viewSizeAccessor:
                    trace.append([\
                        "# uncomment following to set a specific view size",
                        "# %s" % viewSizeAccessor.get_property_trace(in_ctor=False)])
                cls.Output.append_separated(trace.raw_data())
                return True
        if obj.SMProxy.IsA("vtkSMRepresentationProxy"):
            # handle representations.
            if hasattr(obj, "Input"):
                inputAccsr = cls.get_accessor(obj.Input)
                view = simple.LocateView(obj)
                viewAccessor = cls.get_accessor(view)
                pname = obj.SMProxy.GetSessionProxyManager().GetProxyName("representations", obj.SMProxy)
                if pname:
                    varname = "%sDisplay" % inputAccsr
                    accessor = ProxyAccessor(cls.get_varname(varname), obj)
                    cls.Output.append_separated([\
                        "# get display properties",
                        "%s = GetDisplayProperties(%s, view=%s)" %\
                            (accessor, inputAccsr, viewAccessor)])
                    return True
        if cls.get_registered_name(obj, "lookup_tables"):
            pname = cls.get_registered_name(obj, "lookup_tables")
            if cls._create_accessor_for_tf(obj, pname):
                return True
        if cls.get_registered_name(obj, "piecewise_functions"):
            pname = cls.get_registered_name(obj, "piecewise_functions")
            if cls._create_accessor_for_tf(obj, pname):
                return True
        if cls.get_registered_name(obj, "scalar_bars"):
            # trace scalar bar.
            lutAccessor = cls.get_accessor(obj.LookupTable)
            view = simple.LocateView(obj)
            viewAccessor = cls.get_accessor(view)
            varname = cls.get_varname("%sColorBar" % lutAccessor)
            accessor = ProxyAccessor(varname, obj)
            trace = TraceOutput()
            trace.append(\
                "# get color legend/bar for %s in view %s" % (lutAccessor, viewAccessor))
            trace.append(accessor.trace_ctor(\
                "GetScalarBar",
                SupplementalProxy(ScalarBarProxyFilter()),
                ctor_args="%s, %s" % (lutAccessor, viewAccessor)))
            cls.Output.append_separated(trace.raw_data())
            return True
        if cls.get_registered_name(obj, "animation"):
            return cls._create_accessor_for_animation_proxies(obj)
        if cls.get_registered_name(obj, "layouts"):
            view = simple.GetActiveView()
            if view and obj.GetViewLocation(view.SMProxy) != -1:
                viewAccessor = cls.get_accessor(view)
                varname = cls.get_varname(cls.get_registered_name(obj, "layouts"))
                accessor = ProxyAccessor(varname, obj)
                cls.Output.append_separated([\
                    "# get layout",
                    "%s = GetLayout()" % accessor])
                return True
        if obj.SMProxy.IsA("vtkSMTimeKeeperProxy"):
            tkAccessor = ProxyAccessor(cls.get_varname(cls.get_registered_name(obj, "timekeeper")), obj)
            cls.Output.append_separated([\
                    "# get the time-keeper",
                    "%s = GetTimeKeeper()" % tkAccessor])
            return True
        return False

    @classmethod
    def _create_accessor_for_tf(cls, proxy, regname):
        import re
        m = re.match("^[0-9.]*(.+)\\.%s$" % proxy.GetXMLName(), regname)
        if m:
            arrayName = m.group(1)
            if proxy.GetXMLGroup() == "lookup_tables":
                varsuffix = "LUT"
                comment = "color transfer function/color map"
                method = "GetColorTransferFunction"
            else:
                varsuffix = "PWF"
                comment = "opacity transfer function/opacity map"
                method = "GetOpacityTransferFunction"
            varname = cls.get_varname("%s%s" % (arrayName, varsuffix))
            accessor = ProxyAccessor(varname, proxy)
            #cls.Output.append_separated([\
            #    "# get %s for '%s'" % (comment, arrayName),
            #    "%s = %s('%s')" % (accessor, method, arrayName)])
            trace = TraceOutput()
            trace.append("# get %s for '%s'" % (comment, arrayName))
            trace.append(accessor.trace_ctor(\
                method, SupplementalProxy(TransferFunctionProxyFilter()), ctor_args="'%s'" % arrayName))
            cls.Output.append_separated(trace.raw_data())
            return True
        return False

    @classmethod
    def _create_accessor_for_animation_proxies(cls, obj):
        pname = cls.get_registered_name(obj, "animation")
        if obj == simple.GetAnimationScene():
            sceneAccessor = ProxyAccessor(cls.get_varname(pname), obj)
            cls.Output.append_separated([\
                "# get animation scene",
                "%s = GetAnimationScene()" % sceneAccessor])
            return True
        if obj == simple.GetTimeTrack():
            accessor = ProxyAccessor(cls.get_varname(pname), obj)
            cls.Output.append_separated([\
                "# get time animation track",
                "%s = GetTimeTrack()" % accessor])
            return True
        if obj.GetXMLName() == "CameraAnimationCue":
            # handle camera animation cue.
            view = obj.AnimatedProxy
            viewAccessor = cls.get_accessor(view)
            accessor = ProxyAccessor(cls.get_varname(pname), obj)
            cls.Output.append_separated([\
                "# get camera animation track for the view",
                "%s = GetCameraTrack(view=%s)" % (accessor, viewAccessor)])
            return True
        if obj.GetXMLName() == "KeyFrameAnimationCue":
            animatedProxyAccessor = cls.get_accessor(obj.AnimatedProxy)
            animatedElement = int(obj.AnimatedElement)
            animatedPropertyName = obj.AnimatedPropertyName
            varname = cls.get_varname("%s%sTrack" % (animatedProxyAccessor, animatedPropertyName))
            accessor = ProxyAccessor(varname, obj)
            cls.Output.append_separated([\
                "# get animation track",
                "%s = GetAnimationTrack('%s', index=%d, proxy=%s)" %\
                    (accessor, animatedPropertyName, animatedElement, animatedProxyAccessor)])
            return True
        if obj.GetXMLName() == "PythonAnimationCue":
            raise Untraceable("PythonAnimationCue's are currently not supported in trace")
        return False

class Untraceable(Exception):
    def __init__(self, logmessage="<unspecified>"):
        self.LogMessage = logmessage

    def __str__(self):
        return repr(self.LogMessage)

class Accessor(object):
    def __init__(self, varname, obj):
        self.Varname = varname
        self.__Object = obj
        Trace.register_accessor(self)

    def finalize(self):
        Trace.unregister_accessor(self)
        self.__Object = None

    def __str__(self):
        return self.Varname

    def get_object(self):
        return self.__Object

class RealProxyAccessor(Accessor):
    __CreateCallbacks = []

    @classmethod
    def register_create_callback(cls, function):
        cls.__CreateCallbacks.insert(0, function)

    @classmethod
    def unregister_create_callback(cls, function):
        cls.__CreateCallbacks.remove(function)

    @classmethod
    def create(cls, *args, **kwargs):
        for x in cls.__CreateCallbacks:
            try:
                return x(*args, **kwargs)
            except NotImplementedError: pass
        return RealProxyAccessor(*args, **kwargs)

    def __init__(self, varname, proxy):
        Accessor.__init__(self, varname, proxy)

        self.OrderedProperties = []

        # Create accessors for properties on this proxy.
        oiter = sm.vtkSMOrderedPropertyIterator()
        oiter.SetProxy(proxy.SMProxy)
        while not oiter.IsAtEnd():
            prop_name = oiter.GetKey()
            prop_label = oiter.GetPropertyLabel()
            sanitized_label = sm._make_name_valid(prop_label)

            prop = proxy.GetProperty(prop_name)
            if not type(prop) == sm.Property:
                # Note: when PropertyTraceHelper for a property with ProxyListDomain is
                # created, it creates accessors for all proxies in the domain as well.
                prop_accessor = PropertyTraceHelper(sanitized_label, self)
                self.OrderedProperties.append(prop_accessor)
            oiter.Next()
        del oiter

    def finalize(self):
        for x in self.OrderedProperties:
            x.finalize()
        Accessor.finalize(self)

    def get_property(self, name):
        for x in self.OrderedProperties:
            if x.get_property_name() == name:
                return x
        return None

    def get_properties(self):
        return self.OrderedProperties[:]

    def get_ctor_properties(self):
        """Returns a list of property accessors that should be specified
           in the constructor."""
        return [x for x in self.OrderedProperties if self.is_ctor_property(x)]

    def is_ctor_property(self, prop):
        return prop.get_object().IsA("vtkSMInputProperty") or \
                prop.get_object().FindDomain("vtkSMFileListDomain") != None

    def trace_properties(self, props, in_ctor):
        joiner = ",\n    " if in_ctor else "\n"
        return joiner.join([x.get_property_trace(in_ctor) for x in props])

    def trace_ctor(self, ctor, filter, ctor_args=None, skip_assignment=False):
        args_in_ctor = str(ctor_args) if not ctor_args is None else ""
        # trace any properties that the 'filter' tells us should be traced
        # in ctor.
        ctor_props = [x for x in self.OrderedProperties if filter.should_trace_in_ctor(x)]
        ctor_props_trace = self.trace_properties(ctor_props, in_ctor=True)
        if args_in_ctor and ctor_props_trace:
            args_in_ctor = "%s, %s" % (args_in_ctor, ctor_props_trace)
        else:
            args_in_ctor += ctor_props_trace

        # locate all the other properties that should be traced in create.
        other_props = [x for x in self.OrderedProperties \
            if filter.should_trace_in_create(x) and not filter.should_trace_in_ctor(x)]

        trace = TraceOutput()
        if not ctor is None:
            if not skip_assignment:
                trace.append("%s = %s(%s)" % (self, ctor, args_in_ctor))
            else:
                assert len(other_props) == 0
                trace.append("%s(%s)" % (ctor, args_in_ctor))
                return trace.raw_data()

        # FIXME: would like trace_properties() to return a list instead of
        # a string.
        txt = self.trace_properties(other_props, in_ctor=False)
        if txt: trace.append(txt)

        # Now, if any of the props has ProxyListDomain, we should trace their
        # "ctors" as well. Tracing ctors for ProxyListDomain proxies simply
        # means tracing their property values.
        pld_props = [x for x in self.OrderedProperties if x.has_proxy_list_domain()]
        for prop in pld_props:
            paccessor = Trace.get_accessor(prop.get_property_value())
            sub_trace = paccessor.trace_ctor(None, filter)
            if sub_trace:
                trace.append_separated(\
                    "# init the %s selected for '%s'" % (prop.get_value(), prop.get_property_name()))
                trace.append(sub_trace)
        return trace.raw_data()

def ProxyAccessor(*args, **kwargs):
    return RealProxyAccessor.create(*args, **kwargs)

class PropertyTraceHelper(object):
    """PropertyTraceHelper is used by RealProxyAccessor to help with tracing
    properites. In its contructor, RealProxyAccessor creates a
    PropertyTraceHelper for each of its properties that could potentially need
    to be traced."""
    def __init__(self, propertyname, proxyAccessor):
        """Constructor.

          :param propertyname: name used to access the property. This is the
          sanitized property label.
          :param proxyAccessor: RealProxyAccessor instance for the proxy.
        """
        assert isinstance(proxyAccessor, RealProxyAccessor)
        assert type(propertyname) == str

        self.__PyProperty = None
        self.PropertyName = propertyname
        self.ProxyAccessor = proxyAccessor
        self.FullScopedName = "%s.%s" % (proxyAccessor, propertyname)

        pyprop = self.get_object()
        assert not pyprop is None

        pld_domain = pyprop.FindDomain("vtkSMProxyListDomain")
        self.HasProxyListDomain = isinstance(pyprop, sm.ProxyProperty) and pld_domain != None
        self.ProxyListDomainProxyAccessors = []

        if self.HasProxyListDomain:
            # register accessors for proxies in the proxy list domain.
            # This is cheating. Since there's no accessor for a proxy in the domain
            # unless the proxy is "active" in the property. However, since ParaView
            # UI never modifies the other properties, we cheat
            for i in xrange(pld_domain.GetNumberOfProxies()):
                domain_proxy = pld_domain.GetProxy(i)
                plda = ProxyAccessor(self.get_varname(), sm._getPyProxy(domain_proxy))
                self.ProxyListDomainProxyAccessors.append(plda)

    def __del__(self):
        self.finalize()

    def finalize(self):
        for x in self.ProxyListDomainProxyAccessors:
            x.finalize()
        self.ProxyListDomainProxyAccessors = []

    def get_object(self):
        """Returns the servermanager.Property (or subclass) for the
        vtkSMProperty this trace helper is helping with."""
        if self.__PyProperty is None or self.__PyProperty() is None:
            # This will raise Untraceable exception is the ProxyAccessor cannot
            # locate the servermanager.Proxy for the SMProxy it refers to.
            pyproperty = self.ProxyAccessor.get_object().GetProperty(self.get_property_name())
            self.__PyProperty = weakref.ref(pyproperty)
            return pyproperty
        return self.__PyProperty()

    def get_property_trace(self, in_ctor):
        """return trace-text for the property.

        :param in_ctor: if False, the trace is generated trace will use
        fully-scoped name when referring to the property e.g. sphere0.Radius=2,
        else it will use just the property name e.g. Radius=2."""
        varname = self.get_varname(in_ctor)
        if in_ctor: return "%s=%s" % (varname, self.get_value())
        else: return "%s = %s" % (varname, self.get_value())

    def get_varname(self, not_fully_scoped=False):
        """Returns the variable name to use when referring to this property.

        :param not_fully_scoped: if False, this will return
        fully-scoped name when referring to the property e.g. sphere0.Radius,
        else it will use just the property name e.g. Radius"""
        return self.PropertyName if not_fully_scoped else self.FullScopedName

    def get_value(self):
        """Returns the propery value as a string. For proxy properties, this
        will either be a string used to refer to another proxy or a string used
        to refer to the proxy in a proxy list domain."""
        if isinstance(self.get_object(), sm.ProxyProperty):
            data = self.get_object()[:]
            if self.has_proxy_list_domain():
                data = ["'%s'" % x.GetXMLLabel() for x in self.get_object()[:]]
            else:
                data = [str(Trace.get_accessor(x)) for x in self.get_object()[:]]
            try:
                if len(data) > 1:
                  return "[%s]" % (", ".join(data))
                else:
                  return data[0]
            except IndexError:
                return "None"
        else:
            return str(self.get_object())

    def has_proxy_list_domain(self):
        """Returns True if this property has a ProxyListDomain, else False."""
        return self.HasProxyListDomain

    def get_property_name(self):
        return self.PropertyName

    def get_property_value(self):
        """Return the Property value as would be returned by
        servermanager.Proxy.GetPropertyValue()."""
        return self.ProxyAccessor.get_object().GetPropertyValue(self.get_property_name())

# ===================================================================================================
# === Filters used to filter properties traced ===
# ===================================================================================================
class ProxyFilter(object):
    def should_never_trace(self, prop, hide_gui_hidden=True):
        if prop.get_object().GetIsInternal() or prop.get_object().GetInformationOnly():
            return True
        # should we hide properties hidden from panels? yes, generally, except
        # Views.
        if hide_gui_hidden == True and prop.get_object().GetPanelVisibility() == "never":
            if prop.get_property_name() == "ViewSize": print "skipping hidden"
            return True
        # if a property is "linked" to settings, then skip it here too. We
        # should eventually add an option for user to save, yes, save these too.
        if prop.get_object().GetHints():
            plink = prop.get_object().GetHints().FindNestedElementByName("PropertyLink")
            return True if plink and plink.GetAttribute("group") == "settings" else False
        return False

    def should_trace_in_create(self, prop, user_can_modify_in_create=True):
        if self.should_never_trace(prop): return False

        setting = sm.vtkSMTrace.GetActiveTracer().GetPropertiesToTraceOnCreate()
        if setting == sm.vtkSMTrace.RECORD_USER_MODIFIED_PROPERTIES and not user_can_modify_in_create:
            # In ParaView, user never changes properties in Create. It's only
            # afterwords, so skip all properties.
            return False
        trace_props_with_default_values = True \
            if setting == sm.vtkSMTrace.RECORD_ALL_PROPERTIES else False
        return (trace_props_with_default_values or not prop.get_object().IsValueDefault())

    def should_trace_in_ctor(self, prop):
        return False

class PipelineProxyFilter(ProxyFilter):
    def should_trace_in_create(self, prop):
        return ProxyFilter.should_trace_in_create(self, prop, user_can_modify_in_create=False)

    def should_never_trace(self, prop):
        """overridden to avoid hiding "non-gui" properties such as FileName."""
        # should we hide properties hidden from panels?
        if not prop.get_object().FindDomain("vtkSMFileListDomain") is None:
            return False
        else:
            return ProxyFilter.should_never_trace(self, prop)

    def should_trace_in_ctor(self, prop):
        if self.should_never_trace(prop): return False
        return prop.get_object().IsA("vtkSMInputProperty") or \
            prop.get_object().FindDomain("vtkSMFileListDomain") != None

class ExodusIIReaderFilter(PipelineProxyFilter):
    def should_never_trace(self, prop):
        if PipelineProxyFilter.should_never_trace(self, prop): return True
        # Exodus reader has way too many wacky properties tracing them causes
        # the reader to segfault. We need to either remove those properties
        # entirely or fix them. Until I get a chance to get to the bottom of it,
        # I am opting to ignore those properties when tracing.
        return prop.get_property_name() in [\
            "FilePrefix", "XMLFileName", "FilePattern", "FileRange"]

class RepresentationProxyFilter(PipelineProxyFilter):
    def should_trace_in_ctor(self, prop): return False

    def should_never_trace(self, prop):
        if PipelineProxyFilter.should_never_trace(self, prop): return True
        if prop.get_property_name() in ["Input",\
            "SelectionCellFieldDataArrayName",\
            "SelectionPointFieldDataArrayName"] : return True
        return False

class ViewProxyFilter(ProxyFilter):
    def should_never_trace(self, prop):
        # skip "Representations" property and others.
        # The fact that we need to skip so many properties means that we are
        # missing something in the design of vtkSMProperties here. We need to
        # reclassify properties to cleanly address all its "roles".
        if prop.get_property_name() in [\
            "ViewTime", "CacheKey", "Representations", "CameraClippingRange"]: return True
        return ProxyFilter.should_never_trace(self, prop, hide_gui_hidden=False)

class AnimationProxyFilter(ProxyFilter):
    def should_never_trace(self, prop):
        if ProxyFilter.should_never_trace(self, prop): return True
        if prop.get_property_name() in ["AnimatedProxy", "AnimatedPropertyName",
            "AnimatedElement", "AnimatedDomainName"]:
            return True
        return False

class ExporterProxyFilter(ProxyFilter):
    def should_trace_in_ctor(self, prop):
        return not self.should_never_trace(prop) and self.should_trace_in_create(prop)
    def should_never_trace(self, prop):
        if ProxyFilter.should_never_trace(self, prop): return True
        if prop.get_property_name() == "FileName" : return True
        return False

class WriterProxyFilter(ProxyFilter):
    def should_trace_in_ctor(self, prop):
        return not self.should_never_trace(prop) and self.should_trace_in_create(prop)
    def should_never_trace(self, prop):
        if ProxyFilter.should_never_trace(self, prop): return True
        if prop.get_property_name() in ["FileName", "Input"] : return True
        return False

class TransferFunctionProxyFilter(ProxyFilter):
    def should_trace_in_ctor(self, prop): return False
    def should_never_trace(self, prop):
        if ProxyFilter.should_never_trace(self, prop, hide_gui_hidden=False): return True
        if prop.get_property_name() in ["ScalarOpacityFunction"]: return True
        return False

class ScalarBarProxyFilter(ProxyFilter):
    def should_trace_in_ctor(self, prop): return False
    def should_never_trace(self, prop):
        # despite being hidden from the panel, these properties should not be
        # skipped in trace.
        if prop.get_property_name() in ["Position", "Position2", "Orientation"]:
            return False
        return ProxyFilter.should_never_trace(self, prop)

def SupplementalProxy(cls):
    """This function decorates a ProxyFilter. Designed to be
    used for supplemental proxies, so that we can centralize the logic
    to decide whether to trace any of the properties on the supplemental
    proxies the first time that proxy is accessed."""
    setting = sm.vtkSMTrace.GetActiveTracer().GetFullyTraceSupplementalProxies()
    if setting: return cls

    def should_trace_in_ctor(self, *args, **kwargs):
        return False
    def should_trace_in_create(self, *args, **kwargs):
        return False
    cls.should_trace_in_create = should_trace_in_create
    cls.should_trace_in_ctor = should_trace_in_ctor
    return cls

# ===================================================================================================
# === TraceItem types ==
# TraceItems are units of traceable actions triggerred by the application using vtkSMTrace
# ===================================================================================================

class TraceItem(object):
    def __init__(self):
        pass
    def finalize(self):
        pass

class NestableTraceItem(TraceItem):
    """Base class for trace item that can be nested i.e.
    can trace when some other trace item is active."""
    pass

class BookkeepingItem(NestableTraceItem):
    """Base class for trace items that are only used for
    book keeping and don't affect the trace itself."""
    pass

class RegisterPipelineProxy(TraceItem):
    """This traces the creation of a Pipeline Proxy such as
    sources/filters/readers etc."""

    def __init__(self, proxy):
        TraceItem.__init__(self)
        self.Proxy = sm._getPyProxy(proxy)

    def finalize(self):
        pname = Trace.get_registered_name(self.Proxy, "sources")
        varname = Trace.get_varname(pname)
        accessor = ProxyAccessor(varname, self.Proxy)

        ctor = sm._make_name_valid(self.Proxy.GetXMLLabel())
        trace = TraceOutput()
        trace.append("# create a new '%s'" % self.Proxy.GetXMLLabel())
        filter_type = ExodusIIReaderFilter() \
            if isinstance(self.Proxy, sm.ExodusIIReaderProxy) else PipelineProxyFilter()
        trace.append(accessor.trace_ctor(ctor, filter_type))
        Trace.Output.append_separated(trace.raw_data())
        TraceItem.finalize(self)

class Delete(TraceItem):
    """This traces the deletion of a Pipeline proxy"""
    def __init__(self, proxy):
        TraceItem.__init__(self)
        proxy = sm._getPyProxy(proxy)
        accessor = Trace.get_accessor(proxy)
        Trace.Output.append_separated([\
            "# destroy %s" % (accessor),
            "Delete(%s)" % (accessor),
            "del %s" % accessor])
        accessor.finalize()
        del accessor
        import gc
        gc.collect()

class CleanupAccessor(BookkeepingItem):
    def __init__(self, proxy):
        self.Proxy = sm._getPyProxy(proxy)
    def finalize(self):
        if Trace.has_accessor(self.Proxy):
            accessor = Trace.get_accessor(self.Proxy)
            accessor.finalize()
            del accessor
        import gc
        gc.collect()

class PropertiesModified(NestableTraceItem):
    """Traces properties modified on a specific proxy."""
    def __init__(self, proxy, comment=None):
        TraceItem.__init__(self)

        proxy = sm._getPyProxy(proxy)
        self.ProxyAccessor = Trace.get_accessor(proxy)
        self.MTime = vtkTimeStamp()
        self.MTime.Modified()
        self.Comment = "#%s" % comment if not comment is None else \
            "# Properties modified on %s" % str(self.ProxyAccessor)
        try:
            # Hack to track ScalarOpacityFunction property changes since that proxy
            # is shown on the same pqProxyWidget as the ColorTransferFunction proxy --
            # which is non-standard.
            if proxy.ScalarOpacityFunction:
                self.ScalarOpacityFunctionHack = PropertiesModified(proxy.ScalarOpacityFunction)
        except: pass

    def finalize(self):
        props = self.ProxyAccessor.get_properties()
        props_to_trace = [k for k in props if self.MTime.GetMTime() < k.get_object().GetMTime()]
        if props_to_trace:
            Trace.Output.append_separated([
                self.Comment,
                self.ProxyAccessor.trace_properties(props_to_trace, in_ctor=False)])

        # also handle properties on values for properties with ProxyListDomain.
        for prop in [k for k in props if k.has_proxy_list_domain()]:
            val = prop.get_property_value()
            if val:
                valaccessor = Trace.get_accessor(val)
                props = valaccessor.get_properties()
                props_to_trace = [k for k in props if self.MTime.GetMTime() < k.get_object().GetMTime()]
                if props_to_trace:
                  Trace.Output.append_separated([
                      "# Properties modified on %s" % valaccessor,
                      valaccessor.trace_properties(props_to_trace, in_ctor=False)])
        TraceItem.finalize(self)

        try:
            self.ScalarOpacityFunctionHack.finalize()
            del self.ScalarOpacityFunctionHack
        except AttributeError: pass

class Show(TraceItem):
    """Traces Show"""
    def __init__(self, producer, port, view, display, comment=None):
        TraceItem.__init__(self)

        producer = sm._getPyProxy(producer)
        view = sm._getPyProxy(view)
        display = sm._getPyProxy(display)

        self.ProducerAccessor = Trace.get_accessor(producer)
        self.ViewAccessor = Trace.get_accessor(view)
        self.OutputPort = port
        self.Display = display
        self.Comment = comment

    def finalize(self):
        display = self.Display
        if not Trace.has_accessor(display):
            pname = "%sDisplay" % self.ProducerAccessor
            accessor = ProxyAccessor(Trace.get_varname(pname), display)
            trace_ctor = True
        else:
            accessor = Trace.get_accessor(display)
            trace_ctor = False
        port = self.OutputPort

        output = TraceOutput()
        if not self.Comment is None:
            output.append("# %s" % self.Comment)
        else:
            output.append("# show data in view")
        if port > 0:
            output.append("%s = Show(OutputPort(%s, %d), %s)" % \
                (str(accessor), str(self.ProducerAccessor), port, str(self.ViewAccessor)))
        else:
            output.append("%s = Show(%s, %s)" % \
                (str(accessor), str(self.ProducerAccessor), str(self.ViewAccessor)))

        if trace_ctor:
            # Now trace default values.
            ctor_trace = accessor.trace_ctor(None, RepresentationProxyFilter())
            if ctor_trace:
                output.append("# trace defaults for the display properties.")
                output.append(ctor_trace)
        Trace.Output.append_separated(output.raw_data())
        TraceItem.finalize(self)

class Hide(TraceItem):
    """Traces Hide"""
    def __init__(self, producer, port, view):
        TraceItem.__init__(self)

        producer = sm._getPyProxy(producer)
        view = sm._getPyProxy(view)
        producerAccessor = Trace.get_accessor(producer)
        viewAccessor = Trace.get_accessor(view)

        Trace.Output.append_separated([\
          "# hide data in view",
          "Hide(%s, %s)" % (str(producerAccessor), str(viewAccessor)) if port == 0 else \
              "Hide(OutputPort(%s, %d), %s)" % (str(producerAccessor), port, str(viewAccessor))])

class SetScalarColoring(TraceItem):
    """Trace vtkSMPVRepresentationProxy.SetScalarColoring"""
    def __init__(self, display, arrayname, attribute_type):
        TraceItem.__init__(self)

        self.Display = sm._getPyProxy(display)
        self.ArrayName = arrayname
        self.AttributeType = attribute_type

    def finalize(self):
        TraceItem.finalize(self)

        if self.ArrayName:
            Trace.Output.append_separated([\
                "# set scalar coloring",
                "ColorBy(%s, ('%s', '%s'))" % (\
                    str(Trace.get_accessor(self.Display)),
                    sm.GetAssociationAsString(self.AttributeType),
                    self.ArrayName)])
        else:
            Trace.Output.append_separated([\
                "# turn off scalar coloring",
                "ColorBy(%s, None)" % str(Trace.get_accessor(self.Display))])

class RegisterViewProxy(TraceItem):
    """Traces creation of a new view (vtkSMParaViewPipelineController::RegisterViewProxy)."""
    def __init__(self, proxy):
        TraceItem.__init__(self)
        self.Proxy = sm._getPyProxy(proxy)
        assert not self.Proxy is None

    def finalize(self):
        pname = Trace.get_registered_name(self.Proxy, "views")
        varname = Trace.get_varname(pname)
        accessor = ProxyAccessor(varname, self.Proxy)

        # unlike for filters/sources, for views the CreateView function still takes the
        # xml name for the view, not its label.
        ctor_args = "'%s'" % self.Proxy.GetXMLName()
        trace = TraceOutput()
        trace.append("# Create a new '%s'" % self.Proxy.GetXMLLabel())
        filter = ViewProxyFilter()
        trace.append(accessor.trace_ctor("CreateView", filter, ctor_args))
        Trace.Output.append_separated(trace.raw_data())

        viewSizeAccessor = accessor.get_property("ViewSize")
        if viewSizeAccessor and not filter.should_trace_in_create(viewSizeAccessor):
            # trace view size, if present. We trace this commented out so
            # that the playback in the GUI doesn't cause issues.
            Trace.Output.append([\
                "# uncomment following to set a specific view size",
                "# %s" % viewSizeAccessor.get_property_trace(in_ctor=False)])
        # we assume views don't have proxy list domains for now, and ignore tracing them.
        TraceItem.finalize(self)

class ExportView(TraceItem):
    def __init__(self, view, exporter, filename):
        TraceItem.__init__(self)

        view = sm._getPyProxy(view)
        exporter = sm._getPyProxy(exporter)

        viewAccessor = Trace.get_accessor(view)
        exporterAccessor = ProxyAccessor("temporaryExporter", exporter)

        trace = TraceOutput()
        trace.append("# export view")
        trace.append(\
            exporterAccessor.trace_ctor("ExportView", ExporterProxyFilter(),
              ctor_args="'%s', view=%s" % (filename, viewAccessor),
              skip_assignment=True))
        exporterAccessor.finalize() # so that it will get deleted
        del exporterAccessor
        Trace.Output.append_separated(trace.raw_data())

class SaveData(TraceItem):
    def __init__(self, writer, filename, source, port):
        TraceItem.__init__(self)

        source = sm._getPyProxy(source, port)
        sourceAccessor = Trace.get_accessor(source)
        writer = sm._getPyProxy(writer)
        writerAccessor = ProxyAccessor("temporaryWriter", writer)

        if port > 0:
            ctor_args_1 = "OutputPort(%s, %d)" % (sourceAccessor, port)
        else:
            ctor_args_1 = "%s" % sourceAccessor

        trace = TraceOutput()
        trace.append("# save data")
        trace.append(\
            writerAccessor.trace_ctor("SaveData", WriterProxyFilter(),
              ctor_args="'%s', proxy=%s" % (filename, ctor_args_1),
              skip_assignment=True))
        writerAccessor.finalize() # so that it will get deleted.
        del writerAccessor
        del writer
        Trace.Output.append_separated(trace.raw_data())

class EnsureLayout(TraceItem):
    def __init__(self, layout):
        TraceItem.__init__(self)
        layout = sm._getPyProxy(layout)
        accessor = Trace.get_accessor(layout)

class RegisterLayoutProxy(TraceItem):
    def __init__(self, layout):
        TraceItem.__init__(self)
        self.Layout = sm._getPyProxy(layout)
    def finalize(self):
        pname = Trace.get_registered_name(self.Layout, "layouts")
        accessor = ProxyAccessor(Trace.get_varname(pname), self.Layout)
        Trace.Output.append_separated([\
            "# create new layout object",
            "%s = CreateLayout()" % accessor])
        TraceItem.finalize(self)

class CreateAnimationTrack(TraceItem):
    # FIXME: animation tracing support in general needs to be revamped after moving
    # animation control logic to the server manager from Qt layer.
    def __init__(self, cue):
        TraceItem.__init__(self)
        self.Cue = sm._getPyProxy(cue)

    def finalize(self):
        TraceItem.finalize(self)

        # We let Trace create an accessor for the cue. We will then simply log the
        # default property values.
        accessor = Trace.get_accessor(self.Cue)

        trace = TraceOutput()
        trace.append("# create keyframes for this animation track")

        # Create accessors for each of the animation key frames.
        for keyframeProxy in self.Cue.KeyFrames:
            pname = Trace.get_registered_name(keyframeProxy, "animation")
            kfaccessor = ProxyAccessor(Trace.get_varname(pname), keyframeProxy)
            ctor = sm._make_name_valid(keyframeProxy.GetXMLLabel())
            trace.append_separated("# create a key frame")
            trace.append(kfaccessor.trace_ctor(ctor, AnimationProxyFilter()))

        # Now trace properties on the cue.
        trace.append_separated("# initialize the animation track")
        trace.append(accessor.trace_ctor(None, AnimationProxyFilter()))
        Trace.Output.append_separated(trace.raw_data())

class RenameProxy(TraceItem):
    "Trace renaming of a source proxy."
    def __init__(self, proxy):
        TraceItem.__init__(self)
        proxy = sm._getPyProxy(proxy)

        if Trace.get_registered_name(proxy, "sources"):
            self.Accessor = Trace.get_accessor(proxy)
            self.Proxy = proxy
        else:
            raise Untraceable("Only source proxy renames are traced.")

    def finalize(self):
        if self.Accessor:
            newname = Trace.get_registered_name(self.Proxy, "sources")
            Trace.Output.append_separated([\
                "# rename source object",
                "RenameSource('%s', %s)" % (newname, self.Accessor)])
        TraceItem.finalize(self)

class SetCurrentProxy(TraceItem):
    """Traces change in active view/source etc."""
    def __init__(self, selmodel, proxy, command):
        TraceItem.__init__(self)
        if proxy and proxy.IsA("vtkSMOutputPort"):
            # FIXME: need to handle port number.
            proxy = sm._getPyProxy(proxy.GetSourceProxy())
        else:
            proxy = sm._getPyProxy(proxy)
        accessor = Trace.get_accessor(proxy)
        pxm = selmodel.GetSessionProxyManager()
        if selmodel is pxm.GetSelectionModel("ActiveView"):
            Trace.Output.append_separated([\
                "# set active view",
                "SetActiveView(%s)" % accessor])
        elif selmodel is pxm.GetSelectionModel("ActiveSources"):
            Trace.Output.append_separated([\
                "# set active source",
                "SetActiveSource(%s)" % accessor])
        else:
            raise Untraceable("Unknown selection model")

class CallMethod(TraceItem):
    def __init__(self, proxy, methodname, *args, **kwargs):
        TraceItem.__init__(self)
        trace = self.get_trace(proxy, methodname, args, kwargs)
        if trace:
            Trace.Output.append_separated(trace)

    def get_trace(self, proxy, methodname, args, kwargs):
        to_trace = []
        try:
            to_trace.append("# " + kwargs["comment"])
            del kwargs["comment"]
        except KeyError:
            pass
        accessor = Trace.get_accessor(sm._getPyProxy(proxy))
        args = [str(CallMethod.marshall(x)) for x in args]
        args += ["%s=%s" % (key, CallMethod.marshall(val)) for key, val in kwargs.iteritems()]
        to_trace.append("%s.%s(%s)" % (accessor, methodname, ", ".join(args)))
        return to_trace

    @classmethod
    def marshall(cls, x):
        try:
            if x.IsA("vtkSMProxy"):
                return Trace.get_accessor(sm._getPyProxy(x))
        except AttributeError:
            return "'%s'" % x if type(x) == str else x


def _bind_on_event(ref):
    def _callback(obj, string):
        ref().on_event(obj, string)
    return _callback

class CallMethodIfPropertiesModified(CallMethod):
    """Similar to CallMethod, except that the trace will get logged only
    if the proxy fires PropertiesModified event before the trace-item is
    finalized."""
    def __init__(self, proxy, methodname, *args, **kwargs):
        self.proxy = proxy
        self.methodname = methodname
        self.args = args
        self.kwargs = kwargs
        self.tag = proxy.AddObserver("PropertyModifiedEvent", _bind_on_event(weakref.ref(self)))
        self.modified = False
    def on_event(self, obj, string):
        self.modified = True
    def finalize(self):
        self.proxy.RemoveObserver(self.tag)
        self.tag = None
        if self.modified:
            trace = self.get_trace(self.proxy, self.methodname, self.args, self.kwargs)
            Trace.Output.append_separated(trace)
        CallMethod.finalize(self)
    def __del__(self):
        if self.proxy and self.tag:
            self.proxy.RemoveObserver(self.tag)


class CallFunction(TraceItem):
    def __init__(self, functionname, *args, **kwargs):
        TraceItem.__init__(self)
        to_trace = []
        try:
            to_trace.append("# " + kwargs["comment"])
            del kwargs["comment"]
        except KeyError:
            pass
        args = [str(CallMethod.marshall(x)) for x in args]
        args += ["%s=%s" % (key, CallMethod.marshall(val)) for key, val in kwargs.iteritems()]
        to_trace.append("%s(%s)" % (functionname, ", ".join(args)))
        Trace.Output.append_separated(to_trace)

class SaveCameras(BookkeepingItem):
    """This is used to request recording of cameras in trace"""
    # This is a little hackish at this point. We'll figure something cleaner out
    # in time.
    def __init__(self, proxy=None):
        trace = self.get_trace(proxy)
        if trace:
            Trace.Output.append_separated(trace)

    @classmethod
    def get_trace(cls, proxy=None):
        trace = TraceOutput()
        proxy = sm._getPyProxy(proxy)
        if proxy is None:
            views = [x for x in simple.GetViews() if Trace.has_accessor(x)]
            for v in views:
                trace.append_separated(cls.get_trace(proxy=v))
        elif proxy.IsA("vtkSMViewLayoutProxy"):
            views = simple.GetViewsInLayout(proxy)
            for v in views:
                trace.append_separated(cls.get_trace(proxy=v))
        elif proxy.IsA("vtkSMViewProxy"):
            if proxy.GetProperty("CameraPosition"):
                accessor = Trace.get_accessor(proxy)
                trace.append("# current camera placement for %s" % accessor)
                prop_names = ["CameraPosition", "CameraFocalPoint",
                         "CameraViewUp", "CameraViewAngle",
                         "CameraParallelScale", "CameraParallelProjection",
                         "EyeAngle", "InteractionMode"]
                props = [x for x in accessor.get_properties() \
                    if x.get_property_name() in prop_names and \
                       not x.get_object().IsValueDefault()]
                if props:
                    trace.append(accessor.trace_properties(props, in_ctor=False))
            else: pass # non-camera views
        elif proxy.IsA("vtkSMAnimationSceneProxy"):
            for view in proxy.GetProperty("ViewModules"):
                trace.append_separated(cls.get_trace(proxy=view))
        else:
            raise Untraceable("Invalid argument type %r"% proxy)
        return trace.raw_data()


# __ActiveTraceItems is simply used to keep track of items that are currently
# active to avoid non-nestable trace items from being created when previous
# items are active.
__ActiveTraceItems = []

def _create_trace_item_internal(key, args=None, kwargs=None):
    global __ActiveTraceItems

    # trim __ActiveTraceItems to remove None references.
    __ActiveTraceItems = [x for x in __ActiveTraceItems if not x() is None]

    g = globals()
    if g.has_key(key) and callable(g[key]):
        args = args if args else []
        kwargs = kwargs if kwargs else {}
        traceitemtype = g[key]
        if len(__ActiveTraceItems) == 0 or issubclass(traceitemtype, NestableTraceItem):
            instance = traceitemtype(*args, **kwargs)
            if not issubclass(traceitemtype, BookkeepingItem):
                __ActiveTraceItems.append(weakref.ref(instance))
            return instance
        raise Untraceable("Non-nestable trace item. Ignoring in current context.")
    raise Untraceable("Unknown trace item type %s" % key)
    #print "Hello again", key, args
    #return A(key)

def _start_trace_internal():
    """**internal** starts tracing. Called by vtkSMTrace::StartTrace()."""
    Trace.reset()
    Trace.Output.append([\
        "#### import the simple module from the paraview",
        "from paraview.simple import *",
        "#### disable automatic camera reset on 'Show'",
        "paraview.simple._DisableFirstRenderCameraReset()"])
    return True

def _stop_trace_internal():
    """**internal** stops trace. Called by vtkSMTrace::StopTrace()."""
    camera_trace = SaveCameras.get_trace(None)
    if camera_trace:
        Trace.Output.append_separated(\
            "#### saving camera placements for all active views")
        Trace.Output.append_separated(camera_trace)
    Trace.Output.append_separated([\
        "#### uncomment the following to render all views",
        "# RenderAllViews()",
        "# alternatively, if you want to write images, you can use SaveScreenshot(...)."
        ])
    trace = str(Trace.Output)
    Trace.reset()
    return trace

#------------------------------------------------------------------------------
# Public methods
#------------------------------------------------------------------------------
def start_trace():
    """Starting tracing. On successful start, will return a vtkSMTrace object.
    One can set tracing options on it to control how the tracing. If tracing was
    already started, calling this contine with the same trace."""
    return sm.vtkSMTrace.StartTrace()

def stop_trace():
    """Stops the trace and returns the generated trace output string."""
    return sm.vtkSMTrace.StopTrace()

def get_current_trace_output(raw=False):
    """Returns the trace generated so far in the tracing process."""
    return str(Trace.Output) if not raw else Trace.Output.raw_data()

def get_current_trace_output_and_reset(raw=False):
    """Equivalent to calling::

            get_current_trace_output(raw)
            reset_trace_output()
    """

    output = get_current_trace_output(raw)
    reset_trace_output()
    return output


def reset_trace_output():
    """Resets the trace output without resetting the tracing datastructures
    themselves."""
    Trace.Output.reset()

#------------------------------------------------------------------------------
if __name__ == "__main__":
    print "Running test"
    start_trace()

    s = simple.Sphere()
    c = simple.PlotOverLine()
    simple.Show()

    print "***** TRACE RESULT *****"
    print stop_trace()
