import copy
import pickle
import random
from time import time
from gensim import matutils
from math import exp, log, inf, sqrt
import numpy as np
from sklearn.cluster import estimate_bandwidth, MeanShift

from utilitarian import Progresser
from scipy.stats import gaussian_kde
from sklearn.neighbors.kde import KernelDensity

class SuperPredictor:
    """Super Predictor

    Parameters
    ----------
    super_words : dict
        super_words[year][cat][context] is the frequency of that category-context pair in that year.
        Only the new context words should be in this dictionary.

    path : string
        The path to save the pickle file of predictions.

    start : int
        The year to start considering the words.

    threshold: int
        The year to start predicting the words.

    end : int
        The ending year.

    word_vectors : dict
        dictionary of words to their vector representations

    word_vector_yby: dict
        dictionary of years to word vectors in case of LDA and PCA dimension reduction in each year.

    swc : dict
        super-words-complete. a version of the super-words that all the context words and their frequencies are present,
        not just the new ones.

    new_words : dict
        A dictionary of years and the newly appearing words in that year.
    """

    def __init__(self, super_words, path, start, threshold, end, step, hbc, word_vectors, word_vector_yby, swc=None):
        self.word_vectors = word_vectors
        self.super_words = super_words
        self.hbc = hbc
        self.path = path
        self.kernel_width = None
        self.categories = super_words[threshold].keys()
        self.threshold = threshold
        self.start = start
        self.end = end
        self.step = step
        self.new_words = None
        self.predictions = None
        self.prior_dist = None
        self.cat_sim_method = None
        self.vec_sim_method = None
        self.super_words_so_far = None  # super words until current year for each category
        self.word_freq_so_far = None
        self.prg = None
        self.word_vectors_yby = word_vector_yby
        self.kde_functions = dict()
        self.super_words_c = swc
        self.prototypes = None

        # initiate frequency of each category in each year
        self.frequency = dict()
        for yr in range(start, end, step):
            self.frequency[yr] = dict()
            for cat in self.categories:
                self.frequency[yr][cat] = sum(self.super_words_c[yr][cat].values())

        # initialise new word list for each year
        self.new_words = dict()
        for yr in range(start, end, step):
            self.new_words[yr] = set()
            for cat, wrds in self.super_words[yr].items():
                self.new_words[yr] = self.new_words[yr] | wrds.keys()

    def predict_them_all(self, prior_dist, cat_sim_method, vec_sim_method, kw=1.0):
        """
        Predicts for new words of all years from the threshold until the end.

        Parameters
        ----------
        prior_dist : string
            valid prior distributions are
            ['uniform'|'frequency'|'items']

        cat_sim_method : string
            valid category similarity methods are
            ['one'|'gkde'|'nn'|'avg'|'wavg'|'eavg'|
            'tnn'|'tnn-avg'|'*nn'|'*tnn'|
            'pt0-avg'|'pt-avg'|'pt0-mode'|'pt-mode']

        vec_sim_method: string
            valid vector similarity methods are
            ['cos'|'euc'|'exp_euc'|'exp_euc_sq'|'hbc']

        kw: float or dict, (default 1.0)
            kernel width for exp_euc and exp_euc_sq similarity methods
        """

        t = time()
        if type(kw) == float or type(kw) == int or type(kw) == str:
            kw = float(kw)
            self.kernel_width = dict()
            for year in range(self.start, self.end, self.step):
                self.kernel_width[year] = kw
            kw_name = str(kw)
        else:
            self.kernel_width = kw
            kw_name = 'adj'
        self.prior_dist = prior_dist
        self.cat_sim_method = cat_sim_method
        self.vec_sim_method = vec_sim_method

        # initialise super words until current year for each category
        if 'tnn' not in cat_sim_method:
            self.__init_super_words_so_far()

        # iterate on years and predict for each one
        self.prg = Progresser(sum([len(self.new_words[yr]) for yr in range(self.threshold, self.end, self.step)]),
                              msg='predicting for ' + prior_dist + '-' + cat_sim_method + '-' +
                                  vec_sim_method + '_' + kw_name)

        # do the predictions for each year
        self.predictions = dict()
        for year in range(self.threshold, self.end, self.step):
            self.__predict_one_step(year)

        # write the results to file
        with open('./predictions/' + self.path + '/' + prior_dist + '-' + cat_sim_method + '-' + vec_sim_method +
                  '_' + kw_name + '.pkl', 'wb') as prediction_file:
            pickle.dump(self.predictions, prediction_file)

        load_time = time() - t
        print('\t prediction for ' + prior_dist + '-' + cat_sim_method + '-' + vec_sim_method + '_' + kw_name +
              ' done in', int(round(load_time / 60, 0)), ':', int(round(load_time % 60, 0)), 'sec')

    def __init_super_words_so_far(self):
        self.super_words_so_far = dict()
        for cat in self.categories:
            self.super_words_so_far[cat] = set()
        for yr in range(self.start, self.threshold, self.step):
            for cat, wrds in self.super_words[yr].items():
                self.super_words_so_far[cat] = self.super_words_so_far[cat] | wrds.keys()

        if 'pt-wavg' in self.cat_sim_method:
            self.word_freq_so_far = dict()
            for cat in self.categories:
                self.word_freq_so_far[cat] = dict()
            for yr in range(self.start, self.threshold, self.step):
                for cat, wrds in self.super_words[yr].items():
                    for w, f in wrds.items():
                        try:
                            self.word_freq_so_far[cat][w] += f
                        except:
                            self.word_freq_so_far[cat][w] = f

    def __update_prototypes(self, yr):
        self.prototypes = dict()
        for cat in self.categories:
            X = []
            if 'wavg' in self.cat_sim_method:
                ws = []
            for word in self.super_words_so_far[cat]:
                X.append(self.word_vectors[word])
                try:
                    ws.append(self.super_words[yr][cat][word])
                except KeyError:
                    ws.append(0)
                except:
                    continue

            pt_vec = np.nan
            if len(X) == 0:
                pass
            elif 'wavg' in self.cat_sim_method:
                if sum(ws) == 0:
                    pt_vec = np.mean(X, axis=0)
                else:
                    pt_vec = np.average(X, axis=0, weights=ws)
            elif 'avg' in self.cat_sim_method:
                pt_vec = np.mean(X, axis=0)
            elif 'mode' in self.cat_sim_method:
                try:
                    bandwidth = estimate_bandwidth(X, quantile=0.2, n_samples=10000)
                    ms = MeanShift(bandwidth=bandwidth, bin_seeding=True)
                    ms.fit(X)
                    labels_unique, l_counts = np.unique(ms.labels_, return_counts=True)
                    best_label = labels_unique[np.where(l_counts == max(l_counts))]
                    pt_vec = ms.cluster_centers_[best_label]
                except:
                    # if there are any errors just use mean instead
                    pt_vec = np.mean(X, axis=0)

            # print(pt_vec)
            self.prototypes[cat] = pt_vec

    def __predict_one_step(self, year):
        """
        update the super_words_so_far with new words if needed, prepare the word vector of this year if it is year by
        year and calculate the prior for each category and the similarities for each word.

        Parameters
        ----------
        year: (int)
            predict for this year
        """
        # prepare w2v list for this year if the vectors are Year by Year
        if self.word_vectors_yby is not None:
            self.word_vectors = self.word_vectors_yby[year]

        # update super words until now if the method is not based on only the immediate previous year
        if 'tnn' not in self.cat_sim_method:
            for cat, wrds in self.super_words[year - self.step].items():
                self.super_words_so_far[cat] = self.super_words_so_far[cat] | wrds.keys()
            if 'pt-wavg' in self.cat_sim_method:
                for cat, wrds in self.super_words[year - self.step].items():
                    for w, f in wrds.items():
                        try:
                            self.word_freq_so_far[cat][w] += f
                        except:
                            self.word_freq_so_far[cat][w] = f

        # initialise prototypes for each category in this year
        if 'pt' in self.cat_sim_method:
            if '0' not in self.cat_sim_method:
                self.__update_prototypes(yr=year - self.step)
            elif self.prototypes is None:
                self.__update_prototypes(yr=year - self.step)

        # prepare kde functions
        if self.cat_sim_method == 'gkde':
            ne = len(self.new_words[year])
            ol = sum(len(ws) for ws in self.super_words_so_far.values())
            print(year, '\t', '%' + str(int(100 * ne / (ol + ne))), ne, '\t', '%' + str(int(100 * ol / (ol + ne))), ol)
            for cat in self.categories:

                vectors = [self.word_vectors[w] for w in self.super_words_so_far[cat]]
                if len(vectors) > 1:
                    self.kde_functions[cat] = KernelDensity(kernel='gaussian', bandwidth=0.2).fit(vectors)
                else:
                    self.kde_functions[cat] = None

        # p_c = self.__calculate_prior(year=year - self.step,log_res=True)
        p_c = self.__calculate_prior(year=year - self.step, log_res=False)

        p_c_i = dict()  # P(category | item) ~ P(category) * P(item | category)
        # initialise the 2D dict
        for category in self.categories:
            p_c_i[category] = dict()

        for item in self.new_words[year]:
            self.prg.count()
            # standard knn methods:
            if self.cat_sim_method.startswith('s') and self.cat_sim_method.endswith('nn'):
                k = int(self.cat_sim_method[1:-2])
                max_sim = [(0, '') for _ in range(k)]
                for cat in self.categories:
                    for word in self.super_words_so_far[cat]:
                        try:
                            sim = self.__vector_similarity(word, item, year=year)
                            if sim > max_sim[0][0]:
                                max_sim[0] = (sim, cat)
                            max_sim = sorted(max_sim, key=lambda t: t[0])
                        except Exception as e:
                            print(10, e)
                nn_count = {cat: 0 for cat in self.categories}
                max_n = 0
                for x in max_sim:
                    nn_count[x[1]] += 1
                    max_n = max(max_n, nn_count[x[1]])
                best_cats = [cat for cat in self.categories if nn_count[cat] == max_n]
                if self.prior_dist == 'uniform':
                    best_cat = random.choice(best_cats)
                elif self.prior_dist == 'items' or self.prior_dist == 'frequency':
                    best_cats = {bc: p_c[bc] for bc in best_cats}
                    best_cat = sorted(best_cats.items(), key=lambda kv: kv[1])[-1][0]

                for category in self.categories:
                    p_c_i[category][item] = 0
                p_c_i[best_cat][item] = 1
                # TODO: check this
            else:  # other methods
                # for category in self.categories:
                #     # don't predict for categories that had it before
                #     if item in self.super_words_so_far[category]:
                #         p_c_i[category][item] = - inf
                #     else:
                #         # the values are in logarithm so the multiplication would be the sum of the two values
                #         p_c_i[category][item] = p_c[category] + self.__get_similarity(item, year - self.step, category)
                # TODO: the next lines or the previous lines
                sum_over_categories = 0.0
                for category in self.categories:
                    if len(self.super_words_so_far[category]) == 0:
                        p_c_i[category][item] = 0
                    # don't predict for categories that had it before
                    if item in self.super_words_so_far[category]:
                        p_c_i[category][item] = 0
                    else:
                        # the values are in logarithm so the multiplication would be the sum of the two values
                        p_c_i[category][item] = p_c[category] * self.__get_similarity(item, year - self.step, category,
                                                                                False)
                        sum_over_categories += p_c_i[category][item]

                for category in self.categories:
                    if len(self.super_words_so_far[category]) == 0 or item in self.super_words_so_far[category]:
                        p_c_i[category][item] = -inf
                    else:
                        # p_c_i[category][item] = log(p_c_i[category][item]) - log(sum_over_categories)

                        try:
                            p_c_i[category][item] = log(p_c_i[category][item]) - log(sum_over_categories)
                        except:
                            p_c_i[category][item] = -inf
                            # a=self.__get_similarity(item, year - self.step, category, False)
                            # a=self.__get_similarity(item, year - self.step, category, False)
                            # p_c_i[item][category] = -1000000000.0

        self.predictions[year] = p_c_i

    def set_params(self, prior_dist, cat_sim_method, vec_sim_method):
        self.prior_dist = prior_dist
        self.cat_sim_method = cat_sim_method
        self.vec_sim_method = vec_sim_method

    def build_super_words_until(self):
        self.super_words_until = {self.threshold - 2 * self.step: {}}
        for cat in self.categories:
            self.super_words_until[self.threshold - 2 * self.step][cat] = set()
        for yr in range(self.start, self.threshold - self.step):
            for cat, wrds in self.super_words[yr].items():
                self.super_words_until[self.threshold - 2 * self.step][cat] = \
                    self.super_words_until[self.threshold - 2 * self.step][cat] | wrds.keys()
        for yr in range(self.threshold - self.step, self.end, self.step):
            self.super_words_until[yr] = copy.deepcopy(self.super_words_until[yr - self.step])
            for cat, wrds in self.super_words[yr].items():
                self.super_words_until[yr][cat] = self.super_words_until[yr][cat] | wrds.keys()

    def log_likelihood_of_posterior(self, year, kw):
        """
        to find the best kernel width ...
        """
        kw = float(kw)
        self.kernel_width = dict()
        for y in range(self.start, self.end, self.step):
            self.kernel_width[y] = kw
        # self.prg = Progresser(len(self.new_words[year]),
        #                       msg='llp for ' + self.prior_dist + '-' + self.cat_sim_method + '-' + self.vec_sim_method
        #                           + '_' + str(self.kernel_width[year]) + 'y:' + str(year))

        # prepare w2v list for this year if the vectors are Year by Year
        if self.word_vectors_yby is not None:
            self.word_vectors = self.word_vectors_yby[year]

        # update super words until now if the method is not based on only the immediate previous year
        try:
            self.super_words_so_far = self.super_words_until[year - self.step]
        except:
            self.build_super_words_until()
            self.super_words_so_far = self.super_words_until[year - self.step]

        p_c = self.__calculate_prior(year=year - self.step, log_res=False)

        p_c_i = dict()  # P(category | item) ~ P(category) * P(item | category)
        posterior_list = []

        # For each new noun
        for item in self.new_words[year]:
            p_c_i[item] = dict()
            true_cats = []
            sum_over_categories = 0.0
            for category in self.categories:
                if len(self.super_words_so_far[category]) == 0:
                    continue
                # don't count categories that had it before
                # if item in self.super_words_so_far[category]:
                #     p_c_i[item][category] = 0
                # else:
                # the values are in logarithm so the multiplication would be the sum of the two values
                p_c_i[item][category] = p_c[category] * self.__get_similarity(item, year - self.step, category, False)
                sum_over_categories += p_c_i[item][category]
                if item in self.super_words[year][category]:
                    true_cats.append(p_c_i[item][category])

            for x in true_cats:
                try:
                    posterior_list.append(log(x) - log(sum_over_categories))
                except:
                    print('EXCEPTION IN LOG POSTERIOR!')

            # self.prg.count()

        # log_of_posteriors = sum(posterior_list)
        # print('length of the list:', len(posterior_list))
        log_of_posteriors = np.mean(posterior_list)

        return log_of_posteriors

    def __calculate_prior(self, year=None, log_res=True):
        """
        Parameters
        ----------
        year : (int)
            For the case of frequency based prior.

        Returns
        -------
        p_c : (dict)
            A dictionary of the categories and the logarithm of their prior.

        """
        p_c = dict()  # prior: P(category)

        # first methods - probability of each category with uniform distribution
        if self.prior_dist == 'uniform':
            for category in self.categories:
                p_c[category] = 1

        # second method - probability of each category based on frequency
        elif self.prior_dist == 'frequency':
            for category in self.categories:
                p_c[category] = 0
                for yr in range(self.start, year + self.step, self.step):
                    p_c[category] += sum(self.super_words[yr][category].values())

        elif self.prior_dist == 'frequency_sqr':
            for category in self.categories:
                p_c[category] = 0
                for yr in range(self.start, year + self.step, self.step):
                    p_c[category] += sum([sqrt(a) for a in self.super_words[yr][category].values()])

        elif self.prior_dist == 'frequency_log':
            for category in self.categories:
                p_c[category] = 0
                for yr in range(self.start, year + self.step, self.step):
                    p_c[category] += sum([log(a) for a in self.super_words[yr][category].values()])

        # third method - based on longer list of items
        elif self.prior_dist == 'items':
            for category in self.categories:
                p_c[category] = len(self.super_words_so_far[category])
        else:
            raise Exception('Prior calculation method not defined!')

        if log_res:
            # convert to log(p_c)
            for key in p_c.keys():
                if p_c[key] == 0:
                    p_c[key] = - inf
                else:
                    p_c[key] = log(p_c[key])
        return p_c

    def __get_similarity(self, item, year, category, log_res=True):
        """
        Parameters
        ----------
        item : (string)
            The word which its similarity is needed.

        category : (string)
            The category which the item is going to be compared for getting the similarity.

        year : (int)
            For the case of frequency based prior and kernel widths.

        Returns
        -------
        similarity : (float)
            The logarithm of the similarity between the word and the category.

        """
        similarity = 0
        if self.cat_sim_method == 'one':
            similarity = 1
        elif self.cat_sim_method == 'gkde':
            if self.kde_functions[category] is None:
                a = 0
            else:
                # a = self.kde_functions[category].evaluate(self.word_vectors[item])[0]
                a = self.kde_functions[category].score_samples([self.word_vectors[item]])[0]
            # print('--', a)
            # if a == -inf or a == np.nan or a == inf:
            #     a = 0
            return a
        elif self.cat_sim_method == 'nn':
            # nearest neighbour method
            max_similarity = 0
            for word in self.super_words_so_far[category]:
                try:
                    sim = self.__vector_similarity(word, item, year=year)
                    max_similarity = max(sim, max_similarity)
                except Exception as e:
                    print('1', e)
            similarity = max_similarity

        elif self.cat_sim_method == 'avg':
            # average similarity method
            similarity_sum = 0
            words_count = 0
            for word in self.super_words_so_far[category]:
                try:
                    similarity_sum += self.__vector_similarity(word, item, year=year)
                    words_count += 1
                except Exception as e:
                    print('2', e)
            similarity = similarity_sum / words_count if words_count != 0 else 0

        elif self.cat_sim_method == 'wavg':
            # weighted average similarity method
            similarity_sum = 0
            for word in self.super_words_c[year][category].keys():
                try:
                    similarity_sum += self.__vector_similarity(word, item, year=year) * \
                                      self.super_words_c[year][category][word]
                except Exception as e:
                    print('3', e)
            similarity = similarity_sum / self.frequency[year][category] if self.frequency[year][
                                                                                category] != 0 else 0
        elif self.cat_sim_method == 'wavg_sqr':
            # weighted average similarity method
            similarity_sum = 0
            for word in self.super_words_c[year][category].keys():
                try:
                    similarity_sum += self.__vector_similarity(word, item, year=year) * \
                                      sqrt(self.super_words_c[year][category][word])
                except Exception as e:
                    print('4', e)
            total_freq = sum([sqrt(a) for a in self.super_words[year][category].values()])
            similarity = similarity_sum / total_freq if total_freq != 0 else 0

        elif self.cat_sim_method == 'wavg_log':
            # weighted average similarity method
            similarity_sum = 0
            for word in self.super_words_c[year][category].keys():
                try:
                    similarity_sum += self.__vector_similarity(word, item, year=year) * \
                                      log(self.super_words_c[year][category][word])
                except Exception as e:
                    print('5', e)
            total_freq = sum([log(a) for a in self.super_words[year][category].values()])
            similarity = similarity_sum / total_freq if total_freq != 0 else 0

        elif self.cat_sim_method == 'eavg':
            # average similarity method except the 0 frequencies (not use hitherto)
            similarity_sum = 0
            words_count = 0
            for word in self.super_words_c[year][category].keys():
                try:
                    similarity_sum += self.__vector_similarity(word, item, year=year)
                    words_count += 1
                except Exception as e:
                    print('6', e)
            similarity = similarity_sum / words_count if words_count != 0 else 0

        elif self.cat_sim_method == 'tnn':
            # nearest neighbour method for the last decade
            max_similarity = 0
            for word in self.super_words[year][category]:
                try:
                    if word == item:
                        continue
                    sim = self.__vector_similarity(word, item, year=year)
                    max_similarity = max(sim, max_similarity)
                except Exception as e:
                    print('7', e)
            similarity = max_similarity

        elif self.cat_sim_method == 'tnn_avg':
            # average similarity method for the last decade
            similarity_sum = 0
            words_count = 0
            for word in self.super_words[year][category]:
                try:
                    if word == item:
                        continue
                    similarity_sum += self.__vector_similarity(word, item, year=year)
                    words_count += 1
                except Exception as e:
                    print('8', e)
            similarity = similarity_sum / words_count if words_count != 0 else 0

        elif 'tnn' in self.cat_sim_method:
            # avg of K nearest neighbours in each category in the last time point
            max_sim = [0 for _ in range(int(self.cat_sim_method[:-3]))]
            for word in self.super_words[year][category]:
                try:
                    if word == item:
                        continue
                    sim = self.__vector_similarity(word, item, year=year)

                    max_sim[0] = max(sim, max_sim[0])
                    max_sim = sorted(max_sim)
                except Exception as e:
                    print('9', e)
            similarity = np.mean(max_sim)

        elif 'nn' in self.cat_sim_method:
            # avg of K nearest neighbours in each category
            max_sim = [0 for _ in range(int(self.cat_sim_method[:-2]))]
            for word in self.super_words_so_far[category]:
                try:
                    sim = self.__vector_similarity(word, item, year=year)

                    max_sim[0] = max(sim, max_sim[0])
                    max_sim = sorted(max_sim)
                except Exception as e:
                    print(10, e)
            similarity = np.mean(max_sim)
        elif 'pt' in self.cat_sim_method:
            similarity = self.__vector_similarity(w1=item, v2=self.prototypes[category], year=year)
        else:
            raise Exception('Category similarity method not defined!')

        if log_res:
            if similarity == 0:
                return - inf
            return log(similarity) - log(self.kernel_width[year + self.step])  # TODO: I changed this
        else:
            return similarity / self.kernel_width[year + self.step]  # TODO: I changed this

    def __vector_similarity(self, w1=None, w2=None, v1=None, v2=None, year=None):
        """
        Parameters
        ----------
        w1 : (string)
            The first word to get the similarity.

        w2 : (string)
            The Second word to get the similarity.

        year : (int)
            The year parameter for kernel width value

        Returns
        -------
        similarity : (float)
            The similarity between the vectors of the two words.

        """
        if w1 is not None:
            vec1 = self.word_vectors[w1]
        elif v1 is not None:
            vec1 = v1
        if w2 is not None:
            vec2 = self.word_vectors[w2]
        elif v2 is not None:
            vec2 = v2

        if vec2 is np.nan:
            vec2 = np.zeros(len(vec1))

        if self.vec_sim_method == 'cos':
            return (np.dot(matutils.unitvec(vec1), matutils.unitvec(vec2)) + 1) / 2
        elif self.vec_sim_method == 'euc':
            return 1 / np.sqrt(np.sum((vec1 - vec2) ** 2))
        elif self.vec_sim_method == 'exp_euc':
            return exp(
                - np.sqrt(np.sum((vec1 - vec2) ** 2)) / self.kernel_width[year + self.step])
        elif self.vec_sim_method == 'exp_euc_sq':
            return exp(- np.sum((vec1 - vec2) ** 2) / self.kernel_width[year + self.step])
        elif self.vec_sim_method == 'hbc':
            try:
                return self.hbc[w1][w2]
            except IndexError:
                return 0.1
        else:
            raise Exception('Vector similarity method not defined!')

    def find_neighbouring_words(self, word, category, year, starting_year=1988):
        print(word, category, year)

        self.vec_sim_method = 'exp_euc_sq'
        self.kernel_width = dict()
        for year in range(self.threshold, self.end, self.step):
            self.kernel_width[year] = 1.0
        kw_name = str(1.0)
        self.word_vectors = self.word_vectors_yby[year]

        # find nearest neighbour in the specified category
        distances = dict()
        for yr in range(starting_year, year, self.step):
            for cat, wrds in self.super_words[yr].items():
                if cat == category:
                    for w in wrds.keys():
                        if w == word:
                            continue
                        distances[w] = self.__vector_similarity(word, w)
        sorted_words = sorted(distances, key=distances.get, reverse=True)
        print('--', sorted_words[0], distances[sorted_words[0]], '\t', category)
        # words = []
        # for i in range(5):
        #     try:
        #         words.append(sorted_words[i])
        #         print('+', sorted_words[i], distances[sorted_words[i]])
        #     except:
        #         pass
        # words.append('...')
        # for i in range(3):
        #     ind = len(sorted_words) - i - 1
        #     words.append(sorted_words[ind])
        #     print('-', sorted_words[ind], distances[sorted_words[ind]])
        # print(words)

        # find nearest neighbour in all categories
        distances = dict()
        for yr in range(starting_year, year, self.step):
            for cat, wrds in self.super_words[yr].items():
                for w in wrds.keys():
                    if w == word:
                        continue
                    distances[w] = self.__vector_similarity(word, w)
        sorted_words = sorted(distances, key=distances.get, reverse=True)
        for i in range(10):
            # find its real categories
            real_cats = []
            for yr in range(self.start, year, self.step):
                for cat, wrds in self.super_words[yr].items():
                    if sorted_words[i] in wrds:
                        real_cats.append((cat, yr))
            print(i + 1, ')', sorted_words[i], distances[sorted_words[i]], '\t', real_cats)


