
#
# HISTORY
#
# 2020-06, Arjo Segers
#   New module with classes to create index pages.
#
# 2022-06, Arjo Segers, Met-Norway
#   Support index timeseries over years.
#


#-------------------------------------------------
# help
#-------------------------------------------------

"""
***********************
``utopya_index`` module
***********************

The ``utopya_indexer`` module provides tools to create browsable html pages.
These are typically used to take a first look at generated images.


Accessing the index
===================

The index pages are accessed by top level index page that is probably named::

   index.html
   
Load this page into a web browser to view the result.
The top level page could consists of a combination of images and links
to lower-level index pages.

.. figure:: figs/indexer-main.png
   :width: 400px
   :alt: Example of index page.
   :align: center
   
   *Example of top level index with links to sub pages.*
   
.. figure:: figs/indexer-sub.png
   :width: 400px
   :alt: Example of index page.
   :align: center
   
   *Example of index page with figure table.*
   

Producing index pages
=====================

The index pages are defined by rcfile settings. 
The name of the rcfile, and the base of the keywords should be passed
to the :py:class:`.Indexer` class that will create the pages::

    utopya.Indexer( 'postproc.rc', rcbase='index', env={} )

This defines that the settings are read from ``postproc.rc``,
and that onlys settings with the prefix ``index`` are used.


Configuration
=============

The following is an example of a top level definition::

    ! target location:
    index.outdir                    :  /scratch/flexvar/postproc

    ! show info on created page?
    index.info                      :  True

    ! title:
    index.header                    :  WP1

    ! content type:
    index.type                      :  sections

The ``outdir`` setting defines the (optinal) work directory where to create the index pages,
default is current directory.

The ``info`` flag enables that a message is displayed with the url
of the index page that should be loaded into a browser.
For the top-level page this flag is probably enabled;
the displayed message looks like::

    Browse to:
        file:///scratch/flexvar/postproc/index.html

The ``header`` settings defines an explicit header to be displayed.

The ``type`` setting defines the content type of an index part as defined below.


.. _index-type-table:

Index parts
===========

The ``type`` keyword is interpreted by the :py:meth:`.GetIndexPart` method,
and refers to a class derived from the :py:class:`.IndexPart` class.
The following table shows the ``type`` keywords and the corresponding class:
    

+---------------+---------------------------+------------------------------------+
| type          | class                     | produces ...                       |
+===============+===========================+====================================+
|               | :py:class:`IndexPart`     | *(basic settings for all classes)* |
+---------------+---------------------------+------------------------------------+
| ``sections``  | :py:class:`IndexSections` | sections within page               |
+---------------+---------------------------+------------------------------------+
| ``list``      | :py:class:`IndexList`     | bullet list                        |
+---------------+---------------------------+------------------------------------+
| ``table-row`` | :py:class:`IndexTableRow` | table with rows                    |
+---------------+---------------------------+------------------------------------+
| ``table-col`` | :py:class:`IndexTableCol` | columns within table row           |
+---------------+---------------------------+------------------------------------+
| ``text``      | :py:class:`IndexText`     | text                               |
+---------------+---------------------------+------------------------------------+
| ``img``       | :py:class:`IndexImg`      | image                              |
+---------------+---------------------------+------------------------------------+

See the description of the classes for the settings that define an index part. 


Example
=======

As example, the following settings will create index pages for images
produced for different experiments. 
The experiments are named ``A``, ``B``, and ``C``, and for each 
a number of image files is availble in different directories::

    WP1_A/NL1234_location.png
          NL1234_timeseries_2017-01.png
                                  :
          NL1234_timeseries_2017-12.png
          NL1235_location.png
          NL1235_timeseries_2017-01.png
                                  :
          stations.csv
    
    WP2_B/NL1234_location.png
          :
          
    WP2_C/NL1234_location.png
          :

The ``stations.csv`` files contain information on the station for
which images are produced, the first column contains the station codes::

    code;long_name
    NL1234;Oeverloos
    NL1235;Laagwater
    :

A top-level index page is created that has a bullet list with links 
to sub-pages for different experiments::

    ! ~~ top level index

    ! target location:
    index.outdir                    :  /scratch/flexvar/postproc

    ! show info on created page?
    index.info                      :  True

    ! title:
    index.header                    :  WP1

    ! content type:
    index.type                       :  list
    ! generic name, used to define the content:
    index.name                       :  expid
    ! list elements, here experiment names:
    index.values                     :  A B C

For each of the experiments a sub-page should be created::

    ! create new page for each value:
    index.expid.newpage              :  True

The sub-page for an experiment should contain table with images.
Each row should contain the images for a different station, the station codes
are read from the csv file::

    ! content type:
    index.expid.type                 :  table-row
    ! define row values:
    index.expid.name                 :  scode
    index.expid.values               :  CsvFile( '../WP1_%{expid}/stations.csv' )

The first column should show an image that illustrates the station's location,
the other columns contain time series plots per month::

    ! content type:
    index.expid.scode.type           :  table-col
    ! define column values:
    index.expid.scode.name           :  plot
    index.expid.scode.values         :  location ; TimeSeries( '2017-01', '2017-12', '1 month', 'timeseries_%Y-%m' )

The content is finaly defined by image file names (in experiment specific directories)
and optional arguments to limit the heigh of the images in the table::

    ! content type:
    index.expid.scode.plot.type      :  img
    ! define image:
    index.expid.scode.plot.img       :  ../WP1_%{expid}/%{scode}_%{plot}.png
    index.expid.scode.plot.kwargs    :  height=200



Class hierarchy
===============

* :py:class:`.UtopyaRc`

  * :py:class:`.Indexer`
  * :py:class:`.IndexPart`
  
    * :py:class:`.IndexSections`
    * :py:class:`.IndexList`
    * :py:class:`.IndexTableRow`
    * :py:class:`.IndexTableCol`
    * :py:class:`.IndexText`
    * :py:class:`.IndexImg`


Methods
=======

* :py:meth:`GetIndexPart`


Classes and methods
===================
"""


