# Copyright (c) 2011 The Foundry Visionmongers Ltd.  All Rights Reserved.
from . import util
from hiero.core import (IExporterRegistry,
                       log,
                       TaskBase,
                       TaskPresetBase,
                       project,
                       Format,
                       isNC,
                       isIndie,
                       FnFloatRange,
                       util,
                       FolderTask,
                       FolderTaskPreset)
from hiero.core.FnExporterBase import classBasename
import _nuke
import xml.etree.ElementTree as etree
from xml.dom import minidom
import glob
import re
import os
import shutil
import sys
import copy
import _fnpython
import hashlib
import itertools
import errno
class ExportHistory(object):
  def __init__(self, presets=[]):
    self._presets = presets
  def findPreset(self, presetId):
    for preset in self._presets:
      if preset._properties["id"] == presetId:
        return preset
    return None
  def addPreset(self, preset):
    presetId = taskRegistry.getPresetId(preset)
    if self.findPreset(presetId) == None:
      presetCopy = taskRegistry.copyPreset(preset)
      presetCopy.properties()["id"] = presetId
      self._presets.append(presetCopy)
    return presetId
  def presets(self):
    return self._presets
[docs]class TaskRegistry ( IExporterRegistry ):
  
  def __init__( self ):
    IExporterRegistry.__init__(self)
    # _tasks [str(type(task))] = type(task)
    self._tasks = dict()
    # _processors [str(type(processor))] = type(processor)
    self._processors = dict()
    # _presets [preset.parentType()] = type(preset)
    self._presets = dict()
            
    # _presets [filenameWithoutExtenion] = exporterpresetinstance
    self._processorPresets = []
    
    self._defaultPresets = None
    
    # Mapping of string type names to the types. This is used for serializing values
    # in presets
    self._typeDict = dict((classBasename(valuetype), valuetype) for valuetype in (int, tuple, bool, str, float, list, dict, type(None)))
    # In Python 2 Nuke, strings might have been saved as 'str' or 'unicode' make
    # sure they get loaded
    self._typeDict['unicode'] = str
    self._submissions = []
    self._currentSubmissionName = None
    self._projectExportHistories = dict()
    # A list of presets which are stored while the user is editing them.  Kept so the pre-edit state
    # can be restored.
    self._storedPresets = []
    
[docs]  def registerTask(self, preset, task):
    """Register the association between a Task and TaskPreset"""
    self._tasks[classBasename(task)] = task
    self.registerPreset( task, preset )
    log.info( "Task Registered " + str(task) ) 
       
[docs]  def registerProcessor (self, preset, processor):
    """Register the association between a Processor and ProcessorPreset"""
    self._processors[classBasename(processor)] = processor
    self.registerPreset( processor, preset )
    log.info( "Processor Registered " + str(processor) ) 
     
[docs]  def registerPreset( self, parentType, preset ):
    """Register a preset instance and association with parentType"""
    self._presets[classBasename(parentType)] = preset 
  
[docs]  def projectUnloaded(self, project):
    """Called on project unload to remove presets associated with project"""
    log.debug("ExportRegistry : Project %s Presets Unloaded"  % project.name())
    presets = self.projectPresets(project)
    for preset in presets:
      try:
        preset.setMarkedForDeletion()
        self._processorPresets.remove(preset)
      except Exception as e:
        log.error("Couldn't delete preset %s from list %s" % (preset.name(), ", ".join(presets)))
        log.error(str(e))
    if project in self._projectExportHistories:
      del self._projectExportHistories[project] 
[docs]  def projectDuplicated ( self, project, newProject ):
    """Called on project clone to duplicate the associated project presets"""
    log.debug("ExportRegistry : Project %s Presets Duplicated" % project.name())
    # Duplicate the project presets
    presets = self.projectPresets(project)
    for preset in presets:
      newpreset = self.copyPreset(preset)
      # Assign preset to the duplicated project
      newpreset.setProject(newProject)
      newpreset.setReadOnly(preset.readOnly())
      self._processorPresets.append(newpreset)
    # Duplicate the project history xml
    historyXml = self.projectExportHistoryXml(project)
    self.restoreProjectExportHistoryXml(newProject, historyXml) 
  