import os

from flask import Flask, session, render_template, url_for, request, redirect, jsonify, Response
from flask_session import Session
import random, json, time, datetime

from flask_socketio import SocketIO, emit, join_room, leave_room

# configure the flask_socketio
app = Flask(__name__)
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
socketio = SocketIO(app)

# list of channels
channel_list = ["General"]
user_list = []

# Dictionary of users & messages
user_dm_list = {}

# dictionary to track rooms, or private channels
# Rooms = {"dn:" displayname, "room": room}
Rooms = {}

now = datetime.datetime.now()

startup_message = {
    "channel": "General",
    "user_from": "Flack Bot",
    "user_to": "",
    "timestamp": now.strftime("%a %b %d %I:%M:%S %Y"),
    "msg_txt": "Welcome to Flack Messaging"}

channel_messages = {
    "General": {
        'messages': [startup_message]
}}

# this is our index page
@app.route("/")
def index():
    return render_template("index.html")

# this function handles the logout button and returns
# index page as a result
@app.route("/logout", methods=["POST"])
def logout():
    return render_template("index.html")

# this handles the chatrooms
@app.route("/chatbox", methods=["POST", "GET"])
def chatbox():
     user = request.form.get("displayname")
     return render_template("chatbox.html", name=user)

# this is a event of submitting channel to create
# a new channel
@socketio.on("submit channel")
def new_channel(data):
    channel = data["channel"]
    channel_list.append(channel)
    emit("announce channel", {"channel": channel}, broadcast=True)

