"""Tests for :mod:`siptools_research.workflow.create_logical_structmap`."""  # noqa: W505,E501

import copy
import shutil

import pytest
import lxml.etree

from metax_access import Metax

from siptools.scripts.import_description import import_description
from siptools.scripts.import_object import import_object
from siptools.scripts.compile_structmap import compile_structmap
from siptools.scripts.premis_event import premis_event
from siptools.xml.mets import NAMESPACES

import tests.utils
from siptools_research.workflow.create_logical_structmap import (
    CreateLogicalStructMap, find_dir_use_category, get_dirpath_dict
)

@pytest.mark.parametrize(
    ("events", "admids", "provenance_is_from_qvain"),
    [
        (
            [], [], False
        ),
        (
            ['creation'], ['_2005686d72f58850765d1c8147d05cb2'], False
        ),
        (
            ['creation', 'foobar'],
            ['_2005686d72f58850765d1c8147d05cb2',
             '_f8384d1f8b9cbcafcba9370d1b506a26'],
            False
        ),
        (
            ['creation'], ['_2005686d72f58850765d1c8147d05cb2'], True
        ),
    ]
)
@pytest.mark.usefixtures('testmongoclient')
def test_create_structmap_ok(
    workspace, requests_mock, events, admids, provenance_is_from_qvain
):
    """Test the workflow task CreateLogicalStructMap.

    :param workspace: Temporary workspace directory fixture
    :param requests_mock: Mocker object
    :param events: List of provenance events in dataset
    :param admids: List of identifiers of administrative metadata to
                   which the logical structuremap should refer.
    :returns: ``None``
    """
    # Create a dataset
    # Dataset contains two files
    files = [copy.deepcopy(tests.metax_data.files.BASE_FILE),
             copy.deepcopy(tests.metax_data.files.BASE_FILE)]
    files[0]['file_path'] = 'files/file1'
    files[1]['file_path'] = 'files/file2'
    dataset = copy.deepcopy(tests.metax_data.datasets.BASE_DATASET)
    # Dataset contans zero or more events
    dataset["research_dataset"]["provenance"] = []
    for event in events:
        # Provenances made in Qvain have 'lifecycle_event' instead of
        # 'provenance_event'
        if provenance_is_from_qvain:
            provenance = copy.deepcopy(
                tests.metax_data.datasets.QVAIN_PROVENANCE
            )
            provenance["lifecycle_event"]["pref_label"]["en"] = event
        else:
            provenance = copy.deepcopy(
                tests.metax_data.datasets.BASE_PROVENANCE
            )
            provenance["preservation_event"]["pref_label"]["en"] = event
        dataset["research_dataset"]["provenance"].append(provenance)
    if not dataset["research_dataset"]["provenance"]:
        del dataset["research_dataset"]["provenance"]
    tests.utils.add_metax_dataset(requests_mock, dataset=dataset, files=files)

    # Create workspace that already contains dataset files
    sip_directory = workspace / "sip-in-progress"
    sip_directory.mkdir(parents=True)
    file_directory = workspace / 'dataset_files' / 'files'
    file_directory.mkdir(parents=True)

    (file_directory / "file1").write_text("foo")
    (file_directory / "file2").write_text("bar")

    # Create required metadata to workspace:
    # * digital provenance metadata
    # * descriptive metadata
    # * technical metadata
    # * physical structure map
    for event in events:
        premis_event(
            workspace=str(sip_directory),
            event_type=event,
            event_datetime='2014-12-31T08:19:58Z',
            event_detail='foo',
            event_outcome='success',
            event_outcome_detail='bar'
        )
    if events:
        shutil.copy(
            sip_directory / 'premis-event-md-references.jsonl',
            workspace / 'create-provenance-information.jsonl'
        )
    import_description(
        dmdsec_location='tests/data/datacite_sample.xml',
        workspace=str(sip_directory)
    )
    import_object(
        workspace=str(sip_directory),
        base_path=str(workspace),
        skip_wellformed_check=True,
        filepaths=[str(file_directory)]
    )
    compile_structmap(
        workspace=str(sip_directory),
        structmap_type='Fairdata-physical'
    )

    # Init and run CreateLogicalStructMap task
    sip_prerun_files = {path.name for path in sip_directory.iterdir()}
    task = CreateLogicalStructMap(workspace=str(workspace),
                                  dataset_id='dataset_identifier',
                                  config=tests.conftest.UNIT_TEST_CONFIG_FILE)
    task.run()
    assert task.complete()

    validate_logical_structmap_file(
        str(sip_directory / 'logical_structmap.xml'), admids
    )

    sip_postrun_files = {path.name for path in sip_directory.iterdir()}

    # Nothing else should be created SIP directory
    assert sip_postrun_files \
        == sip_prerun_files | {'logical_structmap.xml'}

def test_get_dirpath_dict(requests_mock):
    """Test that get_dirpath_dict returns the correct dictionary.

    The dictionary maps dirpath to use_category.

    :param requests_mock: Mocker object
    """
    requests_mock.get(
        "https://metaksi/rest/v2/directories/1",
        json={
            "identifier": "1",
            "directory_path": "/"
        }
    )
    requests_mock.get(
        "https://metaksi/rest/v2/directories/2",
        json={
            "identifier": "2",
            "directory_path": "/test"
        }
    )

    metax_client = Metax("https://metaksi", "test", "test")
    dataset_metadata = {
        "research_dataset": {
            "directories": [
                {
                    "identifier": "1",
                    "use_category": {"pref_label": {"en": "rootdir"}}
                },
                {
                    "identifier": "2",
                    "use_category": {"pref_label": {"en": "testdir"}}
                }
            ]
        }
    }

    assert get_dirpath_dict(metax_client, dataset_metadata) == {
        "/": {"pref_label": {"en": "rootdir"}},
        "/test": {"pref_label": {"en": "testdir"}}
    }

# pylint: disable=invalid-name
def test_get_dirpath_dict_no_directories():
    """Test get_dirpath_dict function with dataset without directories.

    The function should return an empty dict when no directories are
    defined in the research_dataset.
    """
    metax_client = Metax("https://metaksi", "test", "test")
    assert not get_dirpath_dict(metax_client, {"research_dataset": {}})

def test_find_dir_use_category():
    """Test that find_dir_use_category returns the correct label."""
    dirpath_dict = {
        "/test1": {"pref_label": {"en": "testdir1"}},
        "/test2": {"pref_label": {"en": "testdir2"}}
    }
    languages = ["en"]

    # Straightforward cases
    assert find_dir_use_category("/test1", dirpath_dict, languages) \
        == "testdir1"
    assert find_dir_use_category("/test2", dirpath_dict, languages) \
        == "testdir2"

    # Closest parent that matches
    assert find_dir_use_category(
        "/test1/test", dirpath_dict, languages
    ) == "testdir1"

    # No matches
    assert not find_dir_use_category("/", dirpath_dict, languages)
    assert not find_dir_use_category("/test3", dirpath_dict, languages)

    # No directories were found in the research_dataset
    assert not find_dir_use_category("/test", {}, languages)

    # Match to root
    assert find_dir_use_category(
        "/",
        {"/": {"pref_label": {"en": "root"}}},
        languages
    ) == "root"

def validate_logical_structmap_file(logical_structmap_file, admids):
    """Validate logical structuremap XML-file.

    Checks that XML-file has the correct elements. Raises exception if
    XML is not valid.

    :param logical_structmap_file: XML file to be validated
    :param admids: List of identifiers of administrative metadata to
                   which the logical structuremap should refer.
    :returns: ``None``
    """
    tree = lxml.etree.parse(logical_structmap_file)
    assert tree.xpath('/mets:mets/mets:structMap',
                      namespaces=NAMESPACES)[0].attrib['TYPE'] \
        == "Fairdata-logical"
    assert tree.xpath('/mets:mets/mets:structMap/mets:div',
                      namespaces=NAMESPACES)[0].attrib['TYPE'] \
        == "logical"
    for admid in admids:
        assert admid in \
            tree.xpath('/mets:mets/mets:structMap/mets:div',
                       namespaces=NAMESPACES)[0].attrib['ADMID'].split()

    directories = tree.xpath(
        '/mets:mets/mets:structMap/mets:div/mets:div/@TYPE',
        namespaces=NAMESPACES
    )
    assert len(directories) == 1
    assert 'pid:urn:identifier' in directories

    assert len(tree.xpath('/mets:mets/mets:structMap/mets:div/mets:div'
                          '[@TYPE="pid:urn:identifier"]/mets:fptr/@FILEID',
                          namespaces=NAMESPACES)) == 2


from __future__ import print_function, division, absolute_import, unicode_literals

from .saliency_map_models import SaliencyMapModel

class SIMSaliencyMapModel(SaliencyMapModel):
    def __init__(self, parent_model,
                 kernel_size,
                 train_samples_per_epoch=1000, val_samples=1000,
                 train_seed=43, val_seed=42,
                 fixation_count=100, batch_size=50,
                 max_batch_size=None,
                 initial_learning_rate=1e-7,
                 backlook=1,
                 min_iter=0,
                 max_iter=1000,
                 truncate_gaussian=3,
                 learning_rate_decay_samples=None,
                 learning_rate_decay_scheme=None,
                 learning_rate_decay_ratio=0.333333333,
                 minimum_learning_rate=1e-11,
                 initial_model=None,
                 verbose=True,
                 session_config=None,
                 library='torch',
                 **kwargs
                 ):
        super(SIMSaliencyMapModel, self).__init__(**kwargs)
        self.parent_model = parent_model

        self.kernel_size = kernel_size
        self.train_samples_per_epoch = train_samples_per_epoch
        self.val_samples = val_samples
        self.train_seed = train_seed
        self.val_seed = val_seed
        self.fixation_count = fixation_count
        self.batch_size = batch_size
        self.max_batch_size = max_batch_size
        self.initial_learning_rate = initial_learning_rate
        self.backlook = backlook
        self.min_iter = min_iter
        self.max_iter = max_iter
        self.truncate_gaussian = truncate_gaussian
        self.learning_rate_decay_samples = learning_rate_decay_samples
        self.learning_rate_decay_scheme = learning_rate_decay_scheme
        self.learning_rate_decay_ratio = learning_rate_decay_ratio
        self.minimum_learning_rate = minimum_learning_rate
        self.initial_model = initial_model
        self.verbose = verbose
        self.session_config = session_config
        self.library = library

    def _saliency_map(self, stimulus):
        log_density = self.parent_model.log_density(stimulus)

        if self.initial_model:
            initial_saliency_map = self.initial_model.saliency_map(stimulus)
        else:
            initial_saliency_map = None

        if self.library.lower() == 'tensorflow':
            from .metric_optimization_tf import maximize_expected_sim
        elif self.library.lower() == 'torch':
            from .metric_optimization_torch import maximize_expected_sim
        else:
            raise ValueError(self.library)

        saliency_map, val_scores = maximize_expected_sim(
            log_density,
            kernel_size=self.kernel_size,
            train_samples_per_epoch=self.train_samples_per_epoch,
            val_samples=self.val_samples,
            train_seed=self.train_seed,
            val_seed=self.val_seed,
            fixation_count=self.fixation_count,
            batch_size=self.batch_size,
            max_batch_size=self.max_batch_size,
            verbose=self.verbose,
            session_config=self.session_config,
            initial_learning_rate=self.initial_learning_rate,
            backlook=self.backlook,
            min_iter=self.min_iter,
            max_iter=self.max_iter,
            truncate_gaussian=self.truncate_gaussian,
            learning_rate_decay_samples=self.learning_rate_decay_samples,
            initial_saliency_map=initial_saliency_map,
            learning_rate_decay_scheme=self.learning_rate_decay_scheme,
            learning_rate_decay_ratio=self.learning_rate_decay_ratio,
            minimum_learning_rate=self.minimum_learning_rate
        )
        return saliency_map