#-----------------------------------------------------------------------
# modules
#-----------------------------------------------------------------------

# modules:
import logging

# tools:
import utopya


#-----------------------------------------------------------------------
# main class
#-----------------------------------------------------------------------

class Indexer( utopya.UtopyaRc ) :

    """
    Builder for index pages using rcfile settings.
    See module documentation for description.

    Arguments:

    * ``rcfile``, ``rcbase``, ``env`` : settings etc, passed to base class
    """
    
    def __init__( self, rcfile, rcbase='', env={}, _indent='' ) :
    
        """
        Initialize class.
        """
        
        # modules:
        import collections
    
        # init base object:
        utopya.UtopyaRc.__init__( self, rcfile=rcfile, rcbase=rcbase, env=env )
        
        # info ..
        logging.info( _indent+'create index pages ...' )
        
        # new page, pass this object for accessing the settings:
        page = GetIndexPart( self, collections.OrderedDict() )
        # write:
        page.WriteFile()

    #enddef __init__
    
#endclass Indexer


#-----------------------------------------------------------------------
# type selector
#-----------------------------------------------------------------------


def GetIndexPart( urc, current, _indent='' ) :

    """
    Return object derived from :py:class:`IndexPart` class
    given type description.
    
    Arguments:
    
    * ``urc`` : :py:class:`utopya.UtopyaRc` object for settings
    * ``current`` : :py:class:`collections.OrderedDict` object with elements
      for index levels, ordered from top to current
      
    The keys of the ``current`` dict define the full prefix for keywords
    in the settings. For example, if this method is called for the
    third index level using a dict::
    
        current = { 'main' : { .. },
                    'row'  : { .. },
                    'col'  : { .. } }
                  
    then the prefix is::
    
        main.row.col
    
    The following keys are read from the settings that have been imported
    into the ``urc`` object::
    
        ! type of index part:
        <prefix>.type       :  list
        
    See the table :ref:`index-type-table` for the classes corresponding
    to type keyword.
    """
    
    # generic name in settings:
    keyname = ''
    for level in current.keys() :
        if level == '' : continue
        if len(keyname) > 0 : keyname = keyname+'.'
        keyname = keyname+level
    #endfor
    # last level is current name:
    if len(current) == 0 :
        name = ''
    else :
        name = level
    #endif
    
    # get type:
    ctype = urc.GetSetting( keyname+'.type' )

    ## testing ...
    #logging.info( _indent+'get index part for "%s" ...' % name )
    #logging.info( _indent+'..current : %s' % current )
    #logging.info( _indent+'..ctype   : %s' % ctype )

    # switch:
    #~ sections:
    if ctype == 'sections' :

        # content for sections:
        html = IndexSections( urc, current, _indent=_indent )
    
    #~ bullet list:
    elif ctype == 'list' :

        # content for list:
        html = IndexList( urc, current, _indent=_indent )

    #~ table row:
    elif ctype == 'table-row' :
        # content for text:
        html = IndexTableRow( urc, current, _indent=_indent )

    #~ table column:
    elif ctype == 'table-col' :
        # content for text:
        html = IndexTableCol( urc, current, _indent=_indent )

    #~ text:
    elif ctype == 'text' :

        # content for text:
        html = IndexText( urc, current, _indent=_indent )

    #~ image:
    elif ctype == 'img' :

        # content for text:
        html = IndexImg( urc, current, _indent=_indent )

    #~ unknown ..
    else :
        logging.error( 'unsupported content type "%s"' % ctype )
        raise Exception
    #endif
    
    # ok
    return html
    