# this is the backend to support querying for
# list of new available channels
@app.route("/get_channels", methods=["POST"])
def get_channels():
    return jsonify({"success": True, "channel_list": channel_list})

# this is the backend to support querying for
# users in a channel
@app.route("/get_users", methods=["POST"])
def get_users():
    return jsonify({"success": True, "active_users": user_list})

# this is the backend to support querying for
# messages in a channel
@app.route("/get_messages", methods=["POST"])
def fetch_messages():
    channel = request.form.get("channel")
    dn = request.form.get("displayname")
    msg_status = request.form.get("msg_type")

    if (msg_status == "PUBLIC"):
        my_msgs = channel_messages.get(channel)
    else:
        my_msgs = user_dm_list.get(channel)

    if my_msgs:
        msglist = my_msgs['messages']
        if ((msg_status == 'PUBLIC') or (channel == dn)):
            return jsonify({"success": True, "channel_msgs": msglist})
        else:
            all_msgs = []
            for msg in msglist:
                if((msg["user_from"] == dn) or (msg["user_to"] == dn)):
                    all_msgs.append(msg)
                return jsonify({"success": True, "channel_msgs": all_msgs})
    else:
        return jsonify({"success": False, "error_msg": "No messages"})

# this is the socketio backend to handle the submit messages
# event in the flask, it shows public channel
# first of all we see if channel is in channel_messages as those
# are public channel with messages , then we check public channel
# with first message and then we go for private messages
@socketio.on("submit message")
def new_message(data):
    channel = data["channel"]
    user_from = data["user_from"]
    msg_txt = data["msg_txt"]
    timestamp = time.asctime( time.localtime( time.time() ) )

    msg = {"channel": channel,
           "user_from": user_from,
           "user_to": channel,
           "timestamp": timestamp,
           "msg_txt": msg_txt}

    if channel in channel_messages:
        msgs = channel_messages[channel]
        msg["msg_type"] = "PUBLIC"
        if len(msgs['messages']) >= 100:
            del msgs['messages'][0]
        msgs['messages'].append(msg)
        emit("announce message", msg, broadcast=True)
        return jsonify ({"success": True, "msg_type": "PUBLIC"})
    else:
        if (not (channel in user_dm_list)):
            msg["msg_type"] = "PUBLIC"
            channel_messages[channel] = {"channel": channel, "messages": [msg]}
            emit("announce message", msg, broadcast=True)
            return jsonify ({"success": True, "msg_type": "PUBLIC"})
        else:
            msg["msg_type"] = "PRIVATE"
            if (channel in user_dm_list):
                for user in [user_from, channel]:
                    msgs = user_dm_list[user]
                    if len(msgs['messages']) >= 100:
                        del msgs['messages'][0]
                    msgs['messages'].append(msg)
            else:
                user_dm_list[user] = {"channel": channel, "messages": [msg]}
            print (f"NM: emit msg to ", user_from)
            msg["channel"] = channel
            emit("announce message", msg, room=Rooms[user_from])
            print (f"NM: emit msg to ", channel)
            msg["channel"] = user_from
            emit("announce message", msg, room=Rooms[channel])
            return jsonify ({"success": True, "msg_type": "PRIVATE"})