[docs]  def loadPresets ( self, path ):
    """Load all xml presets within specified path and register"""
    processorPresetResult = self._loadPresets(os.path.join(path, "Processors"), self._processorPresets)
    return processorPresetResult 
  def _loadPresets ( self, path, list ):
    # If path doesn't exist there are no presets to load.
    if not util.filesystem.exists(path):
      return False
    dictionary = dict([ (preset.name(), preset) for preset in list if not preset.project() and not preset.markedForDeletion()])
    # for each file in each subdirectory 
    for dir in os.listdir(path):
      presets = glob.glob(os.path.join(path, dir, "*.xml"))
 
      for filepath in presets:
        try:
          # open file and parse xml
          file = util.filesystem.openFile(filepath, 'r')
        except Exception as e:
          log.error("Unable to open preset file : %s \n%s" % (filepath, str(e)) )
          continue
          
        # extract preset name from the filename
        presetName = os.path.splitext(os.path.basename(filepath))[0]
        preset = None
        try :
          preset = self.presetFromXml ( file.read(), False )
        except Exception as e:
          log.exception( "Failed to build preset from file: %s\n%s",  filepath, str(e) )
        
        # if preset was successfully created
        if preset:
          log.info( "Loaded preset : " + presetName )
          
          # If don't have write permissions for file, set preset as read only
          if not util.filesystem.access(filepath, os.W_OK):
            preset.setReadOnly(True)
          
          # ensure no name clash
          if presetName in dictionary:
            if dictionary[presetName].savePath() != path:
              presetName = util.uniqueKey(presetName, dictionary)
            else:
              continue
              
          preset.setName(presetName)
          # Keep the path the preset was loaded from so it can be saved back there
          preset.setSavePath(path)
          # add to registry
          self.addProcessorPreset(presetName, preset)
    return True
        
[docs]  def presetFromXml ( self, xml, register = True):
    """Deserialize preset from xml string.
    Requires derived TaskPreset classes to be registered."""
    preset = None
    
    # Create a dictionary where the key is a string representation of the Task
    # and the value is the type(Task). Used to figure out the Task type and instantiate.
    taskTypeDict = {}
    taskTypeDict.update(dict((classBasename(value), self._presets[key]) for (key, value) in list(self._tasks.items())))
    taskTypeDict.update(dict((classBasename(value), self._presets[key]) for (key, value) in list(self._processors.items())))
    try:
      root = etree.XML(xml)
    except Exception as e:
      log.error( "Error Parsing XML\n" + str(e) )
      raise
      
    #the Task type is store as a string attribute on the root node
    taskType = root.get("tasktype")        
    presetName = root.get("presetname", "New Preset")
    properties = {}
    
    # ensure this task exists in the registry
    if taskType is not None:
      if taskType in taskTypeDict:
      
        # each element in root represents one preset property        
        for element in root:
          properties[element.tag] = self._loadPresetElement(element)
        
        # Instantiate preset type and add to registry
        preset = taskTypeDict[taskType]("preset", properties)
        preset.setName(presetName)
        
        # Register preset incase it is being passed back to C++ 
        # otherwise object will be cleaned up when reference goes out of scope
        if register:
          self.addProcessorPreset(presetName, preset)
          
      else:
        log.error( "Error! Task type %s Not recognised " % taskType )
        
    return preset 
  
  def _loadPresetElement ( self, element ):
    # lookup 'valuetype' in type dictionary. 
    typeName = element.get("valuetype")
    
    elementtext = element.text
    if elementtext is not None:
      elementtext = elementtext.strip()
    if typeName in self._typeDict:
      valueType = self._typeDict[typeName]
      # If the this is a nested dictionary, recurse
      if valueType is dict:
        properties = {}
        for childElement in element:        
          properties[childElement.tag] = self._loadPresetElement(childElement)
        return properties
      elif valueType in (list, tuple):
        properties = []
        for childElement, index in zip(element, list(range(0, len(element)))):
          properties.append(self._loadPresetElement(childElement))
        return valueType(properties)
      elif valueType is bool:
        return elementtext == 'True'
      elif valueType is type(None):
        return None
      elif valueType is str and not element.text:
        return valueType("")
      else:     
        # instantiate value as correct type and assign to dictionary 
        # with element name as the key
        return valueType(elementtext)
    elif typeName in [ classBasename(value) for value in list(self._presets.values()) ] :
      try:
        preset = self.presetFromXml(etree.tostring(element.find("root")), False)
        return preset
        
      except Exception as e:
        log.error( "Failed to parse nested Preset " + str(e) )
        return None
    else:
      log.error( "Type " + typeName + " not recognised" )
      return None
      
  