#enddef GetIndexPart


#-----------------------------------------------------------------------
# index page
#-----------------------------------------------------------------------


class IndexPart( utopya.UtopyaRc ) :

    """
    Base class for index parts of html lines.
    
    Arguments:
    
    * ``urc`` : :py:class:`utopya.UtopyaRc` object for settings
    * ``current`` : :py:class:`collections.OrderedDict` object with elements per level
    
    An element of the ``current`` argument is a dict with info
    for each level::
      
        current = { 'main' : ...,
                    'row'  : ...,
                    'col'  : { 'value'    : 'timeseries', 
                              'filename' : 'index.S1.expA.html', 
                              'header'   : None, 
                              'alink'    : 'index.S1.expA.html#plot' }
                  }

    For the top level index (main page), the ``current`` is probably empty.
    
    The values are used to pass information between the levels of the index. 
    For example, a lower level could use headers from higher levels to create upward links.

    The keys of the ``current`` dict define the full name of the prefix 
    for the settings that define the content.
    For the above example the prefix is::
    
        main.row.col
            
    The keys that are read from the ``urc`` object by this base class are optional,
    and are mainly used for fine-tuning the index pages.
    
    The first (optional) key that is read defines the work directory where
    the index pages are created; the default is the current directory::
    
        <prefix>.outdir     :  /scratch/flevar/postproc
        
    The next flag defines whether this index part should be written to a new file::
    
        <prefix>.newpage    :  True
        
    The default is ``False``, which means that the index part will be included
    in a higher level page.
    An exception is the top level index (empty ``current`` dict), this will always
    write a new page.
    In case ``newpage`` is enabled, the index part will be written to a filename
    which has by default the level elements in the name::
    
        index__main__row__col.html
        
    Alternatively, the target file could be defined in the settings::
    
        <prefix>.filename   :  whatever.html
        
    Each index part could started with a header.
    For some index parts (derived from this base class) a header is
    defined automatically; alternatively a header could be define explicitly::
    
        <prefix>.header      :  Overview of results
        
    If a header is defined (automatically or explicitly) it will be inserted
    as the first of the html lines. If the index part is written to a new page,
    the heading is composed as a list of headers from higher levels,
    with links to their locations::
    
        <main-label> / <row-label> / <col-label>
    
    For index parts included in the html page of a higher level, 
    the heading will consists of the own header only, with the appropriate
    heading level within the page::
    
        <h1>Main label</h1>
        :
          <h2>Row label</h2>
          :
            <h3>Col label</h3>
    
    """
    
    def __init__( self, urc, current, _indent='' ) :
    
        """
        Initialize class.
        """
        
        # modules:
        import os
    
        # init base object:
        utopya.UtopyaRc.__init__( self, urc=urc )

        # store specific values:
        self.current = current
        
        # top level index?
        is_toplevel = len(self.current) == 0
        
        # last level is current name:
        if len(current) == 0 :
            self.name = ''
            self.current[self.name] = {}
        else :
            self.name = list(self.current.keys())[-1]
        #endif

        # dot seperated concatenation of (generic) lev
        self.keyname = self.GetCurrentLine( key=None, sep='.' )
        # dot seperated concatenation of specific values:
        self.fullname = self.GetCurrentLine( key='value', sep='.' )
    
        # initialize content:
        self.html = utopya.UtopyaHtmlFile()
        
        # target location:
        outdir = self.GetSetting( self.keyname+'.outdir', default=None )
        # defined?
        if outdir is not None :
            # create?
            if not os.path.isdir( outdir ) :
                # info ...
                logging.info( _indent+'create %s ...' % outdir )
                # make:
                os.makedirs( outdir )
            #endif
            # info:
            logging.info( _indent+'change to %s ...' % outdir )
            # goto:
            os.chdir( outdir )
        #endif
        
        # create new page for html lines?
        # default is True for top level index:
        self.newpage = self.GetSetting( self.keyname+'.newpage', totype='bool', 
                                            default=is_toplevel )
        # to be written?
        if self.newpage :
            # default target:
            if len(self.name) > 0 :
                ext = self.GetCurrentLine( key='value', sep='__' )
                indexfile = 'index__%s.html' % ext.replace('/','_')
            else :
                indexfile = 'index.html'
            #endif
            # eventually replace by specific filename:
            self.filename = self.GetSetting( self.keyname+'.filename', default=indexfile )
        else :
            # no file:
            self.filename = None
        #endif
        # if not defined, use latest defined:
        if self.filename is None :
            # loop back to first level:
            for level in reversed(self.current.keys()) :
                # copy if present:
                if 'filename' not in self.current[level].keys() : continue
                self.filename = self.current[level]['filename']
                # defined? then leave:
                if self.filename is not None : break
            #endfor
        #endif
        # store:
        self.current[self.name]['filename'] = self.filename

        # specific header in settings:
        self.header = self.GetSetting( self.keyname+'.header', default=None )
        # .. or from current:
        if (self.header is None) and ('header' in self.current[self.name].keys()) :
            self.header = self.current[self.name]['header']
        #endif
        # store:
        self.current[self.name]['header'] = self.header
        
        # add anchor:
        self.html.Append( '<a name="%s">' % self.name )
        # store link:
        if self.filename is None :
            self.current[self.name]['alink'] = '#%s' % self.name
        else :
            self.current[self.name]['alink'] = '%s#%s' % (self.filename,self.name)
        #endif

        # add header line if this index part has a header ..
        if self.header is not None :
            # heading level:
            iheading = 0
            # loop from here to top level:
            for level in reversed(self.current.keys()) :
                # no need to go to sub-section if higher section has no heading at all ..
                if 'header' not in self.current[level].keys() : continue
                if self.current[level]['header'] == None : continue
                # extract:
                level_filename = self.current[level]['filename']
                if level_filename is None : continue
                # same target file? then increase level:
                if level_filename == self.filename : iheading += 1
            #endfor
            iheading = max(1,iheading)
            # main header?
            if iheading == 1 :
                # full heading including previous header parts,
                # these will also have a link to their anchor:
                fullheader = self.GetCurrentLine( key='header', sep=' / ', alinks=True )
            else :
                # just this name:
                fullheader = self.header
            #endif
            # add header:
            self.html.Append( '<h%i>%s</h%i>' % (iheading,fullheader,iheading) )
        #endif

    #enddef __init__
    
    # *
    
    def GetCurrentLine( self, key=None, sep='.', alinks=False ) :
    
        """
        Return line with concatenation of elements in the ``current`` dict.
        
        Arguments:
        
        * ``key`` : str value; if defined, it should be one of the possible keys
          in the ``current`` elements, for example ``value``;
          if not defined, the level name (``current`` keys) is used;
        * ``sep`` : optional str value inserted between the concatenated elements;
        * ``alinks`` : if ``True``, the elements are surround with links to anchors
          defined for the elements.
        """
        
        # init result:
        line = ''
        # look over levels:
        for level in self.current.keys() :
            # level name or key value?
            if key is None :
                # copy:
                level_value = level
            else :
                # extract value:
                if key not in self.current[level].keys() : continue
                level_value = self.current[level][key]
            #endif
            # filter:
            if level_value is None : continue
            if level_value == '' : continue
            # add link?
            if alinks and ('alink' in self.current[level]) and (level != self.name) :
                level_value = '<a href="%s">%s</a>' % (self.current[level]['alink'],level_value)
            #endif
            # add seperation if necessary:
            if len(line) > 0 : line = line+sep
            # add content:
            line = line+level_value
        #endfor
    
        # ok
        return line
        
    #enddef GetCurrentLine
    
    # *
    
    def ReplaceCurrents( self, line ) :
    
        """
        Return line with templates replaced by level values.
        If the ``currrent`` dict consists of::
        
            current = { .., 'row' : { 'value' : 'NL1234', ..}, ..}
            
        then the input line::
        
            station_%{row}_location.png
            
        is returned as::
        
            station_NL1234_location.png
        
        Alternative only some selected characters can be inserted::
        
            station_%{row[0:2]}_location.png
            station_NL_location.png
            
        """
        
        # loop:
        for level in self.current.keys() :
            # extract:
            if 'value' not in self.current[level].keys() : continue
            level_value = self.current[level]['value']
            if level_value is None : continue
            # replace:
            line = line.replace( '%{'+level+'}', level_value )
            # ... adhoc, needs some regular expression matching in future ..
            for i in range(len(level_value)) :
                for j in range(i+1,len(level_value)+1) :
                    line = line.replace( '%{'+level+'['+str(i)+':'+str(j)+']}', level_value[i:j] )
                #endfor
            #endfor
        #endfor
        
        # ok
        return line
        
    #enddef ReplaceCurrents

    # *
    
    def WriteFile( self ) :
    
        """
        If a target file is defined for this object,
        write the html lines to this file.
        
        An optional flag is read from the settings, if enabled a message 
        will be displayed to show the url of the index page::
  
            Browse to:
              file:///scratch/flexvar/postproc/index.html
        
             
        """

        # apply?
        if self.filename is not None :
            # show info on creation?
            with_info = self.GetSetting( self.keyname+'.info', totype='bool', default=False )
            # write:
            self.html.Write( self.filename, title=self.header, info=with_info )
        #endif
        
    #enddef WriteFile

    # *
    
    def GetFilename( self ) :
    
        """
        If this index part is written to a seperate file,
        return the targe file, or None otherwise.
        """
        
        # ok
        if self.newpage :
            return self.filename
        else :
            return None
        #endif
        
    #enddef GetFilename
    
    # *
    
    def AddNavigation( self, up, prev, next ) :
    
        """
        Add navigation buttons for up/previous/next to top and bottom of html lines.
        The arguments (if not ``None``) define the links around the buttons.
        If an argument is ``None``, 
        """
        
        # labels:
        up_label   = '[up]'
        prev_label = '[prev]'
        next_label = '[next]'
        
        # line with navigation buttons:
        line = ''
        if up is None :
            line = line+up_label
        else :
            line = line+('<a href="%s">%s</a>' % (up,up_label))
        #endif
        if prev is None :
            line = line+prev_label
        else :
            line = line+('<a href="%s">%s</a>' % (prev,prev_label))
        #endif
        if next is None :
            line = line+next_label
        else :
            line = line+('<a href="%s">%s</a>' % (next,next_label))
        #endif
        
        # header:
        self.html.Prepend( '<hr>' )
        self.html.Prepend( line )
        # footer:
        self.html.Append( '<hr>' )
        self.html.Append( line )
        
    #enddef AddNavigation       
    
    # *
    
    def GetValues( self, key ) :
    
        """
        Return value list from settings defined by ``key``.
        
        Supported formats:
        
        * Space seperated list::
        
            <key>    :  aa b 1 2 3 4 5
            
        * Semi-column seperated list; if an element looks like a function
          (including parantheses) then it is passed to the :py:meth:`GetValuesFunction` method::
        
            <key>    :  aa ; b ; Range(1,5)
        
        """
    
        # read list from settings:
        line = self.GetSetting( key )
        # replace templates:
        line = self.ReplaceCurrents( line )

        # interpret values:
        if ';' in line :
            # split:
            xparts = line.split(';')
            # store clean versions:
            parts = []
            for xpart in xparts :
                parts.append( xpart.strip() )
            #endfor
        else :
            # one part only ...
            parts = [line]
        #endfor
        
        # init result:
        values = []
        # loop:
        for part in parts :
            #~ function?
            if part.endswith(')') :
                # add:
                values.extend( self.GetValuesFunction( part ) )
            #
            #~ value list
            else :
                # split on whitespace:
                values.extend( part.split() )
            #endif
        #endif # parts
        
        # ok:
        return values

    #enddef GetValues
    
    # *
    
    def GetValuesFunction( self, part ) :
    
        """
        Evaluate function description, return list of values.
        Supported functions:
        
        * ``CsvFile( 'filename.csv' )``
        
          The csv file is supposed to have a header line and semi-colon seperated values;
          the first column is returned as list of values::
          
              code;long_name
              NL1234;Oeverloos
              NL1235;Laagwater
              :
              
        * ``Range( 0, 23, 1, '%2.2i00' )``
        
          Return list of formatted integer values.
          The arguments included in the function description are evaluated
          to a start, end, step increment, and a formatting template.
              
        * ``TimeSeries( '2017-01', '2017-12', '1 month', 'conc_timeseries_%Y-%m' )``
        
          Return list of formatted time values.
          The arguments included in the function description are evaluated
          to a start time, end time, time step increment, and a formatting template
          that can be handled by the :py:meth:`datetime.datetime.strftime` method.
        """
        
        # modules:
        import os
        import pandas
        import datetime
        import calendar

        # switch:
        #~ read column(s) from csv file:
        if part.startswith('CsvFile(') :

            # format:
            #    CsvFile( 'filename.csv' )
            xargs = part.lstrip('CsvFile(').rstrip(')').split(',')
            args = []
            for xarg in xargs :
                args.append( xarg.replace("'","").replace('"','').strip() )
            #endfor
            # check ...
            if len(args) == 0 :
                logging.error( 'no arguments in function: %s' % part )
                raise Exception
            elif len(args) > 1 :
                logging.error( 'unsupported arguments in function: %s' % part )
                raise Exception
            #endif
            # extract:
            filename = args.pop(0)
            # check ..
            if os.path.isfile(filename) :
                # read:
                df = pandas.read_csv( filename, sep=';', dtype='str' )
                # first column:
                values = list( df.iloc[:,0] )
            else :
                # info ...
                logging.warning( 'file "%s" not found from: %s' % (filename,os.getcwd()) )
                # empty:
                values = []
            #endif
            
        #~ integer range
        elif part.startswith('Range(') :

            # format for hours '00:00',..,'23:00'
            #    Range( 0, 23, 1, '%2.2i00' )
            xargs = part.lstrip('Range(').rstrip(')').split(',')
            args = []
            for xarg in xargs :
                args.append( xarg.replace("'","").replace('"','').strip() )
            #endfor
            # check ...
            if len(args) != 4 :
                logging.error( 'wrong number of arguments in function: %s' % part )
                raise Exception
            #endif
            # extract start:
            i1 = int(args[0] )
            # extract end:
            i2 = int(args[1] )
            # stepping:
            step = int(args[2])
            # formatting:
            fmt = args[3]
            # init list:
            values = []
            # loop:
            for i in range(i1,i2+1) :
                # add value:
                values.append( fmt % i )
            #endfor
            
        #~ timeseries
        elif part.startswith('TimeSeries(') :

            # format:
            #    TimeSeries( '2017-01', '2017-12', '1 month', 'conc_timeseries_%Y-%m' )
            xargs = part.lstrip('TimeSeries(').rstrip(')').split(',')
            args = []
            for xarg in xargs :
                args.append( xarg.replace("'","").replace('"','').strip() )
            #endfor
            # check ...
            if len(args) != 4 :
                logging.error( 'wrong number of arguments in function: %s' % part )
                raise Exception
            #endif
            # extract start time:
            tvals = list( map( int, args[0].replace('-',' ').replace('/',' ').replace(':',' ').split() ) )
            if len(tvals) == 1 : tvals.append( 1 )
            if len(tvals) == 2 : tvals.append( 1 )
            t1 = datetime.datetime( *tvals )
            # extract end time:
            tvals = list( map( int, args[1].replace('-',' ').replace('/',' ').replace(':',' ').split() ) )
            if len(tvals) == 1 : tvals.append( 12 )
            if len(tvals) == 2 : tvals.append( 31 )
            t2 = datetime.datetime( *tvals )
            # stepping:
            svals = args[2].split()
            if len(svals) == 1 :
                n,units = 1,svals[0]
            elif len(svals) == 2 :
                n,units = int(svals[0]),svals[1]
            else :
                logging.error( 'unsupported time stepping "%s" in part: %s' % (args[2],part) )
                raise Exception
            #endif
            # formatting:
            tfmt = args[3]
            # init list:
            values = []
            # time loop:
            t = t1
            while t <= t2 :
                # add value:
                values.append( t.strftime(tfmt) )
                # next:
                for i in range(n) :
                    #~ months:
                    if units in ['month','monthly'] :
                        # month length:
                        weekday,nday = calendar.monthrange(t.year,t.month)
                        # start of next month:
                        t = datetime.datetime(t.year,t.month,1) + datetime.timedelta(nday)
                    #~ days:
                    elif units in ['day','days'] :
                        # next:
                        t = t + datetime.timedelta(1)
                    #~ hours:
                    elif units in ['hour','hours'] :
                        # next:
                        t = t + datetime.timedelta(0,3600)
                    #~ years:
                    elif units in ['year','years'] :
                        # next:
                        t = datetime.datetime(t.year+1,1,1)
                    #~
                    else :
                        logging.error( 'unsupported time step units "%s"' % units )
                        raise Exception
                    #endif
                #endfor # n
            #endwhile # time

        else :
            logging.error( 'unsupported function in value part: %s' % part )
            raise Exception
        #endif
        
        # ok
        return values

    #enddef GetValuesFunction

