"""
Helper functions for Trainers
"""
from typing import Any, Union, Dict, Iterable, List, Optional
import datetime
import logging
import os
import shutil

import torch
from torch.nn.parallel import replicate, parallel_apply
from torch.nn.parallel.scatter_gather import gather

from allennlp.common.checks import ConfigurationError, check_for_gpu
from allennlp.common.params import Params
from allennlp.common.tqdm import Tqdm
from allennlp.data.dataset_readers import DatasetReader
from allennlp.data import Instance
from allennlp.data.iterators import DataIterator
from allennlp.data.iterators.data_iterator import TensorDict
from allennlp.models.model import Model
from allennlp.models.archival import CONFIG_NAME
from allennlp.nn import util as nn_util

logger = logging.getLogger(__name__)

# We want to warn people that tqdm ignores metrics that start with underscores
# exactly once. This variable keeps track of whether we have.
class HasBeenWarned:
    tqdm_ignores_underscores = False

def sparse_clip_norm(parameters, max_norm, norm_type=2) -> float:
    """Clips gradient norm of an iterable of parameters.

    The norm is computed over all gradients together, as if they were
    concatenated into a single vector. Gradients are modified in-place.
    Supports sparse gradients.

    Parameters
    ----------
    parameters : ``(Iterable[torch.Tensor])``
        An iterable of Tensors that will have gradients normalized.
    max_norm : ``float``
        The max norm of the gradients.
    norm_type : ``float``
        The type of the used p-norm. Can be ``'inf'`` for infinity norm.

    Returns
    -------
    Total norm of the parameters (viewed as a single vector).
    """
    # pylint: disable=invalid-name,protected-access
    parameters = list(filter(lambda p: p.grad is not None, parameters))
    max_norm = float(max_norm)
    norm_type = float(norm_type)
    if norm_type == float('inf'):
        total_norm = max(p.grad.data.abs().max() for p in parameters)
    else:
        total_norm = 0
        for p in parameters:
            if p.grad.is_sparse:
                # need to coalesce the repeated indices before finding norm
                grad = p.grad.data.coalesce()
                param_norm = grad._values().norm(norm_type)
            else:
                param_norm = p.grad.data.norm(norm_type)

            # Cast param_norm to appropriate GPU
            if not isinstance(total_norm, int):
                if not total_norm.device.type == 'cpu':
                    param_norm = param_norm.cuda(total_norm.device)

            total_norm += param_norm ** norm_type
        total_norm = total_norm ** (1. / norm_type)
    clip_coef = max_norm / (total_norm + 1e-6)
    if clip_coef < 1:
        for p in parameters:
            if p.grad.is_sparse:
                if not p.device.type == 'cpu':
                    p.grad.data._values().mul_(clip_coef.cuda(p.device))
                else:
                    p.grad.data._values().mul_(clip_coef)
            else:
                if not p.device.type == 'cpu':
                    p.grad.data.mul_(clip_coef.cuda(p.device))
                else:
                    p.grad.data.mul_(clip_coef)
    return total_norm

def move_optimizer_to_cuda(optimizer):
    """
    Move the optimizer state to GPU, if necessary.
    After calling, any parameter specific state in the optimizer
    will be located on the same device as the parameter.
    """
    for param_group in optimizer.param_groups:
        for param in param_group['params']:
            if param.is_cuda:
                param_state = optimizer.state[param]
                for k in param_state.keys():
                    if isinstance(param_state[k], torch.Tensor):
                        param_state[k] = param_state[k].cuda(device=param.get_device())

def get_batch_size(batch: Union[Dict, torch.Tensor]) -> int:
    """
    Returns the size of the batch dimension. Assumes a well-formed batch,
    returns 0 otherwise.
    """
    if isinstance(batch, torch.Tensor):
        return batch.size(0) # type: ignore
    elif isinstance(batch, Dict):
        return get_batch_size(next(iter(batch.values())))
    else:
        return 0

def time_to_str(timestamp: int) -> str:
    """
    Convert seconds past Epoch to human readable string.
    """
    datetimestamp = datetime.datetime.fromtimestamp(timestamp)
    return '{:04d}-{:02d}-{:02d}-{:02d}-{:02d}-{:02d}'.format(
            datetimestamp.year, datetimestamp.month, datetimestamp.day,
            datetimestamp.hour, datetimestamp.minute, datetimestamp.second
    )

def str_to_time(time_str: str) -> datetime.datetime:
    """
    Convert human readable string to datetime.datetime.
    """
    pieces: Any = [int(piece) for piece in time_str.split('-')]
    return datetime.datetime(*pieces)

def datasets_from_params(params: Params) -> Dict[str, Iterable[Instance]]:
    """
    Load all the datasets specified by the config.
    """
    dataset_reader = DatasetReader.from_params(params.pop('dataset_reader'))
    validation_dataset_reader_params = params.pop("validation_dataset_reader", None)

    validation_and_test_dataset_reader: DatasetReader = dataset_reader
    if validation_dataset_reader_params is not None:
        logger.info("Using a separate dataset reader to load validation and test data.")
        validation_and_test_dataset_reader = DatasetReader.from_params(validation_dataset_reader_params)

    train_data_path = params.pop('train_data_path')
    logger.info("Reading training data from %s", train_data_path)
    train_data = dataset_reader.read(train_data_path)

    datasets: Dict[str, Iterable[Instance]] = {"train": train_data}

    validation_data_path = params.pop('validation_data_path', None)
    if validation_data_path is not None:
        logger.info("Reading validation data from %s", validation_data_path)
        validation_data = validation_and_test_dataset_reader.read(validation_data_path)
        datasets["validation"] = validation_data

    test_data_path = params.pop("test_data_path", None)
    if test_data_path is not None:
        logger.info("Reading test data from %s", test_data_path)
        test_data = validation_and_test_dataset_reader.read(test_data_path)
        datasets["test"] = test_data

    return datasets

def create_serialization_dir(
        params: Params,
        serialization_dir: str,
        recover: bool,
        force: bool) -> None:
    """
    This function creates the serialization directory if it doesn't exist.  If it already exists
    and is non-empty, then it verifies that we're recovering from a training with an identical configuration.

    Parameters
    ----------
    params: ``Params``
        A parameter object specifying an AllenNLP Experiment.
    serialization_dir: ``str``
        The directory in which to save results and logs.
    recover: ``bool``
        If ``True``, we will try to recover from an existing serialization directory, and crash if
        the directory doesn't exist, or doesn't match the configuration we're given.
    force: ``bool``
        If ``True``, we will overwrite the serialization directory if it already exists.
    """
    if recover and force:
        raise ConfigurationError("Illegal arguments: both force and recover are true.")

    if os.path.exists(serialization_dir) and force:
        shutil.rmtree(serialization_dir)

    if os.path.exists(serialization_dir) and os.listdir(serialization_dir):
        if not recover:
            raise ConfigurationError(f"Serialization directory ({serialization_dir}) already exists and is "
                                     f"not empty. Specify --recover to recover training from existing output.")

        logger.info(f"Recovering from prior training at {serialization_dir}.")

        recovered_config_file = os.path.join(serialization_dir, CONFIG_NAME)
        if not os.path.exists(recovered_config_file):
            raise ConfigurationError("The serialization directory already exists but doesn't "
                                     "contain a config.json. You probably gave the wrong directory.")
        else:
            loaded_params = Params.from_file(recovered_config_file)

            # Check whether any of the training configuration differs from the configuration we are
            # resuming.  If so, warn the user that training may fail.
            fail = False
            flat_params = params.as_flat_dict()
            flat_loaded = loaded_params.as_flat_dict()

            # Exclude some keys from being checked as matching config
            no_check_keys = ['trainer.cuda_device', 'train_data_path', 'validation_data_path', 'test_data_path']  # Make this the overrides
            for key in no_check_keys:
                flat_params.pop(key, None)
                flat_loaded.pop(key, None)

            for key in flat_params.keys() - flat_loaded.keys():
                logger.error(f"Key '{key}' found in training configuration but not in the serialization "
                             f"directory we're recovering from.")
                fail = True
            for key in flat_loaded.keys() - flat_params.keys():
                logger.error(f"Key '{key}' found in the serialization directory we're recovering from "
                             f"but not in the training config.")
                fail = True
            for key in flat_params.keys():
                if flat_params.get(key, None) != flat_loaded.get(key, None):
                    logger.error(f"Value for '{key}' in training configuration does not match that the value in "
                                 f"the serialization directory we're recovering from: "
                                 f"{flat_params[key]} != {flat_loaded[key]}")
                    fail = True
            if fail:
                raise ConfigurationError("Training configuration does not match the configuration we're "
                                         "recovering from.")
    else:
        if recover:
            raise ConfigurationError(f"--recover specified but serialization_dir ({serialization_dir}) "
                                     "does not exist.  There is nothing to recover from.")
        os.makedirs(serialization_dir, exist_ok=True)

def data_parallel(batch_group: List[TensorDict],
                  model: Model,
                  cuda_devices: List) -> Dict[str, torch.Tensor]:
    """
    Performs a forward pass using multiple GPUs.  This is a simplification
    of torch.nn.parallel.data_parallel to support the allennlp model
    interface.
    """
    assert len(batch_group) <= len(cuda_devices)

    moved = [nn_util.move_to_device(batch, device)
             for batch, device in zip(batch_group, cuda_devices)]

    used_device_ids = cuda_devices[:len(moved)]
    replicas = replicate(model, used_device_ids)
    # We pass all our arguments as kwargs. Create a list of empty tuples of the
    # correct shape to serve as (non-existent) positional arguments.
    inputs = [()] * len(batch_group)
    outputs = parallel_apply(replicas, inputs, moved, used_device_ids)

    # Only the 'loss' is needed.
    # a (num_gpu, ) tensor with loss on each GPU
    losses = gather([output['loss'].unsqueeze(0) for output in outputs], used_device_ids[0], 0)
    return {'loss': losses.mean()}

def enable_gradient_clipping(model: Model, grad_clipping: Optional[float]) -> None:
    if grad_clipping is not None:
        for parameter in model.parameters():
            if parameter.requires_grad:
                parameter.register_hook(lambda grad: nn_util.clamp_tensor(grad,
                                                                          minimum=-grad_clipping,
                                                                          maximum=grad_clipping))

def rescale_gradients(model: Model, grad_norm: Optional[float] = None) -> Optional[float]:
    """
    Performs gradient rescaling. Is a no-op if gradient rescaling is not enabled.
    """
    if grad_norm:
        parameters_to_clip = [p for p in model.parameters()
                              if p.grad is not None]
        return sparse_clip_norm(parameters_to_clip, grad_norm)
    return None

def get_metrics(model: Model, total_loss: float, num_batches: int, trainer_metrics: dict,
                reset: bool = False) -> Dict[str, float]:
    """
    Gets the metrics but sets ``"loss"`` to
    the total loss divided by the ``num_batches`` so that
    the ``"loss"`` metric is "average loss per batch".
    """
    judge = model if model.is_judge else model.judge
    metrics = judge.get_metrics(reset=reset)
    metrics["loss"] = float(total_loss / num_batches) if num_batches > 0 else 0.0
    # NOTE: Temporary fix while figuring out why isinstance(metric.get_metric(reset), int) for RACE a/b SL training
    # Replaces: trainer_metrics = {name: metric.get_metric(reset).item() for name, metric in trainer_metrics.items()}
    trainer_metrics = {name: metric.get_metric(reset) for name, metric in trainer_metrics.items()}
    for name, metric in trainer_metrics.items():
        if not isinstance(trainer_metrics[name], int):
            trainer_metrics[name] = trainer_metrics[name].item()
    metrics.update(trainer_metrics)
    return metrics