from contextlib import contextmanager
import datetime
import itertools
import operator
import os.path
import pkg_resources
import pprint
import re
from functools import partial

from jinja2 import FileSystemLoader
try:
    import babel
except ImportError:
    babel = LazyProxy = None
else:
    from babel.support import LazyProxy

from trac.api import IEnvironmentSetupParticipant, ISystemInfoProvider
from trac.config import *
from trac.core import *
from trac.mimeview.api import RenderingContext, get_mimetype
from trac.perm import IPermissionRequestor
from trac.resource import *
from trac.util import as_bool, as_int, get_pkginfo, get_reporter_id, html, \
                      pathjoin, presentation, to_list, translation
from trac.util.html import (Element, Markup, escape, plaintext, tag,
                            to_fragment, valid_html_bytes)
from trac.util.text import (exception_to_unicode, is_obfuscated,
                            javascript_quote, jinja2env,
                            obfuscate_email_address, pretty_size, shorten_line,
                            to_js_string, to_unicode, unicode_quote_plus)
from trac.util.datefmt import (
    pretty_timedelta, datetime_now, format_datetime, format_date, format_time,
    from_utimestamp, http_date, utc, get_date_format_jquery_ui, is_24_hours,
    get_time_format_jquery_ui, user_time, get_month_names_jquery_ui,
    get_day_names_jquery_ui, get_timezone_list_jquery_ui,
    get_first_week_day_jquery_ui, get_timepicker_separator_jquery_ui,
    get_period_names_jquery_ui, localtz)
from trac.util.translation import _, get_available_locales
from trac.web.api import IRequestHandler, HTTPNotFound
from trac.web.href import Href
from trac.wiki import IWikiSyntaxProvider
from trac.wiki.formatter import format_to, format_to_html, format_to_oneliner

default_mainnav_order = ('wiki', 'timeline', 'roadmap', 'browser',
                         'tickets', 'newticket', 'search', 'admin')
default_metanav_order = ('login', 'logout', 'prefs', 'help', 'about')

class INavigationContributor(Interface):
    """Extension point interface for components that contribute items to the
    navigation.
    """

    def get_active_navigation_item(req):
        """This method is only called for the `IRequestHandler` processing
        the request.

        It should return the name of the navigation item to be highlighted
        as active/current.
        """

    def get_navigation_items(req):
        """Should return an iterable object over the list of navigation items
        to add, each being a tuple in the form (category, name, text).

        The category determines the location of the navigation item and
        can be `mainnav` or `metanav`. The name is a unique identifier that
        must match the string returned by get_active_navigation_item.
        The text is typically a link element with text that corresponds
        to the desired label for the navigation item, and an href.
        """

class ITemplateProvider(Interface):
    """Extension point interface for components that provide their own
    Jinja2 templates and/or accompanying static resources.

    """

    def get_htdocs_dirs():
        """Return a list of directories with static resources (such as style
        sheets, images, etc.)

        Each item in the list must be a `(prefix, abspath)` tuple. The
        `prefix` part defines the path in the URL that requests to these
        resources are prefixed with.

        The `abspath` is the absolute path to the directory containing the
        resources on the local file system.
        """

    def get_templates_dirs():
        """Return a list of directories containing the provided template
        files.
        """

def accesskey(req, key):
    """Helper function for creating accesskey HTML attribute according
    to preference values"""
    return key if req.session.as_int('accesskeys') else None

def add_meta(req, content, http_equiv=None, name=None, scheme=None, lang=None):
    """Add a `<meta>` tag into the `<head>` of the generated HTML."""
    meta = {'content': content, 'http-equiv': http_equiv, 'name': name,
            'scheme': scheme, 'lang': lang, 'xml:lang': lang}
    req.chrome.setdefault('metas', []).append(meta)

def add_link(req, rel, href, title=None, mimetype=None, classname=None,
             **attrs):
    """Add a link to the chrome info that will be inserted as <link> element in
    the <head> of the generated HTML
    """
    linkid = '%s:%s' % (rel, href)
    linkset = req.chrome.setdefault('linkset', set())
    if linkid in linkset:
        return  # Already added that link

    link = {'href': href, 'title': title, 'type': mimetype, 'class': classname}
    link.update(attrs)
    links = req.chrome.setdefault('links', {})
    links.setdefault(rel, []).append(link)
    linkset.add(linkid)

def add_stylesheet(req, filename, mimetype='text/css', **attrs):
    """Add a link to a style sheet to the chrome info so that it gets included
    in the generated HTML page.

    If `filename` is a network-path reference (i.e. starts with a protocol
    or `//`), the return value will not be modified. If `filename` is absolute
    (i.e. starts with `/`), the generated link will be based off the
    application root path. If it is relative, the link will be based off the
    `/chrome/` path.
    """
    href = chrome_resource_path(req, filename)
    add_link(req, 'stylesheet', href, mimetype=mimetype, **attrs)

def add_script(req, filename, mimetype='text/javascript'):
    """Add a reference to an external javascript file to the template.

    If `filename` is a network-path reference (i.e. starts with a protocol
    or `//`), the return value will not be modified. If `filename` is absolute
    (i.e. starts with `/`), the generated link will be based off the
    application root path. If it is relative, the link will be based off the
    `/chrome/` path.
    """
    scriptset = req.chrome.setdefault('scriptset', set())
    if filename in scriptset:
        return False  # Already added that script

    href = chrome_resource_path(req, filename)
    script = {'attrs': {'src': href}}
    if mimetype != 'text/javascript' and mimetype is not None:
        script['attrs']['type'] = mimetype

    req.chrome.setdefault('scripts', []).append(script)
    scriptset.add(filename)

def add_script_data(req, data={}, **kwargs):
    """Add data to be made available in javascript scripts as global variables.

    The keys in `data` and the keyword argument names provide the names of the
    global variables. The values are converted to JSON and assigned to the
    corresponding variables.
    """
    script_data = req.chrome.setdefault('script_data', {})
    script_data.update(data)
    script_data.update(kwargs)

def add_warning(req, msg, *args):
    """Add a non-fatal warning to the request object.

    When rendering pages, all warnings will be rendered to the user. Note that
    the message is escaped (and therefore converted to `Markup`) before it is
    stored in the request object.
    """
    _add_message(req, 'warnings', msg, args)

def add_notice(req, msg, *args):
    """Add an informational notice to the request object.

    When rendering pages, all notices will be rendered to the user. Note that
    the message is escaped (and therefore converted to `Markup`) before it is
    stored in the request object.
    """
    _add_message(req, 'notices', msg, args)

def _add_message(req, name, msg, args):
    if args:
        msg %= args
    if not isinstance(msg, Markup):
        msg = Markup(to_fragment(msg))
    if msg not in req.chrome[name]:
        req.chrome[name].append(msg)

def add_ctxtnav(req, elm_or_label, href=None, title=None):
    """Add an entry to the current page's ctxtnav bar."""
    if href:
        elm = tag.a(elm_or_label, href=href, title=title)
    else:
        elm = elm_or_label
    req.chrome.setdefault('ctxtnav', []).append(elm)

def prevnext_nav(req, prev_label, next_label, up_label=None):
    """Add Previous/Up/Next navigation links.

       :param        req: a `Request` object
       :param prev_label: the label to use for left (previous) link
       :param   up_label: the label to use for the middle (up) link
       :param next_label: the label to use for right (next) link
    """
    links = req.chrome['links']
    prev_link = next_link = None

    if not any(lnk in links for lnk in ('prev', 'up', 'next')):  # Short circuit
        return

    if 'prev' in links:
        prev = links['prev'][0]
        prev_link = tag.a(prev_label, href=prev['href'], title=prev['title'],
                          class_='prev')

    add_ctxtnav(req, tag.span(Markup('← '), prev_link or prev_label,
                              class_='missing' if not prev_link else None))

    if up_label and 'up' in links:
        up = links['up'][0]
        add_ctxtnav(req, tag.a(up_label, href=up['href'], title=up['title']))

    if 'next' in links:
        next_ = links['next'][0]
        next_link = tag.a(next_label, href=next_['href'], title=next_['title'],
                          class_='next')

    add_ctxtnav(req, tag.span(next_link or next_label, Markup(' →'),
                              class_='missing' if not next_link else None))

def web_context(req, resource=None, id=False, version=False, parent=False,
                absurls=False):
    """Create a rendering context from a request.

    The `perm` and `href` properties of the context will be initialized
    from the corresponding properties of the request object.

    >>> from trac.test import Mock, MockPerm
    >>> req = Mock(href=Mock(), perm=MockPerm())
    >>> context = web_context(req)
    >>> context.href is req.href
    True
    >>> context.perm is req.perm
    True

    :param      req: the HTTP request object
    :param resource: the `Resource` object or realm
    :param       id: the resource identifier
    :param  version: the resource version
    :param  absurls: whether URLs generated by the ``href`` object should
                     be absolute (including the protocol scheme and host
                     name)
    :return: a new rendering context
    :rtype: `RenderingContext`

    :since: version 1.0
    """
    if req:
        href = req.abs_href if absurls else req.href
        perm = req.perm
    else:
        href = None
        perm = None
    self = RenderingContext(Resource(resource, id=id, version=version,
                                     parent=parent), href=href, perm=perm)
    self.req = req
    return self

def auth_link(req, link):
    """Return an "authenticated" link to `link` for authenticated users.

    If the user is anonymous, returns `link` unchanged. For authenticated
    users, returns a link to `/login` that redirects to `link` after
    authentication.
    """
    if req.is_authenticated:
        return req.href.login(referer=link)
    return link