#endclass IndexPart


# *


class IndexSections( IndexPart ) :

    """
    Define index page that consists of one or more sections.
    
    Arguments:
    
    * ``urc`` : :py:class:`utopya.UtopyaRc` object for settings
    * ``current`` : :py:class:`collections.OrderedDict` object with elements per level
    
    In the settings, the sectioning is defined by list of values interpreted
    by the :py:meth:`.IndexPart.GetValues` method::
    
        <prefix>.values   :  Sec1 Sec2
        
    For each of these sections, a set of keywords should be present that
    define their content.
    The content types are interpreted by the :py:meth:`GetIndexPart` method,
    and the entire content definition might look like::
    
        <prefix>.Sec1.type     :  list
        <prefix>.Sec1.values   :  A B C
        ..
        
        <prefix>.Sec2.type     :  list
        <prefix>.Sec2.values   :  Range(1,5)
        ..
        
    """
    
    def __init__( self, urc, current, _indent='' ) :
    
        """
        Initialize.
        """
        
        # init base class:
        IndexPart.__init__( self, urc, current, _indent=_indent )
        
        ## testing ..
        #logging.info( _indent+'%s : IndexSection "%s" ...' % (self.keyname,self.fullname) )
        
        # get list with values:
        elements = self.GetValues( self.keyname+'.values')

        # loop to fill content, and collect filenames for prev/next buttons:
        for element in elements :
        
            # info ..
            logging.info( _indent+'section "%s" ...' % element )
            
            ## add seperation:
            #self.html.Append( '<hr>' )
            
            # extend values with own name:
            xcurrent = self.current.copy()
            xcurrent[element] = { 'value' : element }
            # return html for this section:
            section = GetIndexPart( self, xcurrent, _indent=_indent+'  ' )

            # add content:
            self.html.Extend( section.html )
                
        #endfor elements

    #enddef __init__
    