def evaluate(model: Model,
             instances: Iterable[Instance],
             data_iterator: DataIterator,
             cuda_device: int,
             batch_weight_key: str) -> Dict[str, Any]:
    check_for_gpu(cuda_device)
    with torch.no_grad():
        model.eval()

        iterator = data_iterator(instances,
                                 num_epochs=1,
                                 shuffle=False)
        logger.info("Iterating over dataset")
        generator_tqdm = Tqdm.tqdm(iterator, total=data_iterator.get_num_batches(instances))

        # Number of batches in instances.
        batch_count = 0
        # Number of batches where the model produces a loss.
        loss_count = 0
        # Cumulative weighted loss
        total_loss = 0.0
        # Cumulative weight across all batches.
        total_weight = 0.0

        for batch in generator_tqdm:
            batch_count += 1
            batch = nn_util.move_to_device(batch, cuda_device)
            output_dict = model(**batch)
            loss = output_dict.get("loss")

            metrics = model.get_metrics()

            if loss is not None:
                loss_count += 1
                if batch_weight_key:
                    weight = output_dict[batch_weight_key].item()
                else:
                    weight = 1.0

                total_weight += weight
                total_loss += loss.item() * weight
                # Report the average loss so far.
                metrics["loss"] = total_loss / total_weight

            if (not HasBeenWarned.tqdm_ignores_underscores and
                        any(metric_name.startswith("_") for metric_name in metrics)):
                logger.warning("Metrics with names beginning with \"_\" will "
                               "not be logged to the tqdm progress bar.")
                HasBeenWarned.tqdm_ignores_underscores = True
            description = ', '.join(["%s: %.2f" % (name, value) for name, value
                                     in metrics.items() if not name.startswith("_")]) + " ||"
            generator_tqdm.set_description(description, refresh=False)

        final_metrics = model.get_metrics(reset=True)
        if loss_count > 0:
            # Sanity check
            if loss_count != batch_count:
                raise RuntimeError("The model you are trying to evaluate only sometimes " +
                                   "produced a loss!")
            final_metrics["loss"] = total_loss / total_weight

        return final_metrics

def description_from_metrics(metrics: Dict[str, float]) -> str:
    if (not HasBeenWarned.tqdm_ignores_underscores and
                any(metric_name.startswith("_") for metric_name in metrics)):
        logger.warning("Metrics with names beginning with \"_\" will "
                       "not be logged to the tqdm progress bar.")
        HasBeenWarned.tqdm_ignores_underscores = True
    return ', '.join(["%s: %.4f" % (name, value)
                      for name, value in
                      metrics.items() if not name.startswith("_")]) + " ||"


"""module for generating study schedule in the py-study-planner project"""

#import modules
import json
import datetime
import os
import math
from operator import itemgetter
import pandas as pd
import numpy as np

class ScheduleManager:
    def __init__(self, db_file='exam_data.json'):
        self.db_file = db_file

    def print_user_options(self):
        """print user options for the scheduling suite in menu"""
        print("('s' and hit enter to display your schedule)")
        print("('a' and hit enter to display your day's agenda)")
        print("('q' and hit enter to quit the scheduling suite)")

    def gen_tasks(self, exam_name):
        """generate a list of tuples with task names and associated times"""
        with open('exam_data.json',"r", encoding='utf-8') as exam_data_file:
            loaded = json.load(exam_data_file)
        resource_data = loaded[exam_name]['resource_data']
        today = datetime.datetime.today().date()
        date_data = loaded[exam_name]['exam_date']
        date_data = datetime.datetime.strptime(date_data, '%m/%d/%Y').date()
        t_remaining = (date_data-today).days
        resource_names = [tup[0] for tup in resource_data]
        resource_times = [int(tup[1])/t_remaining for tup in resource_data]
        tasks = list(zip(resource_names,resource_times))
        return tasks, t_remaining

    def view_schedule(self):
        """generate a study schedule for a given exam"""
        exam_name = input("What exam would you like to generate a schedule for?\n")
        tasks = self.gen_tasks(exam_name)
        schedule = {}
        td = datetime.datetime.today()
        tdmin = datetime.timedelta(minutes=(td.minute))
        def convert_to_timeform(time):
            """convert to preferred %m/%d/%Y %H:%M time format"""
            return datetime.datetime.strftime((time),'%m/%d/%Y %H:%M')
        for i in range(((24*tasks[1])-int(td.hour))-1):
            raw_time = td-tdmin+datetime.timedelta(hours=i+1)
            schedule[convert_to_timeform(raw_time)] = [np.nan]
        def find_time(n):
            """find first instance of window of empty values in dict"""
            free_in_a_row = 0
            for index, date in enumerate(schedule):
                if (np.isnan(schedule[date])):
                    free_in_a_row += 1
                else:
                    free_in_a_row = 0
                if (free_in_a_row == n):
                    return index-n+1

            return np.nan
        schedule = pd.DataFrame(schedule).T
        print(schedule)
        return True

    def view_agenda(self):
        """view an agenda for a given day"""
        ...

    def del_schedule(self):
        """view an agenda for a given day"""
        ...

    def scheduler_main(self):
        """run main function while constant "run" is True, allows user to navigate main menu or quit"""
        print("Welcome to the scheduling suite.")
        print("What would you like to do? Here are your options:")
        run=True
        while run:
            self.print_user_options()
            usr_input_mm = input("")
            if usr_input_mm=="s":     # display schedules for exams
                self.view_schedule()
                print("schedule viewed! What next?")
            elif usr_input_mm=="a":     # view day agenda
                self.view_agenda()
                print("agenda viewed! What next?")
            elif usr_input_mm=="q":   # quit py-study-planner program
                run=False
            else:                       # exception handling: repeat user options
                print("input invalid. your options are:")

if __name__=="__main__":
    scheduleMgr = ScheduleManager()
    scheduleMgr.scheduler_main()


"""Methods to populate influxdb buckets"""

import json
from django.core.management import call_command

from members.models import Member
from transmission.models import Downlink
from transmission.processing import XTCEParser
from transmission.processing.process_raw_bucket import parse_and_store_frame, store_raw_frame
from transmission.processing.save_raw_data import parse_submitted_frame

def add_dummy_downlink_frames(input_file="transmission/dummy_downlink.json"):
    """Add dummy frames to Downlink table as admin user."""

    # check if admin exists, if not create it
    if len(Member.objects.filter(username="admin")) == 0:
        call_command('initadmin')

    with open(input_file, 'r', encoding="utf-8") as file:
        dummy_data = json.load(file)
        for frame in dummy_data["frames"]:
            frame_entry = Downlink()
            frame_entry.observer = Member.objects.all().filter(username="admin")[0]
            frame_entry = parse_submitted_frame(frame, frame_entry)
            frame_entry.save()

def add_dummy_tlm_data(satellite, input_file):
    """Add dummy processed telemetry intended to test and experiment with dashboards."""
    with open(input_file, encoding="utf-8") as file:
        data = json.load(file)
        # sort messages in chronological order
        data.sort(key=lambda x: x["timestamp"])
        # data = data[:100]
        # process each frame
        for frame in data:
            try:
                parse_and_store_frame(satellite,
                                      frame["timestamp"],
                                      frame["frame"],
                                      frame["observer"],
                                      "downlink"
                                      )
            except XTCEParser.XTCEException as _:
                # ignore
                pass
    return len(data)

def add_dummy_tlm_raw_data(satellite, input_file):
    """Add dummy raw telemetry"""
    stored_frames = 0
    with open(input_file, encoding="utf-8") as file:
        data = json.load(file)
        # sort messages in chronological order
        data.sort(key=lambda x: x["timestamp"])
        # data = data[:100]
        # process each frame
        for frame in data:
            stored = store_raw_frame(satellite,
                                     frame["timestamp"],
                                     frame["frame"],
                                     frame["observer"],
                                     "downlink")
            if stored:
                stored_frames += 1

    return len(data), stored_frames


import subprocess
from collections import OrderedDict
from io import StringIO
from itertools import product

def _parse_categorical(line):
    # Categorical Lines consist of:
    #
    # <name><w*>{<values>}<w*>[<default>]<*w>#Comment
    # where:
    # <name> - name of parameter.
    # <values> - comma seperated list of values (i.e. a,b,c,d...,z)
    # <default> - default value enclosed in braces.
    # <w*> - zero or more whitespace characters
    # has_comment = False
    # comment = ""
    if "#" in line:
        comment_begins = line.find("#")
        line = line[:comment_begins]
        # comment = line[comment_begins:]
        # has_comment = True

    if line.count("{") != 1 or line.count("}") != 1:
        raise ValueError("Malformed parameter line %s" % line)

    first_bracket = line.find("{")
    second_bracket = line.find("}")
    domain_values = line[first_bracket + 1 : second_bracket]
    cat_values = domain_values.split(",")
    if len(cat_values) < 1:
        raise ValueError("Expected at least one value in %s" % line)
    name = line[:first_bracket].strip()
    values = [value.strip() for value in cat_values]

    # Stripped the code for the default value since we don't need it here

    od = OrderedDict()
    od["name"] = name
    od["values"] = values
    return od

def parse_hyperparameter_string(param_string):
    params = OrderedDict()
    lines = param_string.split("\n")
    for line in lines:
        # Logic kind of copied from SMAC ACLIB version 2.06.01-dev,
        # but a little bit more restrictive
        # file: ca.ubc.cs.beta.aclib.configspace.ParamConfigurationSpace.java
        # line 497-546
        # type = ""
        if not line.strip():
            continue
        elif line.count("|") == 1:
            pass
            # print "WARNING: Conditionality is not parsed yet."
            # od = _parse_conditional(line)
        elif line.strip()[0] == "{":
            continue
        elif line.count("[") == 2:
            continue
        elif line.count("{") == 1 and line.count("}") == 1:
            od = _parse_categorical(line)
        else:
            raise ValueError("Cannot parse the following line %s" % line)
        params[od["name"]] = od["values"]
    return params

def construct_cli_call(cli_target, params):
    cli_call = StringIO()
    cli_call.write(cli_target)
    params = OrderedDict(sorted(params.items(), key=lambda t: t[0]))
    for param in params:
        cli_call.write(" -" + param + " \"'" + str(params[param]) + "'\"")
    return cli_call.getvalue()

def command_line_function(cli_target, params):
    call = construct_cli_call(cli_target, params)
    ret = subprocess.check_output(call, shell=True)
    return ret

def build_grid(hyperparameters):
    """Build a grid represented as a list of parameter dictionaries."""
    parameter_dicts = []
    for parameters in product(*hyperparameters.values()):
        parameter_tuples = zip(hyperparameters.keys(), parameters)
        parameter_dict = dict(parameter_tuples)
        parameter_dicts.append(parameter_dict)
    return parameter_dicts


import dash_core_components as dcc
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash.dependencies import Input, Output
import pandas as pd
from app import app
import plotly.express as px
from KNN_Funct import get_sales_comps
import dash_table
import sys
from KNN_Funct import neighborhood_cluster

knnsales_text = '''
**Notes on Input Range**
- Gross Living Area in our data set ranges from 334-4,316
- Lot Area in our data set ranges from 1,470-215,245. The units are ambiguous.
- Basement SF in our data set ranges from 0-3,206
- Garage area in our data set ranges from 0-1,488
'''