def chrome_info_script(req, use_late=None):
    """Get script elements from chrome info of the request object during
    rendering template or after rendering.

    :param      req: the HTTP request object.
    :param use_late: if True, `late_links` will be used instead of `links`.
    """
    chrome = req.chrome
    if use_late:
        links = chrome.get('late_links', {}).get('stylesheet', [])
        scripts = chrome.get('late_scripts', [])
        script_data = chrome.get('late_script_data', {})
    else:
        links = chrome.get('early_links', {}).get('stylesheet', []) + \
                chrome.get('links', {}).get('stylesheet', [])
        scripts = chrome.get('early_scripts', []) + chrome.get('scripts', [])
        script_data = {}
        script_data.update(chrome.get('early_script_data', {}))
        script_data.update(chrome.get('script_data', {}))

    content = []
    content.extend('jQuery.loadStyleSheet(%s, %s);' %
                   (to_js_string(link['href']), to_js_string(link['type']))
                   for link in links or ())
    content.extend('var %s=%s;' % (name, presentation.to_json(value))
                   for name, value in (script_data or {}).items())

    fragment = tag()
    if content:
        fragment.append(tag.script('\n'.join(content), type='text/javascript'))
    for script in scripts:
        attrs = script['attrs']
        fragment.append(tag.script(
            'jQuery.loadScript(%s, %s)' % (to_js_string(attrs['src']),
                                           to_js_string(attrs.get('type')))))

    return fragment

def chrome_resource_path(req, filename):
    """Get the path for a chrome resource given its `filename`.

    If `filename` is a network-path reference (i.e. starts with a protocol
    or `//`), the return value will not be modified. If `filename` is absolute
    (i.e. starts with `/`), the generated link will be based off the
    application root path. If it is relative, the link will be based off the
    `/chrome/` path.
    """
    if filename.startswith(('http://', 'https://', '//')):
        return filename
    elif filename.startswith('common/') and 'htdocs_location' in req.chrome:
        return Href(req.chrome['htdocs_location'])(filename[7:])
    else:
        href = req.href if filename.startswith('/') else req.href.chrome
        return href(filename)

def _save_messages(req, url, permanent):
    """Save warnings and notices in case of redirect, so that they can
    be displayed after the redirect."""
    for type_ in ['warnings', 'notices']:
        for (i, message) in enumerate(req.chrome[type_]):
            req.session['chrome.%s.%d' % (type_, i)] = escape(message, False)

@contextmanager
def component_guard(env, req, component):
    """Traps any runtime exception raised when working with a
    component, logs the error and adds a warning for the user.

    """
    with env.component_guard(component):
        try:
            yield
        except Exception as e:
            add_warning(req, _("%(component)s failed with %(exc)s",
                               component=component.__class__.__name__,
                               exc=exception_to_unicode(e)))
            raise