#endclass IndexSections


# *


class IndexList( IndexPart ) :

    """
    Define index part with bullet list.
    
    Arguments:
    
    * ``urc`` : :py:class:`utopya.UtopyaRc` object for settings
    * ``current`` : :py:class:`collections.OrderedDict` object with elements per level
    
    In the settings, first define a generic name for the list::
    
        <prefix>.name     :  expid
        
    The list elements are interpreted
    by the :py:meth:`.IndexPart.GetValues` method::
    
        <prefix>.values   :  A B C
        
    The generic name should be used to define the content under the list item.
    The content type is interpreted by the :py:meth:`GetIndexPart` method,
    and the entire content definition might look like::
    
        <prefix>.expid.type     :  img
        <prefix>.expid.img      :  figs/results-%{expid}.png
        ..
    """
    
    def __init__( self, urc, current, _indent='' ) :
    
        """
        Initialize.
        """
        
        # init base class:
        IndexPart.__init__( self, urc, current, _indent=_indent )
        
        ## testing ..
        #logging.info( _indent+'%s : IndexList "%s" ...' % (self.keyname,self.fullname) )
        
        # generic name of this level:
        gname = self.GetSetting( self.keyname+'.name' )
        # get list with values:
        elements = self.GetValues( self.keyname+'.values')

        # storage:
        parts = []
        filenames = []
        # loop to fill content, and collect filenames for prev/next buttons:
        for element in elements :
            # extend values:
            xcurrent = self.current.copy()
            xcurrent[gname] = { 'value' : element, 'header' : element }
            # return html given generic lines:
            part = GetIndexPart( self, xcurrent, _indent=_indent+'  ' )
            # target file? could be None:
            filename = part.GetFilename()
            # store:
            parts.append( part )
            filenames.append( filename )
        #endfor
        
        # init list:
        self.html.Append( '<ul>' )
        # count:
        n = len(elements)
        # loop:
        for k in range(n) :

            # current:
            element  = elements[k]
            part     = parts[k]
            filename = filenames[k]

            # write?
            if filename is not None :

                # anchor:
                aname = '%s-%s' % (gname,element)
                # current location for upward:
                if self.filename is None :
                    up = None
                else :
                    up = '%s#%s' % (self.filename,aname)
                #endif
                # previous page:
                if k == 0 :
                    prev = None
                else :
                    prev = filenames[k-1]
                #endif
                # next page:
                if k == n-1 :
                    next = None
                else :
                    next = filenames[k+1]
                #endif            
                # add navigation buttons:
                part.AddNavigation( up, prev, next )
                # write:
                part.WriteFile()
                # list item:
                self.html.Append( '<li> <a name="%s"> <a href="%s">%s</a>' % (aname,filename,element) )
                
            else :
            
                # list item:
                self.html.Append( '<li>' )
                # add content:
                self.html.Extend( part.html )
                
            #endif
 
        #endfor elements
        # end of list:
        self.html.Append( '</ul>' )

    #enddef __init__
    
