#===========================================================================
#
# Copyright (c) 2014, California Institute of Technology.
# U.S. Government Sponsorship under NASA Contract NAS7-03001 is
# acknowledged.  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. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "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 THE COPYRIGHT
# HOLDER OR CONTRIBUTORS 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.
#
#===========================================================================

""": A class for managing styles."""

__version__ = "$Revision: #1 $"

#===========================================================================
import logging
import weakref
import os
import os.path
import glob
from .Style import Style
from .StyleData import StyleData
from .lib import stylePath, cleanupFilename
#===========================================================================

__all__ = [ 'StyleManager' ]

DEFAULT_ENVVAR = "$STYLEPATH"

# The prefix will be added later
ELEMENT_TAG_PROPERTY = "_mpl_style_%s_tag"
ELEMENT_STYLES_PROPERTY = "_mpl_style_%s_styles"

#===========================================================================
def iterable( value, excludeStrings = False ):
   """: Determine if the value is iterable.

   = INPUT VARIABLE
   - value            The value to determine the iterability of.
   - excludeStrings   If set to True, then strings will fail this test.

   = RETURN VALUE
   - Returns True if the value can be iterated over.
   """
   if excludeStrings and ( isinstance( value, str ) or \
                           isinstance( value, unicode ) ):
      return False

   try:
      len( value )
   except:
      return False
   else:
      return True

#===========================================================================
class StyleManager( object ):
   """: An object used to manage one or more Style classes.

   """

   #-----------------------------------------------------------------------
   def __init__( self, styleClass, extension, prefix ):
      """: Create a new StyleManager object.
      """
      # The path to use when searching for styles
      self.path = []

      # The style class this manager manages
      self.styleClass = styleClass

      # The persistent file extension.
      self.extension = extension

      # The prefix used to track elements
      self.prefix = prefix

      # The styles variable is a dictionary of dictionaries.  It contains
      # references to style objects, organized by the type of style.
      # Styles can be accessed in the following way:
      # self._styles[NAME], where NAME is the name of the individual style.
      #  Key: Name
      #  Value: StyleData
      self._styles = {}

      # This is a dictionary mapping tags to elements
      self._tags = {}

   #-----------------------------------------------------------------------
   def loadFile( self, fname, ignoreIfExists = False ):
      """: Load the specified style file.

      = INPUT VARIABLES
      - fname    The name of the style file to load.
      - ignoreIfExists   If set to True, then if a style with the name of the
                         newly loaded style is already loaded, then the newly
                         loaded style will be discarded.

      = RETURN VALUE
      - Returns the loaded style.
      """
      fname = os.path.normpath( os.path.expanduser \
                                ( os.path.expandvars( fname ) ) )

      # call the implementation specific "load" function
      style = self._loadFromFile( fname )

      if not ( self.exists( style.name ) and ignoreIfExists ):
         # Add the style to the manager
         self.add( style )

      # Save the filename with the style
      self._styles[ style.name ].filename = fname

      # Return the newly loaded style.
      return style

   #-----------------------------------------------------------------------
   def load( self, path = None ):
      """: Load all the styles available.

      This will load all styles found in the path as determined by
      'StyleManager.path'.  If this is empty, then it will be assumed to 
      be '$STYLEPATH'.  If that too is not specified, then it will default
      to be [ '.', '~/.matplotlib/styles' ]

      = INPUT VARIABLES
      - path   If specified, then this path will be used to search for
               style files.
      """
      # Get the directories to search
      dirs = self._searchPath( path )

      for d in dirs:
         if os.path.exists( d ):
            files = glob.glob( os.path.join( d, '*.%s' % (self.extension) ) )
            for f in files:
               if os.path.isfile( f ):
                  self.loadFile( f, ignoreIfExists = True )

   #-----------------------------------------------------------------------
   def save( self, outdir = '~/.matplotlib/styles', overwrite = True ):
      """: Save the styles to persistent file.

      If a style was not loaded from a file, then it will be written to
      a file placed in the directory specified by 'outdir'.

      = INPUT VARIABLES
      - outdir      The directory to write styles to that have not yet
                    been written to persistent store.
      - overwrite   If the style already has a file, then it will be
                    overwritten if this is set to True.
      """
      errorMessage = ""
      outdir = os.path.normpath( os.path.expanduser \
                                 ( os.path.expandvars( outdir ) ) )
      if not os.path.exists( outdir ):
         os.makedirs( outdir )

      for styleName in self._styles:
         style = self._styles[ styleName ]
         fname = style.filename

         if fname is None:
            # No filename yet, so create one
            fname = cleanupFilename( styleName )
            fname = os.path.join( outdir, "%s.%s" % (fname, self.extension) )
            # Save the new filename back to the style meta-data
            style.filename = fname

         if os.path.exists( fname ) and not overwrite:
            msg = "Error saving '%s' to file '%s'.  A file with that name" \
                  "already exists.\n" % (styleName, fname)
            errorMessage += msg
            continue

         self._saveToFile( style.style, fname )

      if errorMessage:
         raise Exception( msg )

   #-----------------------------------------------------------------------
   def find( self, name ):
      """: Find a style with the given name.

      = INPUT VARIABLES
      - name    The name of the style to find.

      = RETURN VALUE
      - Will return the named style.  If the named style does not exist,
        then this will return None.
      """
      try:
         return self.__getitem__( name )
      except:
         return None

   #-----------------------------------------------------------------------
   def exists( self, name ):
      """: Check if the named style exists in the manager.

      = INPUT VARIABLES
      - name    The name of the style to look for.

      = RETURN VALUES
      - Will return True if the style is being managed by this manager,
        False otherwise.
      """
      # Make sure we have a name
      if isinstance( name, Style ):
         name = name.name

      return name in self._styles

   #-----------------------------------------------------------------------
   def getAll( self ):
      """: Get a list of the loaded styles.

      = RETURN VALUE
      - This will return a list of the names of the loaded styles.
      """
      names = self._styles.keys()
      names.sort()
      return names

   #-----------------------------------------------------------------------
   def resolve( self, name, styles, ignoreNotFound = False ):
      """: Resolve a list of styles into a new named style.

      = INPUT VARIABLES 
      - name     The name to give to the new style.
      - styles   One or more style instances or names of styles.  Can be
                 a single value if there is only one.
      - ignoreNotFound  If set to True, then if a named style is not
                        found it will be ignored (skipped).

      = RETURN VALUE
      - Returns a newly named style that is a result of resolving all styles
        passed in.  The styles are resolved in the order they appear in the
        list.  This means that a property value set by a later style will
        supercede a property value set by an earlier style.
      """
      # Make sure we have a list
      if isinstance( styles, Style ) or isinstance( styles, str ):
         styles = [ styles ]

      # Create a new style with the given name
      newStyle = self.create( name )

      # Determine the actual styles to use
      for styleName in styles:
         if isinstance( styleName, Style ):
            # We were given a Style instance
            if styleName.name and not self.exists( styleName.name ):
               # we were given a style not in the manager, so add it
               self.add( styleName )

            newStyle.update( styleName.resolve(None) )
         else:
            # we have the name of a style
            s = self.find( styleName )

            if s:
               newStyle.update( s.resolve(None) )