# name_context.py
"""
NameContext defines the context of a particular named asset.

Examples include scenes in a Blender file, but could be applied to other things.
Also a base class for FileContext which applies to the whole file.

NameContext handles the unified needs of these types, and in particular,
hosts the methods used to generate names.
"""

import os, re

import yaml

from .name_schema import FieldSchema

class NameContext(object):
    """
    Single naming context within the file (e.g. a Blender scene).
    
    NameContext defines the characteristics of any particular project
    unit (taxon) within the project. So, for example, it may represent
    the context of an "Episode" or a "Sequence" or a "Shot".
    
    Used in Blender, it will typically be used in two ways: one to represent
    the entire file (as the base class for FileContext) and one to represent
    a particular Blender scene within the file, which may represent a single
    shot, multiple shots with the same camera, or perhaps just an element
    of a compositing shot created from multiple Blender scenes.
    
    Examples of all three uses occur in "Lunatics!" episode 1: the
    "Press Conference" uses multicam workflow, with scenes for each camera
    with multiple shots selected from the timeline, using the VSE; most scenes
    are done 'single camera' on a shot-per-scene basis; but some shots use a
    'Freestyle camera clipping' technique which puts the line render in a
    separate (but linked) scene, while the final shot in the episode combines
    three different Blender scenes in a 2D composite, to effect a smooth
    transition to the titles.
    
    Attributes:
        container (NameContext):
                        The project unit that contains this one. One step
                        up the tree, but not necessarily the next step up
                        in rank (because there can be skipped ranks).
                        
        schemas (list(FieldSchema)):
                        The schema list as seen by this unit, taking into
                        account any schema overrides.
                        
        namepath_segment (list):
                        List of namepath codes defined in this object, not
                        including the container's namepath. (Implementation)
                        
        omit_ranks dict(str:int):
                        How many ranks to omit from the beginning in shortened
                        names for specific uses. (Implementation).
                        Probably a mistake this isn't in the FieldSchema instead.
                        
        fields (dict):  The field values used to initialize the NameContext,
                        May include some details not defined in this attribution
                        API, and it includes the raw state of the 'name', 'code',
                        and 'title' fields, determining which are
                        authoritative -- i.e. fields which aren't specified are
                        left to 'float', being generated by the related ones.
                        Thus, name implies title, or title implies name. You
                        can have one or the other or both, but the other will be
                        generated from the provided one if it isn't specified.
                        (Implementation).
                        
        code (str|int|Enum):
                        Identification code for the unit (I replaced 'id' in earlier
                        versions, because 'id' is a reserved word in Python for Python
                        memory reference / pointer identity.
                        (R/W Property).
        
        namepath (list):
                        List of codes for project units above this one.
                        (R/O Property, generated from namepath_segment and
                        container).
        
        rank (int):     Rank of this unit (same as in Schema).
                        (R/W Property, may affect other attributes).
                        
        name (str):     Short name for the unit.
                        (R/W Property, may affect title).
                        
        title (str):    Full title for the unit.
                        (R/W Property, may affect name).
                        
        designation (str):
                        Full designation for the unit, including all
                        the namepath elements, but no title.
                        
        fullname (str): The full designation, plus the current unit name.
        
        shortname (str):Abbreviated designation, according to omit_ranks,
                        with name.    
    """
    
    def __init__(self, container, fields=None, namepath_segment=(), ):
        self.clear()     
        if container or fields or namepath_segment:
            self.update(container, fields, namepath_segment)
        
    def clear(self):
        self.fields = {}
        self.schemas = ['project']
        self.rank = 0
        self.code = 'untitled'
        self.container = None
        self.namepath_segment = []
        
    def update(self, container=None, fields=None, namepath_segment=()):
        self.container = container
        
        if namepath_segment:
            self.namepath_segment = namepath_segment
        else:
            self.namepath_segment = []
        
        try:
            self.schemas  = self.container.schemas
        except AttributeError:
            self.schemas = []
            
        try:
            self.omit_ranks = self.container.omit_ranks
        except AttributeError:
            self.omit_ranks = {}
            self.omit_ranks.update({
                'edit': 0,
                'render': 1,
                'filename': 1,
                'scene': 3})
            
        if fields:
            if isinstance(fields, dict):
                self.fields.update(fields)
            elif isinstance(fields, str):
                self.fields.update(yaml.safe_load(fields))

    def update_fields(self, data):
        self.fields.update(data)
        
    def _load_schemas(self, schemas, start=0):
        """
        Load schemas from a list of schema dictionaries.
        
        @schemas: list of dictionaries containing schema field data (see FieldSchema).
        The data will typically be extracted from YAML, and is
        expected to be a list of dictionaries, each of which defines
        fields understood by the FieldSchema class, to instantiate
        FieldSchema objects. The result is a linked chain of schemas from
        the top of the project tree down.
        
        @start: if a start value is given, the top of the existing schema
        chain is kept, and the provided schemas starts under the rank of
        the start level in the existing schema. This is what happens when
        the schema is locally overridden at some point in the hierarchy.
        """
        self.schemas = self.schemas[:start]
        if self.schemas:
            last = self.schemas[-1]
        else:
            last = None
        for schema in schemas:
            self.schemas.append(FieldSchema(last, schema['rank'], schema=schema))
            #last = self.schemas[-1]
            
    def _parse_words(self, wordtext):
        words = []
        groups = re.split(r'[\W_]', wordtext)
        for group in groups:
            if len(group)>1:
                group = group[0].upper() + group[1:]
                words.extend(re.findall(r'[A-Z][a-z]*', group))
            elif len(group)==1:
                words.append(group[0].upper())
            else:
                continue
        return words
                
    def _cap_words(self, words):
        return ''.join(w.capitalize() for w in words)
    
    def _underlower_words(self, words):
        return '_'.join(w.lower() for w in words)
    
    def _undercap_words(self, words):
        return '_'.join(w.capitalize() for w in words)
    
    def _spacecap_words(self, words):
        return ' '.join(w.capitalize() for w in words)        
        
    def _compress_name(self, name):
        return self._cap_words(self._parse_words(name))
    
    @property
    def namepath(self):
        if self.container:
            return self.container.namepath + self.namepath_segment
        else:
            return self.namepath_segment
    
    @property
    def rank(self):
        if 'rank' in self.fields:
            return self.fields['rank']
        else:
            return None
        
    @rank.setter
    def rank(self, rank):
        self.fields['rank'] = rank
    
    @property
    def name(self):
        if 'name' in self.fields:
            return self.fields['name']
        elif 'title' in self.fields:
            return self._compress_name(self.fields['title'])