# join channel event for joining the channel or room
@socketio.on('join')
def on_join(data):
    username = data['displayname']
    if (username == ""):
        return jsonify ({"success": False, "error_msg": "No text entered"})

    if (not (username in user_list)):
        user_list.append(username)
        user_dm_list[username] = ({"channel": username, "messages": []})
        emit("new user", {"username": username}, broadcast= True)
    else:
        emit("user logged in", {"username": username}, broadcast=True)

    room = data['room']
    join_room(room)
    Rooms[username] = room
    print (f"username ", username, "has room ", Rooms[username])
    return jsonify ({"success": True})

# socketio logout event to log user out
@socketio.on('logout user')
def on_leave(data):
    username = data['displayname']
    print (f"username ", username, " logging out")
    room = Rooms[username]
    leave_room(room)
    del Rooms[username]
    emit("user logged out", {"username": username}, broadcast=True)

# socketio leave backend to support to leave a channel or room
@socketio.on('leave')
def on_leave(data):
    username = data['displayname']
    print (f"username ", username, " logging out")
    room = Rooms[username]
    leave_room(room)
    del Rooms[username]
    emit("user logged out", {"username": username}, broadcast=True)


import itertools
import random
import os
import time

MAX_ALLOWED_RUNTIME= 1000
start_t = time.time()
elapsed_time = 0
statenum=0

def valid_sudoku(line):
    result = (len(line) == 9 and sum(line) == sum(set(line)))
    return result

def filled_sudoku(line):
    result = 0 not in line
    return result

def no_blanks(grid):
    unfilled_rows = [True for row in grid if not filled_sudoku(row)]
    #transpose the matrix
    return not unfilled_rows

def check_sudoku(grid):
    bad_rows = [row for row in grid if not valid_sudoku(row)]
    grid = list(zip(*grid))
    bad_cols = [col for col in grid if not valid_sudoku(col)]
    if bad_rows or bad_cols:
        return False

    squares = []
    for i in range(0, 9, 3):
        for j in range(0, 9, 3):
            square = list(itertools.chain(row[j:j+3] for row in grid[i:i+3]))
            squares.append(list(itertools.chain(*square)))
    bad_squares = [square for square in squares if not valid_sudoku(square)]
    return not (bad_rows or bad_cols or bad_squares)

def find_unassigned(grid):
    i = 0
    for row in grid:
        j = 0
        for var in row:
            if var == 0:
                return [i,j]
            j+=1
        i+=1
    print("No more blanks found")
    return [-1,-1]

def find_unassigned_random(grid):
    i = 0
    blank_lst = []
    for row in grid:
        j = 0
        for var in row:
            if var == 0:
                blank_lst.append([i,j])
            j+=1
        i+=1
    upper = len(blank_lst) - 1
    index = random.randint(0,upper)
    return blank_lst[index]

def find_remaining_vars(grid):
    i = 0
    blank_lst = []
    for row in grid:
        j = 0
        for var in row:
            if var == 0:
                blank_lst.append([i,j])
            j+=1
        i+=1
    return blank_lst

def forward_check(grid, vars_left):
    state = grid
    for var in vars_left:
        legalnum = 0
        for value in range(1,10):
            state = assign_var(state, var[0], var[1], value)
            if check_sudoku(state):
                #print("Legal assignment of:", value, "to", var[0], var[1])
                legalnum +=1
            state = assign_var(state, var[0], var[1], 0)
        if legalnum == 0:
            #print("Var at", var[0], var[1], "has no more legal values!")
            return False

    return True

def assign_var(grid,i,j,val):
    if grid[i][j] == 0 or val == 0:
        grid[i][j] = val
        #print("Selected Variable", [i,j], "assigned value of", val)
    elif val != 0:
        print("Selected Variable", "Trying to assign a value of", val, "when", [i,j], "is already assigned!")

    return grid

def most_constrained(grid, vars_left):
    mrv = []
    mrv_lst = [mrv]
    min_moves = 10
    for var in vars_left:
        legalnum = 0
        for value in range(1,10):
            grid = assign_var(grid, var[0], var[1], value)
            if check_sudoku(grid):
                legalnum +=1
            #revert to initial state
            grid = assign_var(grid, var[0], var[1], 0)

        #print("var:", var,"has ", legalnum, "possible values")

        if legalnum < min_moves:
            mrv_lst = []
            mrv = var
            min_moves = legalnum
            mrv_lst.append(var)
            #print("mrv is now:", mrv_lst)
        elif legalnum == min_moves:
            mrv_lst.append(var)

    return mrv_lst

#returns the a list of blanks constrained by a var at (i,j)
def get_contraining(index, vars_left):
    degree = 0
    deg_lst =[]
    x = index[0]
    y = index[1]

    for var in vars_left:
        if var[0] == x or var[1] == y:
            degree += 1
            deg_lst.append(var)

        cell_x = x/3
        cell_y = y/3

        for i in range(0,3):
            for j in range(0, 3):
                if [cell_x*3+i, cell_y*3+j] == var and var != [x,y]:
                    #print("Var:", var)
                    degree+= 1
                    deg_lst.append(var)

    return deg_lst

def most_constraining(var_lst, grid):
    max_deg = 0
    max_var = []
    mcv_lst = [max_var]
    blanks_lst = find_remaining_vars(grid)
    for var in var_lst:
        degree = len(get_contraining(var, blanks_lst))
        #print(var, "constrains:", degree, "other vars")
        if degree > max_deg:
            max_deg = degree

            #purge the list of old vars
            mcv_lst = []
            max_var=var
            mcv_lst.append(var)
        elif degree == max_deg:
            mcv_lst.append(var)
    return mcv_lst

def get_domain_size(var, grid):
    mrv_lst = []
    legalnum = 0
    for value in range(1,10):
        grid = assign_var(grid, var[0], var[1], value)
        if check_sudoku(grid):
            legalnum +=1

        #revert to initial state
        grid = assign_var(grid, var[0], var[1], 0)

    return legalnum

def least_constrain_val(grid, index, domain):
    x = index[0]
    y = index[1]
    lcv = 1
    max_dsum = 0
    lcv_lst=[]
    for value in domain:
        domain_lst =[]
        grid = assign_var(grid, x, y, value)
        blank_lst = find_remaining_vars(grid)
        for var in blank_lst:
            dsize = get_domain_size(var, grid)
            #print(var, dsize)
            domain_lst.append(dsize)

        dsum = sum(domain_lst)
        #print("dsum", dsum)
        if dsum > max_dsum:
            max_dsum = dsum
            lcv_lst = []
            lcv = value
            lcv_lst.append(lcv)
        elif dsum == max_dsum:
            lcv_lst.append(value)

        grid = assign_var(grid, x, y, 0)
    return lcv_lst

def solve_sudoku_random(grid, depth, var_lst):
    global statenum
    global elapsed_time
    global start_t
    statenum+=1
    if elapsed_time > MAX_ALLOWED_RUNTIME:
        return False
    if no_blanks(grid) and check_sudoku(grid):
        #if given state is the goal state, just return it
        print("Solution found!")
        print("Total # of Nodes Traversed", statenum)
        statenum = 0
        return grid
    elif no_blanks(grid):
        #puzzle has no blanks, but also invalid
        print("Invalid Input!")
        return False
    else:
        domain = [1,2,3,4,5,6,7,8,9]
        var_ind=random.randrange(0, len(var_lst))
        indices = var_lst[var_ind]

        while len(domain) > 0:
            index = random.randrange(0,len(domain))
            val = domain[index]
            #assign
            grid = assign_var(grid, indices[0], indices[1], val)
            domain.remove(val)
            var_lst.remove(indices)
            if check_sudoku(grid):
                if solve_sudoku_random(grid, (depth + 1), var_lst):
                    return grid
                else:
                    currTime = time.time() - start_t
                    elapsed_time = int(currTime)
                    if elapsed_time > MAX_ALLOWED_RUNTIME and depth == 0:
                        start_t = time.time()
                        elapsed_time = 0
                        print("Total # of Nodes Traversed", statenum)
                        statenum = 0
                        return False

            #backtrack to last valid state
            var_lst.append(indices)
            grid = assign_var(grid, indices[0], indices[1], 0)
            #print("Backtracked to State #", statenum, "reverted var at", indices[0], indices[1])

    return False