#endclass IndexList


# *


class IndexTableRow( IndexPart ) :

    """
    Define index part as table with rows.
    
    Arguments:
    
    * ``urc`` : :py:class:`utopya.UtopyaRc` object for settings
    * ``current`` : :py:class:`collections.OrderedDict` object with elements per level
    
    In the settings, first define an (optional) generic name for the row elements;
    the default is::
    
        <prefix>.name     :  row
    
    The row elements are interpreted by the :py:meth:`.IndexPart.GetValues` method::
    
        <prefix>.values   :  A B C
        
    The generic name should be used to define the content of the rows,
    which are probably the columns of the table.
    The content type is interpreted by the :py:meth:`GetIndexPart` method,
    and the entire content definition might look like::
    
        <prefix>.row.type     :  table-col
        <prefix>.row.name     :  col
        <prefix>.row.values   :  1 2 3 4
        ..
    """
    
    def __init__( self, urc, current, _indent='' ) :
    
        """
        Initialize.
        """
        
        # modules:
        import os
        
        # init base class:
        IndexPart.__init__( self, urc, current, _indent=_indent )
        
        ## testing ..
        #logging.info( _indent+'%s : IndexTableRow "%s" ...' % (self.keyname,self.fullname) )
        
        # values:
        row_name   = self.GetSetting( self.keyname+'.name', default='row' )
        row_values = self.GetValues( self.keyname+'.values' )
        
        # init table:
        self.html.Append( '<table>' )
        # loop over rows:
        for row_value in row_values :
            # info ...
            logging.info( _indent+'row "%s" ..' % row_value )
            # init row:
            self.html.Append( '  <tr>' )
            # extend levels:
            xcurrent = self.current.copy()
            xcurrent[row_name] = { 'value' : row_value }
            # row content:
            row = GetIndexPart( self, xcurrent, _indent=_indent+'  ' )
            # extend content:
            self.html.Extend( row.html )
        #endfor # row
        # close table:
        self.html.Append( '</table>' )

    #enddef __init__
    