#         elif 'code' in self.fields:
#             return self.fields['code']
        else:
            return ''
        
    @name.setter
    def name(self, name):
        self.fields['name'] = name
        
    @property
    def code(self):
        if self.rank:
            return self.fields[self.rank]['code']
        else:
            return self.fields['code']
        
    @code.setter
    def code(self, code):
        if self.rank:
            self.fields[self.rank] = {'code': code}
        else:
            self.fields['code'] = code

    @property
    def description(self):
        if 'description' in self.fields:
            return self.fields['description']
        else:
            return ''
        
    @description.setter
    def description(self, description):
        self.fields['description'] = str(description)
        
    def _get_name_components(self):
        components = []
        for code, schema in zip(self.namepath, self.schemas):
            if code is None: continue
            components.append(schema.format.format(code))
            components.append(schema.delimiter)
        return components[:-1]
        
    @property
    def fullname(self):
        if self.name:
            return (self.designation + 
                    self.schemas[-1].delimiter + 
                    self._compress_name(self.name) )
        else:
            return self.designation
                    
    @property
    def designation(self):
        return ''.join(self._get_name_components())
                   
    @property
    def shortname(self):
        namebase = self.omit_ranks['filename']*2
        return  (''.join(self._get_name_components()[namebase:]) +
                        self.schemas[-1].delimiter +
                        self._compress_name(self.name))
    
    def get_scene_name(self, suffix=''):
        """
        Create a name for the current scene, based on namepath.
        
        Arguments:
            suffix (str):   Optional suffix code used to improve
                            identifications of scenes.
        """
        namebase = self.omit_ranks['scene']*2
        desig = ''.join(self._get_name_components()[namebase:])     
        
        if suffix:
            return desig + ' ' + suffix
        else:
            return desig
        
    def get_render_path(self, suffix='', framedigits=5, ext='png'):
        """
        Create a render filepath, based on namepath and parameters.
        
        Arguments:
            suffix (str):
                Optional unique code (usually for render profile).
            
            framedigits (int):
                How many digits to reserve for frame number.
                
            ext (str):
                Filetype extension for the render.
                
        This is meant to be called by render_profile to combine namepath
        based name with parameters for the specific render, to uniquely
        idenfify movie or image-stream output.
        """        
        desig = ''.join(self._get_name_components()[self.omit_ranks['render']+1:])
        
        if ext in ('avi', 'mov', 'mp4', 'mkv'):
            if suffix:
                path = os.path.join(self.render_root, suffix,
                    desig + '-' + suffix + '.' + ext)
            else:
                path = os.path.join(self.render_root, ext.upper(),
                    desig + '.' + ext)
        else:
            if suffix:
                path = os.path.join(self.render_root, 
                    suffix, desig,
                    desig + '-' + suffix + '-f' + '#'*framedigits + '.' + ext)
            else:
                path = os.path.join(self.render_root,
                    ext.upper(), desig,
                    desig + '-f' + '#'*framedigits + '.' + ext)
        return path