[docs]  def savePresets ( self, path ):
    """ Save all registered presets, as xml, to path specified. """
    try:
      # Clear the stored presets, which means projectPresetsChanged() and localPresetsChanged() will return False until after another startPresetChanges() and then any modifications.
      # The relevant C++ code is now using modification times to keep track of project preset modifications.
      self._storedPresets = []
      processorPresetResult = self._savePresets(os.path.join(path, "Processors"), self._processorPresets, self._processors)
      return processorPresetResult
    except Exception as e:
      log.error("Could not write to preset path %s. Check permissions" % path)
      log.error("Exception : " + str(e))
      return False 
  def _savePresets ( self, path, presetlist, dictionary ):
    # for each preset in the dictionary
    for preset in presetlist:
      
      presetName = preset.name()
      
      newPath = None
      savePath = preset.savePath()
      
      # This preset will be saved within the project file
      if preset.project() is not None:
        continue
      
      # If this preset is set as ReadOnly don't try and write to it.
      if preset.readOnly():
        continue
        
      # If the preset was loaded from XML, try to save it back to its original location.  
      # If that's not writeable, we save it to the default path.      
      if savePath:
        newPath = os.path.join(preset.savePath(), classBasename(preset.parentType()), presetName+".xml")
        if util.filesystem.exists(newPath) and not util.filesystem.access(newPath, os.W_OK):
          newPath = None
    
      if not newPath:
        # build a path <root>/<taskname>/<presetname>.xml
        newPath = os.path.join(path, classBasename(preset.parentType()), presetName+".xml")
    
      if preset.markedForDeletion():
        # Preset has been marked for deletion, remove file and skip xml generation
        try:
          os.remove(newPath)
        except OSError as e:
          if e.errno == errno.ENOENT:
            # The file might not exist if the preset was added then removed without it being saved
            # Don't log an error
            pass
          else:
            hiero.core.log.exception("Failed to delete preset file")
        
        continue
            
      # if path doesn't already exist, create it
      dstDir = os.path.dirname(newPath)
      try:
        util.filesystem.makeDirs(dstDir)
      except:
        log.error("Could not create preset path %s. Check permissions" % dstDir)
        return False
      # build xml from preset
      xml = self.presetToPrettyXml(preset)
      
      try:
        # open and write to file
        file = util.filesystem.openFile(newPath, 'w')
        file.write(xml)
        log.info( "Saved Preset : " + presetName + " to: " + newPath )
      except:
        log.info( "Failed to write preset to path: %s", newPath )
        return False
      
    return True
    