class Chrome(Component):
    """Web site chrome assembly manager.

    Chrome is everything that is not actual page content.
    """

    implements(ISystemInfoProvider, IEnvironmentSetupParticipant,
               IPermissionRequestor, IRequestHandler, ITemplateProvider,
               IWikiSyntaxProvider)

    required = True
    is_valid_default_handler = False

    navigation_contributors = ExtensionPoint(INavigationContributor)
    template_providers = ExtensionPoint(ITemplateProvider)

    shared_templates_dir = PathOption('inherit', 'templates_dir', '',
        """Path to the //shared templates directory//.

        Templates in that directory are loaded in addition to those in the
        environments `templates` directory, but the latter take precedence.

        Non-absolute paths are relative to the Environment `conf`
        directory.
        """)

    shared_htdocs_dir = PathOption('inherit', 'htdocs_dir', '',
        """Path to the //shared htdocs directory//.

        Static resources in that directory are mapped to /chrome/shared
        under the environment URL, in addition to common and site locations.

        This can be useful in site.html for common interface customization
        of multiple Trac environments.

        Non-absolute paths are relative to the Environment `conf`
        directory.
        (''since 1.0'')""")

    auto_reload = BoolOption('trac', 'auto_reload', False,
        """Automatically reload template files after modification.""")

    htdocs_location = Option('trac', 'htdocs_location', '',
        """Base URL for serving the core static resources below
        `/chrome/common/`.

        It can be left empty, and Trac will simply serve those resources
        itself.

        Advanced users can use this together with
        [TracAdmin trac-admin ... deploy <deploydir>] to allow serving the
        static resources for Trac directly from the web server.
        Note however that this only applies to the `<deploydir>/htdocs/common`
        directory, the other deployed resources (i.e. those from plugins)
        will not be made available this way and additional rewrite
        rules will be needed in the web server.""")

    jquery_location = Option('trac', 'jquery_location', '',
        """Location of the jQuery !JavaScript library (version %(version)s).

        An empty value loads jQuery from the copy bundled with Trac.

        Alternatively, jQuery could be loaded from a CDN, for example:
        http://code.jquery.com/jquery-%(version)s.min.js,
        http://ajax.aspnetcdn.com/ajax/jQuery/jquery-%(version)s.min.js or
        https://ajax.googleapis.com/ajax/libs/jquery/%(version)s/jquery.min.js.

        (''since 1.0'')""", doc_args={'version': '3.6.3'})

    jquery_ui_location = Option('trac', 'jquery_ui_location', '',
        """Location of the jQuery UI !JavaScript library (version %(version)s).

        An empty value loads jQuery UI from the copy bundled with Trac.

        Alternatively, jQuery UI could be loaded from a CDN, for example:
        https://ajax.googleapis.com/ajax/libs/jqueryui/%(version)s/jquery-ui.min.js
        or
        http://ajax.aspnetcdn.com/ajax/jquery.ui/%(version)s/jquery-ui.min.js.

        (''since 1.0'')""", doc_args={'version': '1.13.0'})

    jquery_ui_theme_location = Option('trac', 'jquery_ui_theme_location', '',
        """Location of the theme to be used with the jQuery UI !JavaScript
        library (version %(version)s).

        An empty value loads the custom Trac jQuery UI theme from the copy
        bundled with Trac.

        Alternatively, a jQuery UI theme could be loaded from a CDN, for
        example:
        https://ajax.googleapis.com/ajax/libs/jqueryui/%(version)s/themes/start/jquery-ui.css
        or
        http://ajax.aspnetcdn.com/ajax/jquery.ui/%(version)s/themes/start/jquery-ui.css.

        (''since 1.0'')""", doc_args={'version': '1.13.0'})

    mainnav = ConfigSection('mainnav', """Configures the main navigation bar,
        which by default contains //Wiki//, //Timeline//, //Roadmap//,
        //Browse Source//, //View Tickets//, //New Ticket//, //Search// and
        //Admin//.

        The `label`, `href`, and `order` attributes can be specified. Entries
        can be disabled by setting the value of the navigation item to
        `disabled`.

        The following example renames the link to WikiStart to //Home//,
        links the //View Tickets// entry to a specific report and disables
        the //Search// entry.
        {{{#!ini
        [mainnav]
        wiki.label = Home
        tickets.href = /report/24
        search = disabled
        }}}

        See TracNavigation for more details.
        """)

    metanav = ConfigSection('metanav', """Configures the meta navigation
        entries, which by default are //Login//, //Logout//, //Preferences//,
        ''!Help/Guide'' and //About Trac//. The allowed attributes are the
        same as for `[mainnav]`. Additionally, a special entry is supported -
        `logout.redirect` is the page the user sees after hitting the logout
        button. For example:

        {{{#!ini
        [metanav]
        logout.redirect = wiki/Logout
        }}}

        See TracNavigation for more details.
        """)

    logo_link = Option('header_logo', 'link', '',
        """URL to link to, from the header logo.""")

    logo_src = Option('header_logo', 'src', 'site/your_project_logo.png',
        """URL of the image to use as header logo.
        It can be absolute, server relative or relative.

        If relative, it is relative to one of the `/chrome` locations:
        `site/your-logo.png` if `your-logo.png` is located in the `htdocs`
        folder within your TracEnvironment;
        `common/your-logo.png` if `your-logo.png` is located in the
        folder mapped to the [#trac-section htdocs_location] URL.
        Only specifying `your-logo.png` is equivalent to the latter.""")

    logo_alt = Option('header_logo', 'alt',
        "(please configure the [header_logo] section in trac.ini)",
        """Alternative text for the header logo.""")

    logo_width = IntOption('header_logo', 'width', -1,
        """Width of the header logo image in pixels.""")

    logo_height = IntOption('header_logo', 'height', -1,
        """Height of the header logo image in pixels.""")

    show_email_addresses = BoolOption('trac', 'show_email_addresses', 'false',
        """Show email addresses instead of usernames. If false, email
        addresses are obfuscated for users that don't have EMAIL_VIEW
        permission.
        """)

    show_full_names = BoolOption('trac', 'show_full_names', 'true',
        """Show full names instead of usernames. (//since 1.2//)""")

    never_obfuscate_mailto = BoolOption('trac', 'never_obfuscate_mailto',
        'false',
        """Never obfuscate `mailto:` links explicitly written in the wiki,
        even if `show_email_addresses` is false or the user doesn't have
        EMAIL_VIEW permission.
        """)

    resizable_textareas = BoolOption('trac', 'resizable_textareas', 'true',
        """Make `<textarea>` fields resizable. Requires !JavaScript.
        """)

    wiki_toolbars = BoolOption('trac', 'wiki_toolbars', 'true',
        """Add a simple toolbar on top of Wiki <textarea>s.
        (''since 1.0.2'')""")

    auto_preview_timeout = FloatOption('trac', 'auto_preview_timeout', 2.0,
        """Inactivity timeout in seconds after which the automatic wiki preview
        triggers an update. This option can contain floating-point values. The
        lower the setting, the more requests will be made to the server. Set
        this to 0 to disable automatic preview.
        """)

    default_dateinfo_format = ChoiceOption('trac', 'default_dateinfo_format',
                                           ('relative', 'absolute'),
        """The date information format. Valid options are 'relative' for
        displaying relative format and 'absolute' for displaying absolute
        format. (''since 1.0'')""")

    use_chunked_encoding = BoolOption('trac', 'use_chunked_encoding', 'true',
        """If enabled, send contents as chunked encoding in HTTP/1.1.
        Otherwise, send contents with `Content-Length` header after entire of
        the contents are rendered. (''since 1.0.6'')""")

    templates = None
    jenv = None
    jenv_text = None

    # A dictionary of default context data for templates
    _default_context_data = {
        'all': all,
        'any': any,
        'as_bool': as_bool,
        'as_int': as_int,
        'date': datetime.date,
        'datetime': datetime.datetime,
        'find_element': html.find_element,
        'get_reporter_id': get_reporter_id,
        'groupby': itertools.groupby,
        'http_date': http_date,
        'is_obfuscated': is_obfuscated,
        'javascript_quote': javascript_quote,
        'operator': operator,
        'partial': partial,
        'pathjoin': pathjoin,
        'plaintext': plaintext,
        'pprint': pprint.pformat,
        'pretty_size': pretty_size,
        'pretty_timedelta': pretty_timedelta,
        'quote_plus': unicode_quote_plus,
        'reversed': reversed,
        'shorten_line': shorten_line,
        'sorted': sorted,
        'time': datetime.time,
        'timedelta': datetime.timedelta,
        'to_unicode': to_unicode,
        'utc': utc,
    }

    # ISystemInfoProvider methods

    def get_system_info(self):
        # Mandatory Jinja2
        import jinja2
        info = get_pkginfo(jinja2).get('version')
        yield 'Jinja2', info
        # Optional Babel
        if babel is not None:
            info = get_pkginfo(babel).get('version')
            if not get_available_locales():
                info += " (translations unavailable)"  # No i18n on purpose
                self.log.warning("Locale data is missing")
            yield 'Babel', info

    # IEnvironmentSetupParticipant methods

    def environment_created(self):
        """Add the sample templates to the environment templates dir."""

        def write_sample_template(filename, kind, example=''):
            site_path = os.path.join(self.env.templates_dir,
                                     filename + '.sample')
            with open(site_path, 'w', encoding='utf-8') as fileobj:
                fileobj.write("""\
{#  This file allows customizing the appearance of the Trac installation.

    Add your customizations to the %s here
    and rename the file to %s.

    Note that it will take precedence over a global %s placed
    in the directory specified by [inherit] templates_dir.

    More information about site appearance customization can be found here:

      https://trac.edgewall.org/wiki/TracInterfaceCustomization#SiteAppearance
#}
%s
"""                           % (kind, filename, filename, example))

        write_sample_template('site_head.html',
                              'the end of the HTML <head>', """
<link rel="stylesheet" type="text/css" href="${href.chrome('site/style.css')}"/>
""")
        write_sample_template('site_header.html',
                              'the start of the HTML <body> content')
        write_sample_template('site_footer.html',
                              'the end of the HTML <body> content')

        # Set default values for mainnav and metanav ConfigSections.
        def add_nav_order_options(section, default):
            for i, name in enumerate(default, 1):
                self.env.config.set(section, name + '.order', float(i))
        add_nav_order_options('mainnav', default_mainnav_order)
        add_nav_order_options('metanav', default_metanav_order)

    def environment_needs_upgrade(self):
        return False

    def upgrade_environment(self):
        pass

    # IRequestHandler methods

    def match_request(self, req):
        match = re.match(r'/chrome/(?P<prefix>[^/]+)/+(?P<filename>.+)',
                         req.path_info)
        if match:
            req.args['prefix'] = match.group('prefix')
            req.args['filename'] = match.group('filename')
            return True

    def process_request(self, req):
        prefix = req.args['prefix']
        filename = req.args['filename']

        dirs = []
        for provider in self.template_providers:
            for dir in [os.path.normpath(dir[1]) for dir
                        in provider.get_htdocs_dirs() or []
                        if dir[0] == prefix and dir[1]]:
                dirs.append(dir)
                path = os.path.normpath(os.path.join(dir, filename))
                if os.path.commonprefix([dir, path]) != dir:
                    raise TracError(_("Invalid chrome path %(path)s.",
                                      path=filename))
                elif os.path.isfile(path):
                    req.send_file(path, get_mimetype(path))

        self.log.warning('File %s not found in any of %s', filename, dirs)
        raise HTTPNotFound('File %s not found', filename)

    # IPermissionRequestor methods

    def get_permission_actions(self):
        """`EMAIL_VIEW` permission allows for showing email addresses even
        if `[trac] show_email_addresses` is `false`."""
        return ['EMAIL_VIEW']

    # ITemplateProvider methods

    def get_htdocs_dirs(self):
        return [('common', pkg_resources.resource_filename('trac', 'htdocs')),
                ('shared', self.shared_htdocs_dir),
                ('site', self.env.htdocs_dir)]

    def get_templates_dirs(self):
        return list(filter(None, [
            self.env.templates_dir,
            self.shared_templates_dir,
            pkg_resources.resource_filename('trac', 'templates'),
        ]))

    # IWikiSyntaxProvider methods

    def get_wiki_syntax(self):
        return []

    def get_link_resolvers(self):
        yield ('htdocs', self._format_link)

    def _format_link(self, formatter, ns, file, label):
        file, query, fragment = formatter.split_link(file)
        href = formatter.href.chrome('site', file) + query + fragment
        return tag.a(label, href=href)

    # Public API methods

    def get_all_templates_dirs(self):
        """Return a list of the names of all known templates directories."""
        dirs = []
        for provider in self.template_providers:
            dirs.extend(provider.get_templates_dirs() or [])
        return dirs

    def prepare_request(self, req, handler=None):
        """Prepare the basic chrome data for the request.

        :param     req: the request object
        :param handler: the `IRequestHandler` instance that is processing the
                        request
        """
        self.log.debug('Prepare chrome data for request')

        chrome = {'metas': [], 'links': {}, 'scripts': [], 'script_data': {},
                  'ctxtnav': [], 'warnings': [], 'notices': []}
        req.chrome = chrome

        htdocs_location = self.htdocs_location or req.href.chrome('common')
        chrome['htdocs_location'] = htdocs_location.rstrip('/') + '/'

        # HTML <head> links
        add_link(req, 'start', req.href.wiki())
        add_link(req, 'search', req.href.search())
        add_link(req, 'help', req.href.wiki('TracGuide'))
        add_stylesheet(req, 'common/css/trac.css')
        add_script(req, self.jquery_location or 'common/js/jquery.js')
        # Only activate noConflict mode if requested to by the handler
        if handler is not None and \
                getattr(handler.__class__, 'jquery_noconflict', False):
            add_script(req, 'common/js/noconflict.js')
        add_script(req, 'common/js/babel.js')
        if req.locale is not None and str(req.locale) != 'en_US':
            add_script(req, 'common/js/messages/%s.js' % req.locale)
        add_script(req, 'common/js/trac.js')
        add_script(req, 'common/js/search.js')
        add_script(req, 'common/js/folding.js')

        # Shortcut icon
        chrome['icon'] = self.get_icon_data(req)
        if chrome['icon']:
            src = chrome['icon']['src']
            mimetype = chrome['icon']['mimetype']
            add_link(req, 'icon', src, mimetype=mimetype)

        # Logo image
        chrome['logo'] = self.get_logo_data(req.href, req.abs_href)

        # Navigation links
        chrome['nav'] = self.get_navigation_items(req, handler)

        # Default theme file
        chrome['theme'] = 'theme.html'

        # Avoid recursion by registering as late as possible (#8583)
        req.add_redirect_listener(_save_messages)

        return chrome

    def get_icon_data(self, req):
        icon = {}
        icon_src = icon_abs_src = self.env.project_icon
        if icon_src:
            if not icon_src.startswith('/') and icon_src.find('://') == -1:
                if '/' in icon_src:
                    icon_abs_src = req.abs_href.chrome(icon_src)
                    icon_src = req.href.chrome(icon_src)
                else:
                    icon_abs_src = req.abs_href.chrome('common', icon_src)
                    icon_src = req.href.chrome('common', icon_src)
            mimetype = get_mimetype(icon_src)
            icon = {'src': icon_src, 'abs_src': icon_abs_src,
                    'mimetype': mimetype}
        return icon

    def get_logo_data(self, href, abs_href=None):
        # TODO: Possibly, links to 'common/' could use chrome.htdocs_location
        logo = {}
        logo_src = self.logo_src
        if logo_src:
            abs_href = abs_href or href
            if logo_src.startswith(('http://', 'https://', '/')):
                # Nothing further can be calculated
                logo_src_abs = logo_src
            elif '/' in logo_src:
                # Like 'common/trac_banner.png' or 'site/my_banner.png'
                logo_src_abs = abs_href.chrome(logo_src)
                logo_src = href.chrome(logo_src)
            else:
                # Like 'trac_banner.png'
                logo_src_abs = abs_href.chrome('common', logo_src)
                logo_src = href.chrome('common', logo_src)
            width = self.logo_width if self.logo_width > -1 else None
            height = self.logo_height if self.logo_height > -1 else None
            logo = {
                'link': self.logo_link, 'src': logo_src,
                'src_abs': logo_src_abs, 'alt': self.logo_alt,
                'width': width, 'height': height
            }
        else:
            logo = {'link': self.logo_link, 'alt': self.logo_alt}
        return logo

    def get_navigation_items(self, req, handler):

        def get_item_attributes(category, name, text):
            section = self.config[category]
            href = section.get(name + '.href')
            label = section.get(name + '.label')
            if href and href.startswith('/'):
                href = req.href + href
            if LazyProxy and isinstance(text, LazyProxy):
                text = text.value
            if isinstance(text, Element) and text.tag == 'a':
                link = text
                if label:
                    link.children[0] = label
                if href:
                    link = link(href=href)
            else:
                if href or label:
                    link = tag.a(label or text or name, href=href)
                else:
                    link = text
            return {
                'enabled': section.getbool(name, True),
                'order': section.getfloat(name + '.order', float('inf')),
                'label': label,
                'href': href,
                'link': link,
                'perm': section.get(name + '.permission')
            }

        all_items = {}
        active = None
        for contributor in self.navigation_contributors:
            with component_guard(self.env, req, contributor):
                for category, name, text in \
                        contributor.get_navigation_items(req) or []:
                    all_items.setdefault(category, {})[name] = \
                        get_item_attributes(category, name, text)
                if contributor is handler:
                    active = contributor.get_active_navigation_item(req)

        # Extra navigation items.
        categories = ('mainnav', 'metanav')
        for category, other_category in zip(categories, reversed(categories)):
            section = self.config[category]
            for name in section:
                if '.' not in name and \
                        name not in all_items.get(category, []):
                    text = None
                    # Allow items to be moved between categories.
                    if other_category in all_items and \
                            name in all_items[other_category]:
                        all_items[category][name] = \
                            all_items[other_category][name]
                        del all_items[other_category][name]
                        text = all_items[category][name].get('link')
                    attributes = get_item_attributes(category, name, text)
                    all_items.setdefault(category, {})[name] = attributes
                    if attributes['href'] and \
                            attributes['href'] == req.href(req.path_info):
                        active = name

        nav_items = {}
        for category, category_items in all_items.items():
            nav_items.setdefault(category, [])
            for name, attributes in \
                    sorted(iter(category_items.items()),
                           key=lambda name_attr: (name_attr[1]['order'], name_attr[0])):
                if attributes['enabled'] and attributes['link'] and \
                        (not attributes['perm'] or
                         attributes['perm'] in req.perm):
                    nav_items[category].append({
                        'name': name,
                        'label': attributes['link'],
                        'active': name == active
                    })
        return nav_items

    def get_interface_customization_files(self):
        """Returns a dictionary containing the lists of files present in the
        site and shared templates and htdocs directories.
        """
        def list_dir(path, suffix=None):
            if not os.path.isdir(path):
                return []
            return sorted(to_unicode(name) for name in os.listdir(path)
                          if suffix is None or name.endswith(suffix))

        files = {}
        # Collect templates list
        exts = ('.html', '.txt')
        site_templates = list_dir(self.env.templates_dir, exts)
        shared_templates = list_dir(self.shared_templates_dir, exts)

        # Collect static resources list
        site_htdocs = list_dir(self.env.htdocs_dir)
        shared_htdocs = list_dir(self.shared_htdocs_dir)

        if any((site_templates, shared_templates, site_htdocs, shared_htdocs)):
            files = {
                'site-templates': site_templates,
                'shared-templates': shared_templates,
                'site-htdocs': site_htdocs,
                'shared-htdocs': shared_htdocs,
            }
        return files

    # E-mail formatting utilities

    def author_email(self, author, email_map):
        """Returns the author email from the `email_map` if `author`
        doesn't look like an email address."""
        if email_map and '@' not in author and email_map.get(author):
            author = email_map.get(author)
        return author

    def authorinfo(self, req, author, email_map=None, resource=None):
        """Format a username to HTML.

        Calls `Chrome.format_author` to format the username, and wraps
        the formatted username in a `span` with class `trac-author`,
        `trac-author-anonymous` or `trac-author-none`.

        :param req: the `Request` object.
        :param author: the author string to be formatted.
        :param email_map: dictionary mapping usernames to email addresses.
        :param resource: optional `Resource` object for `EMAIL_VIEW`
                         fine-grained permissions checks.

        :since 1.1.6: accepts the optional `resource` keyword parameter.
        """
        author = self.author_email(author, email_map)
        return tag.span(self.format_author(req, author, resource),
                        class_=self.author_class(req, author))

    def author_class(self, req, author):
        suffix = ''
        if author == 'anonymous':
            suffix = '-anonymous'
        elif not author:
            suffix = '-none'
        elif req and author == req.authname:
            suffix = '-user'
        return 'trac-author' + suffix

    _long_author_re = re.compile(r'.*<([^@]+)@[^@]+>\s*|([^@]+)@[^@]+')

    def authorinfo_short(self, author):
        shortened = None
        match = self._long_author_re.match(author or '')
        if match:
            shortened = match.group(1) or match.group(2)
        return self.authorinfo(None, shortened or author)

    def cc_list(self, cc_field):
        """Split a CC: value in a list of addresses."""
        return to_list(cc_field, r'[;,]')

    def format_author(self, req, author, resource=None, show_email=None):
        """Format a username in plain text.

        If `[trac]` `show_email_addresses` is `False`, email addresses
        will be obfuscated when the user doesn't have `EMAIL_VIEW`
        (for the resource) and the optional parameter `show_email` is
        `None`. Returns translated `anonymous` or `none`, when the
        author string is `anonymous` or evaluates to `False`,
        respectively.

        :param req: a `Request` or `RenderingContext` object.
        :param author: the author string to be formatted.
        :param resource: an optional `Resource` object for performing
                         fine-grained permission checks for `EMAIL_VIEW`.
        :param show_email: an optional parameter that allows explicit
                           control of e-mail obfuscation.

        :since 1.1.6: accepts the optional `resource` keyword parameter.
        :since 1.2: Full name is returned when `[trac]` `show_full_names`
                    is `True`.
        :since 1.2: Email addresses are obfuscated when
                    `show_email_addresses` is False and `req` is Falsy.
                    Previously email addresses would not be obfuscated
                    whenever `req` was Falsy (typically `None`).
        """
        if author == 'anonymous':
            return _("anonymous")
        if not author:
            return _("(none)")
        users = self.env.get_known_users(as_dict=True)
        if self.show_full_names and author in users:
            name = users[author][0]
            if name:
                return name
        if show_email is None:
            show_email = self.show_email_addresses
            if not show_email and req:
                show_email = 'EMAIL_VIEW' in req.perm(resource)
        return author if show_email else obfuscate_email_address(author)

    def format_emails(self, context, value, sep=', '):
        """Normalize a list of e-mails and obfuscate them if needed.

        :param context: the context in which the check for obfuscation should
                        be done
        :param   value: a string containing a comma-separated list of e-mails
        :param     sep: the separator to use when rendering the list again
        """
        formatted = [self.format_author(context, author)
                     for author in self.cc_list(value)]
        return sep.join(formatted)

    def get_email_map(self):
        """Get the email addresses of all known users."""
        email_map = {}
        if self.show_email_addresses:
            for username, name, email in self.env.get_known_users():
                if email:
                    email_map[username] = email
        return email_map

    # Element modifiers

    def add_textarea_grips(self, req):
        """Make `<textarea class="trac-resizable">` fields resizable if enabled
        by configuration."""
        if self.resizable_textareas:
            add_script(req, 'common/js/resizer.js')

    def add_wiki_toolbars(self, req):
        """Add wiki toolbars to `<textarea class="wikitext">` fields."""
        if self.wiki_toolbars:
            add_script(req, 'common/js/wikitoolbar.js')
        self.add_textarea_grips(req)

    def add_auto_preview(self, req):
        """Setup auto-preview for `<textarea>` fields."""
        add_script(req, 'common/js/auto_preview.js')
        auto_preview_timeout = req.session.as_float('ui.auto_preview_timeout',
                                                    self.auto_preview_timeout,
                                                    min=0)
        add_script_data(req, auto_preview_timeout=auto_preview_timeout,
                        form_token=req.form_token)

    def add_jquery_ui(self, req):
        """Add a reference to the jQuery UI script and link the stylesheet."""
        add_script(req, self.jquery_ui_location
                        or 'common/js/jquery-ui.js')
        add_stylesheet(req, self.jquery_ui_theme_location
                            or 'common/css/jquery-ui/jquery-ui.css')
        add_script(req, 'common/js/jquery-ui-addons.js')
        add_stylesheet(req, 'common/css/jquery-ui-addons.css')
        is_iso8601 = req.lc_time == 'iso8601'
        now = datetime_now(req.tz)
        tzoffset = now.strftime('%z')
        default_timezone = (-1 if tzoffset.startswith('-') else 1) * \
                           (int(tzoffset[1:3]) * 60 + int(tzoffset[3:5]))
        timezone_list = get_timezone_list_jquery_ui(now) \
                        if is_iso8601 else None
        add_script_data(req, jquery_ui={
            'month_names': get_month_names_jquery_ui(req),
            'day_names': get_day_names_jquery_ui(req),
            'date_format': get_date_format_jquery_ui(req.lc_time),
            'time_format': get_time_format_jquery_ui(req.lc_time),
            'ampm': not is_24_hours(req.lc_time),
            'period_names': get_period_names_jquery_ui(req),
            'first_week_day': get_first_week_day_jquery_ui(req),
            'timepicker_separator': get_timepicker_separator_jquery_ui(req),
            'show_timezone': is_iso8601,
            'default_timezone': default_timezone,
            'timezone_list': timezone_list,
            'timezone_iso8601': is_iso8601,
        })
        add_script(req, 'common/js/jquery-ui-i18n.js')

    # Template data (Jinja2)

    def populate_data(self, req=None, data=None, d=None):
        """Fills a dictionary with the standard set of fields expected
        by templates.

        :param req: a `Request` object; if `None`, no request related fields
                    will be set
        :param data: user-provided `dict`, which can be used to override
                     the defaults; if `None`, the defaults will be returned
        :param d: `dict` which is populated with the defaults; if `None`,
                  an empty `dict` will be used
        """
        if d is None:
            d = {}
        d['trac'] = {
            'version': self.env.trac_version,
            'homepage': 'https://trac.edgewall.org/',  # FIXME: use setup data
        }

        href = req and req.href
        abs_href = req.abs_href if req else self.env.abs_href
        admin_href = None
        if self.env.project_admin_trac_url == '.':
            admin_href = href
        elif self.env.project_admin_trac_url:
            admin_href = Href(self.env.project_admin_trac_url)

        d['project'] = {
            'name': self.env.project_name,
            'descr': self.env.project_description,
            'url': self.env.project_url,
            'admin': self.env.project_admin,
            'admin_href': admin_href,
            'admin_trac_url': self.env.project_admin_trac_url,
        }
        footer = self.env.project_footer
        d['chrome'] = {
            'footer': Markup(footer and translation.gettext(footer))
        }

        if req:
            d['chrome'].update(req.chrome)
        else:
            d['chrome'].update({
                'htdocs_location': self.htdocs_location,
                'logo': self.get_logo_data(self.env.abs_href),
            })

        def pretty_dateinfo(date, format=None, dateonly=False):
            if not date:
                return ''
            if format == 'date':
                absolute = user_time(req, format_date, date)
            else:
                absolute = user_time(req, format_datetime, date)
            now = datetime_now(localtz)
            relative = pretty_timedelta(date, now)
            if not format:
                format = req.session.get('dateinfo',
                                         self.default_dateinfo_format)
            in_or_ago = _("in %(relative)s", relative=relative) \
                        if date > now else \
                        _("%(relative)s ago", relative=relative)
            if format == 'relative':
                label = in_or_ago if not dateonly else relative
                title = absolute
            else:
                if dateonly:
                    label = absolute
                elif req.lc_time == 'iso8601':
                    label = _("at %(iso8601)s", iso8601=absolute)
                elif format == 'date':
                    label = _("on %(date)s", date=absolute)
                else:  # format == 'datetime'
                    label = _("on %(date)s at %(time)s",
                              date=user_time(req, format_date, date),
                              time=user_time(req, format_time, date))
                title = in_or_ago
            return tag.span(label, title=title)

        def dateinfo(date):
            return pretty_dateinfo(date, format='relative', dateonly=True)

        def get_rel_url(resource, **kwargs):
            return get_resource_url(self.env, resource, href, **kwargs)

        def get_abs_url(resource, **kwargs):
            return get_resource_url(self.env, resource, abs_href, **kwargs)

        def accesskey_attr(key):
            key = accesskey(req, key)
            return Markup('accesskey="%s"') % key if key else ''

        dateinfo_format = \
            req.session.get('dateinfo', self.default_dateinfo_format) \
            if req else self.default_dateinfo_format

        locale = req and req.locale
        if locale:
            trac_lang = locale.language
            if locale.territory:
                trac_lang += '-' + locale.territory
        else:
            trac_lang = 'en'

        d.update({
            'env': self.env,
            'context': web_context(req) if req else None,
            'Resource': Resource,
            'url_of': get_rel_url,
            'abs_url_of': get_abs_url,
            'name_of': partial(get_resource_name, self.env),
            'shortname_of': partial(get_resource_shortname, self.env),
            'summary_of': partial(get_resource_summary, self.env),
            'resource_link': partial(render_resource_link, self.env),
            'req': req,
            'abs_href': abs_href,
            'href': href,
            'perm': req and req.perm,
            'form_token': req and req.form_token,
            'authname': req.authname if req else '<trac>',
            'locale': locale,
            'trac_lang': trac_lang,
            'author_email': partial(self.author_email,
                                    email_map=self.get_email_map()),
            'authorinfo': partial(self.authorinfo, req),
            'authorinfo_short': self.authorinfo_short,
            'format_author': partial(self.format_author, req),
            'format_emails': self.format_emails,
            'captioned_button': partial(presentation.captioned_button, req),
            'accesskey': accesskey_attr,

            # Date/time formatting
            'dateinfo': dateinfo,
            'dateinfo_format': dateinfo_format,
            'pretty_dateinfo': pretty_dateinfo,
            'format_datetime': partial(user_time, req, format_datetime),
            'format_date': partial(user_time, req, format_date),
            'format_time': partial(user_time, req, format_time),
            'fromtimestamp': partial(datetime.datetime.fromtimestamp,
                                     tz=req and req.tz),
            'from_utimestamp': from_utimestamp,

            # Wiki-formatting functions
            'wiki_to': partial(format_to, self.env),
            'wiki_to_html': partial(format_to_html, self.env),
            'wiki_to_oneliner': partial(format_to_oneliner, self.env),
        })

        # Finally merge in the page-specific data
        if data:
            d.update(data)
        return d

    # Template rendering (Jinja2)

    def load_template(self, filename, text=False):
        """Retrieves a template with the given name.

        This simply loads the template. If you want to make use of the
        "standard" Trac API for templates, also call `populate_data`,
        or consider using the shortcut method `prepare_template`
        instead.

        :param text: in text mode (``True``) XML/HTML auto-escape of
                     variable expansion is disabled.
        """
        if not self.jenv:
            jinja2_dirs = self.get_all_templates_dirs()
            self.jenv = jinja2env(
                loader=FileSystemLoader(jinja2_dirs),
                auto_reload=self.auto_reload,
                autoescape=True,
            )
            self.jenv.globals.update(self._default_context_data.copy())
            self.jenv.globals.update(translation.functions)
            self.jenv.globals.update(unicode=to_unicode)
            presentation.jinja2_update(self.jenv)
            self.jenv_text = self.jenv.overlay(autoescape=False)
        return (self.jenv_text if text else self.jenv).get_template(filename)

    def render_template(self, req, filename, data, metadata):
        """Renders the ``filename`` template using ``data`` for the context.

        It attempts to load a Jinja2 template, augments the provided
        *data* with standard data, and renders it according to the
        options provided in *metadata*.

        The options that can be specified in ``metadata`` are
        ``content_type``, ``text``, ``domain``, ``fragment`` and ``iterable``.

        When ``fragment`` is ``True``, or ``text`` is ``True``, we generate
        some content which does not need all of the chrome related data,
        typically HTML fragments, XML or plain text.

        If ``iterable`` is ``True``, we use `generate_template_stream` to
        produce the output (iterable of UTF-8 encoded bytes),
        otherwise we use `render_template_string` and UTF-8 encode the
        result.

        :since 1.5.1: The ``fragment``, ``iterable`` and ``method`` parameters
            have been removed. Use the ``metadata`` dictionary with keys of
            the same name. The ``method`` key of ``metadata`` is no longer
            supported, use ``text=True`` to indicate that the template is a
            plain text one, with no need for HTML/XML escaping to take place.
        """
        content_type = metadata.get('content_type')
        text = metadata.get('text')
        domain = metadata.get('domain')
        fragment = metadata.get('fragment')
        iterable = metadata.get('iterable')

        if content_type is None:
            content_type = 'text/html'

        method = {'text/html': 'html',
                  'text/plain': 'text'}.get(content_type, 'xml')

        if method == "html":
            # Retrieve post-redirect messages saved in session
            for type_ in ['warnings', 'notices']:
                try:
                    for i in itertools.count():
                        message = Markup(req.session.pop('chrome.%s.%d'
                                                         % (type_, i)))
                        if message not in req.chrome[type_]:
                            req.chrome[type_].append(message)
                except KeyError:
                    pass

        if text is None:
            text = method == 'text'

        template, data = self.prepare_template(req, filename, data, text,
                                               domain)

        # TODO (1.5.1) - have another try at simplifying all this...
        # With Jinja2, it's certainly possible to do things
        # differently, but for as long as we have to support Genshi,
        # better keep one way.
        links = req.chrome.get('links')
        scripts = req.chrome.get('scripts')
        script_data = req.chrome.get('script_data')
        req.chrome.update({'early_links': links, 'early_scripts': scripts,
                           'early_script_data': script_data,
                           'links': {}, 'scripts': [], 'script_data': {}})
        data.setdefault('chrome', {}).update({
            'late_links': req.chrome['links'],
            'late_scripts': req.chrome['scripts'],
            'late_script_data': req.chrome['script_data'],
        })

        if fragment or text:
            if iterable:
                return self.generate_template_stream(template, data, text,
                                                     iterable)
            else:
                s = self.render_template_string(template, data, text)
                return s.encode('utf-8')

        data['chrome']['content_type'] = content_type

        try:
            return self.generate_template_stream(template, data, text,
                                                 iterable)
        except Exception:
            # restore what may be needed by the error template
            req.chrome.update({'early_links': None, 'early_scripts': None,
                               'early_script_data': None, 'links': links,
                               'scripts': scripts, 'script_data': script_data})
            raise

    def generate_fragment(self, req, filename, data, text=False, domain=None):
        """Produces content ready to be sent from the given template
        *filename* and input *data*, with minimal overhead.

        It calls `prepare_template` to augment the *data* with the
        usual helper functions that can be expected in Trac templates,
        including the translation helper functions adapted to the given
        *domain*, except for some of the chrome "late" data.

        If you don't need that and don't want the overhead, use
        `load_template` and `generate_template_stream` directly.

        The generated output is suitable to pass directly to
        `Request.send`, see `generate_template_stream` for details.

        See also `render_fragment`, which returns a string instead.

        """
        template, data = self.prepare_template(req, filename, data, text,
                                               domain)
        return self.generate_template_stream(template, data, text)

    def render_fragment(self, req, filename, data, text=False, domain=None):
        """Produces a string from given template *filename* and input *data*,
        with minimal overhead.

        It calls `prepare_template` to augment the *data* with the
        usual helper functions that can be expected in Trac templates,
        including the translation helper functions adapted to the given
        *domain*, except for some of the chrome "late" data.

        If you don't need that and don't want the overhead, use
        `load_template` and `render_template_string` directly.

        :rtype: the generated output is a `str` string if *text*
                is ``True``, or a `Markup` string otherwise.

        See also `generate_fragment`, which produces an output
        suitable to pass directly to `Request.send` instead.

        """
        template, data = self.prepare_template(req, filename, data, text,
                                               domain)
        return self.render_template_string(template, data, text)

    def prepare_template(self, req, filename, data, text=False, domain=None):
        """Prepares the rendering of a Jinja2 template.

        This loads the template and prepopulates a data `dict` with
        the "standard" Trac API for templates.

        :param req: a `Request` instance (optional)
        :param filename: the name of a Jinja2 template, which must be
                         found in one of the template directories (see
                         `get_templates_dirs`)
        :param data: user specified data dictionary, used to override
                     the default context set from the request *req*
                     (see `populate_data`)
        :param text: in text mode (``True``) XML/HTML auto-escape of
                     variable expansion is disabled.

        :rtype: a pair of Jinja2 `Template` and a `dict`.

        """
        template = self.load_template(filename, text)
        if domain:
            symbols = list(translation.functions)
            domain_functions = translation.domain_functions(domain, symbols)
            data.update(dict(zip(symbols, domain_functions)))
        data = self.populate_data(req, data)
        return template, data

    def generate_template_stream(self, template, data, text=False,
                                 iterable=None):
        """Returns the rendered template in a form that can be "sent".

        This will be either a single UTF-8 encoded `btyes` object, or an
        iterable made of chunks of the above.

        :param template: the Jinja2 template
        :type template: ``jinja2.Template``

        :param text: in text mode (``True``) the generated bytes will
                     not be sanitized (see `valid_html_bytes`).

        :param iterable: determine whether the output should be
                         generated in chunks or as a single `btyes`; if
                         `None`, the `use_chunked_encoding` property
                         will be used to determine this instead

        :rtype: `btyes` or an iterable of `btyes`, depending on *iterable*

        .. note:

        Turning off the XML/HTML auto-escape feature for variable
        expansions has to be disabled when loading the template (see
        `load_template`), so remember to stay consistent with the
        *text* parameter.

        """
        stream = template.stream(data)
        stream.enable_buffering(75)  # buffer_size
        if iterable or iterable is None and self.use_chunked_encoding:
            return self.iterable_content(stream, text)
        else:
            if text:
                def generate():
                    for chunk in stream:
                        yield chunk.encode('utf-8')
            else:
                def generate():
                    for chunk in stream:
                        yield valid_html_bytes(chunk.encode('utf-8'))
            return b''.join(generate())

    def render_template_string(self, template, data, text=False):
        """Renders the template as a str or Markup string.

        :param template: the Jinja2 template
        :type template: ``jinja2.Template``

        :param text: in text mode (``True``) the generated string
                     will not be wrapped in `Markup`

        :rtype: `str` if *text* is ``True``, `Markup` otherwise.

        .. note:

        Turning off the XML/HTML auto-escape feature for variable
        expansions has to be disabled when loading the template (see
        `load_template`), so remember to stay consistent with the
        *text* parameter.

        """
        string = template.render(data)
        return string if text else Markup(string)

    def iterable_content(self, stream, text=False, **kwargs):
        """Generate an iterable object which iterates `btyes` instances
        from the given stream instance.

        :param text: in text mode (``True``) XML/HTML auto-escape of
                     variable expansion is disabled.
        """
        try:
            if text:
                for chunk in stream:
                    yield chunk.encode('utf-8')
            else:
                for chunk in stream:
                    yield valid_html_bytes(chunk.encode('utf-8'))
        except TracBaseError:
            raise
        except Exception as e:
            self.log.error('Jinja2 %s error while rendering %s template %s',
                           e.__class__.__name__,
                           'text' if text else 'XML/HTML',
                           exception_to_unicode(e, traceback=True))