clustertext='''### Analysis
This graph gives a better understanding of similar neighborhoods in our Ames dataset. When selecting two clusters, we can see a distinct neighborhood in the mid right. Thats the North Ridge, Stone Bridge area. You can observe based on the Get to know Ames maps that these houses are new and expensive. This is clearly a fancier part of the town, the who's who of Ames, IA, if you will, which is what sets it so apart from other neighborhoods. \n\n
When you increase to three clusters, we see Clear Creek neighborhood stands out in yellow. This neighborhood is interesting because we actually ended up dropping many houses from this neighborhood from our dataset. This was because many of them were more industrial used properties, ie agricultural etc, and were not useful in our model. This distinction is likely why it is set apart in our clustering. \n\n This continues as increasing the cluster numbers, next we see Timbers neighborhood broken out, then BrookDale. Ultimately after feeding these clusters into our stepwise model, we found the most informative was the North Ridge distinction, as it remained non-zero after penalization.
'''
tdata=pd.read_csv('apps/data/tdata_for_realtor_map.csv')
Neighborhood_as_instance_narrowed=pd.read_csv('apps/data/data_for_clusters.csv')
knn_table_columns=['House','GrLivArea','LotArea','OverallQual','YearBuilt','TotalBsmtSF','GarageArea','Remodeled','SalePrice']
# tdataKNN=pd.read_csv('apps/data/Data_For_KNN_salescomps.csv')
# Miles=2
# numofneighbors=10
# instance=pd.DataFrame({'GrLivArea':1786,   'LotArea':164660,  'OverallQual':5, 'YearBuilt':1965,
# 'TotalBsmtSF':1499,
# 'GarageArea':529,'Lat':41.99854744897959,'Long':-93.6589044489796,
# 'Remodeled':bool(0)}, index=[0])
# KNN_table=get_sales_comps(tdataKNN,instance,Miles,numofneighbors,features=None)
#

SIDEBAR_STYLE = {"position": "fixed", "top": 0, "left": 0,"bottom": 0,
    "width": "16rem", "padding": "2rem 1rem"} #"background-color": "#f8f9fa",

CONTENT_STYLE = {"margin-left": "18rem", "margin-right": "2rem", "padding": "2rem 1rem",}
Padded_STYLE = {"margin-left": "-1rem", "margin-right": "2rem","padding": "2rem 1rem",}

kitchen_text = '''
Kitchens are very expensive to remodel, so if a buyer is looking for an excellent-quality kitchen
it is recommended to purchase a home with one rather than planning to remodel an existing kitchen.
Online sources indicate a midrange kitchen renovation often totals $40,000 - $50,000. Our machine learning model
indicates homes in Ames with excellent kitchens command just a $24,300 premium, so it is cheaper to buy a
home with a high-quality kitchen than to upgrade one post-purchase. Also, it is recommended that sellers
avoid doing full kitchen renovations as it is unlikely they will recoup their investment.
'''

asbestos_text = '''
Asbestos was a widely used house exterior material until the 1970's, when it was banned.
The negative health effects of asbestos are widely known, yet many homes still have their
original asbestos exteriors. It is not a serious health issue until the asbestos becomes cracked and
worn and fibers become airborne, but the shingles will eventually need to be replaced. It is an
expensive process requiring special safety gear and permitting. Our model indicates a $13,400 hit
to home resale value from having asbestos shingles rather than more typical vinyl siding or plywood
covering. The cost to both dispose of the asbestos and replace it with another exterior material is
likely to be significantly higher than this, so we recommend that buyers avoid homes with asbestos
siding and that sellers forego replacing it and sell their home as-is.
'''

bathroom_text = '''
Available online sources indicate that the cost of adding a new bathroom to a home averages $10,000.
Given that our pricing algorithm does not increase in accuracy when including bathroom count,
it is not recommended that sellers add to the bathroom count of their home. It is, however,
recommended that buyers make sure to purchase a home with the number of bathrooms they desire.
Money invested in adding bathrooms after purchase is unlikely to be recouped at a future sale of the home.
'''

remodeled_text = '''
One question we had when reviewing the data was ‘how much value does remodeling add to your resale value’.\n
In an attempt to answer it we decided to split the dataset- one containing the remodeled houses and one containing the unremodeled houses,
and then using regression to explore the difference in coefficients. However, there is a clear confounding factor here, Age.
Older homes are much more likely to have been remodeled. In order to control for this, we selected only houses built within
a year range where there was a strong mixture of remodeled and unremodeled houses. We chose 1950-1999. Houses before 1950 were
mostly 100% remodeled and in this year range, a consistent 30% had been remodeled and 70% were unremodeled. Running two simple regressions on
each dataset to isolate the effect of year built in both the remodeled and unremodeled cases we obtained the following equations:
The Remodeled Houses SLR Equation is:\n
*SalePrice=140,060.3 + 1,913.6 Year After 1950*\n
The Unremodeled Houses SLR Equation is:\n
*SalePrice=117,752.01 + 1,892.37 Year After 1950*\n
We can see there is a clear discrepancy of over $20k in base price for unremodeled versus remodeled houses of the same year.
The effect of house age is slightly different between the two based on the coefficients however much less significantly than
the base prices. We conclude based on this simple model that remodeled houses of the same year are worth about $20k more than
their unremodeled counterparts and age has a similar effect on both groups.
'''

sidebar = html.Div([
        html.H2("Housing Prices in Ames, Iowa", style={'textAlign': 'center'}),
        html.Hr(),
        dbc.Nav(
            [
                dbc.NavLink("Introduction", href="/apps/app1", active="exact"),
                dbc.NavLink("Project", href="/apps/app2", active="exact"),
                dbc.NavLink("Pricing Model", href="/apps/app3", active="exact"),
                dbc.NavLink("Real Estate Handbook", href="/apps/app4", active="exact"),
                dbc.NavLink("Miscellaneous", href="/apps/app5", active="exact"),
            ], vertical=True, pills=True
        )
    ],
    style=SIDEBAR_STYLE,
)

content = html.Div([

    html.Div([
        html.Div([html.H3('Real Estate Handbook'),
    dcc.Tabs(id='tabs', value='tab-1', children=[
    dcc.Tab(label='Welcome Realtors!', value='tab-1'),
        dcc.Tab(label='Get to know Ames', value='tab-2'),
        dcc.Tab(label='Research Questions', value='tab-3'),
        dcc.Tab(label='Neighborhood Analysis', value='tab-4'),
        dcc.Tab(label='Sales Comps', value='tab-5'),
    ]),
    html.Div(id='tabs-content', style=Padded_STYLE)
])

], style=CONTENT_STYLE)])

layout = html.Div([sidebar, content])

@app.callback(
    Output('app-4-display-value', 'children'),
    Input('app-4-dropdown', 'value'))
def display_value(value):
    return 'You have selected "{}"'.format(value)
@app.callback(Output('tabs-content', 'children'),
    Input('tabs', 'value'))
def display_value(value):
    if value=='tab-1':
        return (html.Div([
        html.H3('Welcome Realtors',style={'font-weight': 'bold'}),]),
        dcc.Markdown('''Introducing our Real Estate Handbook, a data rich review of Ames to help real estate agents better understand the market and further assist customers. Here you will find analysis of different house features effect on price as found in our model, a look into the similar and different neighborhoods in Ames, as well as an ML generated sales comps tools for your convenience.'''),
        html.Div([html.Img(src='/assets/shutterstock_752498263-1400x621.jpg')], style={'textAlign': 'center','height':'10%', 'width':'10%'}))
    elif value=='tab-2':
        return (html.Div([
        html.H3('Lets get Ames-icable', style={'font-weight': 'bold'})]),
        dcc.Markdown('''Welcome to Ames, Iowa; named one of the “15 Cities That Have Done the Best Since the Recession” by Bloomberg Business in 2015 and known as the ‘healthiest city in America’ by USA Today. Home to Iowa State University, Ames has become a player in developing the world’s bio-economic future. An attractive location for business, some of the names housed here include 3M, Danfoss, Boehringer Ingelheim, and more. Below, see a simplified map of the houses sold in Ames between 06-09. Interacting with the map will give a general understanding of the city.'''),
        html.Div([html.H3('Visualize the City', style={'font-weight': 'bold'})]),
        dcc.Markdown('''Use this graph to visualize some of the trends of the neighborhood. Use the dropdown menu to color the points by different attributes.'''),
        html.Div([
        dcc.Dropdown(id='dropdown_for_realt_map',options=[{'label': 'Year Built','value': 'YearBuilt'},{'label': 'Gross Living Area', 'value': 'GrLivArea'} ,{'label': 'Sale Price','value': 'SalePrice'},{'label': 'Neighborhood','value': 'Neighborhood'},{'label': 'Overall Quality','value': 'OverallQual'}],value='colorvalue')]),
        dcc.Graph(id='realt_map'))
    elif value=='tab-3':
        return (html.Div([
        html.H3('Research Questions', style={'font-weight': 'bold'})]),
        dbc.Card(
            dbc.CardBody([
            html.H5('Remodeled vs Not Remodeled House Prices:'),
            dbc.Row([
            dbc.Col([dcc.Markdown(remodeled_text)]),
            dbc.Col([html.Div([html.Img(src='/assets/Mod_vs_Remod.jpg')], style={'textAlign': 'center'})]),
            ])
            ]),
        ),
        dbc.Card(
            dbc.CardBody([
                html.H5('Kitchen Renovations:'),
                dcc.Markdown(kitchen_text)
                ]),
            ),
        dbc.Card(
            dbc.CardBody([
                html.H5('Asbestos Shingles:'),
                dcc.Markdown(asbestos_text)
                ]),
            ),
        dbc.Card(
            dbc.CardBody([
                html.H5('Bathrooms:'),
                dcc.Markdown(bathroom_text)
                ]),
            ),

        )
    elif value=='tab-4':
        return (html.Div([
        html.H3('Neighborhood Clustering', style={'font-weight': 'bold'})]),
        dcc.Markdown('''Below is a tool which helps illustrate which neighborhoods in Ames are most similar. This is helpful to realtors and buyers alike, it gives a sense of comparable neighborhoods to allow a buyer to expand their search. This is also how the neighborhood cluster columns were created in our ML models.'''),
        dcc.Slider(id='sliderforclusters',min=1,max=9,step=None,marks={1: '1',2: '2',3: '3',4: '4',5: '5',6:'6',7:'7',8:'8',9:'9'},value=1),
        dcc.Graph(id='cluster_map'),
        dcc.Markdown(clustertext)
    )
    elif value=='tab-5':
        return (html.Div([
        html.H3('KNN Sales Comps', style={'font-weight': 'bold'})]),
        dcc.Markdown('''This tool uses K-Nearest Neighbors to generate automatic sales comps. Input attributes below to find houses most similar to it and how much they sold for. See below for accepted ranges.'''),
        dcc.Input(id='grlivarea',placeholder='Enter Gross Living Area',type='text',value=''),
        dcc.Input(id='lotarea',placeholder='Enter Lot Area value',type='text',value=''),
        dcc.Input(id='qualval',placeholder='Enter Quality value 1-10',type='text',value=''),
        dcc.Input(id='yearbuiltval',placeholder='Enter Year Built',type='text',value=''),
        dcc.Input(id='bsmtsfval',placeholder='Enter Basement SF',type='text',value=''),
        dcc.Input(id='garageareaval',placeholder='Enter Garage Area',type='text',value=''),
        dcc.Input(id='latval',placeholder='Enter Latitude',type='text',value=''),
        dcc.Input(id='longval',placeholder='Enter Longitude',type='text',value=''),
        dcc.Input(id='addval',placeholder='Enter Address',type='text',value=''),
        dbc.FormGroup([dbc.Label("Remodeled?"),
        dbc.RadioItems(options=[{"label": "True", "value": 1},{"label": "False", "value":0},],value=0,id="remodeledtf",inline=True)], style=Padded_STYLE),
        dcc.Input(id='milerad',placeholder='Enter radius to search (miles)',type='text',value=''),
        dcc.Checklist(id='usedef',options=[{'label': 'Use Default Values?', 'value': 'def'}],value=['def']),
        html.Hr(),
        html.H3('Sales Comps'),
        dash_table.DataTable(id='knn_table',columns=[{"name": i, "id": i} for i in knn_table_columns],style_cell=dict(textAlign='center')),
        html.Hr(),
        dcc.Markdown(knnsales_text))