[docs]  def presetToXml ( self, preset ):
    """Serialise a TaskPreset to XML and return as string. 
    Returns an empty string on failure.
    """
    try:
      root = self._presetToXml(preset)
      return etree.tostring(root).decode()
    except TypeError:
      return '' 
    
  def _presetToXml ( self, preset ):
    """ Serialize a preset to XML and return the root element. Throws on failure. """
    if issubclass(type(preset), TaskPresetBase):
      # create a root node with the task type as an attribute
      root = etree.Element("root", tasktype=classBasename(preset.parentType()), presetname=preset.name())       
      # for each key value pair in the properties dictionary, create an element
      for (key, value) in list(preset._properties.items()):
        # add the type(value) as an attribute
        self._savePresetElement( key, value, root)
      return root
    else:
      raise TypeError("Unexpected type %s" % type(preset))
    
  def _savePresetElement (self, key, value, parent):
    """ Save a preset key/value as an XML element and append it to the parent.  Called recursively for containers. """
    # Check for the objects with the Default type, and extract the wrapped value
    if isinstance(value, FnFloatRange.Default):
      value = value.value()
    valueType = type(value)
    # Is the export of this value type supported?
    if classBasename(valueType) in self._typeDict or issubclass( valueType, TaskPresetBase ) or hasattr(value, '_toXml'):
      # Add New Element
      element = etree.Element( str(key), valuetype=classBasename(valueType) )
      parent.append(element)
      # If this is a nested dictionary, recurse
      if valueType is dict:
        for (childKey, childValue) in list(value.items()):
          self._savePresetElement( childKey, childValue, element )
      elif valueType in (list, tuple):
        for child, index in zip(value, list(range(0, len(value)))):
          self._savePresetElement( "SequenceItem", child, element)
      elif issubclass( valueType, TaskPresetBase ):
        element.append(self._presetToXml(value))
      elif hasattr(value, '_toXml'):
        value._toXml(element)
      else:
        element.text = str(value)
    else:
      log.error( "Warning: Invalid property Name: " + str(key) + " of Type: " + str(valueType) )
[docs]  def presetsSubDirectory(self):
    # Task presets are now versioned. They are stored in a sub-directory with
    # Nuke version information under TaskPresets.
    env = _nuke.env
    major = env["NukeVersionMajor"]
    minor = env["NukeVersionMinor"]
    phase = env["NukeVersionPhase"]
    versionString = "%s.%s" % (major, minor)
    # For non-final release builds, a 'beta' suffix is also added. Phase will
    # empty for a release build.
    if phase:
      versionString += "beta"
    return os.path.join("TaskPresets", versionString) 
[docs]  def revertDefaultPresets(self):
    if callable(self._defaultPresets):
      self._defaultPresets(True)
    else:
      log.error("Default Preset callback not set") 
[docs]  def setDefaultPresets(self, defaultPresets):
    self._defaultPresets = defaultPresets 
  
[docs]  def addDefaultPresets(self, overwrite=False):
    if callable(self._defaultPresets):
      self._defaultPresets(overwrite)
    else:
      log.error("Default Preset callback not set") 
[docs]  def startPresetChanges(self, project):
    # Create copies of all the local and project presets and store them so they can be restored if necessary
    local = self.localPresets()
    project = self.projectPresets(project) if project is not None else []
    self._storedPresets = []
    for preset in itertools.chain(local, project):
      presetCopy = self.copyPreset(preset)
      presetCopy.setProject(preset.project()) # Give the copy the same project
      self._storedPresets.append(presetCopy) 
[docs]  def discardPresetChanges(self, project):
    if not self._storedPresets:
      return
    # Remove presets which are local or belong to this project from the main preset list
    self._processorPresets = [preset for preset in self._processorPresets if preset.project() not in (project,None)]
    # Restore the stored presets to the list
    self._processorPresets.extend(self._storedPresets)
    self._storedPresets = [] 
[docs]  def createAndAddProcessorPreset ( self, name, typeTemplate):
    project = None
    dictionary = dict([ (p.name(), p) for p in self._processorPresets if p.project() == project and not p.markedForDeletion()])
    fixedName = util.uniqueKey(name, dictionary)
    preset = type(typeTemplate)(fixedName, dict())
    self._processorPresets.append(preset)
    preset.setName(fixedName)
    return preset 