def solve_sudoku_FC(grid, depth):
    global statenum
    global elapsed_time
    global start_t
    currTime = time.time() - start_t
    elapsed_time = int(currTime)

    if elapsed_time > MAX_ALLOWED_RUNTIME:
        return False

    statenum+=1

    if no_blanks(grid) and check_sudoku(grid):
        #if given state is the goal state, just return it
        print("Solution found!")
        elapsed_time = 0
        start_t = time.time()
        print("Total # of Nodes Traversed", statenum)
        statenum = 0
        return grid
    elif no_blanks(grid):
        #puzzle has no blanks, but also invalid
        print("Invalid Input!")
        return False
    else:
        domain = [1,2,3,4,5,6,7,8,9]
        indices = find_unassigned_random(grid)

        while len(domain) > 0:
            index = random.randrange(0,len(domain))
            val = domain[index]
            #assign
            grid = assign_var(grid, indices[0], indices[1], val)
            domain.remove(val)
            vars_left = find_remaining_vars(grid)
            if check_sudoku(grid):
                if forward_check(grid,vars_left):
                    if solve_sudoku_FC(grid, (depth + 1)):
                        return grid
                    else:
                        currTime = time.time() - start_t
                        elapsed_time = int(currTime)
                        if elapsed_time > MAX_ALLOWED_RUNTIME and depth == 0:
                            start_t = time.time()
                            elapsed_time = 0
                            statenum = 0
                            print("Total # of Nodes Traversed", statenum)
                            return False

            #backtrack to last valid state
            grid = assign_var(grid, indices[0], indices[1], 0)

    return False

#backtracking, Forward checking + heuristics
def solve_sudoku_FCH(grid):
    global statenum
    statenum+=1

    if no_blanks(grid) and check_sudoku(grid):
        #if given state is the goal state, just return it
        print("Solution found!")
        print("Total # of Nodes Traversed", statenum)
        statenum = 0
        return grid
    elif no_blanks(grid):
        #puzzle has no blanks, but also invalid
        print("Invalid Input!")
        return False
    else:
        domain = [1,2,3,4,5,6,7,8,9]
        vars_lst = find_remaining_vars(grid)
        mcv_lst = most_constrained(grid,vars_lst)
        if len(mcv_lst) == 1:
            indices = mcv_lst[0]
        else:
            #break the tie with most constraining
            new_lst = most_constraining(mcv_lst, grid)
            if len(new_lst) == 1:
                indices = new_lst[0]
            else:
                #if there is still a tie, pick one randomly
                index = random.randrange(0,len(new_lst))
                indices = new_lst[index]

        #print("Chose index of:", indices)
        while len(domain) > 0:
            val_lst = least_constrain_val(grid, indices,domain)

            if len(val_lst) == 1:
                value = val_lst[0]
            else:
                #if there is a tie, pick one randomly
                index = random.randrange(0,len(val_lst))
                value = val_lst[index]

            #print("Chose value of:", value)
            #assign
            grid = assign_var(grid, indices[0], indices[1], value)
            domain.remove(value)
            vars_left = find_remaining_vars(grid)
            if check_sudoku(grid):
                if forward_check(grid,vars_left):
                    if check_sudoku(grid):
                        if solve_sudoku_FCH(grid):
                            return grid

            #backtrack to last valid state
            grid = assign_var(grid, indices[0], indices[1], 0)

    return False


"""
Location of the "AdfDiag" object, which
is used to store all relevant data and
info needed for generating CAM/ADF
diagnostics, including info
on the averaging, regridding, and
plotting methods themselves.
"""

#++++++++++++++++++++++++++++++
#Import standard python modules
#++++++++++++++++++++++++++++++

import sys
import os
import os.path
import glob
import subprocess
import importlib

from pathlib import Path
from typing import Optional

#Check if "PyYAML" is present in python path:
# pylint: disable=unused-import
try:
    import yaml
except ImportError:
    print("PyYAML module does not exist in python path.")
    print("Please install module, e.g. 'pip install pyyaml'.")
    sys.exit(1)

#Check if "xarray" is present in python path:
try:
    import xarray as xr
except ImportError:
    print("Xarray module does not exist in python path.")
    print("Please install module, e.g. 'pip install xarray'.")
    sys.exit(1)

#Check if "numpy" is present in python path:
try:
    import numpy as np
except ImportError:
    print("Numpy module does not exist in python path.")
    print("Please install module, e.g. 'pip install numpy'.")
    sys.exit(1)

#Check if "matplolib" is present in python path:
try:
    import matplotlib as mpl
except ImportError:
    print("Matplotlib module does not exist in python path.")
    print("Please install module, e.g. 'pip install matplotlib'.")
    sys.exit(1)

#Check if "cartopy" is present in python path:
try:
    import cartopy.crs as ccrs
except ImportError:
    print("Cartopy module does not exist in python path.")
    print("Please install module, e.g. 'pip install Cartopy'.")
    sys.exit(1)
# pylint: enable=unused-import

#+++++++++++++++++++++++++++++
#Add ADF diagnostics 'scripts'
#directories to Python path
#+++++++++++++++++++++++++++++

#Determine local directory path:
_LOCAL_PATH = os.path.dirname(os.path.abspath(__file__))

#Add "scripts" directory to path:
_DIAG_SCRIPTS_PATH = os.path.join(_LOCAL_PATH,os.pardir,"scripts")

#Check that "scripts" directory actually exists:
if not os.path.isdir(_DIAG_SCRIPTS_PATH):
    #If not, then raise error:
    ermsg = f"'{_DIAG_SCRIPTS_PATH}' directory not found. Has 'AdfDiag.py' been moved?"
    raise FileNotFoundError(ermsg)

#Walk through all sub-directories in "scripts" directory:
for root, dirs, files in os.walk(_DIAG_SCRIPTS_PATH):
    #Add all sub-directories to python path:
    for dirname in dirs:
        sys.path.append(os.path.join(root,dirname))

#+++++++++++++++++++++++++++++

#Finally, import needed ADF modules:
from adf_config import AdfConfig

#################
#Helper functions
#################

def construct_index_info(page_dict, fnam, opf):

    """
    Helper function for generating web pages.
    d : dictionary for the index page information
    fnam : the image filename, img.stem  --> then decompose the img file's parts.
    opf: outputfile for the image
    """
    vname, plot_desc = fnam[0:fnam.index("_")], fnam[fnam.index("_")+1:]
    if 'ANN' in plot_desc:
        temporal = 'ANN'
    elif 'DJF' in plot_desc:
        temporal = 'DJF'
    elif 'JJA' in plot_desc:
        temporal = 'JJA'
    elif 'MAM' in plot_desc:
        temporal = 'MAM'
    elif 'SON' in plot_desc:
        temporal = 'SON'
    else:
        temporal = 'NoInfo'
    plot_type = plot_desc.replace(temporal+"_", "")
    if vname not in page_dict:
        page_dict[vname] = {}
    if plot_type not in page_dict[vname]:
        page_dict[vname][plot_type] = {}
    page_dict[vname][plot_type][temporal] = opf

######################################
#Main ADF diagnostics class (AdfDiag)
######################################