@app.callback(
    Output('realt_map', 'figure'),
    Input('dropdown_for_realt_map', 'value'))
def return_realt_map(colorvalue):
    tdata=pd.read_csv('apps/data/tdata_for_realtor_map.csv')
    if colorvalue=='YearBuilt':
        return px.scatter(tdata, x="Lat", y="Long", color='YearBuilt',title="Houses Sold in Ames Iowa \'06-\'09'")
    elif colorvalue=='GrLivArea':
        return px.scatter(tdata, x="Lat", y="Long", color='GrLivArea',title="Houses Sold in Ames Iowa \'06-\'09'")
    elif colorvalue=='SalePrice':
        return px.scatter(tdata, x="Lat", y="Long", color='SalePrice',title="Houses Sold in Ames Iowa \'06-\'09")

    elif colorvalue=='Neighborhood':
        return px.scatter(tdata, x="Lat", y="Long", color='Neighborhood',title="Houses Sold in Ames Iowa \'06-\'09")
    elif colorvalue=='OverallQual':
        return px.scatter(tdata, x="Lat", y="Long", color='OverallQual',title="Houses Sold in Ames Iowa \'06-\'09")
    else:
        return px.scatter(tdata, x="Lat", y="Long",title="Houses Sold in Ames Iowa \'06-\'09")

@app.callback(
    Output('knn_table', 'data'),
    [Input('grlivarea', 'value'),
    Input('lotarea', 'value'),
    Input('qualval', 'value'),
    Input('yearbuiltval', 'value'),
    Input('bsmtsfval', 'value'),
    Input('garageareaval', 'value'),
    Input('remodeledtf', 'value'),
    Input('latval', 'value'),
    Input('longval', 'value'),
    Input('milerad', 'value'),
    Input('addval', 'value'),
    Input('usedef', 'value')])
def return_knn(valuegr,valuela,valuequality,valueyearbuilt,valuebsmtsf,valuegarage,valueremodeled,valuelat,valuelong,milerad,addval,usedef):
    tdata=pd.read_csv('apps/data/Data_For_KNN_salescomps.csv')
    Miles=milerad
    numofneighbors=10
    # if valuela=='' or valuegr=='' or valuequality=='' or valueyearbuilt=='' or valuebsmtsf=='' or valuegarage=='' or valuelat=='' or valuelong=='':
    #     instance=pd.DataFrame({'GrLivArea':1786,   'LotArea':164660,  'OverallQual':5, 'YearBuilt':1965,
    #     'TotalBsmtSF':1499,
    #     'GarageArea':529,'Lat':41.99854744897959,'Long':-93.6589044489796,
    #     'Remodeled':bool(0)}, index=[0])
    #
    instance=pd.DataFrame({'GrLivArea':valuegr,   'LotArea':valuela,  'OverallQual':valuequality, 'YearBuilt':valueyearbuilt,
    'TotalBsmtSF':valuebsmtsf,
    'GarageArea':valuegarage,'Lat':valuelat,'Long':valuelong,
    'Remodeled':bool(valueremodeled)}, index=[0])
    if usedef==['def']:
        instance=pd.DataFrame({'GrLivArea':1632,   'LotArea':250000,  'OverallQual':5, 'YearBuilt':1967,
        'TotalBsmtSF':1632,
        'GarageArea':576,'Lat':41.993081,'Long':-93.607439,
        'Remodeled':bool(0),'Prop_Addr':'3310 OPAL DR 3312'}, index=[0])
        Miles=2
    else:
        instance=pd.DataFrame({'GrLivArea':valuegr,   'LotArea':valuela,  'OverallQual':valuequality, 'YearBuilt':valueyearbuilt,
        'TotalBsmtSF':valuebsmtsf,
        'GarageArea':valuegarage,'Lat':valuelat,'Long':valuelong,
        'Remodeled':bool(valueremodeled),'Prop_Addr':addval}, index=[0])
        Miles=int(milerad)
    KNN_table=get_sales_comps(tdata,instance,Miles,numofneighbors,features=None)
    return KNN_table.to_dict('records')

@app.callback(
    Output('cluster_map', 'figure'),
    Input('sliderforclusters', 'value'))
def return_cluster_map(sliderforclusters):
    df=neighborhood_cluster(sliderforclusters)
    return px.scatter(df, x="Lat", y="Long", color='Label',title="Clustered Neighborhood Map")


from scipy.stats import poisson
from backward_dp import BackwardDP
from typing import List, Tuple, Mapping, Any, Sequence
from matplotlib.ticker import PercentFormatter
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import time

def get_clearance_backward_dp(
    time_steps: int,
    init_inv: int,
    base_demand: float,
    el: List[Tuple[float, float]],  # (price, poisson mean) pairs
) -> BackwardDP:
    # include (0, 0) into the elasticity pairs
    aug_el =  el
    rvs = [poisson(l) for _, l in aug_el]
    num_el = len(aug_el)
    # The reward process is (remaining inventory, current price) ->
    # action: choose a price -> (inventory left, new price) -> (prob, revenue)
    tr_rew_dict = {
        (s, p): {
            p1: {
                (s - d, p1): (
                    rvs[p1].pmf(d) if d < s else 1. - rvs[p1].cdf(s - 1),
                    d * (1 - aug_el[p1][0])
                ) for d in range(s + 1)
            } for p1 in range(p, num_el)
        } for s in range(init_inv + 1) for p in range(num_el)
    }
    return BackwardDP(
        transitions_rewards=[tr_rew_dict] * time_steps,
        terminal_opt_val={(s, p): 0. for s in range(init_inv + 1)
                          for p in range(num_el)},
        gamma=1.
    )

def get_performance(
    time_steps: int,
    init_inv: int,
    base_demand: float,
    el: List[Tuple[float, float]],
    num_traces: int
) -> Mapping[str, Any]:
    vf_and_pol = get_clearance_backward_dp(
        time_steps,
        init_inv,
        base_demand,
        el
    ).vf_and_policy
    opt_vf = vf_and_pol[0][(init_inv, 0)][0]
    aug_el =  el
    rvs = [poisson(base_demand * (1 + l)) for _, l in aug_el]

    all_revs = np.empty(num_traces)
    all_rem = np.empty((num_traces, time_steps))
    all_actions = np.empty((num_traces, time_steps))
    for i in range(num_traces):
        rev = 0.
        state = (init_inv, 0)
        for t in range(time_steps):
            action = vf_and_pol[t][state][1]
            price = 1 - aug_el[action][0]
            demand = rvs[action].rvs()
            rev += (min(state[0], demand) * price)
            state = (max(0, state[0] - demand), action)
            all_rem[i, t] = state[0]
            all_actions[i, t] = aug_el[action][0]
        all_revs[i] = rev

    mean_remaining = np.mean(all_rem, axis=0) /init_inv
    mean_salvage = mean_remaining[-1]
    mean_revenue = np.mean(all_revs) / init_inv
    mean_a_markdown = 1. - mean_salvage - mean_revenue
    mean_actions = np.mean(all_actions, axis=0) / init_inv
    stdev_remaining = np.std(all_rem, axis=0) / init_inv
    stdev_salvage = stdev_remaining[-1]
    stdev_revenue = np.std(all_revs) / init_inv
    stdev_a_markdown = np.sqrt(stdev_salvage ** 2 + stdev_revenue ** 2)
    stdev_actions = np.std(all_actions, axis=0) / init_inv

    return {
        "Optimal VF": opt_vf,
        "Mean Revenue": mean_revenue,
        "Mean AMarkdown": mean_a_markdown,
        "Mean Salvage": mean_salvage,
        "Stdev Revenue": stdev_revenue,
        "Stdev AMarkdown": stdev_a_markdown,
        "Stdev Salvage": stdev_salvage,
        "Mean Remaining": mean_remaining,
        "Mean Price Reductions": mean_actions,
        "Stdev Remaining": stdev_remaining,
        "Stdev Price Reductions": stdev_actions,
    }

def graph_perf(
    time_steps: int,
    demand: float,
    inv: Sequence[int],
    elasticity: Tuple[float, float, float]
) -> None:
    revs = []
    ams = []
    sals = []
    for initial_inv in inv:
        perf = get_performance(
            time_steps,
            initial_inv,
            demand,
            list(zip((0.3, 0.5, 0.7), elasticity)),
            10000
        )
        revs.append(perf["Mean Revenue"] * 100)
        ams.append(perf["Mean AMarkdown"] * 100)
        sals.append(perf["Mean Salvage"] * 100)
    plt.grid()
    plt.plot(inv, revs, "k", label="Revenue")
    plt.plot(inv, ams, "b", label="A-Markdown")
    plt.plot(inv, sals, "r", label="Salvage")
    plt.gca().yaxis.set_major_formatter(PercentFormatter())
    plt.xlabel("Initial Inventory", fontsize=10)
    plt.ylabel("Percentage of Initial Value", fontsize=10)
    tup = (
        time_steps,
        demand,
        elasticity[0] * 100,
        elasticity[1] * 100,
        elasticity[2] * 100
    )
    plt.title(
        "Weeks=%d,WeeklyDemand=%.1f,Elasticity=[%d,%d,%d]" % tup,
        fontsize=10
    )
    plt.legend(loc="upper right")
    file_name = str(Path.home()) + ("/wks=%d&dem=%d&el=%d-%d-%d.png" % tup)
    print("Created png file: " + file_name)
    plt.savefig(file_name)
    plt.close()

def graph_trace(
    time_steps: int,
    init_inv: int,
    base_demand: float,
    el: List[Tuple[float, float]]
) -> None:
    #print('Computing value function and optimal policy function.')
    vf_and_pol = get_clearance_backward_dp(
        time_steps,
        init_inv,
        base_demand,
        el
    ).vf_and_policy
    #print('Got value function and optimal policy function')
    aug_el =  el
    rvs = [poisson(l) for _, l in aug_el]
    rev = 0.
    state = (init_inv, 0)
    all_rem = []
    all_prices = []
    for t in range(time_steps):
        action = vf_and_pol[t][state][1]
        price = 1 - aug_el[action][0]
        demand = rvs[action].rvs()
        rev += (min(state[0], demand) * price)
        state = (max(0, state[0] - demand), action)
        all_rem.append(state[0])
        all_prices.append(price)
    # plot inventory
    fig = plt.figure(figsize=(12,5))
    plt.subplot(1, 2, 1)
    plt.grid()
    plt.plot(all_rem, 'k')
    plt.xlabel("Week", fontsize=10)
    plt.ylabel("Remaining Inventory", fontsize=10)
    tup = (
        time_steps,
        base_demand,
        init_inv
    )
    # plot price
    plt.subplot(1, 2, 2)
    plt.grid()
    plt.plot(all_prices, 'k')
    plt.xlabel("Week", fontsize=10)
    plt.ylabel("Price", fontsize=10)
    fig.suptitle(
        "Simulation of one trace: time_steps %d, base_demand %.1f,init_inv %d"\
        % tup, fontsize=12
    )
    file_name = str(Path.home()) + ("/time_steps%dbase_demand%dinit_inv%d.png"\
                                     % tup)
    print("Created png file: " + file_name)
    plt.savefig(file_name)
    plt.show()
    plt.close()

    return