[docs]  def copyPreset(self, preset):
    """ Create a copy of a preset.  The copy is not added to the registry. """
    # The best way to deep copy this is just to serialize to/from xml
    # Python deep copy doesn't work quite right.
    xml = self.presetToXml(preset)
    presetCopy = self.presetFromXml(xml, False)
    return presetCopy 
    
[docs]  def copyAndAddProcessorPreset ( self, preset ):
    project = preset.project()
    dictionary = dict([ (p.name(), p) for p in self._processorPresets if p.project() == project and not p.markedForDeletion()])
    fixedName = util.uniqueKey(preset.name(), dictionary)
    newpreset = self.copyPreset(preset)
    newpreset.setName(fixedName)
    newpreset.setProject(project)
    self._processorPresets.append(newpreset)
    return newpreset 
  
[docs]  def copyAndAddProjectPreset ( self, preset, project ):
    """Duplicate a preset and assign it to a project imediately to prevent name clashes"""
    newpreset = self.copyPreset(preset)
    newpreset.setProject(project)
    self.addProcessorPreset(newpreset.name(), newpreset)
    return newpreset 
    
[docs]  def addProcessorPreset (self, name, preset):
    """Register Processor Preset Instance"""
    dictionary = dict([ (p.name(), p) for p in self._processorPresets if p.project() == project and not p.markedForDeletion()])
    uniqueName = util.uniqueKey(name, dictionary)
    preset.setName(uniqueName)
    self._processorPresets.append(preset) 
  
[docs]  def removeProcessorPreset ( self, preset ):
    """Remove Processor preset from registry"""
    presetName = None
    
    # This function used to accept a preset name to identify preset from removal
    # Now preset name uniqueness isn't as strictly enforced because presets can be in different projects
    # If preset is a string then search for a preset with that name 
    if isinstance(preset, str):
      presetName = preset
      for p in self._processorPresets:
        if presetName == p.name():
          preset = p
          break
    elif hasattr(preset, "name"):
      presetName = preset.name()
      
    if preset in self._processorPresets:
      self._processorPresets.remove(preset)
    else:
      log.info( "Preset '%s' cannot be deleted as it is not registered" % (presetName, ) ) 
    
[docs]  def renameProcessorPreset (self, preset, newName):
    """Validate and update name of Processor Preset"""
    project = preset.project()
    presets = dict([ (p.name(), p) for p in self._processorPresets if p.project() == project and not p.markedForDeletion()])   
    
    fixedName = util.uniqueKey(newName, presets)
    preset.setName(fixedName) 
    
[docs]  def assignPresetToProject (self, preset, project):
    """Assign preset to project and ensure name is unique within project. Project may be None in which case preset will be assigned 'local'"""
    if project and project.isNull():
      project = None
    # Build a dictionary of presets already assigned to specified project (Even if project is None)
    presets = dict([ (p.name(), p) for p in self._processorPresets if p.project() == project and not p.markedForDeletion()])    
    
    # If preset name already exists within this subset
    if preset.name() in presets:
      # Find a unique name
      fixedName = util.uniqueKey(preset.name(), presets)
      preset.setName(fixedName)
    
    preset.setProject(project) 
[docs]  def numProcessorPresets(self) :
    """Return the total number of Processor preset instances registered"""
    return len(self._processorPresets); 
  
[docs]  def processorPresetName(self, index):
    """Return the name of Processor preset by index"""
    return list(self._processorPresets.values())[index].name() 
 
[docs]  def numTasks( self ) :
    """Returns the number of Tasks Registered"""
    return len(self._tasks) 
    
[docs]  def numProcessors(self):
    """Return the number or processors in the Registry"""
    return len(self._processors) 
      
[docs]  def taskName ( self, index ):
    """Returns a Task name by Index"""
    return list(self._tasks.keys())[index] 
[docs]  def processorName ( self, index ):
    """Returns a Processor name by index"""
    return list(self._processors.keys())[index] 
    
[docs]  def processorPresetNames(self):
    """Returns a tuple of Processor Preset names"""
    return [preset.name() for preset in self._processorPresets] 
    