#endclass IndexTableRow


# *


class IndexTableCol( IndexPart ) :

    """
    Define index part as column of table.
    
    Arguments:
    
    * ``urc`` : :py:class:`utopya.UtopyaRc` object for settings
    * ``current`` : :py:class:`collections.OrderedDict` object with elements per level
    
    In the settings, first define an (optional) generic name for the column elements;
    the default is::
    
        <prefix>.name     :  col
    
    The column elements are interpreted by the :py:meth:`.IndexPart.GetValues` method::
    
        <prefix>.values   :  1 2 3 4
        
    The generic name should be used to define the content of the columns,
    which are probably the figures to be displayed.
    The content type is interpreted by the :py:meth:`GetIndexPart` method,
    and the entire content definition might look like::
    
        <prefix>.col.type     :  img
        <prefix>.col.img      :  figs/timeseries_%{col}_%{row}.png
        <prefix>.col.kwargs   :  height=200
        ..
    """
    
    def __init__( self, urc, current, _indent='' ) :
    
        """
        Initialize.
        """
        
        # modules:
        import os
        
        # init base class:
        IndexPart.__init__( self, urc, current, _indent=_indent )
        
        ## testing ..
        #logging.info( _indent+'%s : IndexTableCol "%s" ...' % (self.keyname,self.fullname) )
        
        # values:
        col_name   = self.GetSetting( self.keyname+'.name', default='col' )
        col_values = self.GetValues( self.keyname+'.values' )
        
        # loop over columns:
        for col_value in col_values :
            # info ...
            logging.info( _indent+'col "%s" ..' % col_value )
            # init data:
            self.html.Append( '    <td>' )
            # extend levels:
            xcurrent = self.current.copy()
            xcurrent[col_name] = { 'value' : col_value }
            # cell content:
            cell = GetIndexPart( self, xcurrent, _indent=_indent+'  ' )
            # extend content:
            self.html.Extend( cell.html )                
        #endfor # col

    #enddef __init__
    