def heat_graph(
    time_steps: int,
    init_inv: int,
    base_demand: float,
    el: List[Tuple[float, float]]
) -> None:
    ts_list = np.linspace(1, time_steps, time_steps)
    ii_list = np.linspace(1, init_inv, init_inv)
    heat = np.zeros((int(init_inv), int(time_steps)))
    for j,ts in enumerate(reversed(ts_list)):
        for i,ii in enumerate(reversed(ii_list)):
            vf_and_pol = get_clearance_backward_dp(int(ts), int(ii), base_demand, el).vf_and_policy
            #opt_vf = vf_and_pol[0][(ii, 0)][0]
            opt_pol = vf_and_pol[0][(int(ii), 0)][1]
            opt_price = 1- el[opt_pol][0]
            heat[i,j] = opt_price
            print(i, j, ts, ii)
    # plot 3D heat graph
    ax = sns.heatmap(heat)
    plt.savefig('heatmap.png')
    plt.show()
    plt.close()

    return heat

if __name__ == '__main__':
    #ts: int = 10  # time steps
    #ii: int = 12  # initial inventory
    #bd: float = 1.0  # base demand
    #this_el: List[Tuple[float, float]] = [
    #    (0.3, 0.5), (0.5, 1.1), (0.7, 1.4)
    #]
    #this_el: List[Tuple[float, float]] = [
    #    (0.3, 1), (0.5, 2), (0.7, 3)
    #]
    # bdp = get_clearance_backward_dp(ts, ii, bd, this_el)
    #
    # for i in range(ts):
    #     print([(x, y) for x, (y, _) in bdp.vf_and_policy[i].items()])
    # for i in range(ts):
    #     print([(x, z) for x, (_, z) in bdp.vf_and_policy[i].items()])

    #traces = 10000
    #graph_trace(ts, ii, bd, this_el)
    #per = get_performance(ts, ii, bd, this_el, traces)
    #pprint(per)

    # ts: int = 8  # time steps
    # bd: float = 1.0  # base demand
    # invs: Sequence[int] = list(range(2, 30, 2))
    #
    # elasticities = [
    #     (0.1, 0.3, 0.5),
    #     (0.3, 0.7, 1.0),
    #     (0.5, 0.8, 1.1),
    #     (0.7, 1.2, 1.5),
    #     (0.8, 1.3, 1.7),
    #     (1.0, 1.5, 2.0),
    #     (1.0, 2.0, 2.5),
    #     (1.5, 2.5, 3.5),
    #     (2.0, 4.0, 6.0)
    # ]
    # for els in elasticities:
    #     graph_perf(ts, bd, invs, els)

    # Simulation with larger state and action space
    ts: int = 20
    ii: int = 18
    n_prices: int = 50
    el_func = lambda x, alpha, beta: alpha * np.e ** (-beta*x)
    price_list = np.linspace(0, 1, n_prices+1)
    alpha, beta = 1.0, 5
    this_el = [(1 - p, el_func(p, alpha, beta)) for i,p in enumerate(reversed(price_list))]
    start = time.time()
    vf_and_pol = get_clearance_backward_dp(
        ts,
        ii,
        1,
        this_el
    ).vf_and_policy
    end = time.time()
    print("Testing general backward DP in python: ")
    print("Number of time steps: ", ts)
    print("Initial inventory: ", ii)
    print("Number of actions: ", n_prices)
    print("Optimal value function at initial state: ", vf_and_pol[0][(ii, 0)][0])
    print("Optimal policy at initial state: ", 1-this_el[vf_and_pol[0][(ii, 0)][1]][0])
    print("Time: ", end - start)


#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Polynomial transformation: implementation and test
--------------------------------------------------

Tools of making polynomial transformation function which transforms raw data into polynomial basis.

Test perceptron with a linear classifier in the transformed space and see the non-linear separator in
the prior (untransformed) space (2D to have a visualization).