[docs]  def processorPresetByName(self, name, project=None):
    """Returns the preset with specified name associated with project. If project is None preset will be searched for in local presets"""
    presets = self.projectPresets(project)
    for preset in presets:
      if name == preset.name():
        return preset
    return None 
  def _getPresetsFromList( self , presetsList , projectToFilter ):
    """Returns a list of presets name associated with 'projectToFilter' from the specified 'presetsList'"""
    presets = [ preset for preset in presetsList if preset.project() == projectToFilter and not preset.markedForDeletion()]
    return presets
[docs]  def projectPresets(self, project):
    """Returns a list of preset names associated with the specified project"""
    return self._getPresetsFromList( self._processorPresets , project ) 
    
[docs]  def localPresets(self):
    """Returns a list of preset names NOT associated with the specified project"""
    return self._getPresetsFromList( self._processorPresets , None ) 
[docs]  def projectExportHistoryXml(self, project):
    """ Get the project export history as a list of xml fragments. Use the xml to avoid problems with reference
        counting the preset objects when calling from C++. """
    try:
      return [ self.presetToXml(preset) for preset in self._projectExportHistories[project].presets() ]
    except:
      return [] 
[docs]  def restoreProjectExportHistoryXml(self, project, presetsXml):
    """ Set the project export history as a list of xml fragments. Use the xml to avoid problems with reference
        counting the preset objects when calling from C++. """
    presets = [ self.presetFromXml(presetXml, False) for presetXml in presetsXml ]
    self._projectExportHistories[project] = ExportHistory(presets) 
[docs]  def addPresetToProjectExportHistory(self, project, preset):
    """ Add a preset to the export history for a project. """
    try:
      history = self._projectExportHistories[project]
    except:
      history = ExportHistory()
      self._projectExportHistories[project] = history
    return history.addPreset(preset) 
[docs]  def findPresetInProjectExportHistory(self, project, presetId):
    """ Attempt to find a preset in a project's export history. """
    try:
      return self._projectExportHistories[project].findPreset(presetId)
    except:
      return None 
  def _computePresetsHash(self, presets):
    # Create a hash of the current state of the given list of task presets, returning None if the list is empty.
    if not presets:
      return None
    m = hashlib.md5()
    for preset in presets:
      m.update( self.presetToXml(preset) )
    return m.digest()
[docs]  def getPresetId(self, preset):
    """ Get the id (hash) of the given preset. """
    # If the preset has the id set as a property on it, use that.  This should only be the case
    # for presets which have been added to the export history.
    if "id" in preset.properties():
      return preset.properties()["id"]
    else:
      # We don't want the version to change the id, set it to 1 before computing the hash
      version = preset.properties()["versionIndex"]
      preset.properties()["versionIndex"] = 1
      presetXml = self.presetToXml(preset)
      preset.properties()["versionIndex"] = version
      presetHash = hashlib.md5()
      presetHash.update(presetXml.encode())
      return presetHash.hexdigest() 
[docs]  def localPresetsChanged(self):
    """ Check if the local task presets have changed since startPresetChanges() was called. """
    localPresets = list( sorted( self.localPresets() , key = lambda preset : preset.name() ) )
    storedLocalPresets = list( sorted( self._getPresetsFromList( self._storedPresets , None ) , key = lambda preset : preset.name() ) )
    return localPresets != storedLocalPresets 
[docs]  def projectPresetsChanged(self, project):
    """ Check if the task presets for the given project have changed since startPresetChanges(project) was called. """
    storedProjectPresets = list( sorted( self._getPresetsFromList( self._storedPresets , project ) , key = lambda preset : preset.name() ) )
    projectPresets = list( sorted( self.projectPresets(project) , key = lambda preset : preset.name() ) )
    return storedProjectPresets != projectPresets 
    
[docs]  def getProcessor(self, index):
    return self.processorByIndex(index) 
  
[docs]  def processorByIndex(self, index):
    """Returns a processor by index"""
    return list(self._processors.values())[index] 
  
