text: # Copyright (c) <2021> Side Effects Software Inc.
  #  All rights reserved.
  #
  #  Redistribution and use in source and binary forms, with or without
  #  modification, are permitted provided that the following conditions are met:
  #
  #  1. Redistributions of source code must retain the above copyright notice,
  #     this list of conditions and the following disclaimer.
  #
  #  2. The name of Side Effects Software may not be used to endorse or
  #     promote products derived from this software without specific prior
  #     written permission.
  #
  #  THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS
  #  OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  #  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN
  #  NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
  #  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  #  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
  #  OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  #  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  #  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
  #  EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  
  import unreal
  
  
  class ProcessHDA(object):
      """ An object that wraps async processing of an HDA (instantiating,
      cooking/processing/baking an HDA), with functions that are called at the
      various stages of the process, that can be overridden by subclasses for
      custom funtionality:
  
          - on_failure()
          - on_complete(): upon successful completion (could be PostInstantiation
            if auto cook is disabled, PostProcessing if auto bake is disabled, or
            after PostAutoBake if auto bake is enabled.
          - on_pre_instantiation(): before the HDA is instantiated, a good place
            to set parameter values before the first cook.
          - on_post_instantiation(): after the HDA is instantiated, a good place
            to set/configure inputs before the first cook.
          - on_post_auto_cook(): right after a cook
          - on_pre_process(): after a cook but before output objects have been
            created/processed
          - on_post_processing(): after output objects have been created
          - on_post_auto_bake(): after outputs have been baked
  
      Instantiate the processor via the constructor and then call the activate()
      function to start the asynchronous process.
  
      """
      def __init__(
              self,
              houdini_asset,
              instantiate_at=unreal.Transform(),
              parameters=None,
              node_inputs=None,
              parameter_inputs=None,
              world_context_object=None,
              spawn_in_level_override=None,
              enable_auto_cook=True,
              enable_auto_bake=False,
              bake_directory_path="",
              bake_method=unreal.HoudiniEngineBakeOption.TO_ACTOR,
              remove_output_after_bake=False,
              recenter_baked_actors=False,
              replace_previous_bake=False,
              delete_instantiated_asset_on_completion_or_failure=False):
          """ Instantiates an HDA in the specified world/level. Sets parameters
          and inputs supplied in InParameters, InNodeInputs and parameter_inputs.
          If bInEnableAutoCook is true, cooks the HDA. If bInEnableAutoBake is
          true, bakes the cooked outputs according to the supplied baking
          parameters.
  
          This all happens asynchronously, with the various output pins firing at
          the various points in the process:
  
              - PreInstantiation: before the HDA is instantiated, a good place
                to set parameter values before the first cook (parameter values
                from ``parameters`` are automatically applied at this point)
              - PostInstantiation: after the HDA is instantiated, a good place
                to set/configure inputs before the first cook (inputs from
                ``node_inputs`` and ``parameter_inputs`` are automatically applied
                at this point)
              - PostAutoCook: right after a cook
              - PreProcess: after a cook but before output objects have been
                created/processed
              - PostProcessing: after output objects have been created
              - PostAutoBake: after outputs have been baked
              - Completed: upon successful completion (could be PostInstantiation
                if auto cook is disabled, PostProcessing if auto bake is disabled,
                or after PostAutoBake if auto bake is enabled).
              - Failed: If the process failed at any point.
  
          Args:
              houdini_asset (HoudiniAsset): The HDA to instantiate.
              instantiate_at (Transform):  The Transform to instantiate the HDA with.
              parameters (Map(Name, HoudiniParameterTuple)): The parameters to set before cooking the instantiated HDA.
              node_inputs (Map(int32, HoudiniPublicAPIInput)): The node inputs to set before cooking the instantiated HDA.
              parameter_inputs (Map(Name, HoudiniPublicAPIInput)): The parameter-based inputs to set before cooking the instantiated HDA.
              world_context_object (Object): A world context object for identifying the world to spawn in, if spawn_in_level_override is null.
              spawn_in_level_override (Level): If not nullptr, then the HoudiniAssetActor is spawned in that level. If both spawn_in_level_override and world_context_object are null, then the actor is spawned in the current editor context world's current level.
              enable_auto_cook (bool): If true (the default) the HDA will cook automatically after instantiation and after parameter, transform and input changes.
              enable_auto_bake (bool): If true, the HDA output is automatically baked after a cook. Defaults to false.
              bake_directory_path (str): The directory to bake to if the bake path is not set via attributes on the HDA output.
              bake_method (HoudiniEngineBakeOption): The bake target (to actor vs blueprint). @see HoudiniEngineBakeOption.
              remove_output_after_bake (bool): If true, HDA temporary outputs are removed after a bake. Defaults to false.
              recenter_baked_actors (bool): Recenter the baked actors to their bounding box center. Defaults to false.
              replace_previous_bake (bool): If true, on every bake replace the previous bake's output (assets + actors) with the new bake's output. Defaults to false.
              delete_instantiated_asset_on_completion_or_failure (bool): If true, deletes the instantiated asset actor on completion or failure. Defaults to false.
  
          """
          super(ProcessHDA, self).__init__()
          self._houdini_asset = houdini_asset
          self._instantiate_at = instantiate_at
          self._parameters = parameters
          self._node_inputs = node_inputs
          self._parameter_inputs = parameter_inputs
          self._world_context_object = world_context_object
          self._spawn_in_level_override = spawn_in_level_override
          self._enable_auto_cook = enable_auto_cook
          self._enable_auto_bake = enable_auto_bake
          self._bake_directory_path = bake_directory_path
          self._bake_method = bake_method
          self._remove_output_after_bake = remove_output_after_bake
          self._recenter_baked_actors = recenter_baked_actors
          self._replace_previous_bake = replace_previous_bake
          self._delete_instantiated_asset_on_completion_or_failure = delete_instantiated_asset_on_completion_or_failure
  
          self._asset_wrapper = None
          self._cook_success = False
          self._bake_success = False
  
      @property
      def asset_wrapper(self):
          """ The asset wrapper for the instantiated HDA processed by this node. """
          return self._asset_wrapper
  
      @property
      def cook_success(self):
          """ True if the last cook was successful. """
          return self._cook_success
  
      @property
      def bake_success(self):
          """ True if the last bake was successful. """
          return self._bake_success
  
      @property
      def houdini_asset(self):
          """ The HDA to instantiate. """
          return self._houdini_asset
  
      @property
      def instantiate_at(self):
          """ The transform the instantiate the asset with. """
          return self._instantiate_at
  
      @property
      def parameters(self):
          """ The parameters to set on on_pre_instantiation """
          return self._parameters
  
      @property
      def node_inputs(self):
          """ The node inputs to set on on_post_instantiation """
          return self._node_inputs
  
      @property
      def parameter_inputs(self):
          """ The object path parameter inputs to set on on_post_instantiation """
          return self._parameter_inputs
  
      @property
      def world_context_object(self):
          """ The world context object: spawn in this world if spawn_in_level_override is not set. """
          return self._world_context_object
  
      @property
      def spawn_in_level_override(self):
          """ The level to spawn in. If both this and world_context_object is not set, spawn in the editor context's level. """
          return self._spawn_in_level_override
  
      @property
      def enable_auto_cook(self):
          """ Whether to set the instantiated asset to auto cook. """
          return self._enable_auto_cook
  
      @property
      def enable_auto_bake(self):
          """ Whether to set the instantiated asset to auto bake after a cook. """
          return self._enable_auto_bake
  
      @property
      def bake_directory_path(self):
          """ Set the fallback bake directory, for if output attributes do not specify it. """
          return self._bake_directory_path
  
      @property
      def bake_method(self):
          """ The bake method/target: for example, to actors vs to blueprints. """
          return self._bake_method
  
      @property
      def remove_output_after_bake(self):
          """ Remove temporary HDA output after a bake. """
          return self._remove_output_after_bake
  
      @property
      def recenter_baked_actors(self):
          """ Recenter the baked actors at their bounding box center. """
          return self._recenter_baked_actors
  
      @property
      def replace_previous_bake(self):
          """ Replace previous bake output on each bake. For the purposes of this
          node, this would mostly apply to .uassets and not actors.
  
          """
          return self._replace_previous_bake
  
      @property
      def delete_instantiated_asset_on_completion_or_failure(self):
          """ Whether or not to delete the instantiated asset after Complete is called. """
          return self._delete_instantiated_asset_on_completion_or_failure
  
      def activate(self):
          """ Activate the process. This will:
  
                  - instantiate houdini_asset and wrap it as asset_wrapper
                  - call on_failure() for any immediate failures
                  - otherwise bind to delegates from asset_wrapper so that the
                  various self.on_*() functions are called as appropriate
  
              Returns immediately (does not block until cooking/processing is
              complete).
  
          Returns:
              (bool): False if activation failed.
  
          """
          # Get the API instance
          houdini_api = unreal.HoudiniPublicAPIBlueprintLib.get_api()
          if not houdini_api:
              # Handle failures: this will unbind delegates and call on_failure()
              self._handle_on_failure()
              return False
  
          # Create an empty API asset wrapper
          self._asset_wrapper = unreal.HoudiniPublicAPIAssetWrapper.create_empty_wrapper(houdini_api)
          if not self._asset_wrapper:
              # Handle failures: this will unbind delegates and call on_failure()
              self._handle_on_failure()
              return False
  
          # Bind to the wrapper's delegates for instantiation, cooking, baking
          # etc events
          self._asset_wrapper.on_pre_instantiation_delegate.add_callable(
              self._handle_on_pre_instantiation)
          self._asset_wrapper.on_post_instantiation_delegate.add_callable(
              self._handle_on_post_instantiation)
          self._asset_wrapper.on_post_cook_delegate.add_callable(
              self._handle_on_post_auto_cook)
          self._asset_wrapper.on_pre_process_state_exited_delegate.add_callable(
              self._handle_on_pre_process)
          self._asset_wrapper.on_post_processing_delegate.add_callable(
              self._handle_on_post_processing)
          self._asset_wrapper.on_post_bake_delegate.add_callable(
              self._handle_on_post_auto_bake)
  
          # Begin the instantiation process of houdini_asset and wrap it with
          # self.asset_wrapper
          if not houdini_api.instantiate_asset_with_existing_wrapper(
                  self.asset_wrapper,
                  self.houdini_asset,
                  self.instantiate_at,
                  self.world_context_object,
                  self.spawn_in_level_override,
                  self.enable_auto_cook,
                  self.enable_auto_bake,
                  self.bake_directory_path,
                  self.bake_method,
                  self.remove_output_after_bake,
                  self.recenter_baked_actors,
                  self.replace_previous_bake):
              # Handle failures: this will unbind delegates and call on_failure()
              self._handle_on_failure()
              return False
  
          return True
  
      def _unbind_delegates(self):
          """ Unbinds from self.asset_wrapper's delegates (if valid). """
          if not self._asset_wrapper:
              return
  
          self._asset_wrapper.on_pre_instantiation_delegate.add_callable(
              self._handle_on_pre_instantiation)
          self._asset_wrapper.on_post_instantiation_delegate.add_callable(
              self._handle_on_post_instantiation)
          self._asset_wrapper.on_post_cook_delegate.add_callable(
              self._handle_on_post_auto_cook)
          self._asset_wrapper.on_pre_process_state_exited_delegate.add_callable(
              self._handle_on_pre_process)
          self._asset_wrapper.on_post_processing_delegate.add_callable(
              self._handle_on_post_processing)
          self._asset_wrapper.on_post_bake_delegate.add_callable(
              self._handle_on_post_auto_bake)
  
      def _check_wrapper(self, wrapper):
          """ Checks that wrapper matches self.asset_wrapper. Logs a warning if
          it does not.
  
          Args:
              wrapper (HoudiniPublicAPIAssetWrapper): the wrapper to check
              against self.asset_wrapper
  
          Returns:
              (bool): True if the wrappers match.
  
          """
          if wrapper != self._asset_wrapper:
              unreal.log_warning(
                  '[UHoudiniPublicAPIProcessHDANode] Received delegate event '
                  'from unexpected asset wrapper ({0} vs {1})!'.format(
                      self._asset_wrapper.get_name() if self._asset_wrapper else '',
                      wrapper.get_name() if wrapper else ''
                  )
              )
              return False
          return True
  
      def _handle_on_failure(self):
          """ Handle any failures during the lifecycle of the process. Calls
          self.on_failure() and then unbinds from self.asset_wrapper and
          optionally deletes the instantiated asset.
  
          """
          self.on_failure()
  
          self._unbind_delegates()
  
          if self.delete_instantiated_asset_on_completion_or_failure and self.asset_wrapper:
              self.asset_wrapper.delete_instantiated_asset()
  
      def _handle_on_complete(self):
          """ Handles completion of the process. This can happen at one of 
          three stages:
  
              - After on_post_instantiate(), if enable_auto_cook is False.
              - After on_post_auto_cook(), if enable_auto_cook is True but
                enable_auto_bake is False.
              - After on_post_auto_bake(), if both enable_auto_cook and
                enable_auto_bake are True.
  
          Calls self.on_complete() and then unbinds from self.asset_wrapper's
          delegates and optionally deletes the instantiated asset.
  
          """
          self.on_complete()
  
          self._unbind_delegates()
  
          if self.delete_instantiated_asset_on_completion_or_failure and self.asset_wrapper:
              self.asset_wrapper.delete_instantiated_asset()
  
      def _handle_on_pre_instantiation(self, wrapper):
          """ Called during pre_instantiation. Sets ``parameters`` on the HDA
          and calls self.on_pre_instantiation().
  
          """
          if not self._check_wrapper(wrapper):
              return
  
          # Set any parameters specified for the HDA
          if self.asset_wrapper and self.parameters:
              self.asset_wrapper.set_parameter_tuples(self.parameters)
  
          self.on_pre_instantiation()
  
      def _handle_on_post_instantiation(self, wrapper):
          """ Called during post_instantiation. Sets inputs (``node_inputs`` and
          ``parameter_inputs``) on the HDA and calls self.on_post_instantiation().
  
          Completes execution if enable_auto_cook is False.
  
          """
          if not self._check_wrapper(wrapper):
              return
  
          # Set any inputs specified when the node was created
          if self.asset_wrapper:
              if self.node_inputs:
                  self.asset_wrapper.set_inputs_at_indices(self.node_inputs)
              if self.parameter_inputs:
                  self.asset_wrapper.set_input_parameters(self.parameter_inputs)
  
          self.on_post_instantiation()
  
          # If not set to auto cook, complete execution now
          if not self.enable_auto_cook:
              self._handle_on_complete()
  
      def _handle_on_post_auto_cook(self, wrapper, cook_success):
          """ Called during post_cook. Sets self.cook_success and calls
          self.on_post_auto_cook().
  
          Args:
              cook_success (bool): True if the cook was successful.
  
          """
          if not self._check_wrapper(wrapper):
              return
  
          self._cook_success = cook_success
  
          self.on_post_auto_cook(cook_success)
  
      def _handle_on_pre_process(self, wrapper):
          """ Called during pre_process. Calls self.on_pre_process().
  
          """
          if not self._check_wrapper(wrapper):
              return
  
          self.on_pre_process()
  
      def _handle_on_post_processing(self, wrapper):
          """ Called during post_processing. Calls self.on_post_processing().
  
          Completes execution if enable_auto_bake is False.
  
          """
          if not self._check_wrapper(wrapper):
              return
  
          self.on_post_processing()
  
          # If not set to auto bake, complete execution now
          if not self.enable_auto_bake:
              self._handle_on_complete()
  
      def _handle_on_post_auto_bake(self, wrapper, bake_success):
          """ Called during post_bake. Sets self.bake_success and calls
          self.on_post_auto_bake().
  
          Args:
              bake_success (bool): True if the bake was successful.
  
          """
          if not self._check_wrapper(wrapper):
              return
  
          self._bake_success = bake_success
  
          self.on_post_auto_bake(bake_success)
  
          self._handle_on_complete()
  
      def on_failure(self):
          """ Called if the process fails to instantiate or fails to start
          a cook.
  
          Subclasses can override this function implement custom functionality.
  
          """
          pass
  
      def on_complete(self):
          """ Called if the process completes instantiation, cook and/or baking,
          depending on enable_auto_cook and enable_auto_bake.
  
          Subclasses can override this function implement custom functionality.
  
          """
          pass
  
      def on_pre_instantiation(self):
          """ Called during pre_instantiation.
  
          Subclasses can override this function implement custom functionality.
  
          """
          pass
  
      def on_post_instantiation(self):
          """ Called during post_instantiation.
  
          Subclasses can override this function implement custom functionality.
  
          """
          pass
  
      def on_post_auto_cook(self, cook_success):
          """ Called during post_cook.
  
          Subclasses can override this function implement custom functionality.
  
          Args:
              cook_success (bool): True if the cook was successful.
  
          """
          pass
  
      def on_pre_process(self):
          """ Called during pre_process.
  
          Subclasses can override this function implement custom functionality.
  
          """
          pass
  
      def on_post_processing(self):
          """ Called during post_processing.
  
          Subclasses can override this function implement custom functionality.
  
          """
          pass
  
      def on_post_auto_bake(self, bake_success):
          """ Called during post_bake.
  
          Subclasses can override this function implement custom functionality.
  
          Args:
              bake_success (bool): True if the bake was successful.
  
          """
          pass
  