#
# HISTORY
#
# 2020-06, Arjo Segers
#   New module with classes to create index pages.
#
# 2022-06, Arjo Segers
#   Support index timeseries over years.
#
# 2023-08, Arjo Segers
#   Reformatted using 'black'.
#


# -------------------------------------------------
# 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
# -----------------------------------------------------------------------