[docs]  def getPresetType (self, ident):
    return self.presetTypeFromIdent(ident) 
    
[docs]  def presetTypeFromIdent(self, ident):
    """Resolve preset ident string to Preset class type"""
    if ident in self._presets:
      return self._presets[ident]
    return None 
    
[docs]  def createTaskFromPreset(self, preset, initDictionary):
    if preset.ident() in self._tasks:
      taskType = self._tasks[preset.ident()]
      return (taskType)(initDictionary)
    return None 
    
[docs]  def getProcessorFromPreset (self, presetName):
    return self.processorFromPreset (presetName) 
    
[docs]  def processorFromPreset (self, presetName):
    """Return type of task from preset name"""
    preset = self._processorPresets[presetName]
    processor = self._processors[preset.ident()]
    return processor   
  def _checkPresetToFormat(self, presetName, exportTemplate):
    """Checks if 'exportTemplate' defines a specific output format and if its
    valid by creating a hiero.core.Format.
    Raises a ValueError exception if a format is not valid.
    """
    for exportPath, presetTask in exportTemplate:
      toFormat = presetTask.properties().get('reformat',None)
      if toFormat and toFormat['to_type'] == 'to format':
        try:
          Format( toFormat['width'],toFormat['height'],toFormat['pixelAspect'],toFormat['name'])
        except ValueError as e:
          raise ValueError( "The selected Preset '%s' has an invalid output resolution.\n%s" % (presetName, e.message) )
  def _getToScaleFromPreset(self, exportTemplate):
    """Returns a list of scale options defined in 'exportTemplate'
    """
    outToScale = []
    for exportPath, presetTask in exportTemplate:
      toScale = presetTask.properties().get('reformat',None)
      if toScale and toScale['to_type'] == 'scale':
        outToScale.append( toScale['scale'] )
    return outToScale
  def _checkToScaleForSequence(self, presetName, toScaleOptions, itemName, seqFormat):
    """Checks if the output format for a specific sequence/clip is valid when
    scalled with th 'scale' option defined in the exportTemplate preset.
    Raises a ValueError exception if a format is not valid.
    """
    for toScale in toScaleOptions:
      try:
        Format( seqFormat.width() * toScale, seqFormat.height() * toScale, seqFormat.pixelAspect() , seqFormat.name() )
      except ValueError as e:
        raise ValueError("Unable to export '%s' because Preset '%s' is scalling it to an invalid output resolution.\n%s" % (itemName, presetName, e.message) )
  def _checkOutputResolution(self, preset, items):
    """Checks if the output resolution defined in the preset or defined by the
    items to be exported are valid for PLE / Indie variants.
    Raises a ValueError exception if a format is not valid.
    """
    if isNC() or isIndie():
      exportTemplate = preset.properties()['exportTemplate']
      presetName = preset.name()
      # check 'to format' for preset
      self._checkPresetToFormat(presetName, exportTemplate)
      presetToScale = self._getToScaleFromPreset(exportTemplate)
      for itemWrapper in items:
        item = itemWrapper.clip()
        if not item:
          item = itemWrapper.sequence()
        # checks item format
        itemFormat = None
        try:
          itemFormat = item.format()
          # create format to know if it's allowed
          itemFormat = Format( itemFormat.width(), itemFormat.height(), itemFormat.pixelAspect() , itemFormat.name())
        except ValueError as e:
          raise ValueError("Unable to export item %s.\n%s" % (item.name(), e.message))
        # checks scalled item format
        self._checkToScaleForSequence(presetName, presetToScale, item.name(), itemFormat)
[docs]  def validateExport(self, preset, items):
    """ Implements IExporterRegistry.validateExport """
    # Check the output resolution
    try:
      self._checkOutputResolution(preset, items)
    except ValueError as e:
      return str(e)
    # Generate tasks and validate them
    try:
      processor = self.createProcessor(preset)
      tasks = processor.startProcessing(items, preview=True)
      for task in tasks:
        task.validate()
    except Exception as e:
      return str(e)
    # All ok, return an empty string
    return str() 