class AdfDiag(AdfConfig):

    """
    Main ADF diagnostics object.

    This object is initalized using
    an ADF diagnostics configure (YAML) file,
    which specifies various user inputs,
    including CAM history file names and
    locations, years being analyzed,
    types of averaging, regridding,
    and other post-processing options being
    used, and the type of plots that will
    be created.

    This object also contains various methods
    used to actually generate the plots and
    post-processed data.
    """

    def __init__(self, config_file, debug=False):

        """
        Initalize ADF diagnostics object.
        """

        #Initialize Config/Base attributes:
        super().__init__(config_file, debug=debug)

        #Add basic diagnostic info to object:
        self.__basic_info = self.read_config_var('diag_basic_info', required=True)

        #Expand basic info variable strings:
        self.expand_references(self.__basic_info)

        #Add CAM climatology info to object:
        self.__cam_climo_info = self.read_config_var('diag_cam_climo', required=True)

        #Expand CAM climo info variable strings:
        self.expand_references(self.__cam_climo_info)

        #Check if a CAM vs CAM baseline comparison is being performed:
        if not self.read_config_var('compare_obs', conf_dict=self.__basic_info):
            #If so, then add CAM baseline climatology info to object:
            self.__cam_bl_climo_info = self.read_config_var('diag_cam_baseline_climo',
                                                            required=True)

            #Expand CAM baseline climo info variable strings:
            self.expand_references(self.__cam_bl_climo_info)

        #Add averaging script names:
        self.__time_averaging_scripts = self.read_config_var('time_averaging_scripts')

        #Add regridding script names:
        self.__regridding_scripts = self.read_config_var('regridding_scripts')

        #Add analysis script names:
        self.__analysis_scripts = self.read_config_var('analysis_scripts')

        #Add plotting script names:
        self.__plotting_scripts = self.read_config_var('plotting_scripts')

        #Add CAM variable list:
        self.__diag_var_list = self.read_config_var('diag_var_list', required=True)

        #Add CAM observation type list (filename prefix for observation files):
        self.__obs_type_list = self.read_config_var('obs_type_list')

        #Create plot location variable for potential use by the website generator.
        #Please note that this variable is only set if "create_plots" or
        #is called:
        self.__plot_loc = ""

    # Create property needed to return "compare_obs" logical to user:
    @property
    def compare_obs(self):
        """Return the "compare_obs" logical to user if requested."""
        return self.read_config_var('compare_obs', conf_dict=self.__basic_info)

    # Create property needed to return "create_html" logical to user:
    @property
    def create_html(self):
        """Return the "create_html" logical to user if requested."""
        return self.read_config_var('create_html', conf_dict=self.__basic_info)

    #########

    def __diag_scripts_caller(self, scripts_dir: str, func_names: list,
                              default_args: Optional[list] = None,
                              default_kwargs: Optional[dict] = None,
                              log_section: Optional[str] = None):

        """
        Parse a list of scripts as provided by the config file,
        and call them as functions while passing in the correct inputs.

        scripts_dir    : string, sub-directory under "scripts" where scripts are located
        func_names     : list of function/scripts (either string or dictionary):
        default_args   : optional list of default arguments for the scripts if
                         none are specified by the config file
        default_kwargs : optional list of default keyword arguments for the scripts if
                         none are specified by the config file
        log_section    : optional variable that specifies where the log entries are coming from.
                         Note:  Is it better to just make a child log instead?

        """

        #Loop over all averaging script names:
        for func_name in func_names:

            #Check if func_name is a dictonary,
            #this implies that the function has user-defined inputs:
            if isinstance(func_name, dict):
                emsg = "Function dictionary must be of the form: "
                emsg += "{function_name : {args:[...], kwargs:{...}, module:'xxxx'}}"
                assert len(func_name) == 1, emsg
                has_opt = True
                opt = func_name[list(func_name.keys())[0]]  # un-nests the dictionary
                # not ideal, but change to a str representation; iteration will continue ok:
                func_name = list(func_name.keys())[0]
            elif isinstance(func_name, str):
                has_opt = False
            else:
                raise TypeError("Provided script must either be a string or a dictionary.")

            func_script = func_name + '.py'  # default behavior: Add file suffix to script name
            if has_opt:
                if 'module' in opt:
                    func_script = opt['module']

            #Create full path to function script:
            func_script_path = \
                os.path.join(os.path.join(_DIAG_SCRIPTS_PATH, scripts_dir), func_script)

            #Check that file exists in specified directory:
            if not os.path.exists(func_script_path):
                emsg = f"Script file '{func_script_path}' is missing. Diagnostics are ending here."
                self.end_diag_fail(emsg)

            if func_script_path not in sys.path:
                #Add script path to debug log if requested:
                if log_section:
                    dmsg = f"{log_section}: Inserting to sys.path: {func_script_path}"
                    self.debug_log(dmsg)
                else:
                    dmsg = f"diag_scripts_caller: Inserting to sys.path: {func_script_path}"
                    self.debug_log(dmsg)

                #Add script to python path:
                sys.path.insert(0, func_script_path)

            # NOTE: when we move to making this into a proper package,
            #       this path-checking stuff should be removed and dealt with on the package-level.

            # Arguments; check if user has specified custom arguments
            func_args   = default_args
            func_kwargs = default_kwargs
            if has_opt:
                if 'args' in opt:
                    # RULES: it has to be a list of strings,
                    #        and then we will take whatever of those are in locals
                    assert isinstance(opt['args'], list), "Function arguments must be of type list."
                    emsg = "Function argument list elements must be of type string."
                    assert all(isinstance(item, str) for item in opt['args']), emsg
                    func_args = list()  # start over
                    for variable_to_check in opt['args']:
                        if variable_to_check in locals():
                            func_args.append(locals()[variable_to_check])
                        else:
                            print("{} is not available".format(variable_to_check))
                if 'kwargs' in opt:
                    func_kwargs = opt['kwargs']

            #Add function calls debug log if requested:
            if log_section:
                dmsg = f"{log_section}: \n \t func_name = {func_name}\n "
                dmsg += f"\t func_args = {func_args}\n \t func_kwargs = {func_kwargs}"
                self.debug_log(dmsg)
            else:
                dmsg = f"diag_scripts_caller: \n \t func_name = {func_name}\n "
                dmsg += f"\t func_args = {func_args}\n \t func_kwargs = {func_kwargs}"
                self.debug_log(dmsg)

            #Call function
            self.__function_caller(func_name, func_args,
                                   func_kwargs=func_kwargs,
                                   module_name=func_name)

    #########

    # pylint: disable=no-self-use

    def __function_caller(self, func_name: str, func_args: list,
                          func_kwargs: Optional[dict] = None, module_name=None):

        """
        Call a function with given arguments.

        func_name : string, name of the function to call
        func_args : list, the arguments to pass to the function
        func_kwargs : [optional] dict, the keyword arguments to pass to the function
        module_name : [optional] string, the name of the module where func_name is defined;
                      if not provided, assume func_name.py

        return : the output of func_name(*func_args, **func_kwargs)
        """

        if module_name is None:
            module_name = func_name #+'.py'

        # note: when we use importlib, specify the module name without the ".py" extension.
        module = importlib.import_module(func_name)
        if hasattr(module, func_name) and callable(getattr(module, func_name)):
            func = getattr(module, func_name)

        #Run function and return result:
        if func_kwargs:
            return func(*func_args, **func_kwargs)
        else:
            return func(*func_args)
        #End if

    # pylint: enable=no-self-use

    #########

    def create_time_series(self, baseline=False):

        """
        Generate time series versions of the CAM history file data.
        """

        #Check if baseline time-series files are being created:
        if baseline:
            #Then use the CAM baseline climo dictionary
            #and case name:
            cam_climo_dict = self.__cam_bl_climo_info
        else:
            #If not, then just extract the standard CAM climo dictionary
            #and case name::
            cam_climo_dict = self.__cam_climo_info

        #Extract case name:
        case_name = self.read_config_var('cam_case_name',
                                         conf_dict=cam_climo_dict,
                                         required=True)

        #Check if climatologies are being calculated:
        if self.read_config_var('calc_cam_climo', conf_dict=cam_climo_dict):
            # Skip history file stuff if time series are pre-computed:
            if self.read_config_var('cam_ts_done', conf_dict=cam_climo_dict):
                # skip time series generation, and just make the climo
                emsg = "  Configuration file indicates time series files have been pre-computed,"
                emsg += " will rely on those files only."
                print(emsg)
                return

            #Notify user that script has started:
            print("  Generating CAM time series files...")

            #Extract cam time series directory:
            ts_dir = cam_climo_dict['cam_ts_loc']

            #Extract start and end year values:
            # pylint: disable=raise-missing-from
            try:
                start_year = int(cam_climo_dict['start_year'])
            except TypeError:
                if cam_climo_dict['start_year'] is None:
                    start_year = "*"
                else:
                    emsg = "start_year needs to be a year-like value or None, "
                    emsg += f"got '{cam_climo_dict['start_year']}'"
                    raise IOError(emsg)
                #End if
            #End try

            try:
                end_year   = int(cam_climo_dict['end_year'])
            except TypeError:
                if cam_climo_dict['end_year'] is None:
                    end_year = "*"
                else:
                    emsg = "end_year needs to be a year-like value or None, "
                    emsg += f"got '{cam_climo_dict['end_year']}'"
                    raise IOError(emsg)
                #End if
            #End try
            # pylint: enable=raise-missing-from

            #Extract cam variable list:
            var_list = self.__diag_var_list

            #Create path object for the CAM history file(s) location:
            cam_hist_loc = self.read_config_var('cam_hist_loc',
                                                conf_dict=cam_climo_dict,
                                                required=True)
            starting_location = Path(cam_hist_loc)

            #Check that path actually exists:
            if not starting_location.is_dir():
                if baseline:
                    emsg = f"Provided baseline 'cam_hist_loc' directory '{starting_location}' "
                    emsg += "not found.  Script is ending here."
                else:
                    emsg = "Provided 'cam_hist_loc' directory '{starting_location}' not found."
                    emsg += " Script is ending here."
                #End if

                self.end_diag_fail(emsg)
            #End if

            #Check if history files actually exist. If not then kill script:
            if not list(starting_location.glob('*.cam.h0.*.nc')):
                emsg = f"No CAM history (h0) files found in '{starting_location}'."
                emsg += " Script is ending here."
                self.end_diag_fail(emsg)
            #End if

            # NOTE: We need to have the half-empty cases covered, too. (*, end) & (start, *)
            if start_year == end_year == "*":
                files_list = sorted(list(starting_location.glob('*.cam.h0.*.nc')))
            else:
                #Create empty list:
                files_list = list()

                #Loop over start and end years:
                for year in range(start_year, end_year+1):
                    #Add files to main file list:
                    for fname in starting_location.glob(f'*.cam.h0.*{year}-*.nc'):
                        files_list.append(fname)

            #Create ordered list of CAM history files:
            hist_files = sorted(files_list)

            # Check if time series directory exists, and if not, then create it:
            # Use pathlib to create parent directories, if necessary.
            Path(ts_dir).mkdir(parents=True, exist_ok=True)

            #Loop over CAM history variables:
            for var in var_list:

                #Create full path name:
                ts_outfil_str = ts_dir + os.sep + case_name + \
                              ".ncrcat."+var+".nc"

                #Check if files already exist in time series directory:
                ts_file_list = glob.glob(ts_outfil_str)

                #If files exist, then check if over-writing is allowed:
                if ts_file_list:
                    if not cam_climo_dict['cam_overwrite_ts']:
                        #If not, then simply skip this variable:
                        continue

                #Notify user of new time series file:
                print("\t \u231B time series for {}".format(var))

                #Run "ncrcat" command to generate time series file:
                cmd = ["ncrcat", "-O", "-4", "-h", "-v", f"{var},hyam,hybm,hyai,hybi,PS"] + \
                      hist_files + ["-o", ts_outfil_str]
                subprocess.run(cmd, check=True)

            #Notify user that script has ended:
            print("  ...CAM time series file generation has finished successfully.")

        else:
            #If not, then notify user that time series generation is skipped.
            print("  Climatology files are not being generated, so neither will time series files.")

    #########

    def create_climo(self, baseline=False):

        """
        Temporally average CAM time series data
        in order to generate CAM climatologies.

        The actual averaging is done using the
        scripts listed under "time_averaging_scripts"
        as specified in the config file.  This is done
        so that the user can specify the precise kinds
        of averaging that are done (e.g. weighted vs.
        non-weighted averaging).
        """

        #Check if CAM Baselines are being calculated:
        if baseline:
            #If so, then use the CAM baseline climo dictionary
            #case name, and output location:
            cam_climo_dict = self.__cam_bl_climo_info
        else:
            #If not, then just extract the standard CAM climo dictionary
            #case name, and output location:
            cam_climo_dict = self.__cam_climo_info

        #Check if user wants climatologies to be calculated:
        if self.read_config_var('calc_cam_climo', conf_dict=cam_climo_dict):

            #Extract case name:
            case_name = self.read_config_var('cam_case_name',
                                             conf_dict=cam_climo_dict,
                                             required=True)

            #Extract output location:
            output_loc = self.read_config_var('cam_climo_loc',
                                              conf_dict=cam_climo_dict,
                                              required=True)

            #If so, then extract names of time-averaging scripts:
            avg_func_names = self.__time_averaging_scripts  # this is a list of script names
                                                            # _OR_
                                                            # a **list** of dictionaries with
                                                            # script names as keys that hold
                                                            # args(list), kwargs(dict), and
                                                            # module(str)

            if not avg_func_names:
                emsg = "No time_averaging_scripts provided for calculating"
                emsg += " climatologies, but climatologies were requested.\n"
                emsg += "Please either provide a valid averaging script,"
                emsg += " or skip the calculation of climatologies."
                raise AdfError(emsg)

            #Extract necessary variables from configure dictionary:

            #Location of time series files:
            input_ts_loc = self.read_config_var('cam_ts_loc',
                                                conf_dict=cam_climo_dict,
                                                required=True)

            #Will climatologies be overwritten (If not present, then will default to False):
            overwrite_climo = self.read_config_var('cam_overwrite_climo', conf_dict=cam_climo_dict)

            #Variable list:
            var_list = self.__diag_var_list

            #Set default script arguments:
            avg_func_args = [case_name, input_ts_loc, output_loc, var_list]
            avg_func_kwargs = {"clobber":overwrite_climo}

            #Run the listed scripts:
            self.__diag_scripts_caller("averaging", avg_func_names,
                                       default_args = avg_func_args,
                                       default_kwargs = avg_func_kwargs,
                                       log_section = "create_climo")

        else:
            #If not, then notify user that climo file generation is skipped.
            print("  No climatology files were requested by user, so averaging will be skipped.")

    #########

    def regrid_climo(self):

        """
        Re-grid CAM climatology files to observations
        or baseline climatologies, in order to allow
        for direct comparisons.

        The actual regridding is done using the
        scripts listed under "regridding_scripts"
        as specified in the config file.  This is done
        so that the user can specify the precise kinds
        of re-gridding that are done (e.g. bilinear vs.
        nearest-neighbor regridding).
        """

        #Extract names of re-gridding scripts:
        regrid_func_names = self.__regridding_scripts # this is a list of script names
                                                      # _OR_
                                                      # a **list** of dictionaries with
                                                      # script names as keys that hold
                                                      # args(list), kwargs(dict), and module(str)

        if not regrid_func_names or all([func_names is None for func_names in regrid_func_names]):
            print("No regridding options provided, continue.")
            return
            # NOTE: if no regridding options provided, we should skip it, but
            #       do we need to still copy (symlink?) files into the regrid directory?

        #Check if comparison is being made to observations:
        if self.compare_obs:
            #Set regridding target to observations:
            target_list = self.__obs_type_list
            target_loc  = self.read_config_var('obs_climo_loc',
                                               conf_dict=self.__basic_info,
                                               required=True)
        else:
            #Assume a CAM vs. CAM comparison is being run,
            #so set target to baseline climatologies:
            target_list = [self.read_config_var('cam_case_name',
                                                conf_dict=self.__cam_bl_climo_info,
                                                required=True)]

            target_loc = self.read_config_var('cam_climo_loc',
                                              conf_dict=self.__cam_bl_climo_info,
                                              required=True)

        #Extract remaining required info from configure dictionaries:

        #Case name:
        case_name = self.read_config_var('cam_case_name',
                                         conf_dict=self.__cam_climo_info,
                                         required=True)

        #Case climo files:
        input_climo_loc =  self.read_config_var('cam_climo_loc',
                                         conf_dict=self.__cam_climo_info,
                                         required=True)

        #Regridded data output location:
        output_loc =  self.read_config_var('cam_regrid_loc',
                                         conf_dict=self.__basic_info,
                                         required=True)

        #Regrid overwrite check (if missing, then assume False):
        overwrite_regrid = self.read_config_var('cam_overwrite_regrid',
                                                conf_dict=self.__basic_info)

        #Variable list:
        var_list = self.__diag_var_list

        #Set default script arguments:
        regrid_func_args = [case_name, input_climo_loc,
                            output_loc, var_list,
                            target_list, target_loc,
                            overwrite_regrid]

        #Run the listed scripts:
        self.__diag_scripts_caller("regridding", regrid_func_names,
                                   default_args = regrid_func_args,
                                   log_section = "regrid_climo")

    #########

    def perform_analyses(self, baseline=False):

        """
        Performs statistical and other analyses as specified by the
        user.  This currently only includes the AMWG table generation.

        This method also assumes that the analysis scripts require model
        inputs in a time series format.
        """

        #Extract names of plotting scripts:
        anly_func_names = self.__analysis_scripts  # this is a list of script names
                                                   # _OR_
                                                   # a **list** of dictionaries with
                                                   # script names as keys that hold
                                                   # args(list), kwargs(dict), and module(str)

        #If no scripts are listed, then exit routine:
        if not anly_func_names:
            print("Nothing listed under 'analysis_scripts', exiting 'perform_analyses' method.")
            return

        #Check if CAM Baselines are being calculated:
        if baseline:
            #If so, then use the CAM baseline climo dictionary
            #case name, and output location:
            cam_climo_dict = self.__cam_bl_climo_info
        else:
            #If not, then just extract the standard ADF climo dictionary
            #case name, and output location:
            cam_climo_dict = self.__cam_climo_info

        #Extract necessary variables from ADF configure dictionary:

        #case name:
        case_name = self.read_config_var('cam_case_name',
                                         conf_dict=cam_climo_dict,
                                         required=True)

        #Case time series location:
        input_ts_loc = self.read_config_var('cam_ts_loc',
                                            conf_dict=cam_climo_dict,
                                            required=True)

        #HTML-writing logical (assumed False if missing):
        write_html = self.read_config_var('create_html',
                                          conf_dict=self.__basic_info)

        #Variable list:
        var_list = self.__diag_var_list

        #Set "data_name" variable, which depends on "compare_obs":
        if self.compare_obs:
            data_name = "obs"
        else:
            data_name = self.read_config_var('cam_case_name',
                                             conf_dict = self.__cam_bl_climo_info,
                                             required=True)

        #Set "plot_location" variable, if it doesn't exist already, and save value in diag object.
        #Please note that this is also assumed to be the output location for the analyses scripts.
        if not self.__plot_loc:

            #Plot directory:
            plot_dir = self.read_config_var('cam_diag_plot_loc',
                                            conf_dict = self.__basic_info,
                                            required=True)

            #Start year (not currently required):
            syear = self.read_config_var('start_year',
                                         conf_dict = self.__cam_climo_info)

            #End year (not currently rquired):
            eyear = self.read_config_var('end_year',
                                         conf_dict = self.__cam_climo_info)

            if syear and eyear:
                self.__plot_loc = os.path.join(plot_dir,
                                               f"{case_name}_vs_{data_name}_{syear}_{eyear}")
            else:
                self.__plot_loc = os.path.join(plot_dir, f"{case_name}_vs_{data_name}")

        #Set default script arguments:
        anly_func_args = [case_name, input_ts_loc, self.__plot_loc, var_list, write_html]

        #Run the listed scripts:
        self.__diag_scripts_caller("analysis", anly_func_names,
                                   default_args = anly_func_args,
                                   log_section = "perform_analyses")

    #########

    def create_plots(self):

        """
        Generate ADF diagnostic plots.

        The actual plotting is done using the
        scripts listed under "plotting_scripts"
        as specified in the config file.  This is done
        so that the user can add their own plotting
        script(s) without having to modify the
        main ADF diagnostics routines.
        """

        #Extract names of plotting scripts:
        plot_func_names = self.__plotting_scripts  # this is a list of script names
                                                   # _OR_
                                                   # a **list** of dictionaries with
                                                   # script names as keys that hold
                                                   # args(list), kwargs(dict), and module(str)

        #If no scripts are listed, then exit routine:
        if not plot_func_names:
            print("Nothing listed under 'plotting_scripts', so no plots will be made.")
            return

        #Extract required input variables:

        #Case name:
        case_name = self.read_config_var('cam_case_name',
                                         conf_dict = self.__cam_climo_info,
                                         required=True)

        #Regridded model data:
        model_rgrid_loc = self.read_config_var('cam_regrid_loc',
                                               conf_dict = self.__basic_info,
                                               required=True)

        #Variable list:
        var_list = self.__diag_var_list

        #Set "data" variables, which depend on "compare_obs":
        if self.compare_obs:

            data_name = "obs"

            data_loc = self.read_config_var('obs_climo_loc',
                                            conf_dict = self.__basic_info,
                                            required=True)

            data_list = self.__obs_type_list

        else:

            data_name = self.read_config_var('cam_case_name',
                                             conf_dict = self.__cam_bl_climo_info,
                                             required=True)

            data_loc = self.read_config_var('cam_climo_loc',
                                            conf_dict = self.__cam_bl_climo_info,
                                            required=True)

            data_list = [data_name]

        #Set "plot_location" variable, if it doesn't exist already, and save value in diag object:
        if not self.__plot_loc:
            plot_dir = self.read_config_var('cam_diag_plot_loc',
                                            conf_dict=self.__basic_info,
                                            required=True)

            #Start year (not currently required):
            syear = self.read_config_var('start_year',
                                         conf_dict = self.__cam_climo_info)

            #End year (not currently rquired):
            eyear = self.read_config_var('end_year',
                                         conf_dict = self.__cam_climo_info)

            if syear and eyear:
                self.__plot_loc = os.path.join(plot_dir,
                                               f"{case_name}_vs_{data_name}_{syear}_{eyear}")
            else:
                self.__plot_loc = os.path.join(plot_dir, f"{case_name}_vs_{data_name}")

        #Set default script arguments:
        plot_func_args = [case_name, model_rgrid_loc, data_name, data_loc,
                          var_list, data_list, self.__plot_loc]

        #Run the listed scripts:
        self.__diag_scripts_caller("plotting", plot_func_names,
                                   default_args = plot_func_args,
                                   log_section = "create_plots")

    #########

    def create_website(self):

        """
        Generate webpages to display diagnostic results.
        """

        #import needed standard modules:
        import shutil
        from collections import OrderedDict

        #Import "special" modules:
        try:
            import jinja2
        except ImportError:
            print("Jinja2 module does not exist in python path, but is needed for website.")
            print("Please install module, e.g. 'pip install Jinja2'.")
            sys.exit(1)

        #Notify user that script has started:
        print("  Generating Diagnostics webpages...")

        #Check where the relevant plots are located:
        if self.__plot_loc:
            plot_location = self.__plot_loc
        else:
            plot_location  = self.read_config_var('cam_diag_plot_loc',
                                                  conf_dict=self.__basic_info,
                                                  required=True)
        #End if

        #Extract needed variables from yaml file:
        case_name = self.read_config_var('cam_case_name',
                                         conf_dict=self.__cam_climo_info,
                                         required=True)

        var_list = self.__diag_var_list

        #Set name of comparison data, which depends on "compare_obs":
        if self.compare_obs:
            data_name = "obs"
        else:
            data_name = self.read_config_var('cam_case_name',
                                             conf_dict=self.__cam_bl_climo_info,
                                             required=True)
        #End if

        #Set preferred order of seasons:
        season_order = ["ANN", "DJF", "MAM", "JJA", "SON"]

        #Set preferred order of plot types:
        plot_type_order = ["LatLon", "Zonal"]

        #Create new path object from user-specified plot directory path:
        plot_path = Path(plot_location)

        #Create the directory where the website will be built:
        website_dir = plot_path / "website"
        website_dir.mkdir(exist_ok=True)

        #Create a directory that will hold just the html files for individual images:
        img_pages_dir = website_dir / "html_img"
        img_pages_dir.mkdir(exist_ok=True)

        #Create a directory that will hold copies of the actual images:
        assets_dir = website_dir / "assets"
        assets_dir.mkdir(exist_ok=True)

        #Specify where CSS files will be stored:
        css_files_dir = website_dir / "templates"
        css_files_dir.mkdir(exist_ok=True)

        #Set path to Jinja2 template files:
        jinja_template_dir = Path(_LOCAL_PATH, 'website_templates')

        #Copy CSS files over to output directory:
        for css_file in jinja_template_dir.glob('*.css'):
            shutil.copyfile(css_file, css_files_dir / css_file.name)

        #Copy images into the website image dictionary:
        for img in plot_path.glob("*.png"):
            idest = assets_dir / img.name
            shutil.copyfile(img, idest) # store image in assets

        mean_html_info = OrderedDict()  # this is going to hold the data for building the mean
                                        # plots provisional structure:
                                        # key = variable_name
                                        # values -> dict w/ keys being "TYPE" of plots
                                        # w/ values being dict w/ keys being TEMPORAL sampling,
                                        # values being the URL

        #Create the jinja Environment object:
        jinenv = jinja2.Environment(loader=jinja2.FileSystemLoader(jinja_template_dir))

        #Create alphabetically-sorted variable list:
        var_list_alpha = sorted(var_list)

        #Loop over variables:
        for var in var_list_alpha:
            #Loop over plot type:
            for ptype in plot_type_order:
                #Loop over seasons:
                for season in season_order:
                    #Create the data that will be fed into the template:
                    for img in assets_dir.glob(f"{var}_{season}_{ptype}_*.png"):
                        alt_text  = img.stem #Extract image file name text

                        #Create output file (don't worry about analysis type for now):
                        outputfile = img_pages_dir / f'plot_page_{var}_{season}_{ptype}.html'
                        # Hacky - how to get the relative path in a better way?:
                        img_data = [os.pardir+os.sep+assets_dir.name+os.sep+img.name, alt_text]
                        title = f"Variable: {var}"              #Create title
                        tmpl = jinenv.get_template('template.html')  #Set template
                        rndr = tmpl.render(title=title, value=img_data, case1=case_name,
                                           case2=data_name) #The template rendered

                        #Open HTML file:
                        with open(outputfile,'w') as ofil:
                            ofil.write(rndr)
                        #End with

                        #Initialize Ordered Dictionary for variable:
                        if var not in mean_html_info:
                            mean_html_info[var] = OrderedDict()

                        #Initialize Ordered Dictionary for plot type:
                        if ptype not in mean_html_info[var]:
                            mean_html_info[var][ptype] = OrderedDict()

                        mean_html_info[var][ptype][season] = outputfile.name

        #Construct mean_diag.html
        mean_title = "AMP Diagnostic Plots"
        mean_tmpl = jinenv.get_template('template_mean_diag.html')
        mean_rndr = mean_tmpl.render(title=mean_title,
                        case1=case_name,
                        case2=data_name,
                        mydata=mean_html_info)

        #Write mean diagnostic plots HTML file:
        outputfile = img_pages_dir / "mean_diag.html"
        with open(outputfile,'w') as ofil:
            ofil.write(mean_rndr)
        #End with

        #Search for AMWG Table HTML files:
        table_html_files = list(plot_path.glob("amwg_table_*.html"))

        #Determine if any AMWG tables were generated:
        if table_html_files:

            #Set Table HTML generation logical to "TRUE":
            gen_table_html = True

            #Create a directory that will hold table html files:
            table_pages_dir = website_dir / "html_table"
            table_pages_dir.mkdir(exist_ok=True)

            #Move all table html files to new directory:
            for table_html in table_html_files:
                shutil.move(table_html, table_pages_dir / table_html.name)

            #Construct dictionary needed for HTML page:
            amwg_tables = OrderedDict()

            #Loop over cases:
            for case in [case_name, data_name]:

                #Search for case name in moved HTML files:
                table_htmls = table_pages_dir.glob(f"amwg_table_{case}.html")

                #Check if file exists:
                if table_htmls:

                    #Initialize loop counter:
                    count = 0

                    #Loop over globbed files:
                    for table_html in table_htmls:

                        #Create relative path for HTML file:
                        amwg_tables[case] = table_html.name

                        #Update counter:
                        count += 1

                        #If counter greater than one, then throw an error:
                        if count > 1:
                            emsg = f"More than one AMWG table is associated with case '{case}'."
                            emsg += "\nNot sure what is going on, "
                            emsg += "so website generation will end here."
                            self.end_diag_fail(emsg)

            #Construct mean_table.html
            mean_title = "AMP Diagnostic Tables:"
            mean_tmpl = jinenv.get_template('template_mean_table.html')
            mean_rndr = mean_tmpl.render(title=mean_title,
                            amwg_tables=amwg_tables)

            #Write mean diagnostic tables HTML file:
            outputfile = table_pages_dir / "mean_table.html"
            with open(outputfile,'w') as ofil:
                ofil.write(mean_rndr)
            #End with

        else:
            #No Tables exist, so no link will be added to main page:
            gen_table_html = False

        #Construct index.html
        index_title = "AMP Diagnostics Prototype"
        index_tmpl = jinenv.get_template('template_index.html')
        index_rndr = index_tmpl.render(title=index_title,
                         case1=case_name,
                         case2=data_name,
                         gen_table_html=gen_table_html)

        #Write Mean diagnostics HTML file:
        outputfile = website_dir / "index.html"
        with open(outputfile,'w') as ofil:
            ofil.write(index_rndr)
        #End with

        #Notify user that script has finishedd:
        print("  ...Webpages have been generated successfully.")

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