Plotting functions and some simple datasets are included.
"""

import itertools
import functools
import operator
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import colors

__author__ = "Saeed Entezari"
__email__ = "s.entezari144@gmail.com"

################################################################
# Plotting
################################################################

def tidy_plot(xmin, xmax, ymin, ymax, ax = None, center = False, equal_aspect = True, grid = True,
                title = None, xlabel = None, ylabel = None):
    '''
    Set up axes for 2D plotting.

    Parameters:
        xmin, xmax, ymin, ymax (float): plot extents
        ax (matplotlib axes, optional = None): if axes is given, make it tidy.
            Otherwise, make a tidy axes.
        center (bool, optional = False): if True, just zero axes would be ticked and shown.
            if False, axes would be shown on the borders,
            but zero axes would be shown without ticks.
        title, xlabel, ylabel (string, optional = None)
        grid (bool, optional = True): draw a grid for plot
        equal_aspect (bool, optional = True): equal unit length for axes

    Returns: ax (matplotlib.axes.Axes instance)
    '''
    # plt.ion()
    if ax is None: fig, ax = plt.subplots()

    epsx = 0.1 * (xmax-xmin)
    epsy = 0.1 * (ymax-ymin)
    ax.set_xlim(xmin-epsx, xmax+epsx)
    ax.set_ylim(ymin-epsy, ymax+epsy)

    if center:
        ax.spines[['left', 'bottom']].set_position('zero')
        ax.spines[['right', 'top']].set_visible(False)
    else:
        ax.spines[['right', 'top']].set_visible(False)
        ax.plot([0, 0], [ymin-epsy, ymax+epsy], c='k', lw=0.5)
        ax.plot([xmin-epsx, xmax+epsx], [0, 0], c='k', lw=0.5)

    if title: ax.set_title(title)
    if xlabel: ax.set_xlabel(xlabel)
    if ylabel: ax.set_ylabel(ylabel)
    if equal_aspect: ax.set_aspect('equal')
    if grid: ax.grid(visible=True, ls=':')

    return ax

def plot_data(datapoints, labels, ax = None, xmin = None, xmax = None, ymin = None, ymax = None,
                clear = False, **kwargs):
    '''
    Scatter plot data in 2D with labels as colors.

    Parameters:
        datapoints (2xn array): n 2d datapoints
        labels (1xn array): entries from set {-1, +1}
        ax (matplotlib axes, optional = None): if given, draw on given axes.
            if not given, draw a new axes based on datapoints.
        xmin, xmax, ymin, ymax (float, optional = None): set limits of the plot.
            if not given, would be set based on datapoints.
        clear (bool, optional = False): in case of axes is given,
            it could be True to clear the axes before plotting.

    Returns:
        ax (matplotlib axes)
    '''
    # get marker value from kwargs
    marker = kwargs.get('marker', '.')
    # if axis is not given, make a new axis
    if ax is None:
        # if limits not given, take limits from datapoints
        if xmin == None: xmin = np.min(datapoints[0,:])
        if xmax == None: xmax = np.max(datapoints[0,:])
        if ymin == None: ymin = np.min(datapoints[1,:])
        if ymax == None: ymax = np.max(datapoints[1,:])
        # in case of max min equality, for example if we have just a datapoint
        # then extent the limits
        if xmax == xmin:
            xmin = xmin - 1
            xmax = xmax + 1
        if ymax == ymin:
            ymin = ymin - 1
            ymax = ymax + 1
        # create axes by the help of tidy_plot()
        ax = tidy_plot(xmin, xmax, ymin, ymax)
        # save limits into xlim and ylim variables for compatibility
        xlim, ylim = ax.get_xlim(), ax.get_ylim()

    # if axis is given but if clear is True
    elif clear:
        # keep track of limits then clear plot
        xlim, ylim = ax.get_xlim(), ax.get_ylim()
        # clear the ax then make it tidy with limits we have
        ax.clear()
        ax = tidy_plot(xlim[0], xlim[1], ylim[0], ylim[1], ax=ax)
    # otherwise, just take limits of plot before plotting
    else:
        xlim, ylim = ax.get_xlim(), ax.get_ylim()

    # transform labels to colors for plotting
    colors = np.choose(labels > 0, ['r', 'g'])[0]
    # scatter plot
    ax.scatter(datapoints[0,:], datapoints[1,:], c=colors, marker=marker)
    # set limits again to try to keep them from moving around
    ax.set_xlim(xlim)
    ax.set_ylim(ylim)

    # return axes
    return ax

def plot_nonlinear_separator(predictor, ax = None, npix = 30,
                                xmin = None, xmax = None, ymin = None, ymax = None):
    '''
    Plot non-linear separator in 2D, by a predictor given.
        Prediction value would be calculated for every pixel of the axes
        and it would be painted accordingly.
        Notice that you must specify an axisting axes or/and limits.

    Params:
        predictor (func): a function which gets two coordinates x and y
            as two float arguments respectively, and returns the value of prediction
            as int between -1 and +1.
        ax (matplotlib axes, optional = None)
        npix (int, optional = 30): number of pixels in each dimension.
        xmin, xmax, ymin, ymax (int, optional = None)

    Returns:
        None
        There is no need to return anything,
        because image would be displayed on the axes
    '''
    # plot limits
    if ax == None:
        ax = tidy_plot(xmin, xmax, ymin, ymax)
    else:
        if xmin == None:
            xmin, xmax = ax.get_xlim()
            ymin, ymax = ax.get_ylim()
        else:
            ax.set_xlim((xmin, xmax))
            ax.set_ylim((ymin, ymax))

    # create Colormap and Normalize instances
    cmap = colors.ListedColormap(['silver', 'white'])
    norm = colors.BoundaryNorm([-2, 0, 2], cmap.N)

    # create the image data array by giving every points to the predictor
    ima = np.array([[predictor(x, y) \
                        for x in np.linspace(xmin, xmax, npix)] \
                            for y in np.linspace(ymin, ymax, npix)])

    # create the image on the axes by imshow()
    im = ax.imshow(np.flipud(ima), cmap=cmap, norm=norm,
                    extent=[xmin, xmax, ymin, ymax], interpolation='none')
    # there is no need to return, image is displayed on the axes

################################################################
# Utilities
################################################################

def rv(value_list):
    '''
    Returns row vector (1xd array) from a list of values.
    '''
    return np.array([value_list])

def cv(value_list):
    '''
    Returns column vector (dx1 array) from a list of values.
    '''
    return np.transpose(rv(value_list))

def prediction(x, th, th_0):
    '''
    Prediction of label of datapoints x (dxn array)
        with respect to separator specified by parameters
        theta (dx1 array) and theta_0 (1x1 array).

    Returns (1xn array of) sign (int in {-1, 0, 1})
    '''
    return np.sign(np.dot(th.T, x) + th_0)

def score(datapoints, labels, th, th_0):
    '''
    Calculate the ratio of correct predictions on dataset
        based on separator th, th_0.

    Parameters:
        datapoints (dxn array)
        labels (1xn array)
        th (dx1 array), th_0 (1x1 array or float)

    Returns:
        ratio (float) between 0 and 1
    '''
    d, n = datapoints.shape
    ncorrect = np.sum(prediction(datapoints, th, th_0) == labels)
    return ncorrect / n

################################################################
# Data Sets
################################################################

def super_simple_separable_through_origin():
    '''
    Returns 2x4 (dxn) data array and their labels (1x4).
    '''
    data = np.array([[2, 3, 9, 12],
                     [5, 1, 6, 5]])
    labels = np.array([[1, -1, 1, -1]])
    return data, labels

def super_simple_separable():
    '''
    Returns 2x4 (dxn) data array and their labels (1x4).
        datapoints are seprable but not through origin.
    '''
    data = np.array([[2, 3, 9, 12],
                  [5, 2, 6, 5]])
    labels = np.array([[1, -1, 1, -1]])
    return data, labels

def xor():
    '''
    Returns 2x4 (dxn) data array and their labels (1x4).
        each label is the xor (result in {-1, +1}) of its datapoint's coordinates.
    Datapoints are not seprable.
    '''
    data = np.array([[1, 2, 1, 2],
                     [1, 2, 2, 1]])
    labels = np.array([[1, 1, -1, -1]])
    return data, labels

def xor_centered():
    '''
    Returns 2x4 (dxn) data array and their labels (1x4).
        each label is the xor (result in {-1, +1}) of its datapoint's coordinates.
        Datapoints are centered around origin.
    Datapoints are not seprable.
    '''
    data = np.array([[-1, 1, -1, 1],
                     [-1, -1, 1, 1]])
    labels = np.array([[-1, 1, 1, -1]])
    return data, labels

def xor_more():
    '''
    Returns 2x8 (dxn) data array and their labels (1x8).
        4 labels are the xor (result in {-1, +1}) of their datapoints's coordinates.
        and other 4 extra datapoint with their labels.
    Datapoints are not seprable.
    '''
    data = np.array([[1, 2, 1, 2, 2, 4, 1, 3],
                  [1, 2, 2, 1, 3, 1, 3, 3]])
    labels = np.array([[1, 1, -1, -1, 1, 1, -1, -1]])
    return data, labels

################################################################
# Polynomial Transformation
################################################################

# we want to create a polynomial transofrmation function
# which gets raw feature vectors as input
# and returns transformed feature vectores as output
# to do this we have 2 ways:

# (1)
# we can import polynomial_transformation module written by me and use make_poly_trans_func
# not recommended for big data

# (2)
# use Functional Programming Modules in python, like:
### itertools.combinations_with_replacement(iterable, k)
#       Gives a generator which
#       Returns k-length (combination) tuples of elements from the input iterable (set to be choosen from),
#       allowing individual elements to be repeated more than once.
#           >>> for combTuple in itertools.combinations_with_replacement('ABC', 2):
#           ...     print(combTuple)
                #  ('A', 'A')
                #  ('A', 'B')
                #  ('A', 'C')
                #  ('B', 'B')
                #  ('B', 'C')
                #  ('C', 'C')
#           >>> for combTuple in itertools.combinations_with_replacement(range(2), 3):
#           ...     print(combTuple)
                # indicate the indices of polynomial transformation of order 3, on raw feature vector [x0, x1]
                #  (0, 0, 0) -> x0 * x0 * x0 = x0^3 x1^0 ~ equivalent with powersTuple (3, 0);
                #                find out the meaning of powersTuple in polynomial_transofrmation.py
                #  (0, 0, 1) -> x0 * x0 * x1 = x0^2 x1^1 ~ (2, 1)
                #  (0, 1, 1) -> x0 * x1 * x1 = x0^1 x1^2 ~ (1, 2)
                #  (1, 1, 1) -> x1 * x1 * x1 = x0^0 x1^3 ~ (0, 3)

### functools.reduce(function, iterable[, initializer])
#       Apply function of two arguments cumulatively to the items of iterable,
#       from left to right, so as to reduce the iterable to a single value.
#       If optional initializer is present, it is placed before the items of the iterable
#       and serve as a default when the iterable is empty.
#       If initializer is not given and iterable contains only one item, return the first item,
#                                                contains no item, interrupt and raise a TypeError.
#           >>> functools.reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])
                # ((((1+2)+3)+4)+5) = 15
#           >>> functools.reduce(lambda x, y: x+y, [1, 2, 3, 4, 5], 100)
                # (((((100+1)+2)+3)+4)+5) = 115
#           >>> functools.reduce(lambda x, y: x+y, [], 100)
                # 100
#           >>> functools.reduce(lambda x, y: x+y, [])
                # TypeError: reduce() of empty sequence with no initial value
#           >>> functools.reduce(lambda x, y: x+y, [1])
                # 1
#           >>> functools.reduce(lambda x, y: x*y, range(1, 4+1))
                # 24 = 4!

### operator.mul(a, b)
#       Returns a * b, for a and b numbers.
#       This is the same as multiplication.

################################################################
# Way (2): Neat code by functional programming modules of python

def mul(seq):
    '''
    Multiplies every elements of a sequence.
        If sequence is empty, returns 1.

    Params:
        seq (list of numbers)

    Returns:
        multiplied value (number)
    '''
    return functools.reduce(operator.mul, seq, 1)

def make_poly_trans_func(K):
    '''
    Wrap polynomial transformation function for max order K given.
    '''
    def poly_transformer(raw_features):
        '''
        Transforms raw feature vectors consisting of n d-dimensional raw feature vector
            in polynomial basis to order K.

        Params:
            raw_features (dxn array)

        Returns:
            transformed feature vectors (Sxn array)
                S is the sum of the number of polynomial features to max order K.
        '''
        d, n = raw_features.shape
        # loop over raw datapoints
        result = []
        for j in range(n):
            # loop over order k, from 0 to K
            transfeat = []
            for k in range(K+1):
                # get the combinations_with_replacement of indices,
                # for indices in range(d) and combinations of order k
                indexTuples = itertools.combinations_with_replacement(range(d), k)
                # loop over combinations of order k
                for it in indexTuples:
                    # make polynomial feature for each combination
                    # by multiplying the value of corresponding indices in raw datapoint
                    # then append it to the polynomial feature vector
                    transfeat.append(mul([raw_features[i, j] for i in it]))
            # append the transformed feature vectore for this raw datapoint to the result list (of arrays)
            result.append(cv(transfeat))
        # make an array out of the result and return the transformed input
        return np.hstack(result)

    return poly_transformer

# # test case
# K = 2
# raw_features = np.array([[2, 4], [3, 5]])
# poly_trans_func = make_poly_trans_func(K)
# print('Raw features as input:\n', raw_features)
# poly_features = poly_trans_func(raw_features)
# print(f'Transformed features in polynomial basis to order K = {K}:\n', poly_features)

################################################################
# Learner: Perceptron
################################################################

def perceptron(datapoints, labels, params = {}, hook = None, count_mistakes = False):
    '''
    Perform perceptron algorithm for datapoints with labels,
        by going through dataset T times.

    Parameters:
        datapoints (dxn array)
        labels (1xn array): values in {-1, 0, +1}
        params (dict, optional = {}): extra parameters to algorithm,
            such as T, the number of iterations through dataset.
        hook (func, optional = None): a function for plotting separator, if given.
        count_mistakes (bool, optional = False): if True, prints the number of mistake.

    Returns:
        final th (dx1 array)
        final th_0 (1x1 array)
    '''
    # if T not in params, set it to 100
    T = params.get('T', 100)
    # initialization
    d, n = datapoints.shape
    th = np.zeros((d, 1))
    th_0 = np.zeros((1, 1))
    # iteration
    nmistakes = 0
    for iter in range(T):
        for i in range(n):
            y_i = labels[0, i]
            x_i = datapoints[:, i:i+1]
            if y_i * prediction(x_i, th, th_0) <= 0:   # mistake and update
                # mistake counter
                nmistakes += 1
                # updata parameters
                th = th + y_i * x_i
                th_0 = th_0 + y_i
                # hook
                if hook:
                    hook((th, th_0))

    if count_mistakes: print('Number of misatkes happened:', nmistakes)
    return th, th_0

################################################################
# Tests
################################################################

def test_linear_classifier_in_trans_space(datagen, transfunc, learner, learner_params = {},
                                            draw = True, pause = True, count_mistakes = False):
    '''
    Test linear classifier in transformed space in action.
        In fact, this function first transforms data generated by data generator
        to get the transformed data in transformed space,
        and then perform (call the) learner which has linear hypothesis in the transformed feature space.
        If you want to draw, the non-linear separator in raw space
        (which is the reverse transformation of linear separator in transformed space)
        would be drawn in each step of the learning.

        Raw feature space should be 2-dimensional.

    Params:
        datagen (func): data generator function with all default arguments will be called
        transfunc (func): transformation function, which gives raw data and returns transformed data
        learner (func): perceptron or averaged perceptron
        draw (bool, optional = True): if True, the hook function would be made for
            2D plotting each step (mistakes) of learning algorithm
        pause (bool, optional = True): if True, for going to next step of learning algorithm,
            user should press the enter key
        count_mistakes (bool, optional = False): print the number of mistakes happened at the end

    Returns:
        None
        Print learned parameters and score on them.
        This function just made the hook function for plotting (if draw)
        and prepare learner arguments then call it.
    '''
    # get the raw dataset from data generator
    raw_data, labels = datagen()
    # get the transformed data by giving raw data to the transformer
    trans_data = transfunc(raw_data)

    # if draw, define the axes and hook function for plotting
    if draw:
        ax = plot_data(raw_data, labels)
        def hook(params):
            th, th_0 = params
            print('th =', th.T, 'th_0 =', th_0)
            plot_nonlinear_separator(
                lambda x, y: int(prediction(transfunc(cv([x, y])), th, th_0)), # predictor
                ax=ax
            )
            plt.pause(0.05)
            if pause: input('Continue?')
    else:
        hook = None

    # pass the prepared arguments to learner
    learned_th, learned_th_0 = learner(trans_data, labels, hook=hook,
                                        params=learner_params, count_mistakes=count_mistakes)
    # final result
    print('Learner finished!')
    if hook: hook((learned_th, learned_th_0)); input('Final plot is shown, Continue?')
    print('Final result:\nth =', learned_th, 'th_0 =', learned_th_0)
    print('Score =', score(trans_data, labels, learned_th, learned_th_0))

# # test case
# K = 3
# poly_trans = make_poly_trans_func(K)
# test_linear_classifier_in_trans_space(
#     xor, poly_trans, perceptron, learner_params={'T':100},
#     draw=True, pause=False, count_mistakes=True
# )


import requests
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import pandas as pd
from datetime import datetime, timedelta
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import math
import numpy as np

def calculate_infection_probability_and_ventilation_rates(
    datetimes,
    co2_data,
    number_of_people,
    co2_generation_rate,
    room_volume,
    outdoor_co2_concentration_ppm,
    pulmonary_ventilation_rate,
    time,
    number_of_infectors,
    infection_quanta=3.33,
    co2_conversion_factor=1.98,
):
    # Calculate constants
    emission_rate = number_of_people * co2_generation_rate
    outdoor_co2_concentration = outdoor_co2_concentration_ppm * co2_conversion_factor

    # Function to calculate ventilation rate (Q)
    def calculate_ventilation_rate(df, room_volume, emission_rate, outdoor_co2_concentration):
        delta_time = (df.index[-1] - df.index[0]).seconds / 3600
        delta_co2 = df.iloc[-1] - df.iloc[0]
        dC_dt = delta_co2 / delta_time
        Q = (room_volume * dC_dt + emission_rate) / (outdoor_co2_concentration - df.mean())
        return Q

    # Create a DataFrame from the input data
    co2_levels_df = pd.DataFrame({'timestamp': datetimes, 'co2': co2_data})
    co2_levels_df.set_index('timestamp', inplace=True)

    # Convert CO2 levels to concentrations (mg·m−3)
    co2_levels_df['co2'] = co2_levels_df['co2'] * co2_conversion_factor

    # Calculate ventilation rate (Q) for a rolling hour
    # rolling_hour_df = co2_levels_df.rolling('1H', min_periods=2)
    # ventilation_rates = rolling_hour_df.apply(calculate_ventilation_rate, args=(room_volume, emission_rate, outdoor_co2_concentration))

    # Calculate ventilation rate (Q) for a rolling 20 minutes
    rolling_20_minutes_df = co2_levels_df.rolling('20T', min_periods=2)
    ventilation_rates = rolling_20_minutes_df.apply(calculate_ventilation_rate, args=(room_volume, emission_rate, outdoor_co2_concentration))

    # Calculate the probability of infection for the latest ventilation rate (Q)
    latest_ventilation_rate = ventilation_rates.iloc[-1]
    probability_of_infection = 1 - math.exp(-number_of_infectors * infection_quanta * pulmonary_ventilation_rate * time / latest_ventilation_rate)

    return probability_of_infection

def fetch_data(start_datetime, end_datetime):
    # Get the data from the API
    response = requests.get("http://192.168.0.107:5000/data")
    data = response.json()

    # Process the measurements data
    measurements_data = data["measurements"]
    measurements_data = [d for d in measurements_data if start_datetime <= datetime.strptime(d['datetime'], "%Y-%m-%d %H:%M:%S") <= end_datetime]

    datetimes = [datetime.strptime(d['datetime'], "%Y-%m-%d %H:%M:%S") for d in measurements_data]
    co2 = [d['co2'] for d in measurements_data]
    temperature = [d['temperature'] for d in measurements_data]
    humidity = [d['humidity'] for d in measurements_data]

    # Process the lft data
    lft_data = data["lft"]
    lft_outcomes = [d["outcome"] for d in lft_data if start_datetime <= datetime.strptime(d['datetime'], "%Y-%m-%d %H:%M:%S") <= end_datetime]

    return datetimes, co2, temperature, humidity, lft_outcomes

def update_data_with_range():
    start_datetime = datetime.now() - timedelta(hours=start_datetime_scale.get())
    end_datetime = datetime.now() - timedelta(hours=end_datetime_scale.get())

    datetimes, co2, temperature, humidity, lft_outcomes = fetch_data(start_datetime, end_datetime)

    # Calculate the number of infectors in the past rolling hour
    number_of_infectors = np.sum(np.array(lft_outcomes) == "Positive")

    # Get the number_of_people and room_volume from the Scale widgets
    number_of_people = number_of_people_scale.get()
    room_volume = room_volume_scale.get()

    # Calculate infection probability and ventilation rates
    probability_of_infection = calculate_infection_probability_and_ventilation_rates(
        datetimes=datetimes,
        co2_data=co2,
        number_of_people=number_of_people,
        co2_generation_rate=28.6,
        room_volume=room_volume,
        outdoor_co2_concentration_ppm=450,
        pulmonary_ventilation_rate=6,
        time=0.33,
        number_of_infectors=number_of_infectors,
        infection_quanta=3.33,
    )

    # Update plots
    for ax in axs:
        ax.clear()

    axs[0].plot(datetimes, co2, label='CO2', color='r')
    axs[1].plot(datetimes, temperature, label='Temperature', color='g')
    axs[2].plot(datetimes, humidity, label='Relative Humidity', color='b')

    # Update side panel values
    current_co2.config(text=f"Current CO2: {round(co2[-1], 1)} ppm")
    current_temp.config(text=f"Current Temperature: {round(temperature[-1], 1)} °C")
    current_humidity.config(text=f"Current Humidity: {round(humidity[-1], 1)} %")
    current_probability_of_infection.config(text=f"Probability of Infection: {round(probability_of_infection * 100, 2)} %")

    # Redraw the canvas
    canvas.draw()

    # Schedule the next update
    root.after(2000, update_data_with_range)

# Set up the plot
fig, axs = plt.subplots(3, 1, figsize=(10, 15), sharex=True)
plt.subplots_adjust(hspace=0.3)

# Configure the date formatter
date_fmt = mdates.DateFormatter('%Y-%m-%d %H:%M:%S')

# Set axis labels and grid
for ax in axs:
    ax.grid()
    ax.xaxis.set_major_formatter(date_fmt)
    plt.setp(ax.get_xticklabels(), rotation=30, ha='right')

axs[0].set_title('CO2 vs DateTime')
axs[0].set_ylabel('CO2 (ppm)')
axs[1].set_title('Temperature vs DateTime')
axs[1].set_ylabel('Temperature (°C)')
axs[2].set_title('Relative Humidity vs DateTime')
axs[2].set_ylabel('Relative Humidity (%)')

# Create tkinter window
root = tk.Tk()
root.title("Sensor Data")

# Create the matplotlib canvas
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.get_tk_widget().pack(side=tk.LEFT, fill=tk.BOTH, expand=1)

# Create side panel
side_panel = tk.Frame(root)
side_panel.pack(side=tk.RIGHT, fill=tk.BOTH)

start_datetime_label = tk.Label(side_panel, text="Start time (hours ago):", font=("Arial", 14))
start_datetime_label.pack(pady=10)
start_datetime_scale = tk.Scale(side_panel, from_=0, to=48, orient=tk.HORIZONTAL)
start_datetime_scale.set(12)  # Set default value to 12 hours ago
start_datetime_scale.pack(pady=10)

end_datetime_label = tk.Label(side_panel, text="End time (hours ago):", font=("Arial", 14))
end_datetime_label.pack(pady=10)
end_datetime_scale = tk.Scale(side_panel, from_=0, to=48, orient=tk.HORIZONTAL)
end_datetime_scale.set(0)  # Set default value to now (0 hours ago)
end_datetime_scale.pack(pady=10)

# Add two Scale widgets to the side panel for number_of_people and room_volume
number_of_people_label = tk.Label(side_panel, text="Number of people:", font=("Arial", 14))
number_of_people_label.pack(pady=10)
number_of_people_scale = tk.Scale(side_panel, from_=1, to=50, orient=tk.HORIZONTAL)
number_of_people_scale.set(1)  # Set default value to 1
number_of_people_scale.pack(pady=10)

room_volume_label = tk.Label(side_panel, text="Room volume (m³):", font=("Arial", 14))
room_volume_label.pack(pady=10)
room_volume_scale = tk.Scale(side_panel, from_=1, to=500, orient=tk.HORIZONTAL)
room_volume_scale.set(36)  # Set default value to 36
room_volume_scale.pack(pady=10)

# Display current values
current_co2 = tk.Label(side_panel, font=("Arial", 14))
current_co2.pack(pady=10)
current_temp = tk.Label(side_panel, font=("Arial", 14))
current_temp.pack(pady=10)
current_humidity = tk.Label(side_panel, font=("Arial", 14))
current_humidity.pack(pady=10)
current_probability_of_infection = tk.Label(side_panel, font=("Arial", 14))
current_probability_of_infection.pack(pady=10)

# Add a quit button to the window
quit_button = tk.Button(master=root, text="Quit", command=root.quit)
quit_button.pack(side=tk.BOTTOM)

# Fetch initial data and start the update loop
update_data_with_range()

# Run the tkinter main loop
tk.mainloop()


import wx
import wx.lib.agw.hyperlink as li
from wx import *
import os
from datetime import datetime
app = App()
root = Frame(None, title="Eat.it",size=(1100,650))
root.Centre()
root.SetBackgroundColour("#80CBC4")
panel = Panel(root)
mainbox = BoxSizer(VERTICAL)

top = BoxSizer(HORIZONTAL)
bottom = BoxSizer(HORIZONTAL)

logo = StaticBitmap(panel,-1,Bitmap("image/Food Stuff.png"))
font = Font(28,FONTFAMILY_DECORATIVE,FONTSTYLE_NORMAL,FONTWEIGHT_BOLD)
d_font = Font(11,FONTFAMILY_DECORATIVE,FONTSTYLE_NORMAL,FONTWEIGHT_BOLD)
fonts = Font(16,FONTFAMILY_DECORATIVE,FONTSTYLE_ITALIC,FONTWEIGHT_BOLD)
small_font = Font(11,FONTFAMILY_DECORATIVE,FONTSTYLE_ITALIC,FONTWEIGHT_BOLD)

top.Add(logo,4,EXPAND|ALL,border=20)

left = BoxSizer(VERTICAL)

recipes = GridSizer(3,3,10,10)

samosa = Button(panel,1,label="Samosa")
idli = Button(panel,1,label="idli")
sandwitch = Button(panel,1,label="sandwitch")
dosa = Button(panel,1,label="dosa")
momos = Button(panel,1,label="momos")
coke = Button(panel,1,label="coke")
rolls = Button(panel,1,label="rolls")
dhokla = Button(panel,1,label="dhokla")
icecream = Button(panel,1,label="icecream")
samosa.SetFont(fonts)
idli.SetFont(fonts)
sandwitch.SetFont(fonts)
dosa.SetFont(fonts)
momos.SetFont(fonts)
coke.SetFont(fonts)
rolls.SetFont(fonts)
dhokla.SetFont(fonts)
icecream.SetFont(fonts)
recipes.AddMany([
    (samosa,1,EXPAND|ALL),
    (idli,1,EXPAND|ALL),
    (sandwitch,1,EXPAND|ALL),
    (dosa,1,EXPAND|ALL),
    (momos,1,EXPAND|ALL),
    (coke,1,EXPAND|ALL),
    (rolls,1,EXPAND|ALL),
    (dhokla,1,EXPAND|ALL),
    (icecream,1,EXPAND|ALL)
])
left.Add(recipes,3,ALL|EXPAND)

display = TextCtrl(panel,style=TE_MULTILINE|TE_READONLY)
display.SetFont(small_font)

left.Add(display,1,EXPAND|TOP,border=10)

def close_window(event):
    root.Close()

def clear_display(event):
    display.SetValue("")
    global text
    text = ""
    global total
    total = 0
    samosa.Enable()
    sandwitch.Enable()
    order.Disable()
    dosa.Enable()
    idli.Enable()
    coke.Enable()
    totals.Enable()
    dhokla.Enable()
    icecream.Enable()
    momos.Enable()
    rolls.Enable()

def save(event):
    os.chdir("orders")
    now = datetime.now()
    files = open(str(datetime.now().strftime("%d-%m-%Y %H-%I-%S"))+".txt",'w')
    files.write(display.GetValue())
    files.close()
    viewframe = Frame(root,title="Order Details",size=(400,600))
    childpanel = Panel(viewframe)
    box = BoxSizer()
    viewbox = TextCtrl(childpanel,style=TE_MULTILINE|TE_READONLY)
    viewbox.SetValue(display.GetValue())
    box.Add(viewbox,1,ALL|EXPAND)
    childpanel.SetSizer(box)
    viewframe.Show()
    order.Disable()
    samosa.Enable()
    sandwitch.Enable()
    dosa.Enable()
    idli.Enable()
    coke.Enable()
    totals.Enable()
    dhokla.Enable()
    icecream.Enable()
    momos.Enable()
    rolls.Enable()

total = 0
text = ""
def setsamosa(event):
    global total
    global text
    text += "Samosa \t7/-\n"
    total += 7
    display.SetValue(text)

def setmomos(event):
    global total
    global text
    text += "Momos \t35/-\n"
    total += 35
    display.SetValue(text)

def seticecream(event):
    global total
    total += 150
    global text
    text += "Icecream \t 150/-\n"
    display.SetValue(text)

def setidli(event):
    global total
    total += 45
    global text
    text += "Idli \t 45/-\n"
    display.SetValue(text)

def setdosa(event):
    global total
    global text
    total += 50
    text += "Dosa \t50/-\n"
    display.SetValue(text)

def setroll(event):
    global total
    global text
    text += "Rolls \t40/-\n"
    total += 40
    display.SetValue(text)

def setdhokla(event):
    global total
    global text
    text += "Dhokla \t36/-\n"
    total += 36
    display.SetValue(text)

def setcoke(event):
    global total
    global text
    text += "Coke \t45/- \n"
    total += 45
    display.SetValue(text)

def setsandwitch(event):
    global total
    global text
    text += "Sandwitch \t15/-\n"
    total += 15
    display.SetValue(text)

def totals_value(event):
    global total
    gst = round(total*0.18)
    display.AppendText("----------------\nTotal \t"+str(total)+"/-")
    display.AppendText("\n----------------\nGST(18%)\t"+str(gst)+"/-")
    display.AppendText("\n----------------\nSub Total\t"+str(gst+total)+"/-")
    samosa.Disable()
    sandwitch.Disable()
    totals.Disable()
    order.Enable()
    dosa.Disable()
    idli.Disable()
    coke.Disable()
    dhokla.Disable()
    icecream.Disable()
    momos.Disable()
    rolls.Disable()

right = GridSizer(5,1,10,10)

order = Button(panel,1,label="order")
# order.SetBackgroundColour("#2E7D32")
# order.SetForegroundColour("white")
order.Disable()
totals = Button(panel,1,label="total")
clear = Button(panel,1,label="clear")
setting = Button(panel,1,label="setting")
setting.Disable()
exits = Button(panel,1,label="exits")
order.SetFont(fonts)
totals.SetFont(fonts)
clear.SetFont(fonts)
setting.SetFont(fonts)
exits.SetFont(fonts)

exits.Bind(EVT_BUTTON, close_window, exits)
clear.Bind(EVT_BUTTON, clear_display, clear)
samosa.Bind(EVT_BUTTON, setsamosa, samosa)
sandwitch.Bind(EVT_BUTTON, setsandwitch, sandwitch)
dosa.Bind(EVT_BUTTON, setdosa, dosa)
idli.Bind(EVT_BUTTON, setidli, idli)
coke.Bind(EVT_BUTTON, setcoke, coke)
dhokla.Bind(EVT_BUTTON, setdhokla, dhokla)
icecream.Bind(EVT_BUTTON, seticecream, icecream)
momos.Bind(EVT_BUTTON, setmomos, momos)
rolls.Bind(EVT_BUTTON, setroll, rolls)
totals.Bind(EVT_BUTTON, totals_value, totals)
order.Bind(EVT_BUTTON, save, order)

right.AddMany([
    (order,1,EXPAND|ALL),
    (totals,1,EXPAND|ALL),
    (clear,1,EXPAND|ALL),
    (setting,1,EXPAND|ALL),
    (exits,1,EXPAND|ALL)
])

bottom.Add(left,3,EXPAND|ALL,border=20)
bottom.Add(right,1,TOP|BOTTOM|RIGHT|EXPAND,border=20)
footer = BoxSizer(HORIZONTAL)
col1 = BoxSizer(VERTICAL)
col2 = BoxSizer(VERTICAL)
col3 = BoxSizer(VERTICAL)
footer.AddMany([
    (col1,2,EXPAND|ALL),
    (col2,2,EXPAND|ALL),
    (col3,1,EXPAND|ALL)
])

comp_name = StaticText(panel,label="CWS Lab..")
comp_address = StaticText(panel,label="Madhubani, Purnea")
comp_web = li.HyperLinkCtrl(panel,label="www.cws.co.in",URL="www.cws.co.in")
comp_contact = StaticText(panel,label="9060201899")
comp_support = StaticText(panel,label="support@cwslab.com")
comp_name.SetForegroundColour("white")
comp_address.SetForegroundColour("white")
comp_web.SetForegroundColour("white")
comp_web.SetBackgroundColour("#80CBC4")
comp_contact.SetForegroundColour("white")
comp_support.SetForegroundColour("white")
comp_name.SetFont(small_font)
comp_address.SetFont(small_font)
comp_web.SetFont(small_font)
comp_contact.SetFont(small_font)
comp_support.SetFont(small_font)

col1.Add(comp_name,0,EXPAND|ALL)
col1.Add(comp_address,0,EXPAND|ALL)
col1.Add(comp_web,0,EXPAND|ALL)
col1.Add(comp_contact,0,EXPAND|ALL)
col1.Add(comp_support,0,EXPAND|ALL)

name = StaticText(panel,label="Jayant Raj")
post = StaticText(panel,label="Software Developer")
contact = StaticText(panel,label="7322866041")
website = li.HyperLinkCtrl(panel,label="www.aboutjayant.epizy.com",URL="www.aboutjayant.epizy.com")
website.SetBackgroundColour("#80CBC4")
website.SetForegroundColour("white")
email = StaticText(panel,label="jayantraj66041@gmail.com")
name.SetForegroundColour("white")
post.SetForegroundColour("white")
contact.SetForegroundColour("white")
email.SetForegroundColour("white")
name.SetFont(small_font)
post.SetFont(small_font)
contact.SetFont(small_font)
website.SetFont(small_font)
email.SetFont(small_font)

col2.AddMany([
    (name,0,EXPAND|ALL),
    (post,0,EXPAND|ALL),
    (contact,0,EXPAND|ALL),
    (website,0,EXPAND|ALL),
    (email,0,EXPAND|ALL)
])

time = StaticText(panel)
time.SetForegroundColour("white")
time.SetFont(font)
date = StaticText(panel)
date.SetForegroundColour("white")
date.SetFont(d_font)
t = Timer(root,1)
t.Start(100)
def on_timer(event):
    time.SetLabel(str(datetime.now().strftime("%H:%I:%S")))
    date.SetLabel(str(datetime.now().strftime("%d/%m/%Y")))
on_timer(None)
root.Bind(EVT_TIMER,on_timer)
col3.Add(time,0,EXPAND|ALL)
col3.Add(date,0,EXPAND|ALL)
mainbox.Add(top,0,ALL|EXPAND)
mainbox.Add(bottom,1,ALL|EXPAND)
mainbox.Add(footer,0,EXPAND|LEFT|RIGHT|BOTTOM,border=20)
panel.SetSizer(mainbox)
root.Show()
root.ShowFullScreen(True)
app.MainLoop()


import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import common
baseyear = common.getTaxYear()
projection = pd.read_csv(str(baseyear)+"/projectionssummary.csv")
projection = projection.set_index(projection.Year)
tenyears = baseyear + 10

plt.close()
plt.figure(figsize=(8, 6), dpi=200)
width = 0.7
print(projection)

p1 = plt.bar(x="Projected Current Growth",
             height=projection['12.5k'][tenyears], color='#dc3912')
p2 = plt.bar(x="Limited to CPI (Price)",
             height=projection['CPI 12.5k'][tenyears], color='#3366cc')
p3 = plt.bar(x="Limited to AWI (Wage) ",
             height=projection['AWI 12.5k'][tenyears], color='#990099')

plt.title('Projected '+str(tenyears)+' tax bill for current bill of $12,500')
# plt.xticks(years, (
#	str(baseyear), '5 years', '10 years', '15 years', '20 years'))

def annotate(bar):
    rects = bar.patches
    # For each bar: Place a label
    for rect in rects:
        # Get X and Y placement of label from rect.
        y_value = rect.get_height()
        x_value = rect.get_x() + rect.get_width() / 2

        # Number of points between bar and label. Change to your liking.
        space = -20
        # Vertical alignment for positive values
        va = 'bottom'

        # If value of bar is negative: Place label below bar
        if y_value < 0:
            # Invert space to place label below
            space *= -1
            # Vertically align label at top
            va = 'top'

        # Use Y value as label and format number with one decimal place
        label = "${:,.0f}".format(y_value)

        # Create annotation
        plt.annotate(
            label,                      # Use `label` as label
            (x_value, y_value),         # Place label at end of the bar
            xytext=(0, space),          # Vertically shift label by `space`
            textcoords="offset points",  # Interpret `xytext` as offset in points
            ha='center',                # Horizontally center label
            va=va,
            color='white',
            size=14)

annotate(p1)
annotate(p2)
annotate(p3)

# hide borders
for spine in plt.gca().spines.values():
    spine.set_visible(False)

plt.tick_params(top=False, bottom=False, left=False,
                right=False, labelleft=False, labelbottom=True)

plt.savefig(str(baseyear)+'/charts/twelvekcomparison.png')


import os

import pytest

import apiautomationtools.helpers.directory_helpers as dh
from apiautomationtools.client import HttpxRequests

pytestmark = pytest.mark.client

root_dir = f"{os.path.dirname(__file__)}/{__name__.split('.')[-1]}"
expected_form_data = """{'data': {'field1': 'value1'}, 'files': [('file', <_io.BufferedReader name='root_dir/tests/client/httpx/test_form_data_separation.py'>)]}"""
expected_form_data = expected_form_data.replace("root_dir", dh.get_root_dir())

def test_dict_as_form_data():
    pytest.async_requests = HttpxRequests(root_dir=root_dir)
    form_data = pytest.async_requests.separate_form_data(field1="value1", file=__file__)
    assert str(form_data) == expected_form_data

def test_dict_as_form_data_kwargs():
    body = {"field1": "value1", "file": __file__}
    form_data = pytest.async_requests.separate_form_data(**body)
    assert str(form_data) == expected_form_data

    pytest.async_requests.logging.delete_run_info(root_dir)
    path = pytest.async_requests.logging.log_file_path
    assert not os.path.exists(path)


#!/usr/bin/env python

#This program is free software: you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation, either version 3 of the License, or
#(at your option) any later version.
#
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with this program.  If not, see <http://www.gnu.org/licenses/>

import re
import logging
from ZimArchivist import utils
#from ZimArchivist import archive

def add_label(html_path, url, line):
    """
    Add the label "Archive" to the archive after the url in the line
    Return the line
    """
    integrated_link = re.compile('\[\[' + utils.protect(url) + '(\|.*\]\])')
    matching = integrated_link.search(line)
    if matching == None:
        #It is not an integrated link
        effective_pattern = url
        new_url = effective_pattern + " [[" + utils.get_unexpanded_path(html_path) + "|(Archive)]]"
    else:
        #It is...
        effective_pattern = '[[' + url + matching.groups()[0]
        new_url = effective_pattern + " [[" + utils.get_unexpanded_path(html_path) + "|(Archive)]]"
    effective_pattern = utils.protect(effective_pattern)
    line = re.sub(effective_pattern, new_url, line)
    return line

def link_archive_status(url, line):
    """
    Is the url in the line already archived?
    Basically, it checks if [[path|(Archive)]] follows the url
    False: Not archived
    True: Archived
    """
    logging.debug('line: ' + str(line))
    link_archived = re.compile(utils.protect(url) + '(\|.*\]\])?\s\[\[.*\|\(Archive\)\]\]')
    matching = link_archived.search(line)
    if matching == None:
        return False
    else:
        return True

def extract_labels_filepath(line):
    filepaths = []
    #First, we detect labels
    re_archive = re.compile('\s\[\[(.+)\|\(Archive\)\]\]')
    labels = re.findall('\s\[\[(.+?)\|\(Archive\)\]\]', line) #? is for non-greedy (minimal) fashion
    for label in labels:
        filepaths.append(label)
    return filepaths


#!/usr/bin/python3
"""module for the app.py"""
from flask import Flask, render_template, jsonify, make_response
from models import storage
from api.v1.views import app_views
import os
from flask_cors import CORS

app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": "0.0.0.0"}})
host = os.getenv('HBNB_API_HOST', '0.0.0.0')
port = os.getenv('HBNB_API_PORT', '5000')

app.register_blueprint(app_views)

@app.teardown_appcontext
def teardown_close(error):
    """method that closes the database"""
    storage.close()

@app.errorhandler(404)
def error_four04(error):
    """method that raies a 404 error"""
    status_code = 404
    message = {"error": "Not found"}
    return make_response(jsonify(message), status_code)

if __name__ == "__main__":
    app.run(host=host, port=port, threaded=True)