[docs]  def createProcessor(self, preset, submissionName=None, synchronous=False):
    """ Create the processor for an export and return it. This doesn't start
    the export.
    """
    # Create the submission.  If no name was given choose the first in the list
    if not submissionName:
      submissionName = self._submissions[0][0]
    submission = self.submissionByName(submissionName)
    if not submission:
      raise RuntimeError( "Submission class not found for name %s, valid names are: %s" % (submissionName, ", ".join(self.submissionNames())) )
    submission.initialise()
    processor = self._processors[preset.ident()](preset, submission, synchronous=synchronous)
    processor.setPreset(preset)
    return processor 
[docs]  def createAndExecuteProcessor( self, preset, items, submissionName=None, synchronous=False ):
    """Instantiate the Processor associated with preset and startProcessing items"""
    self._checkOutputResolution(preset, items)
    try:
      processor = self.createProcessor(preset, submissionName, synchronous)
      processor.startProcessing(items)
    except:
      # Problems with the export are caught and set error messages on the individual tasks, and the exception
      # that arrives here doesn't contain any useful information.  Get the error string from the processor and
      # raise a new exception from that.
      errorString = processor.errors()
      if errorString:
        raise RuntimeError( processor.errors() )
      else:
        raise 
[docs]  def getProcessorPreset ( self, index ):
    return self.processorPresetByIndex ( index ) 
    
[docs]  def processorPresetByIndex ( self, index ):
    """Return instance of TaskPreset Object"""
    return list(self._processorPresets.values())[index] 
[docs]  def submissionNames(self):
    return [ submission[0] for submission in self._submissions ] 
[docs]  def submissionByName(self, name):
    for n, submissionClass in self._submissions:
      if n == name:
        return (submissionClass)() 
[docs]  def addSubmission(self, name, submissionClass):
    self._submissions.append( (name, submissionClass) ) 
[docs]  def submissionChanged(self, submissionName):
    self._currentSubmissionName = submissionName 
[docs]  def isNukeShotExportPreset(self, preset):
    """ Check if a preset is valid for Nuke shot export.  To be considered valid, the preset must contain
        a NukeShotExporter and a NukeRenderTask (write node). """
    kNukeShotTaskName = 'hiero.exporters.FnNukeShotExporter.NukeShotExporter'
    kWriteNodeTaskName = 'hiero.exporters.FnExternalRender.NukeRenderTask'
    hasNukeShotExporter = False
    hasWriteNode = False
    for itemPath, itemPreset in preset.properties()["exportTemplate"]:
      if isinstance(itemPreset, TaskPresetBase):
        itemIdent = itemPreset.ident()
        if itemIdent == kNukeShotTaskName:
          hasNukeShotExporter = True
        elif itemIdent == kWriteNodeTaskName:
          hasWriteNode = True
    return (hasNukeShotExporter and hasWriteNode) 
[docs]  def nukeShotExportPresets(self, project):
    """ Get a list of presets which can export shots as Nuke scripts.
        Includes local presets and those in the project. """
    presets = []
    for preset in itertools.chain(self.localPresets(), self.projectPresets(project)):
      if self.isNukeShotExportPreset(preset):
        presets.append(preset)
    return presets 
[docs]  def isSingleSocketAllowed(self):
    """ Return whether or not single socket exports are allowed. """
    singleSocketAllowed = self._currentSubmissionName == "Single Render Process" and util.hasMultipleCPUSockets()
    return singleSocketAllowed  
taskRegistry = TaskRegistry()
log.debug( str(taskRegistry) )
taskRegistry.registerme()
# Register Base Task - This will create structures when no format 
# Do this here, because FnExporterBase gets imported in __init__.py before FnExportRegistry.py
taskRegistry.registerTask(TaskPresetBase, TaskBase)
# Register folder task
taskRegistry.registerTask(FolderTaskPreset, FolderTask)