#endclass IndexTableCol


# *


class IndexText( IndexPart ) :

    """
    Define index part with text only.
    
    Arguments:
    
    * ``urc`` : :py:class:`utopya.UtopyaRc` object for settings
    * ``current`` : :py:class:`collections.OrderedDict` object with elements per level
    
    In the settings, the text to be displayed should be defined by::
    
        <prefix>.value     :  Very important message.
    """
    
    def __init__( self, urc, current, _indent='' ) :
    
        """
        Initialize.
        """
        
        # init base class:
        IndexPart.__init__( self, urc, current, _indent=_indent )
        
        ## testing ...
        #logging.info( _indent+'%s : IndexText "%s" ...' % (self.keyname,self.fullname) )

        # content:
        text = self.GetSetting( self.keyname+'.value' )
        # replace keywords:
        text = self.ReplaceCurrents( text )
        # add:
        self.html.Append( text )

    #enddef __init__
    
#endclass IndexText


# *


class IndexImg( IndexPart ) :

    """
    Fill html content for image.
    
    Arguments:
    
    * ``urc`` : :py:class:`utopya.UtopyaRc` object for settings
    * ``current`` : :py:class:`collections.OrderedDict` object with elements per level
    
    The settings should define the image filename, and optional keyword
    arguments passed to the html ``img`` tag.
    The filename probably includes templates to insert index level values::
    
        <prefix>.col.img      :  figs/timeseries_%{col}_%{row}.png
        <prefix>.col.kwargs   :  height=200
        ..
    """
    
    def __init__( self, urc, current, _indent='' ) :
    
        """
        Initialize.
        """
        
        # modules:
        import os
        
        # init base class:
        IndexPart.__init__( self, urc, current, _indent=_indent )
        
        ## info ...
        #logging.info( _indent+'%s : IndexImg "%s" ...' % (self.keyname,self.fullname) )

        # img file description:
        imgfile = self.GetSetting( self.keyname+'.img' )
        # replace keywords:
        imgfile = self.ReplaceCurrents( imgfile )
        # file present?
        if os.path.isfile(imgfile) :
            # info ...
            logging.info( _indent+'found   %s ..' % imgfile )
            # extra:
            kwargs = urc.GetSetting( self.keyname+'.kwargs', default='' )
            # add content:
            self.html.Append( '      <a href="%s"><img src="%s" %s></a>' % (imgfile,imgfile,kwargs) )
        else :
            # info ...
            logging.info( _indent+'missing %s ..' % imgfile )
        #endif

    #enddef __init__
    
#endclass IndexImg

#-----------------------------------------------------------------------
# end
#-----------------------------------------------------------------------