#!/usr/bin/env python
# -*- coding: utf-8 -*-
# TensorFlow and tf.keras
import tensorflow as tf
from tensorflow import keras

# Helper libraries
import numpy as np
import matplotlib.pyplot as plt
#download minist
fashion_mnist = keras.datasets.fashion_mnist

(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()
class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat',
               'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']

train_images = train_images / 255.0

test_images = test_images / 255.0

model = keras.Sequential([
    keras.layers.Flatten(input_shape=(28, 28)),
    keras.layers.Dense(128, activation=tf.nn.relu),
    keras.layers.Dense(10, activation=tf.nn.softmax)
])

model.compile(optimizer=tf.train.AdamOptimizer(),
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

model.fit(train_images, train_labels, epochs=5)
test_loss, test_acc = model.evaluate(test_images, test_labels)

print('Test accuracy:', test_acc)
# Grab an image from the test dataset
img = test_images[0]
# Add the image to a batch where it's the only member.
img = (np.expand_dims(img,0))
predictions_single = model.predict(img)

print('prediction result:',np.argmax(predictions_single[0]))


import os
import sys
import subprocess
from time import sleep
from datetime import datetime
import argparse

def change_link(loss = 0,delay = 0,th = 0,target_NIC='ens6'):
	os.system("sudo tc qdisc change dev " +  target_NIC + " root netem delay " + str(delay) + "ms loss "+str(loss)+"%")

if __name__ == '__main__':
	parser = argparse.ArgumentParser()
	parser.add_argument("-d","--delay",help="delay of simulator",type=int)
	parser.add_argument("-l","--loss",help="loss of simulator(%)",type=int)
	args = parser.parse_args()
	delay = args.delay
	loss = args.loss
	while True:
		#interrupt the link quality every 10s
		sleep(3)
		#print('start')
		#print(datetime.now())
		change_link(loss,delay)
		#the handover takes 2s
		sleep(1)
		#print('finish')
		#print(datetime.now())
		#the handover finished
		change_link(0,0)


#euler 19
#1 Jan 1900 was a Monday.
#How many Sundays fell on the first of the month during the twentieth century (1 Jan 1901 to 31 Dec 2000)?

def leapyear(y):
	if y % 4 == 0:
		if y % 100 == 0:
			if y % 400 == 0:
				return True
			else:
				return False
		else:
			return True

	else:
		return False

days_of_the_week = []
week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']

for i in range(0, 60000):
	days_of_the_week.extend(week)

months = {1:31, 3: 31, 4:30, 5:31, 6:30, 7:31, 8:31, 9:30, 10:31, 11:30, 12:31}

year = 1900
month = 1
day = 1
day_it = 0

result = 0

while year <= 2000:

	#if leapyear(year):
		#print(year)

	month = 1

	while month <=  12:

		if month == 2:
			if leapyear(year):
				days_in_month = 29
			else:
				days_in_month = 28
		else:
			days_in_month = months[month]

		day = 1

		while day <= days_in_month:

			today = days_of_the_week[day_it]

			#print(today + " " + str(day) + " " + str(month) + " " + str(year))
			#input()

			if today == 'Sunday' and day == 1:
				result += 1
				#print("WINNER")
				print(today + " " + str(day) + " " + str(month) + " " + str(year))

			day += 1
			day_it += 1

		month += 1

	year += 1

print(result)


#Vraag een dag van de week op (ma,di,wo,do,vr,za,zo) Print alle dagen tot en met de opgegeven dag.

dagen = {
    "ma" : 1,
    "di" : 2,
    "wo" : 3,
    "do" : 4,
    "vr" : 5,
    "za" : 6,
    "zo" : 7
}

while True:
    print("Welke dag? [ma/di/wo/do/vr/za/zo]")
    dag = input()
    if dag not in dagen:
        print("A.U.B. een dag invullen.")
        continue
    else:
        print()
        for i in range(dagen[dag]):
            print(list(dagen.keys())[i])
        break


from google.appengine.ext import ndb

#Database
class Comment(ndb.Model):

    post_id = ndb.IntegerProperty(required = True)
    comment = ndb.TextProperty(required = True)
    poster = ndb.StringProperty(required = True)
    commented = ndb.DateProperty(auto_now_add = True)

    @classmethod
    def create_new_comment(cls, post_id, comment, poster):
        return Comment(post_id = post_id,
                    comment = comment,
                    poster = poster)


import pandas as pd
import numpy as np
import os
import sys
import matplotlib.pyplot as plt
import geopandas as gpd
import json
import glob
import datetime
from functools import reduce
from _1_calculate_revised_snow_drought_new_SWE import FormatData,CalcSnowDroughts
import snow_drought_definition_revision_new_SWE as sd
from snow_drought_definition_revision_new_SWE import DefineClusterCenters
from sklearn.metrics import confusion_matrix
from sklearn.metrics import accuracy_score
from sklearn.metrics import classification_report
import seaborn as sns
from scipy.stats import pearsonr
import matplotlib.ticker as ticker
import pymannkendall as mk
from scipy import stats

def mk_test(input_data):
	"""Run a version of the Mann-Kendall trend test."""

	trend, h, p, z, Tau, s, var_s, slope, intercept = mk.original_test(input_data)

	return trend, h, p, z, Tau, s, var_s, slope, intercept

def plot_counts(df_list,output_dir,huc_col='huc8',**kwargs):
	print('Entered the plotting function: ')
	labels=['Snotel','UA SWE']

	nrow=3
	ncol=3
	fig,axs = plt.subplots(nrow,ncol,sharex=True,sharey=True,
				gridspec_kw={'wspace':0.01,'hspace':0},
                                    #'top':0.95, 'bottom':0.075, 'left':0.05, 'right':0.95},
                figsize=(8,6))
	cols=['dry','warm','warm_dry']
	xlabels=['Dry', 'Warm', 'Warm/dry']
	ylabels=['Early','Mid','Late']
	output_dict = {}
	mk_dict = {}
	count = 0
	for x in range(3):
		for y in range(3):

			#produce the count plots
			s_counts = df_list[x][f's_{cols[y]}'].value_counts().sort_index().astype('int')
			ua_counts = df_list[x][f'ua_{cols[y]}'].value_counts().sort_index().astype('int')
			# print(s_counts)
			# print(ua_counts)
			df = pd.DataFrame({"snotel":s_counts,"ua_swe":ua_counts})
			#reformat a few things in the df
			df.index=df.index.astype(int)
			df.replace(np.nan,0,inplace=True)

			#there are not droughts in all the years and timeframes but these gaps mess up plotting
			#so we want to infill them with zeros so all the timeperiods have all of the years.
			df=df.reindex(np.arange(1991,2021), fill_value=0)
			mk_dict.update({f's_{ylabels[x]}_{xlabels[y]}':mk_test(df.snotel)[0],
				f'ua_{ylabels[x]}_{xlabels[y]}':mk_test(df.ua_swe)[0]})
			#add the counts to a dict so we can output the actual counts and look at them
			output_dict.update({f's_{ylabels[x]}_{xlabels[y]}':df['snotel'],f'ua_{ylabels[x]}_{xlabels[y]}':df['ua_swe']})

			# calculate Pearson's correlation
			corr, _ = pearsonr(df.snotel, df.ua_swe)
			rho, pval = stats.spearmanr(df.snotel, df.ua_swe)
			print(f'Pearsons correlation: {corr}')

			df.plot.bar(ax=axs[x][y],color=['#D95F0E','#267eab'],width=0.9,legend=False)#,label=['Dry','Warm','Warm/dry']) #need to do something with the color scheme here and maybe error bars? This could be better as a box plot?
			axs[x][y].grid(axis='both',alpha=0.25)

			#set axis labels and annotate
			axs[x][y].annotate(f'\u03C1 = {round(rho,2)}',xy=(0.05,0.9),xycoords='axes fraction',fontsize=10)
			#add a subplot letter id
			axs[x][y].annotate(f'{chr(97+count)}',xy=(0.85,0.9),xycoords='axes fraction',fontsize=10,weight='bold')#f'{chr(97)}'
			axs[0][y].set_title(xlabels[y],fontdict={'fontsize': 10})
			axs[x][0].set_ylabel(ylabels[x],fontsize=10)
			# ax1.set_ylabel("Snow drought quantities")
			axs[2][2].legend(labels=labels,loc='center right',prop={'size': 10})
			axs[x][y].xaxis.set_major_locator(ticker.MultipleLocator(5))
			# for tick in axs[x][y].get_xticklabels():
			# 	tick.set_rotation(90)
			# 	print('tick is: ',tick)
			# 	print(type(tick))
			# 	#tick.label.set_fontsize(14)
			axs[x][y].tick_params(axis='x', labelsize=8)
			count += 1
	fig.text(0.025, 0.5, 'Snow drought count', va='center', rotation='vertical')

	print('mk dict is')
	print(mk_dict)
	stats_fn = os.path.join(output_dir,f'{huc_col}_ua_swe_snotel_snow_drought_counts_spearman_w_delta_swe_proj_final1.csv')
	output_df = pd.DataFrame(output_dict)
	if not os.path.exists(stats_fn):
		output_df.to_csv(stats_fn)
	fig_fn = os.path.join(kwargs.get('fig_dir'),f'ua_swe_counts_snotel_{huc_col}_spearman_w_delta_swe_proj_final_new_legend.jpg')
	if not os.path.exists(fig_fn):
		plt.savefig(fig_fn,
			dpi=500,
			bbox_inches = 'tight',
    		pad_inches = 0.1
    	)
	# plt.show()
	# plt.close('all')

def main(model_dir,pickles,start_date='1980-10-01',end_date='2020-09-30',huc_col = 'huc8', **kwargs):
	"""Testing improved definitions of snow drought. Original definitions based on Dierauer et al 2019 but trying to introduce some more nuance into the approach."""
	print(f'The huc col being processed is: {huc_col}')
	################################################################
	#first do the UA swe data - this is now (9/20/2021) in two different files, one from UA SWE and one from PRISM.
	#These are combined in _0_combine_ua_swe_w_prism.py
	early=FormatData(glob.glob(model_dir+f'*_12_months_data_output_formatted_combined.csv'),drop_cols=[]).read_in_csvs()
	mid=FormatData(glob.glob(model_dir+f'*_2_months_data_output_formatted_combined.csv'),drop_cols=[]).read_in_csvs()
	late=FormatData(glob.glob(model_dir+f'*_4_months_data_output_formatted_combined.csv'),drop_cols=[]).read_in_csvs()
	################################################################
	#next do the snotel data
	output=[]
	print('early')
	print(early)
	#read in some pickled objects, these look like a list of dfs with each being a station for the full time period
	for item in ['PREC','TAVG','WTEQ']:
		#get the pickled objects for each parameter
		files = glob.glob(pickles+f'*{item}_{start_date}_{end_date}_snotel_data_list') #hardcoded currently
		df=FormatData(files,drop_cols=['year','month','day']).read_in_pickles()
		output.append(df) #the df here is 365 days x ~30 yrs x 237 stations so these are pretty big dfs

	#join the three enviro params
	output_df = reduce(lambda left,right: pd.merge(left,right,how='inner',on=['date','id']), output)

	#convert the temp column from F to C
	output_df['TAVG'] = (output_df['TAVG']-32)*(5/9)
	#there are a couple of erroneous temp values, remove those
	output_df = output_df.loc[output_df['TAVG'] < 50]
	output_df = output_df.loc[output_df['TAVG'] > -40]

	#convert prec and swe cols from inches to mm
	output_df['PREC'] = output_df['PREC']*25.4
	output_df['WTEQ'] = output_df['WTEQ']*25.4

	#remove rows that have one of the data types missing- this might need to be amended because
	#it means that there are different numbers of records in some of the periods.
	output_df=output_df.dropna() #commented out 9/21/2021

	#cast the snotel id col to int to add the hucs
	output_df['id'] = output_df['id'].astype('int')

	#add the as yet nonexistant hucs data to the outputs
	hucs = kwargs.get('hucs')
	output_df[huc_col] = output_df['id'].map(hucs)
	#just doing this to get the snotel ids that we're using in later steps before we take the mean below
	id_df = output_df
	#there are multiple snotel stations in some of the basins,
	#combine those so there is just one number per basin like the
	#ua swe and RS data.

	output_df=output_df.groupby([huc_col,'date'])[['PREC','WTEQ','TAVG']].mean().reset_index()

	period_list = []
	for p1,p2 in zip(['early','mid','late'],[early,mid,late]):
		#get snotel first
		#make a temporal chunk of data- this is all seasonal windows irrespective of the year
		snotel_chunk=FormatData(None,time_period=p1).split_yearly_data(output_df)

		#####
		#####
		#new for UA SWE- there are some basins which extend into Canada but PRISM and UA SWE do not.
		#as of 9/22/2021 these basins are removed because there are no stats for the area north of the border.
		if huc_col == 'huc8':
			snotel_chunk = snotel_chunk.loc[~snotel_chunk[huc_col].isin(kwargs.get('remove_ids'))]
			p2 = p2.loc[~p2[huc_col].isin(kwargs.get('remove_ids'))]
		else:
			pass
		# print('Snotel chunk looks like: ')
		# pd.set_option("display.max_rows", None, "display.max_columns", None)
		# print(snotel_chunk.head(100))
		##########working below here
		############################
		#calculate the snow droughts for that chunk
		if (p1 == 'mid') | (p1 == 'late'):
			print('processing snotel')
			snotel_drought=CalcSnowDroughts(snotel_chunk,swe_c='WTEQ',precip='PREC',temp='TAVG',start_year=1991,sort_col=huc_col).prepare_df_cols()
		else:
			print('processing snotel')
			snotel_drought=CalcSnowDroughts(snotel_chunk,swe_c='WTEQ',precip='PREC',temp='TAVG',sort_col=huc_col).prepare_df_cols()

		#then do the same for ua SWE
		if (p1 == 'mid') | (p1 == 'late'):
			print('processing uaswe')
			ua_swe_drought=CalcSnowDroughts(p2,start_year=1991,sort_col=huc_col).prepare_df_cols()
		else:
			print('processing uaswe')
			ua_swe_drought=CalcSnowDroughts(p2,sort_col=huc_col).prepare_df_cols()

	##########################################

		#run the kmeans with drought types as intiilization conditions (centroids) for the clusters

		#these are all of the huc 4 basins in the study area
		huc4s = ['1708','1801','1710','1711','1709','1701','1702','1705','1703','1601','1707','1706','1712','1704']
		#use a subset to run a test for maritime snowpack
		#huc4s = ['1711','1709','1702','1703','1708','1707','1703','1702']
		#use a subset to run an alpine(ish) snowpack
		#huc4s = ['1704','1706','1601','1705']
		s_output = []
		ua_output = []
		for huc4 in huc4s:
			huc4_s = sd.prep_clusters(snotel_drought,huc4,p1,huc_col=huc_col) #get the subset of the snow drought data for a given huc4
			huc4_ua = sd.prep_clusters(ua_swe_drought,huc4,p1,huc_col=huc_col) #period added 9/21/2021 to make sure these get attributed to the correct water year
			#make the centroids that serve as the intialization for the kmeans clusters- these are like endmembers (ish)
			s_centroids = DefineClusterCenters(huc4_s,'WTEQ','PREC','TAVG').combine_centroids() #makes a numpy array with four centroids
			ua_centroids = DefineClusterCenters(huc4_ua,'swe','ppt','tmean').combine_centroids() #makes a numpy array with four centroids

			#clusters should be like: {0:dry, 1:warm, 2:warm_dry, 3:no_drought} 6/8/2021 DOUBLE CHECK
			#run kmeans for the snotel data
			s_clusters = sd.run_kmeans(huc4_s[['WTEQ','PREC','TAVG']].to_numpy(),huc4_s['label'],s_centroids)
			s_clusters = sd.add_drought_cols_to_kmeans_output(s_clusters, huc_col=huc_col) #add a few cols needed for plotting
			#run kmeans for the daymet data
			ua_clusters = sd.run_kmeans(huc4_ua[['swe','ppt','tmean']].to_numpy(),huc4_ua['label'],ua_centroids)
			ua_clusters = sd.add_drought_cols_to_kmeans_output(ua_clusters, huc_col=huc_col) #add a few cols needed for plotting

			s_output.append(s_clusters)
			ua_output.append(ua_clusters)
		s_plot = pd.concat(s_output)

		#select the cols of interest and rename so there's no confusion when dfs are merged
		s_plot=s_plot[[huc_col,'year','dry','warm','warm_dry']]
		s_plot.columns=[huc_col,'year']+['s_'+column for column in s_plot.columns if not (column == huc_col) | (column=='year')]

		ua_plot = pd.concat(ua_output)
		ua_plot=ua_plot[[huc_col,'year','dry','warm','warm_dry']]
		ua_plot.columns=[huc_col,'year']+['ua_'+column for column in ua_plot.columns if not (column == huc_col) | (column=='year')]

		#merge the two datasets into one df
		dfs = s_plot.merge(ua_plot,on=[huc_col,'year'],how='inner')
		# print('The final ds looks like: ')
		# print(dfs.head(50))
		#deal with the scenario that there are basins with less than 30 years of data, remove those here
		dfs = sd.remove_short_dataset_stations(dfs,huc_col)
		print()
		print(f'There are now {len(dfs[huc_col].unique())} unique stations in the seasonal window')
		hucs=list(dfs[huc_col].astype(int).unique())
		print(hucs)
		print(len(hucs))
		# test = id_df.loc[id_df['huc8'].isin(dfs['huc8'].astype(int).unique())]['id'].unique()
		# print(test)
		period_list.append(dfs)

	plot_counts(period_list,kwargs.get('stats_dir'),huc_col=huc_col,**kwargs)

if __name__ == '__main__':
	params = sys.argv[1]
	with open(str(params)) as f:
		variables = json.load(f)

		#construct variables from param file
		pickles=variables['pickles']
		stations=variables['stations']
		ua_swe_dir=variables['ua_swe_dir']
		stats_dir = variables['stats_dir']
		fig_dir = variables['fig_dir']

	hucs=pd.read_csv(stations)

	#get just the id cols
	hucs = hucs[['huc_08','id']]
	print('hucs shape is: ')
	print(hucs.shape)
	#rename the huc col
	hucs.rename({'huc_08':'huc8'},axis=1,inplace=True)

	hucs_dict=dict(zip(hucs.id,hucs.huc8))
	remove_ids = [17110004,
				17110001,
				17110005,
				17020006,
				17020002,
				17020001,
				17010216,
				17010215,
				17010104]

	# maritime_test = [17110012,
	# 			17110010,
	# 			17110009,
	# 			17110013,
	# 			17110014,
	# 			17110015,
	# 			17090011,
	# 			17070204,
	# 			17020009,
	# 			17030001,
	# 			17030002,
	# 			17080004,
	# 			17080005,
	# 			17080002,
	# 			17070106,
	# 			17030003,
	# 			17020010,
	# 			17020011,
	# 			17080001,
	# 			17070105,
	# 			17070305,
	# 			17070301,
	# 			17090004,
	# 			17070304,
	# 			17070306,
	# 			17090005,
	# 			17090006]
	# alpine_test = [
	# 			17040207,
	# 			17040205,
	# 			17040208,
	# 			17040218,
	# 			17040217,
	# 			17060201,
	# 			17040214,
	# 			17060204,
	# 			17060203,
	# 			17040204,
	# 			17040202,
	# 			17040209,
	# 			17050120,
	# 			17050111,
	# 			17040219,
	# 			17050113,
	# 			17040220,
	# 			17040221,
	# 			17040212,
	# 			17040213,
	# 			16010202,
	# 			16010204,
	# 			16010201,
	# 			]

	main(ua_swe_dir,pickles,
		huc_col='huc8',
		hucs=hucs_dict,
		stats_dir=stats_dir,
		fig_dir=fig_dir,
		remove_ids=remove_ids
		#test_ids = maritime_test
		)


from typing import List, Optional, Union, cast

from librespot.audio.decoders import AudioQuality
from sanic.request import Request

from internals.spotify.shims import SpotifyAudioFormat

__all__ = (
    "get_spotify_audio_format",
    "get_spotify_audio_quality",
)

QueryParam = Optional[Union[str, List[str]]]

def select_first(single_or_multi: Union[str, List[str]]) -> str:
    if isinstance(single_or_multi, list):
        return single_or_multi[0]
    return single_or_multi

def get_spotify_audio_format(request: Request) -> Optional[SpotifyAudioFormat]:
    # Fallback from `format` to `fmt`
    query_param = cast(QueryParam, request.args.get("format") or request.args.get("fmt"))
    if query_param is None:
        return None
    query_param = select_first(query_param)

    query_param = query_param.lower()
    audio_mappings = {
        "mp3": SpotifyAudioFormat.MP3,
        "aac": SpotifyAudioFormat.AAC,
        "vorbis": SpotifyAudioFormat.VORBIS,
        "ogg": SpotifyAudioFormat.VORBIS,
        "m4a": SpotifyAudioFormat.AAC,
        "opus": SpotifyAudioFormat.VORBIS,
        "flac": SpotifyAudioFormat.FLAC,
        "hires": SpotifyAudioFormat.FLAC,
    }

    return audio_mappings.get(query_param)

def get_spotify_audio_quality(request: Request) -> Optional[AudioQuality]:
    # Fallback from `format` to `fmt`
    query_param = cast(QueryParam, request.args.get("q") or request.args.get("qual") or request.args.get("quality"))
    if query_param is None:
        return None

    query_param = query_param.lower()
    quality_mappings = {
        "lowest": AudioQuality.NORMAL,
        "low": AudioQuality.NORMAL,
        "lq": AudioQuality.NORMAL,
        "medium": AudioQuality.HIGH,
        "normal": AudioQuality.HIGH,
        "high": AudioQuality.VERY_HIGH,
        "hq": AudioQuality.VERY_HIGH,
        "highest": AudioQuality.VERY_HIGH,
    }

    return quality_mappings.get(query_param)