# -*- coding: utf-8 -*-
"""Main plotting source code, creates output figures and data-tables.

marmot_plot_main.py is the main plotting script within Marmot which calls on
supporting files to read in data, create the plot, and then return the plot and
data to marmot_plot_main.py. The supporting modules can be viewed within the repo
plottingmodules folder and have descriptive names such as total_generation.py,
generation_stack.py, curtailment.py etc.

@author: Daniel Levie
"""
# ========================================================================================
# Import Python Libraries
# ========================================================================================

import importlib
import sys
import time
from pathlib import Path
from typing import Union, List

import matplotlib.pyplot as plt
import pandas as pd

try:
    import marmot.utils.mconfig as mconfig
except ModuleNotFoundError:
    from utils.definitions import INCORRECT_ENTRY_POINT

    print(INCORRECT_ENTRY_POINT.format(Path(__file__).name))
    sys.exit()
from marmot.metamanagers.read_metadata import MetaData
from marmot.plottingmodules.plotutils.plot_data_helper import GenCategories
from marmot.plottingmodules.plotutils.plot_exceptions import (
    DataSavedInModule,
    InputSheetError,
    MissingInputData,
    MissingMetaData,
    MissingZoneData,
    UnderDevelopment,
    UnsupportedAggregation,
)
from marmot.plottingmodules.plotutils.styles import (
    ColorList,
    GeneratorColorDict,
    PlotMarkers,
)
from marmot.utils.definitions import INPUT_DIR, Module_CLASS_MAPPING
from marmot.utils.loggersetup import SetupLogger

# A bug in pandas requires this to be included, otherwise df.to_string truncates
# long strings.
# Fix available in Pandas 1.0 but leaving here in case user version not up to date
pd.set_option("display.max_colwidth", 1000)

class MarmotPlot(SetupLogger):
    """Main module class to be instantiated to run the plotter.

    MarmotPlot handles the selection of plotting module to
    create the desired figure and saving of outputs.
    It also handles the area aggregation selection
    """

    def __init__(
        self,
        Scenarios: Union[str, list],
        AGG_BY: str,
        model_solutions_folder: Union[str, Path],
        gen_names_dict: Union[str, Path, pd.DataFrame, dict],
        ordered_gen_categories: Union[str, Path, pd.DataFrame],
        color_dictionary: Union[str, Path, pd.DataFrame, dict],
        marmot_plot_select: Union[str, Path, pd.DataFrame],
        marmot_solutions_folder: Union[str, Path] = None,
        scenario_diff: Union[str, list] = None,
        zone_region_sublist: Union[str, list] = None,
        xlabels: Union[str, list] = None,
        ylabels: Union[str, list] = None,
        ticklabels: Union[str, list] = None,
        region_mapping: Union[str, Path, pd.DataFrame] = pd.DataFrame(),
        tech_subset: Union[str, list] = None,
        **kwargs,
    ):
        """
        Args:
            Scenarios (Union[str, list]): Name of scenarios
                to process.
            AGG_BY (str): Informs region type to aggregate by
                when creating plots.
            model_solutions_folder (Union[str, Path]): Directory containing model simulation
                results subfolders and their files.
            gen_names_dict (Union[str, Path, pd.DataFrame, dict]): Path to, Dataframe or dict
                of generator technologies to rename.
            ordered_gen_categories (Union[str, Path, pd.DataFrame]): Path to or Dataframe
                containing ordered generation and columns to specify technology subsets.
            color_dictionary (Union[str, Path, pd.DataFrame, dict]): Path to, Dataframe or dict
                containing list of colors to assign to each generator category.
            marmot_plot_select (Union[str, Path, pd.DataFrame]): Path to or DataFrame
                containing information on plots to create and certain settings.
            marmot_solutions_folder (Union[str, Path], optional): Directory to save
                Marmot solution files.
                Defaults to None.
            scenario_diff (Union[str, list], optional): 2 value string
                or list, used to compare 2 scenarios.
                Defaults to None.
            zone_region_sublist (Union[str, list], optional): Subset of regions
                to plot from AGG_BY.
                Defaults to None.
            xlabels (Union[str, list], optional): x axis labels for facet plots.
                Defaults to None.
            ylabels (Union[str, list], optional): y axis labels for facet plots.
                Defaults to None.
            ticklabels (Union[str, list], optional): custom ticklabels for plots,
                not available for every plot type.
                Defaults to None.
            region_mapping (Union[str, Path, pd.DataFrame], optional): Path to or Dataframe
                to map custom regions/zones to create custom aggregations.
                Aggregations are created by grouping PLEXOS regions.
                Defaults to pd.DataFrame().
            tech_subset (Union[str, list], optional): Tech subset category to plot.
                The tech_subset value should be a column in the
                ordered_gen_categories.csv. If left None all techs will be plotted
                Defaults to None.
            **kwargs
                These parameters will be passed to the
                marmot.utils.loggersetup.SetupLogger class.
        """
        super().__init__("plotter", **kwargs)  # Instantiation of SetupLogger

        self.Scenarios = self.convert_str_to_list(Scenarios)
        self.AGG_BY = AGG_BY
        self.model_solutions_folder = Path(model_solutions_folder)
        self.gen_names_dict = gen_names_dict
        self.ordered_gen_categories = ordered_gen_categories
        self.color_dictionary = color_dictionary
        self.marmot_plot_select = marmot_plot_select

        if marmot_solutions_folder is None:
            self.marmot_solutions_folder = self.model_solutions_folder
        else:
            self.marmot_solutions_folder = Path(marmot_solutions_folder)

        self.scenario_diff = self.convert_str_to_list(scenario_diff)
        self.zone_region_sublist = self.convert_str_to_list(zone_region_sublist)
        self.xlabels = self.convert_str_to_list(xlabels)
        self.ylabels = self.convert_str_to_list(ylabels)
        self.custom_xticklabels = self.convert_str_to_list(ticklabels)
        self.region_mapping = region_mapping
        self.tech_subset = tech_subset
        self._ordered_gen_list = None

    @property
    def ordered_gen_list(self) -> List[str]:
        """List of ordered generator technolgies.

        Oder is specified in the ordered_gen_categories input.

        Returns:
            List[str]: Ordered list of generator technolgies
        """

        if self._ordered_gen_list is None:
            # Subset ordered_gen to user desired generation
            if self.tech_subset:
                if self.tech_subset not in self.ordered_gen_categories.columns:
                    self.logger.warning(
                        f"{self.tech_subset} column was not found "
                        "in the ordered_gen_categories.csv. "
                        "All generator technologies will be plotted"
                    )
                    self._ordered_gen_list = (
                        self.ordered_gen_categories["Ordered_Gen"].str.strip().tolist()
                    )
                else:
                    ordered_gen = self.ordered_gen_categories.loc[
                        self.ordered_gen_categories[self.tech_subset] == True
                    ]
                    self._ordered_gen_list = (
                        ordered_gen["Ordered_Gen"].str.strip().tolist()
                    )
                    self.logger.info(f"Tech Aggregation selected: {self.tech_subset}")
            else:
                self._ordered_gen_list = (
                    self.ordered_gen_categories["Ordered_Gen"].str.strip().tolist()
                )
            # If Other category does not exist in ordered_gen, create entry
            if "Other" not in self._ordered_gen_list:
                self._ordered_gen_list.append("Other")
        return self._ordered_gen_list

    @property
    def gen_names_dict(self) -> dict:
        """Dictionary of existing gen technology names to new names.

        Used to rename technologies.

        Returns:
            dict: Keys: Existing names, Values: New names
        """
        return self._gen_names_dict

    @gen_names_dict.setter
    def gen_names_dict(self, gen_names_dict) -> None:

        if isinstance(gen_names_dict, (str, Path)):
            try:
                gen_names_dict = pd.read_csv(gen_names_dict)
            except FileNotFoundError:
                msg = (
                    "Could not find specified gen_names_dict csv file; "
                    "check file name and path."
                )
                self.logger.error(msg)
                raise FileNotFoundError(msg)

        if isinstance(gen_names_dict, pd.DataFrame):
            if len(gen_names_dict.axes[1]) == 2:
                self._gen_names_dict = (
                    gen_names_dict.set_index(gen_names_dict.columns[0])
                    .squeeze()
                    .to_dict()
                )
            else:
                msg = (
                    "Expected exactly 2 columns for gen_names_dict input, "
                    f"{len(input.axes[1])} columns were in the DataFrame."
                )
                self.logger.error(msg)
                raise ValueError(msg)
        elif isinstance(gen_names_dict, dict):
            self._gen_names_dict = gen_names_dict
        else:
            msg = (
                "Expected a DataFrame, dict, or a file path to csv for the gen_names_dict input but "
                f"recieved a {type(gen_names_dict)}"
            )
            self.logger.error(msg)
            raise NotImplementedError(msg)

    @property
    def ordered_gen_categories(self) -> pd.DataFrame:
        """DataFrame containing generator order and category information

        Has at least one column named Ordered_Gen.
        Other columns define different generator technology category groupings.

        Returns:
            pd.DataFrame: ordered_gen_categories DataFrame
        """
        return self._ordered_gen_categories

    @ordered_gen_categories.setter
    def ordered_gen_categories(self, ordered_gen_categories) -> None:

        if isinstance(ordered_gen_categories, (str, Path)):
            try:
                ordered_gen_categories = pd.read_csv(ordered_gen_categories)
            except FileNotFoundError:
                msg = (
                    "Could not find specified ordered_gen_categories csv file; "
                    "check file name and path."
                )
                self.logger.error(msg)
                raise FileNotFoundError(msg)
        if isinstance(ordered_gen_categories, pd.DataFrame):
            if "Ordered_Gen" in ordered_gen_categories.columns:
                self._ordered_gen_categories = ordered_gen_categories
            else:
                msg = "Misssing 'Ordered_Gen' column from ordered_gen_categories input."
                self.logger.error(msg)
                raise ValueError(msg)
        else:
            msg = (
                "Expected a DataFrame or a file path to csv for the ordered_gen_categories input but "
                f"recieved a {type(ordered_gen_categories)}"
            )
            self.logger.error(msg)
            raise NotImplementedError(msg)

        # Compare gen_names_dict to ordered_gen_categories
        if (
            set(self.gen_names_dict.values()).issubset(
                self._ordered_gen_categories["Ordered_Gen"].str.strip().tolist()
            )
        ) == False:
            missing_gen = set(self.gen_names_dict.values()) - (
                set(self._ordered_gen_categories["Ordered_Gen"].str.strip().tolist())
            )
            self.logger.warning(
                "The following tech categories from the "
                "gen_names_dict input do not exist in "
                "ordered_gen_categorie input!: "
                f"{missing_gen}"
            )

    @property
    def color_dictionary(self) -> dict:
        """Dictionary of gen technology names to plotting colors.

        Returns:
            dict: Keys gen technologies, Values colors
        """
        return self._color_dictionary

    @color_dictionary.setter
    def color_dictionary(self, color_dictionary) -> None:

        if isinstance(color_dictionary, (str, Path)):
            try:
                color_dictionary = pd.read_csv(color_dictionary)
            except FileNotFoundError:
                msg = (
                    "Could not find specified color dictionary csv file; "
                    "check file name and path."
                )
                self.logger.error(msg)
                raise FileNotFoundError(msg)

        if isinstance(color_dictionary, pd.DataFrame):
            if len(color_dictionary.axes[1]) == 2:
                self._color_dictionary = GeneratorColorDict.set_colors_from_df(
                    color_dictionary
                ).color_dict
            else:
                msg = (
                    "Expected exactly 2 columns for color_dictionary input, "
                    f"{len(color_dictionary.axes[1])} columns were in the DataFrame."
                )
                self.logger.error(msg)
                raise ValueError(msg)
        elif isinstance(color_dictionary, dict):
            self._color_dictionary = GeneratorColorDict(color_dictionary).color_dict
        else:
            msg = (
                "Expected a DataFrame, dict, or file path to csv for the color_dictionary input but "
                f"recieved a {type(color_dictionary)}"
            )
            self.logger.error(msg)
            raise NotImplementedError(msg)

    @property
    def marmot_plot_select(self) -> pd.DataFrame:
        """DataFrame containing information on plots to create and certain settings.

        Returns:
            pd.DataFrame:
        """
        return self._marmot_plot_select

    @marmot_plot_select.setter
    def marmot_plot_select(self, marmot_plot_select) -> None:

        if isinstance(marmot_plot_select, (str, Path)):
            try:
                self._marmot_plot_select = pd.read_csv(marmot_plot_select)
            except FileNotFoundError:
                msg = (
                    "Could not find specified marmot_plot_select csv file; "
                    "check file name and path."
                )
                self.logger.error(msg)
                raise FileNotFoundError(msg)

        elif isinstance(marmot_plot_select, pd.DataFrame):
            self._marmot_plot_select = marmot_plot_select
        else:
            msg = (
                "Expected a DataFrame or a file path to csv for the marmot_plot_select input but "
                f"recieved a {type(marmot_plot_select)}"
            )
            self.logger.error(msg)
            raise NotImplementedError(msg)

    @property
    def region_mapping(self) -> pd.DataFrame:
        """Region mapping Dataframe to map custom aggregations.

        Returns:
            pd.DataFrame:
        """
        return self._region_mapping

    @region_mapping.setter
    def region_mapping(self, region_mapping) -> None:
        if isinstance(region_mapping, (str, Path)):
            try:
                region_mapping = pd.read_csv(region_mapping)
            except FileNotFoundError:
                msg = (
                    "Could not find specified region_mapping csv file; "
                    "check file name and path."
                )
                self.logger.error(msg)
                raise FileNotFoundError(msg)

        if isinstance(region_mapping, pd.DataFrame):
            self._region_mapping = region_mapping.astype(str)
            if "category" in region_mapping.columns:
                # delete category columns if exists
                self._region_mapping = self._region_mapping.drop(["category"], axis=1)
        else:
            msg = (
                "Expected a DataFrame or a file path to csv for the region_mapping input but "
                f"recieved a {type(region_mapping)}"
            )
            self.logger.error(msg)
            raise NotImplementedError(msg)

    @staticmethod
    def convert_str_to_list(string_object: str) -> List[str]:
        """Converts a comma separated string to a list.

        Args:
            string_object (str): A comma separated string

        Returns:
            List[str]: list of strings.
        """
        if isinstance(string_object, str):
            list_obj = [x.strip() for x in string_object.split(",")]
        else:
            list_obj = string_object
        return list_obj

    def get_geographic_regions(self, meta: MetaData) -> List[str]:
        """Gets the geographic regions to plot based on the geographic aggregation.

        The aggregation is determined with the AGG_BY attribute.

        region and zone (PLEXOS) will pull model defined aggregations.
        Other aggregations will use values from the region mapping file.

        The zone_region_sublist attribute can be used to reduce the set of geographic regions
        to plot.

        Args:
            meta (MetaData): instance of MetaData class

        Returns:
            List[str]: List of geographic regions to plot
        """

        if self.AGG_BY in {"zone", "zones", "Zone", "Zones"}:
            self.AGG_BY = "zone"
            zones = pd.concat([meta.zones(scenario) for scenario in self.Scenarios])
            if zones.empty == True:
                self.logger.warning(
                    "Input Sheet Data Incorrect! Your model does "
                    "not contain Zones, enter a different aggregation"
                )
                sys.exit()
            Zones = zones["name"].unique()

            if self.zone_region_sublist:
                zsub = []
                for zone in self.zone_region_sublist:
                    if zone in Zones:
                        zsub.append(zone)
                    else:
                        self.logger.info(
                            "metadata does not contain zone: " f"{zone}, SKIPPING ZONE"
                        )
                if zsub:
                    Zones = zsub
                else:
                    self.logger.warning(
                        f"None of: {self.zone_region_sublist} "
                        "in model Zones. Plotting all Zones"
                    )

        elif self.AGG_BY in {"region", "regions", "Region", "Regions"}:
            self.AGG_BY = "region"
            regions = pd.concat([meta.regions(scenario) for scenario in self.Scenarios])
            if regions.empty == True:
                self.logger.warning(
                    "Input Sheet Data Incorrect! Your model does "
                    "not contain Regions, enter a different aggregation"
                )
                sys.exit()

            Zones = regions["region"].unique()
            if self.zone_region_sublist:
                zsub = []
                for region in self.zone_region_sublist:
                    if region in Zones:
                        zsub.append(region)
                    else:
                        self.logger.info(
                            "metadata does not contain region: "
                            f"{region}, SKIPPING REGION"
                        )
                if zsub:
                    Zones = zsub
                else:
                    self.logger.warning(
                        f"None of: {self.zone_region_sublist} "
                        "in model Regions. Plotting all Regions"
                    )

        elif not self.region_mapping.empty:
            self.logger.info(
                "Plotting Custom region aggregation from " "region_mapping File"
            )
            regions = pd.concat([meta.regions(scenario) for scenario in self.Scenarios])
            self.region_mapping = regions.merge(
                self.region_mapping, how="left", on="region"
            )
            self.region_mapping.dropna(axis=1, how="all", inplace=True)

            try:
                Zones = self.region_mapping[self.AGG_BY].unique()
            except KeyError:
                self.logger.warning(
                    f"AGG_BY = '{self.AGG_BY}' is not in the "
                    "region_mapping File, enter a different aggregation"
                )
                sys.exit()

            # remove any nan that might end  up in list
            Zones = [x for x in Zones if str(x) != "nan"]

            if self.zone_region_sublist:
                zsub = []
                for region in self.zone_region_sublist:
                    if region in Zones:
                        zsub.append(region)
                    else:
                        self.logger.info(
                            "region_mapping File does not contain region: "
                            f"{region}, SKIPPING REGION"
                        )
                if zsub:
                    Zones = zsub
                else:
                    self.logger.warning(
                        f"None of: {self.zone_region_sublist} "
                        "in region_mapping File. Plotting all "
                        f"Regions of aggregation '{self.AGG_BY}'"
                    )
        else:
            self.logger.warning(
                "AGG_BY is not defined correctly, aggregation "
                "specified was not found, system will now exit"
            )
            sys.exit()
        return Zones

    def run_plotter(self):
        """Main method to call to begin plotting figures.

        This method takes no input variables, all required
        variables are passed in via the __init__ method.
        """

        self.logger.info(f"Area Aggregation selected: {self.AGG_BY}")

        if self.zone_region_sublist:
            self.logger.info(
                f"Only plotting {self.AGG_BY}: " f"{self.zone_region_sublist}"
            )

        processed_hdf5_folder = self.marmot_solutions_folder.joinpath(
            "Processed_HDF5_folder"
        )

        figure_format = mconfig.parser("figure_file_format")
        if figure_format == "nan":
            figure_format = "png"

        # Create an instance of MetaData.
        meta = MetaData(processed_hdf5_folder, region_mapping=self.region_mapping)
        Zones = self.get_geographic_regions(meta)

        # ================================================================================
        # Start Main plotting loop
        # ================================================================================

        # Filter for chosen figures to plot
        plot_selection = self.marmot_plot_select.loc[
            self.marmot_plot_select["Plot Graph"] == True
        ]
        plot_selection = plot_selection.sort_values(by=["Marmot Module", "Method"])

        list_modules = plot_selection["Marmot Module"].unique()

        start_timer = time.time()

        for module in list_modules:
            module_plots = plot_selection.loc[plot_selection["Marmot Module"] == module]
            # List of required arguments
            argument_list = [
                Zones,
                self.Scenarios,
                self.AGG_BY,
                self.ordered_gen_list,
                self.marmot_solutions_folder,
            ]
            # dictionary of keyword arguments passed to plotting modules;
            # key names match the instance variables in each module
            argument_dict = {
                "gen_names_dict": self.gen_names_dict,
                "gen_categories": GenCategories().set_categories(
                    self.ordered_gen_categories
                ),
                "marmot_color_dict": self.color_dictionary,
                "scenario_diff": self.scenario_diff,
                "ylabels": self.ylabels,
                "xlabels": self.xlabels,
                "custom_xticklabels": self.custom_xticklabels,
                "color_list": ColorList().colors,
                "marker_style": PlotMarkers().markers,
                "region_mapping": self.region_mapping,
                "tech_subset": self.tech_subset,
            }

            # Import plot module from plottingmodules package
            plot_module = importlib.import_module("marmot.plottingmodules." + module)
            # Instantiate the module class

            class_name = getattr(plot_module, Module_CLASS_MAPPING[module])
            instantiate_mplot = class_name(*argument_list, **argument_dict)
            # Create output folder for each plotting module
            figures: Path = instantiate_mplot.figure_folder.joinpath(
                f"{self.AGG_BY}_{module}"
            )
            figures.mkdir(exist_ok=True)

            # Main loop to process each figure and pass
            # plot specific variables to methods
            for _, row in module_plots.iterrows():

                print("\n\n\n")
                self.logger.info(f"Plot =  {row['Figure Output Name']}")

                if pd.isna(row.iloc[2]):
                    prop = None
                else:
                    prop = row.iloc[2]
                # Modifies timezone string before plotting
                if pd.isna(row.iloc[6]):
                    timezone_string: str = "Date"
                else:
                    timezone_string: str = f"Date ({row.iloc[6]})"

                if pd.isna(row.iloc[4]):
                    days_before = 2
                else:
                    days_before = float(row.iloc[4])
                if pd.isna(row.iloc[5]):
                    days_after = 2
                else:
                    days_after = float(row.iloc[5])

                if pd.notna(row["Custom Data File"]):
                    custom_data_file_path = Path(row["Custom Data File"])
                else:
                    custom_data_file_path = None

                if (
                    pd.notna(row["Timeseries Plot Resolution"])
                    and row["Timeseries Plot Resolution"] == "Annual"
                ):
                    data_resolution: str = "_Annual"
                else:
                    data_resolution: str = ""

                if row["Group by Scenario or Year-Scenario"] == "Year-Scenario":
                    scenario_groupby: str = "Year-Scenario"
                else:
                    scenario_groupby: str = "Scenario"

                # Get figure method and run plot
                try:
                    figure_method = getattr(instantiate_mplot, row["Method"])
                except AttributeError:
                    self.logger.warning(
                        f"{Module_CLASS_MAPPING[module]} has no attribute '{row['Method']}'"
                    )
                    continue
                Figure_Out = figure_method(
                    figure_name=row.iloc[0],
                    prop=prop,
                    y_axis_max=float(row.iloc[3]),
                    start=days_before,
                    end=days_after,
                    timezone=timezone_string,
                    start_date_range=row.iloc[7],
                    end_date_range=row.iloc[8],
                    custom_data_file_path=custom_data_file_path,
                    data_resolution=data_resolution,
                    scenario_groupby=scenario_groupby,
                )

                if isinstance(Figure_Out, MissingInputData):
                    self.logger.info(
                        "Add Inputs With Formatter Before " "Attempting to Plot!\n"
                    )
                    continue

                if isinstance(Figure_Out, DataSavedInModule):
                    self.logger.info(
                        f"Plotting Completed for " f'{row["Figure Output Name"]}\n'
                    )
                    self.logger.info("Plots & Data Saved Within Module!\n")
                    continue

                if isinstance(Figure_Out, UnderDevelopment):
                    self.logger.info("Plot is Under Development, Plotting Skipped!\n")
                    continue

                if isinstance(Figure_Out, InputSheetError):
                    self.logger.info("Input Sheet Data Incorrect!\n")
                    continue

                if isinstance(Figure_Out, MissingMetaData):
                    self.logger.info(
                        "Required Meta Data Not Available For " "This Plot!\n"
                    )
                    continue

                if isinstance(Figure_Out, UnsupportedAggregation):
                    self.logger.info(
                        f"Aggregation Type: '{self.AGG_BY}' "
                        "not supported for This plot!\n"
                    )
                    continue

                for zone_input in Zones:
                    if isinstance(Figure_Out[zone_input], MissingZoneData):

                        self.logger.info(f"No Data to Plot in {zone_input}")

                    else:
                        # Save figures
                        Figure_Out[zone_input]["fig"].savefig(
                            figures.joinpath(
                                f"{zone_input}_"
                                f'{row["Figure Output Name"]}'
                                f".{figure_format}"
                            ),
                            dpi=600,
                            bbox_inches="tight",
                        )
                        # Save .csv's.
                        if Figure_Out[zone_input]["data_table"].empty:
                            self.logger.info(
                                f'{row["Figure Output Name"]} '
                                "does not return a data table"
                            )
                        else:
                            Figure_Out[zone_input]["data_table"].to_csv(
                                figures.joinpath(
                                    f"{zone_input}_" f'{row["Figure Output Name"]}.csv'
                                )
                            )

                self.logger.info(
                    "Plotting Completed for " f'{row["Figure Output Name"]}\n'
                )

                # plt.tight_layout()
                # plt.show()
                plt.close("all")

        end_timer = time.time()
        time_elapsed = end_timer - start_timer
        self.logger.info(f"Main Plotting loop took {round(time_elapsed/60,2)} minutes")
        self.logger.info("All Plotting COMPLETED")
        meta.close_h5()

def main():
    """Run the plotting code and create desired plots and data-tables based on user input files."""
    # ====================================================================================
    # Load Input Properties
    # ====================================================================================

    Marmot_user_defined_inputs = pd.read_csv(
        INPUT_DIR.joinpath(mconfig.parser("user_defined_inputs_file")),
        usecols=["Input", "User_defined_value"],
        index_col="Input",
        skipinitialspace=True,
    )

    marmot_plot_select = pd.read_csv(
        INPUT_DIR.joinpath(mconfig.parser("plot_select_file"))
    )

    # Folder to save your processed solutions
    if pd.isna(
        Marmot_user_defined_inputs.loc["Marmot_Solutions_folder", "User_defined_value"]
    ):
        marmot_solutions_folder = None
    else:
        marmot_solutions_folder = Marmot_user_defined_inputs.loc[
            "Marmot_Solutions_folder", "User_defined_value"
        ].strip()

    Scenarios = Marmot_user_defined_inputs.loc["Scenarios", "User_defined_value"]

    # These variables (along with region_mapping) are used to initialize MetaData
    model_solutions_folder = Marmot_user_defined_inputs.loc[
        "Model_Solutions_folder", "User_defined_value"
    ].strip()

    # For plots using the difference of the values between two scenarios.
    # Max two entries, the second scenario is subtracted from the first.
    if pd.isna(
        Marmot_user_defined_inputs.loc["Scenario_Diff_plot", "User_defined_value"]
    ):
        scenario_diff = None
    else:
        scenario_diff = Marmot_user_defined_inputs.loc[
            "Scenario_Diff_plot", "User_defined_value"
        ]

    Mapping_folder = INPUT_DIR.joinpath("mapping_folder")

    if pd.isna(
        Marmot_user_defined_inputs.loc["Region_Mapping.csv_name", "User_defined_value"]
    ):
        region_mapping = pd.DataFrame()
    else:
        region_mapping = Mapping_folder.joinpath(
            Marmot_user_defined_inputs.loc[
                "Region_Mapping.csv_name", "User_defined_value"
            ]
        )

    gen_names_dict = Mapping_folder.joinpath(
        Marmot_user_defined_inputs.loc["gen_names.csv_name", "User_defined_value"]
    )

    ordered_gen_cat_file = Mapping_folder.joinpath(
        Marmot_user_defined_inputs.loc[
            "ordered_gen_categories_file", "User_defined_value"
        ]
    )
    color_dictionary = Mapping_folder.joinpath(
        Marmot_user_defined_inputs.loc["color_dictionary_file", "User_defined_value"]
    )

    AGG_BY = Marmot_user_defined_inputs.loc["AGG_BY", "User_defined_value"].strip()

    if pd.notna(Marmot_user_defined_inputs.loc["TECH_SUBSET", "User_defined_value"]):
        tech_subset = Marmot_user_defined_inputs.loc[
            "TECH_SUBSET", "User_defined_value"
        ].strip()
    else:
        tech_subset = None

    # Facet Grid Labels (Based on Scenarios)
    if pd.isna(
        Marmot_user_defined_inputs.loc["zone_region_sublist", "User_defined_value"]
    ):
        zone_region_sublist = None
    else:
        zone_region_sublist = Marmot_user_defined_inputs.loc[
            "zone_region_sublist", "User_defined_value"
        ]

    if pd.isna(Marmot_user_defined_inputs.loc["Facet_ylabels", "User_defined_value"]):
        ylabels = None
    else:
        ylabels = Marmot_user_defined_inputs.loc["Facet_ylabels", "User_defined_value"]

    if pd.isna(Marmot_user_defined_inputs.loc["Facet_xlabels", "User_defined_value"]):
        xlabels = None
    else:
        xlabels = Marmot_user_defined_inputs.loc["Facet_xlabels", "User_defined_value"]

    # option to change tick labels on plot
    if pd.isna(Marmot_user_defined_inputs.loc["Tick_labels", "User_defined_value"]):
        ticklabels = None
    else:
        ticklabels = Marmot_user_defined_inputs.loc["Tick_labels", "User_defined_value"]

    initiate = MarmotPlot(
        Scenarios,
        AGG_BY,
        model_solutions_folder,
        gen_names_dict,
        ordered_gen_cat_file,
        color_dictionary,
        marmot_plot_select,
        marmot_solutions_folder=marmot_solutions_folder,
        scenario_diff=scenario_diff,
        zone_region_sublist=zone_region_sublist,
        xlabels=xlabels,
        ylabels=ylabels,
        ticklabels=ticklabels,
        region_mapping=region_mapping,
        tech_subset=tech_subset,
    )

    initiate.run_plotter()

if __name__ == "__main__":
    main()


"""Module for the traffic simulation menu."""
import pygame

from model_and_simulate.utilities.pygame_simple import Color
from model_and_simulate.utilities.pygame_button import SwitchButton, TextButton, Button
from model_and_simulate.utilities.start_screen import SimulationStartScreen
from .traffic_simulation import TrafficParameters

class TrafficStartScreen(SimulationStartScreen):
    """The start screen for TrafficSimulation`."""

    def create_menu_items(self) -> tuple[int, int, TrafficParameters, list[Button]]:
        """Creates the GUI buttons with `TrafficParameters` logic."""
        simulation_parameters = TrafficParameters()

        width, height = pygame.display.get_window_size()
        col_w, row_h = round(width / 8), round(height / 8)
        menu_items, buttons = self.create_default_items(col_w, row_h)
        button_all_at_once = SwitchButton(
            (col_w, row_h),
            (col_w, 4 * row_h),
            text="All at once",
            in_and_active_color=(Color.RED, Color.SILVER),
        )
        button_all_at_once.on = simulation_parameters.all_vehicles_at_once

        def on_click_listener_all_at_once(*args):
            """The callback function for the switch button."""
            simulation_parameters.all_vehicles_at_once = (
                not simulation_parameters.all_vehicles_at_once
            )
            self.simple_pygame.play_effect("hit_low")

        self.add_switch_buttons([button_all_at_once], menu_items, on_click_listener_all_at_once)
        buttons.append(button_all_at_once)
        buttons_text_input: dict[TextButton, str] = {
            TextButton(
                (col_w * 2, row_h),
                (5 * col_w, y * row_h),
                str(getattr(simulation_parameters, attr) * 100)[:2],
            ): attr
            for y, attr in zip(
                range(3, 5),
                ("occupation", "dawdling_factor"),
            )
        }
        button_length = TextButton(
            (col_w * 2, row_h),
            (6 * col_w, 5 * row_h),
            str(simulation_parameters.length),
            text_color=Color.GREEN,
        )
        buttons_text_input[button_length] = "length"

        def on_start_input_listener(text_button: TextButton):
            """The callback function for start input event."""
            if self.read_input_to is not None:
                self.read_input_to.finish_input()
            self.read_input_to = text_button

        def on_finish_input_listener(text_button: TextButton):
            """The callback function for finish input."""
            simulation_par = buttons_text_input[text_button]
            old_value = getattr(simulation_parameters, simulation_par)
            if text_button.text == "":
                if simulation_par == "length":
                    text_button.text = str(old_value)
                else:
                    text_button.text = str(old_value*100)[:2]
            else:
                user_value = int(text_button.text)
                if simulation_par == "length":
                    if user_value < 300:
                        user_value = 300
                    elif user_value > 3500:
                        user_value = 3500
                    setattr(simulation_parameters, simulation_par, user_value)
                else:
                    if simulation_par == "occupation":
                        user_value = max(user_value, 10)
                    setattr(simulation_parameters, simulation_par, user_value / 100)  # percentage
                text_button.text = str(user_value)

            self.read_input_to = None

        on_hover_text = "Click and use Digits, BACKSPACE or ENTER"

        def on_hover_text_listener(*args):
            """The callback function for hovering text input buttons."""
            self.simple_pygame.draw_text(on_hover_text, (4 * col_w, 2.5 * row_h))

        for button in buttons_text_input.keys():
            button.add_on_start_listener(on_start_input_listener)
            button.add_on_finish_listener(on_finish_input_listener)
            button.add_on_hover_listener(on_hover_text_listener)
            menu_items.add(button)
        buttons += list(buttons_text_input.keys())
        return col_w, row_h, simulation_parameters, buttons

    @staticmethod
    def create_menu_texts(col_w: int, row_h: int) -> list:
        """Gets the raw texts for the start menu. This texts are just drawn, with no logic."""
        menu_texts = SimulationStartScreen.create_menu_texts(col_w, row_h)
        y = 3
        for text, limit in zip(("total density", "dawdling probability"), (10, 0)):
            menu_texts.append((text, (3.4 * col_w, y * row_h + row_h / 2)))
            menu_texts.append((f">= {limit:0>2d} %", (7 * col_w, y * row_h + row_h / 2)))
            y += 1
        menu_texts += [
            ("increment density one by one", (0, 3.5 * row_h, 25, Color.RED)),
            (f"{300} <= length <= {3500}", (4 * col_w, y * row_h + row_h / 2, 20, Color.HGREEN)),
        ]
        return menu_texts


from collections import defaultdict, deque
import re

def parse_input(lines):
	return construct_rooms(lines[0][1:-1])

def follow_dir(adj_rooms, x, y, direc):
	if direc == "N":
		end = (x, y+1)
	elif direc == "S":
		end = (x, y-1)
	elif direc == "W":
		end = (x-1, y)
	else:
		end = (x+1, y)

	adj_rooms[(x, y)].add(end)
	adj_rooms[end].add((x, y))
	return end

def construct_rooms(directions):
	adj_rooms = defaultdict(set)

	# | is intentionally left out of here. It just breaks up \w+, and is otherwise not needed.
	#   The string '|(' does not exist anywhere in the regex.
	chunks = re.findall(r"\w+|[()]|\|+", directions + ")")

	def parse(cur):
		parts = [[]]
		while True:
			if chunks[cur] == "|":
				parts.append([])

			elif chunks[cur] == "(":
				cur, part = parse(cur+1)
				parts[-1].append(part)

			elif chunks[cur] == ")":
				# Empty pipe part only ever comes at end of capture group.
				if not parts[-1]:
					parts[-1].append("")
				return cur, parts

			else:
				parts[-1].append(chunks[cur])

			cur += 1

	parts = parse(0)[1]

	# Note: The first character in the regex must not be a (.

	def follow_ppart(ppart, strt):
		curs = {strt}
		for part in ppart:
			new_curs = set()
			if isinstance(part, str):
				for cur in curs:
					for direc in part:
						cur = follow_dir(adj_rooms, *cur, direc)
					new_curs.add(cur)
			else:
				for cur in curs:
					for pp in part:
						new_curs |= follow_ppart(pp, cur)
			curs = new_curs

		return curs

	for pp in parts:
		follow_ppart(pp, (0, 0))

	return adj_rooms

def part1(adj_rooms):
	# Do a bfs, and return the very last thing found.
	found = set()
	to_explore = deque()
	to_explore.appendleft(((0, 0), 0))
	while to_explore:
		cur, dist = to_explore.pop()
		for adj_room in adj_rooms[cur]:
			if adj_room not in found:
				found.add(adj_room)
				to_explore.appendleft((adj_room, dist+1))

	return dist

def part2(adj_rooms):
	# Do a bfs, keeping track of rooms with distance > 1000
	far_rooms = 0
	found = set()
	to_explore = deque()
	to_explore.appendleft(((0, 0), 0))
	while to_explore:
		cur, dist = to_explore.pop()
		if dist >= 1000:
			far_rooms += 1
		for adj_room in adj_rooms[cur]:
			if adj_room not in found:
				found.add(adj_room)
				to_explore.appendleft((adj_room, dist+1))

	return far_rooms


import numpy as np
import shelve
from cvxopt import matrix, solvers
from collections import Counter
from lib.preprocessing.read_data import read_file

# soft margin SVM (dual from with gaussian kernel)
class SVM:

    def __init__(self, file_name, c=1.0, sigma2=1, precision=1e-05, no_kernel=False):
        self.file_name = file_name
        self.c, self.sigma2 = c, sigma2
        self.sol = None
        self.X, self.Y = SVM.read_file(file_name)
        self.precision = precision
        if not no_kernel:
            self.kernel = SVM.gauss_kernel_generator(sigma2)
        else:
            self.kernel = np.inner
        self.lmda = None
        self.sv, self.sv_label, self.sv_index, self.sv_lmda = None, None, None, None
        self.B = 0
        # self.sv_bs, self.b = None, 0
        # self.W, self.B = None, None
        self.fit()

    @staticmethod
    def kernel_matrix_key(file_name, sigma2):
        return file_name + "..." + str(sigma2)

    @staticmethod
    def b_key(file_name, c, sigma2, precision):
        return file_name + "..." + str(c) + "..." + str(sigma2) + "..." + str(precision)

    # cache of kernel constants matrix of same sigma and same filename
    @staticmethod
    def get_kernel_matrix(file_name, X, Y, sigma2):
        K = np.zeros((X.shape[0], X.shape[0]))
        kernel = SVM.gauss_kernel_generator(sigma2)
        for i in range(X.shape[0]):
            for j in range(X.shape[0]):
                K[i, j] = kernel(X[i], X[j])
        P = matrix(np.outer(Y, Y) * K)
        return P

    def fit(self):
        # P
        P = SVM.get_kernel_matrix(self.file_name, self.X, self.Y, self.sigma2)

        # q
        q = -np.ones(self.X.shape[0])
        q = matrix(q)

        # G
        G = np.zeros((self.X.shape[0] * 2, self.X.shape[0]))
        for i in range(self.X.shape[0]):
            G[i][i] = -1
        for i in range(self.X.shape[0], 2 * self.X.shape[0]):
            G[i][i - self.X.shape[0]] = 1
        G = matrix(G)

        # h
        h = []
        for i in range(self.X.shape[0]):
            # h = h + [self.c]
            h = h + [0.0]
        for i in range(self.X.shape[0]):
            # h = h + [0.0]
            h = h + [self.c]
        h = matrix(h)

        # A
        A = np.zeros((1, self.X.shape[0]))
        for i in range(self.X.shape[0]):
            A[0][i] = self.Y[i]
        A = matrix(A)

        b = matrix(0.0)
        self.sol = solvers.qp(P, q, G=G, h=h, A=A, b=b)
        self.lmda = np.ravel(np.array(self.sol['x']))

        # find support vector index
        sv_index = []
        for l in self.lmda:
            if l >= self.precision:
                sv_index += [True]
            else:
                sv_index += [False]
        self.sv_index = np.array(sv_index)
        self.sv, self.sv_label, self.sv_lmda = self.X[self.sv_index], self.Y[self.sv_index], self.lmda[self.sv_index]
        self.B = self.calc_B()

    def calc_B(self):
        b_projects = self.project(self.sv)
        b_projects_count = Counter(b_projects).most_common()[0][0]
        # return 1 - b_projects_count
        return 1 - np.mean(b_projects)

    def project(self, X):
        projects = np.zeros(X.shape[0])
        for a in range(X.shape[0]):
            pa = np.zeros(self.X.shape[0])
            for i in range(self.X.shape[0]):
                xi = self.X[i]
                yi = self.Y[i]
                lmdai = self.lmda[i]
                pa[i] = lmdai * yi * self.kernel(X[a], xi)
            projects[a] = sum(pa)
        return projects + self.B

    def predict(self, X):
        return np.sign(self.project(X))

    @staticmethod
    def gradient(X, Y, W, B, c):
        grad_W, grad_B = np.array(W), 0
        hinge_grad = np.zeros(X.shape[1])
        for i in range(X.shape[0]):
            xi = X[i]
            yi = Y[i]
            for j in range(X.shape[1]):
                if yi * xi[j] * W[j] < 1:
                    hinge_grad[j] = hinge_grad[j] - yi * xi[j]
            if yi * xi[j] < 1:
                grad_B = grad_B - yi
        hinge_grad = c * hinge_grad
        grad_W = grad_W + hinge_grad
        grad_B = c * grad_B
        return grad_W, grad_B

    @staticmethod
    def loss(X, Y, W, B, c):
        nm = (1/2) * np.linalg.norm(W)
        hinge = 0
        for i in range(X.shape[0]):
            xi = X[i]
            yi = Y[i]
            u = 1 - yi * (np.inner(W, xi)) + B
            hinge = hinge + max(0, u)
        hinge = c * hinge
        return nm + hinge

    @staticmethod
    def linear_kernel_generator():
        def linear_kernel(x1, x2):
            return np.inner(x1, x2)
        return linear_kernel

    @staticmethod
    def gauss_kernel_generator(sigma2):
        def gauss_kernel(x1, x2):
            return np.exp(-np.linalg.norm(x1 - x2) ** 2 / (2. * sigma2))
        return gauss_kernel

def distance(hyperplane, point):
    return np.inner(hyperplane.flatten(), point) / np.linalg.norm(hyperplane[1:-1])

if __name__ == "__main__":
    # read data
    train_X, train_Y = read_file("train.data")
    validation_X, validation_Y = read_file("validation.data")
    test_X, test_Y = read_file("test.data")

    train_len = train_X.shape[0]
    validation_len = validation_X.shape[0]
    test_len = test_X.shape[0]
    #
    c_arr = [1, 10, 100, 1000, 1e4, 1e5, 1e6, 1e7, 1e8]
    sigma2_arr = [0.1, 1, 10, 100, 1000]
    #
    s = dict()
    for c in c_arr:
        for sigma2 in sigma2_arr:
            # s[c, sigma2] = SVM("spam_train.data", c=c, sigma2=sigma2)
            print("c: ", c, " sigma2: ", sigma2)
            s[c, sigma2] = SVM("train.data", c=c, sigma2=sigma2, no_kernel=False)

    ## training
    accu_training = dict()
    pred_training = dict()
    for c in c_arr:
        for sigma2 in sigma2_arr:
            pred_training[c, sigma2] = s[c, sigma2].predict(train_X)
            accu_training[c, sigma2] = np.sum(pred_training[c, sigma2] == train_Y) / train_len

    ## validation
    accu_validation = dict()
    pred_validation = dict()
    for c in c_arr:
        for sigma2 in sigma2_arr:
            pred_validation[c, sigma2] = s[c, sigma2].predict(validation_X)
            accu_validation[c, sigma2] = np.sum(pred_validation[c, sigma2] == validation_Y) / validation_len

    ## test
    accu_test = dict()
    pred_test = dict()
    for c in c_arr:
        for sigma2 in sigma2_arr:
            pred_test[c, sigma2] = s[c, sigma2].predict(test_X)
            accu_test[c, sigma2] = np.sum(pred_test[c, sigma2] == test_Y) / test_len


from __future__ import annotations

__all__ = (
    "LoadVector",
    "DownselectVector",
    "MultiCriteriaDownselectVector",
    "ConvertFluxToMag",
    "ConvertUnits",
    "CalcSn",
    "MagDiff",
    "ExtinctionCorrectedMagDiff",
    "PerGroupStatistic",
    "ResidualWithPerGroupStatistic",
    "RAcosDec",
)

import logging
import warnings
from typing import Optional, cast

import numpy as np
import pandas as pd
from astropy import units as u
from lsst.pex.config import DictField, Field
from lsst.pex.config.configurableActions import ConfigurableActionField, ConfigurableActionStructField

from ...interfaces import KeyedData, KeyedDataSchema, Vector, VectorAction
from .selectors import VectorSelector

_LOG = logging.getLogger(__name__)

# Basic vectorActions

class LoadVector(VectorAction):
    """Load and return a Vector from KeyedData."""

    vectorKey = Field[str](doc="Key of vector which should be loaded")

    def getInputSchema(self) -> KeyedDataSchema:
        return ((self.vectorKey, Vector),)

    def __call__(self, data: KeyedData, **kwargs) -> Vector:
        return np.array(cast(Vector, data[self.vectorKey.format(**kwargs)]))

class DownselectVector(VectorAction):
    """Get a vector from KeyedData, apply specified selector, return the
    shorter Vector.
    """

    vectorKey = Field[str](doc="column key to load from KeyedData")

    selector = ConfigurableActionField[VectorAction](
        doc="Action which returns a selection mask", default=VectorSelector
    )

    def getInputSchema(self) -> KeyedDataSchema:
        yield (self.vectorKey, Vector)
        yield from cast(VectorAction, self.selector).getInputSchema()

    def __call__(self, data: KeyedData, **kwargs) -> Vector:
        mask = cast(VectorAction, self.selector)(data, **kwargs)
        return cast(Vector, data[self.vectorKey.format(**kwargs)])[mask]

class MultiCriteriaDownselectVector(VectorAction):
    """Get a vector from KeyedData, apply specified set of selectors with AND
    logic, and return the shorter Vector.
    """

    vectorKey = Field[str](doc="column key to load from KeyedData")

    selectors = ConfigurableActionStructField[VectorAction](
        doc="Selectors for selecting rows, will be AND together",
    )

    def getInputSchema(self) -> KeyedDataSchema:
        yield (self.vectorKey, Vector)
        for action in self.selectors:
            yield from action.getInputSchema()

    def __call__(self, data: KeyedData, **kwargs) -> Vector:
        mask: Optional[Vector] = None
        for selector in self.selectors:
            subMask = selector(data, **kwargs)
            if mask is None:
                mask = subMask
            else:
                mask *= subMask  # type: ignore
        return cast(Vector, data[self.vectorKey.format(**kwargs)])[mask]

# Astronomical vectorActions

class CalcSn(VectorAction):
    """Calculate the signal-to-noise ratio from a single flux vector."""

    fluxType = Field[str](doc="Flux type (vector key) to calculate the S/N.", default="{band}_psfFlux")
    uncertaintySuffix = Field[str](
        doc="Suffix to add to fluxType to specify the uncertainty column", default="Err"
    )

    def getInputSchema(self) -> KeyedDataSchema:
        yield self.fluxType, Vector
        yield f"{self.fluxType}{self.uncertaintySuffix}", Vector

    def __call__(self, data: KeyedData, **kwargs) -> Vector:
        signal = np.array(data[self.fluxType.format(**kwargs)])
        noise = np.array(data[f"{self.fluxType}{self.uncertaintySuffix}".format(**kwargs)])
        sn = signal / noise

        return np.array(sn)

class ConvertFluxToMag(VectorAction):
    """Turn nano janskies into magnitudes."""

    vectorKey = Field[str](doc="Key of flux vector to convert to mags")
    fluxUnit = Field[str](doc="Astropy unit of flux vector", default="nJy")
    returnMillimags = Field[bool](doc="Use millimags or not?", default=False)

    def getInputSchema(self) -> KeyedDataSchema:
        return ((self.vectorKey, Vector),)

    def __call__(self, data: KeyedData, **kwargs) -> Vector:
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", r"invalid value encountered")
            warnings.filterwarnings("ignore", r"divide by zero")
            vec = cast(Vector, data[self.vectorKey.format(**kwargs)])
            mags = (np.array(vec) * u.Unit(self.fluxUnit)).to(u.ABmag).value  # type: ignore
            if self.returnMillimags:
                mags *= 1000
            return mags

class ConvertUnits(VectorAction):
    """Convert the units of a vector."""

    buildAction = ConfigurableActionField(doc="Action to build vector", default=LoadVector)
    inUnit = Field[str](doc="input Astropy unit")
    outUnit = Field[str](doc="output Astropy unit")

    def getInputSchema(self) -> KeyedDataSchema:
        return tuple(self.buildAction.getInputSchema())

    def __call__(self, data: KeyedData, **kwargs) -> Vector:
        dataWithUnit = self.buildAction(data, **kwargs) * u.Unit(self.inUnit)
        return dataWithUnit.to(self.outUnit).value

class MagDiff(VectorAction):
    """Calculate the difference between two magnitudes;
    each magnitude is derived from a flux column.
    Parameters
    ----------
    TO DO:
    Returns
    -------
    The magnitude difference in milli mags.
    Notes
    -----
    The flux columns need to be in units (specifiable in
    the fluxUnits1 and 2 config options) that can be converted
    to janskies. This action doesn't have any calibration
    information and assumes that the fluxes are already
    calibrated.
    """

    col1 = Field[str](doc="Column to subtract from")
    fluxUnits1 = Field[str](doc="Units for col1", default="nanojansky")
    col2 = Field[str](doc="Column to subtract")
    fluxUnits2 = Field[str](doc="Units for col2", default="nanojansky")
    returnMillimags = Field[bool](doc="Use millimags or not?", default=True)

    def getInputSchema(self) -> KeyedDataSchema:
        return ((self.col1, Vector), (self.col2, Vector))

    def __call__(self, data: KeyedData, **kwargs) -> Vector:
        flux1 = np.array(data[self.col1.format(**kwargs)]) * u.Unit(self.fluxUnits1)
        mag1 = flux1.to(u.ABmag)

        flux2 = np.array(data[self.col2.format(**kwargs)]) * u.Unit(self.fluxUnits2)
        mag2 = flux2.to(u.ABmag)

        magDiff = mag1 - mag2

        if self.returnMillimags:
            magDiff = magDiff.to(u.mmag)

        return np.array(magDiff.value)

class ExtinctionCorrectedMagDiff(VectorAction):
    """Compute the difference between two magnitudes and correct for extinction
    By default bands are derived from the <band>_ prefix on flux columns,
    per the naming convention in the Object Table:
    e.g. the band of 'g_psfFlux' is 'g'. If column names follow another
    convention, bands can alternatively be supplied via the band1 or band2
    config parameters.
    If band1 and band2 are supplied, the flux column names are ignored.
    """

    magDiff = ConfigurableActionField[VectorAction](
        doc="Action that returns a difference in magnitudes", default=MagDiff
    )
    ebvCol = Field[str](doc="E(B-V) Column Name", default="ebv")
    band1 = Field[str](
        doc="Optional band for magDiff.col1. Supercedes column name prefix",
        optional=True,
        default=None,
    )
    band2 = Field[str](
        doc="Optional band for magDiff.col2. Supercedes column name prefix",
        optional=True,
        default=None,
    )
    extinctionCoeffs = DictField[str, float](
        doc="Dictionary of extinction coefficients for conversion from E(B-V) to extinction, A_band."
        "Key must be the band",
        optional=True,
        default=None,
    )

    def getInputSchema(self) -> KeyedDataSchema:
        return self.magDiff.getInputSchema() + ((self.ebvCol, Vector),)

    def __call__(self, data: KeyedData, **kwargs) -> Vector:
        diff = self.magDiff(data, **kwargs)
        if not self.extinctionCoeffs:
            _LOG.debug("No extinction Coefficients. Not applying extinction correction")
            return diff

        col1Band = self.band1 if self.band1 else self.magDiff.col1.split("_")[0]
        col2Band = self.band2 if self.band2 else self.magDiff.col2.split("_")[0]

        # Return plain MagDiff with warning if either coeff not found
        for band in (col1Band, col2Band):
            if band not in self.extinctionCoeffs:
                _LOG.warning(
                    "%s band not found in coefficients dictionary: %s" " Not applying extinction correction",
                    band,
                    self.extinctionCoeffs,
                )
                return diff

        av1: float = self.extinctionCoeffs[col1Band]
        av2: float = self.extinctionCoeffs[col2Band]

        ebv = data[self.ebvCol]
        # Ignore type until a more complete Vector protocol
        correction = np.array((av1 - av2) * ebv) * u.mag  # type: ignore

        if self.magDiff.returnMillimags:
            correction = correction.to(u.mmag)

        return np.array(diff - correction.value)

class RAcosDec(VectorAction):
    """Construct a vector of RA*cos(Dec) in order to have commensurate values
    between RA and Dec."""

    raKey = Field[str](doc="RA coordinate", default="coord_ra")
    decKey = Field[str](doc="Dec coordinate", default="coord_dec")

    def getInputSchema(self) -> KeyedDataSchema:
        return ((self.decKey, Vector), (self.raKey, Vector))

    def __call__(self, data: KeyedData, **kwargs) -> Vector:
        ra = data[self.raKey]
        dec = data[self.decKey]
        return ra.to_numpy() * np.cos((dec.to_numpy() * u.degree).to(u.radian).value)

# Statistical vectorActions

class PerGroupStatistic(VectorAction):
    """Compute per-group statistic values and return result as a vector with
    one element per group. The computed statistic can be any function accepted
    by pandas DataFrameGroupBy.aggregate passed in as a string function name.
    """

    groupKey = Field[str](doc="Column key to use for forming groups", default="obj_index")
    buildAction = ConfigurableActionField[VectorAction](doc="Action to build vector", default=LoadVector)
    func = Field[str](doc="Name of function to be applied per group")

    def getInputSchema(self) -> KeyedDataSchema:
        return tuple(self.buildAction.getInputSchema()) + ((self.groupKey, Vector),)

    def __call__(self, data: KeyedData, **kwargs) -> Vector:
        df = pd.DataFrame({"groupKey": data[self.groupKey], "value": self.buildAction(data, **kwargs)})
        result = df.groupby("groupKey")["value"].aggregate(self.func)
        return np.array(result)

class ResidualWithPerGroupStatistic(VectorAction):
    """Compute residual between individual elements of group and the per-group
    statistic."""

    groupKey = Field[str](doc="Column key to use for forming groups", default="obj_index")
    buildAction = ConfigurableActionField(doc="Action to build vector", default=LoadVector)
    func = Field[str](doc="Name of function to be applied per group", default="mean")

    def getInputSchema(self) -> KeyedDataSchema:
        return tuple(self.buildAction.getInputSchema()) + ((self.groupKey, Vector),)

    def __call__(self, data: KeyedData, **kwargs) -> Vector:
        values = self.buildAction(data, **kwargs)
        df = pd.DataFrame({"groupKey": data[self.groupKey], "value": values})
        result = df.groupby("groupKey")["value"].aggregate(self.func)

        joinedDf = df.join(result, on="groupKey", validate="m:1", lsuffix="_individual", rsuffix="_group")

        result = joinedDf["value_individual"] - joinedDf["value_group"]
        return np.array(result)


import socket
import ipaddress

def get_ip_address():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.connect(("8.8.8.8", 80))
    return s.getsockname()[0]

def checkCentralRef(ip):
    new_ip = None
    if ip is None:
        new_ip = get_ip_address()
    else:
        try:
            new_ip = ipaddress.ip_address(ip)
            new_ip = ip
        except:
            print('invalid ip, using local ip')

    return new_ip


from selenium import webdriver

chrome_driver_path ="C:\\Users\\LadyDeveloper\\Documents\\Projetos\\software\\chromedriver.exe"
driver = webdriver.Chrome(executable_path=chrome_driver_path)

driver.get('https://www.python.org/')

# time = [li.text for li in driver.find_elements_by_tag_name('time')]
time = [li.text for li in driver.find_elements_by_css_selector('.event-widget > div:nth-child(1) > ul:nth-child(3) > li > time')]
events = [li.text for li in driver.find_elements_by_css_selector('.event-widget > div:nth-child(1) > ul:nth-child(3) > li > a')]

dict_events = {}

for item in range(len(events)):
    dict_events[item] = {
        "name": events[item],
        "time": time[item]
    }
print(dict_events)

driver.quit()


from django.contrib import admin
from django.utils.translation import ugettext_lazy as _

from planfood.common.models import Group
from .models import Dish, Norm, Output

class NormInline(admin.TabularInline):
    model = Norm

class OutputInline(admin.TabularInline):
    model = Output

@admin.register(Dish)
class DishAdmin(admin.ModelAdmin):
    fields = ('name', 'products')
    list_display = ('name', 'products_count', 'outputs_count', 'norms_count')
    filter_horizontal = ('products',)
    inlines = (OutputInline, NormInline)
    actions = ['create_norms_dishes', 'create_outputs_dishes']

    def products_count(self, obj):
        return obj.products.all().count()

    def norms_count(self, obj):
        return '{} ({})'.format(
            obj.norms.all().count(), obj.norms.filter(value=0).count()
        )

    def outputs_count(self, obj):
        return '{} ({})'.format(
            obj.outputs.all().count(), obj.outputs.filter(value='0').count()
        )

    def create_norms_dishes(self, request, queryset):
        groups = Group.objects.all()
        for dish in queryset:
            for group in groups.iterator():
                for product in dish.products.iterator():
                    for age_category in group.age_categories.iterator():
                        has_norm = dish.norms.filter(
                            product=product, group=group, age_category=age_category
                        ).exists()
                        if not has_norm:
                            norm = Norm.objects.create(
                                dish=dish,
                                product=product,
                                group=group,
                                age_category=age_category,
                                value=0.0,
                            )

    def create_outputs_dishes(self, request, queryset):
        groups = Group.objects.all()
        for dish in queryset:
            for group in groups.iterator():
                for age_category in group.age_categories.iterator():
                    has_output = dish.outputs.filter(
                        group=group, age_category=age_category
                    ).exists()
                    if not has_output:
                        output = Output.objects.create(
                            dish=dish, group=group, age_category=age_category, value='0'
                        )

    norms_count.short_description = _('Number of Products Norms')  # type: ignore
    outputs_count.short_description = _('Number of Products Outputs')  # type: ignore
    products_count.short_description = _('Number of Products')  # type: ignore
    create_norms_dishes.short_description = _(  # type: ignore
        'Creates norms for selected dishes'
    )
    create_outputs_dishes.short_description = _(  # type: ignore
        'Creates outputs for selected dishes'
    )


import sys
sys.dont_write_bytecode = True

from PyQt4 import QtCore, QtGui

import lib.gui.settings
import lib.settings
import stations
import lib.database

class MainWindow(QtGui.QMainWindow, lib.gui.settings.Ui_MainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        # inherited from Ui_MainWindow
        self.setupUi(self)
        self.setConnections()
        self.parent = parent

    def main(self):
        # access to parent's variables (main window's variables)
        self.parent.settings = self.parent.db_scisola.loadSettings(
                                                         self.parent.settings)
        if self.parent.settings:
            self.settings2GUI(self.parent.settings)

        # center window
        frame = self.frameGeometry()
        frame.moveCenter(QtGui.QDesktopWidget().availableGeometry().center())
        self.move(frame.topLeft())
        self.showMaximized()

    def closeEvent(self, event):
        self.parent.main()

    def setConnections(self):
        # right arrow -add- (distance selection)
        self.toolButton.clicked.connect(self.toolButtonClicked)
        # left arrow -remove- (distance selection)
        self.toolButton_2.clicked.connect(self.toolButton_2Clicked)
        # right arrow -add- (inversion time)
        self.toolButton_3.clicked.connect(self.toolButton_3Clicked)
        # right arrow -remove- (inversion time)
        self.toolButton_4.clicked.connect(self.toolButton_4Clicked)
        # right arrow -add- (inversion frequency)
        self.toolButton_5.clicked.connect(self.toolButton_5Clicked)
        # right arrow -remove- (inversion frequency)
        self.toolButton_6.clicked.connect(self.toolButton_6Clicked)
        # edit stations button
        self.pushButton_17.clicked.connect(self.editStationsButtonClicked)
        # apply settings
        self.pushButton.clicked.connect(self.applyButtonClicked)
        # browse button
        self.pushButton_14.clicked.connect(self.browseFileOnClicked_14)
        self.pushButton_2.clicked.connect(self.browseFileOnClicked_2)
        self.pushButton_20.clicked.connect(self.browseFileOnClicked_20)
        self.pushButton_18.clicked.connect(self.browseFileOnClicked_18)
        self.pushButton_16.clicked.connect(self.browseFileOnClicked_16)
        self.pushButton_19.clicked.connect(self.browseFileOnClicked_19)
        self.pushButton_9.clicked.connect(self.browseFileOnClicked_9)
        # update button
        self.pushButton_13.clicked.connect(self.updateButtonClicked)

    # browse functions
    def browseFileOnClicked_14(self):
        _text = QtGui.QFileDialog.getOpenFileName()
        if _text:
            self.lineEdit_19.setText(_text)

    def browseFileOnClicked_2(self):
        _text = QtGui.QFileDialog.getExistingDirectory()
        if _text:
            self.lineEdit_2.setText(_text)

    def browseFileOnClicked_20(self):
        _text = QtGui.QFileDialog.getOpenFileName()
        if _text:
            self.lineEdit_24.setText(_text)

    def browseFileOnClicked_18(self):
        _text = QtGui.QFileDialog.getOpenFileName()
        if _text:
            self.lineEdit_21.setText(_text)

    def browseFileOnClicked_16(self):
        _text = QtGui.QFileDialog.getOpenFileName()
        if _text:
            self.lineEdit_20.setText(_text)

    def browseFileOnClicked_19(self):
        _text = QtGui.QFileDialog.getOpenFileName()
        if _text:
            self.lineEdit_22.setText(_text)

    def browseFileOnClicked_9(self):
        _text = QtGui.QFileDialog.getExistingDirectory()
        if _text:
            self.lineEdit_3.setText(_text)

    def toolButtonClicked(self):
        _mag_min = self.doubleSpinBox.text()
        _mag_max = self.doubleSpinBox_2.text()
        _dist_min = self.spinBox_6.text()
        _dist_max = self.spinBox_8.text()
        _value = _mag_min + " <= magnitude <= " + _mag_max + " and " + \
                 _dist_min + " <= distance <= " + _dist_max
        self.listWidget.addItem(_value)

    def toolButton_2Clicked(self):
        self.listWidget.takeItem(self.listWidget.currentRow())

    def toolButton_3Clicked(self):
        _mag_min = self.doubleSpinBox_3.text()
        _mag_max = self.doubleSpinBox_5.text()
        _tl = self.comboBox_2.currentText()
        _value = _mag_min + " <= magnitude <= " + _mag_max + " and tl = " + \
                 _tl
        self.listWidget_2.addItem(_value)

    def toolButton_4Clicked(self):
        self.listWidget_2.takeItem(self.listWidget_2.currentRow())

    def toolButton_5Clicked(self):
        _mag_min = self.doubleSpinBox_9.text()
        _mag_max = self.doubleSpinBox_12.text()
        _f1 = self.doubleSpinBox_13.text()
        _f2 = self.doubleSpinBox_14.text()
        _f3 = self.doubleSpinBox_18.text()
        _f4 = self.doubleSpinBox_19.text()
        _value = _mag_min + " <= magnitude <= " + _mag_max + " and " + \
                 "frequencies = [" + _f1 + ", " + _f2 + ", " + _f3 + ", " + \
                 _f4 + "]"
        self.listWidget_3.addItem(_value)

    def toolButton_6Clicked(self):
        self.listWidget_3.takeItem(self.listWidget_3.currentRow())

    def editStationsButtonClicked(self):
        sta_win = stations.StationsWindow(self)
        sta_win.main()
        self.hide()

    def GUI2Settings(self):
        settings = lib.settings.Settings()
        settings.center_latitude = float(self.lineEdit_15.text())
        settings.center_longitude = float(self.lineEdit_14.text())
        settings.distance_range = int(self.spinBox_5.value())
        settings.magnitude_threshold = float(self.doubleSpinBox_4.value())
        settings.min_sectors = int(self.comboBox.currentText())
        settings.stations_per_sector = int(self.spinBox_7.value())
        settings.sources = int(self.spinBox_13.value())
        settings.source_step = int(self.spinBox_14.value())
        settings.clipping_threshold = float(self.doubleSpinBox_8.value())
        settings.time_grid_start = int(self.lineEdit_17.text())
        settings.time_grid_step = int(self.spinBox_15.value())
        settings.time_grid_end = int(self.lineEdit_18.text())
        settings.watch_interval = int(self.spinBox_2.value())
        settings.process_delay = int(self.spinBox_3.value())
        settings.process_timeout = int(self.spinBox_4.value())
        settings.crustal_model_path = str(self.lineEdit_19.text())
        settings.output_dir = str(self.lineEdit_2.text())
        settings.isola_path = str(self.lineEdit_3.text())
        settings.sc3_path = str(self.lineEdit_24.text())
        settings.sc3_scevtls = str(self.lineEdit_21.text())
        settings.sc3_scxmldump = str(self.lineEdit_20.text())
        settings.seedlink_path = str(self.lineEdit_22.text())
        settings.seedlink_host = str(self.lineEdit_23.text())
        settings.seedlink_port = int(self.spinBox_9.value())

         # distance selection list
        for i in xrange(self.listWidget.count()):
            _row = str(self.listWidget.item(i).text()).split(' ')
            settings.distance_selection.append([float(_row[0]),
                                                float(_row[4]),
                                                int(_row[6]),
                                                int(_row[10])
                                                ])

         # inversion time list
        for i in xrange(self.listWidget_2.count()):
            _row = str(self.listWidget_2.item(i).text()).split(' ')
            settings.inversion_time.append([float(_row[0]),
                                            float(_row[4]),
                                            float(_row[8])
                                            ])

         # inversion frequency list
        for i in xrange(self.listWidget_3.count()):
            _row = str(self.listWidget_3.item(i).text()).split(' ')
            settings.inversion_frequency.append([float(_row[0]),
                                                 float(_row[4]),
                                                 float(str(_row[8])[1:-1]),
                                                 float(str(_row[9])[:-1]),
                                                 float(str(_row[10])[:-1]),
                                                 float(str(_row[11])[:-1])
                                                 ])

        return settings

    def settings2GUI(self, settings):
        self.lineEdit_15.setText(str(settings.center_latitude))
        self.lineEdit_14.setText(str(settings.center_longitude))
        self.spinBox_5.setValue(int(settings.distance_range))
        self.doubleSpinBox_4.setValue(float(settings.magnitude_threshold))
        _index = self.comboBox.findText(str(settings.min_sectors))
        self.comboBox.setCurrentIndex(_index)
        self.spinBox_7.setValue(int(settings.stations_per_sector))
        self.spinBox_13.setValue(int(settings.sources))
        self.spinBox_14.setValue(int(settings.source_step))
        self.doubleSpinBox_8.setValue(float(settings.clipping_threshold))
        self.lineEdit_17.setText(str(settings.time_grid_start))
        self.spinBox_15.setValue(int(settings.time_grid_step))
        self.lineEdit_18.setText(str(settings.time_grid_end))
        self.spinBox_2.setValue(int(settings.watch_interval))
        self.spinBox_3.setValue(int(settings.process_delay))
        self.spinBox_4.setValue(int(settings.process_timeout))
        self.lineEdit_19.setText(str(settings.crustal_model_path))
        self.lineEdit_2.setText(str(settings.output_dir))
        self.lineEdit_3.setText(str(settings.isola_path))
        self.lineEdit_24.setText(str(settings.sc3_path))
        self.lineEdit_21.setText(str(settings.sc3_scevtls))
        self.lineEdit_20.setText(str(settings.sc3_scxmldump))
        self.lineEdit_22.setText(str(settings.seedlink_path))
        self.lineEdit_23.setText(str(settings.seedlink_host))
        self.spinBox_9.setValue(int(settings.seedlink_port))
        self.label_4.setText(str(settings.timestamp))
        self.listWidget.clear()
        self.listWidget_2.clear()
        self.listWidget_3.clear()

        for _entry in settings.distance_selection:
            _value = str(_entry[0]) + " <= magnitude <= " + str(_entry[1]) + \
                     " and " + str(_entry[2]) + " <= distance <= " + \
                     str(_entry[3])

            self.listWidget.addItem(_value)

        for _entry in settings.inversion_time:
            _value = str(_entry[0]) + " <= magnitude <= " + str(_entry[1]) + \
                     " and tl = " + str(_entry[2])

            self.listWidget_2.addItem(_value)

        for _entry in settings.inversion_frequency:
            _value = str(_entry[0]) + " <= magnitude <= " + str(_entry[1]) + \
                     " and frequencies = [" + str(_entry[2]) + ", " + \
                     str(_entry[3]) + ", " + str(_entry[4]) + ", " + \
                     str(_entry[5]) + "]"

            self.listWidget_3.addItem(_value)

        self.label_4.setText(str(settings.timestamp))

    # apply button
    def applyButtonClicked(self):
        self.__settings = self.GUI2Settings()
        self.setEnabled(False)
        _message = self.parent.db_scisola.saveSettings(self.__settings)

        if not _message:
            QtGui.QMessageBox.information(self, 'Saving to scisola',
                                          'Settings successfully applied.')
            self.parent.master_log.info("Settings successfully applied.")
            # apply new settings to system
            self.parent.settings = self.__settings
            # apply new settings to watcher
            if self.parent.watcher:
                self.parent.watcher.setConf(self.parent.settings)
                self.parent.watcher.restart()

        else:
            QtGui.QMessageBox.critical(self, 'Saving to scisola',
                                       "Could not save settings.\nError:\n" +\
                                       str(_message[:2]))
        self.setEnabled(True)

    # update button
    def updateButtonClicked(self):
        if self.checkBox.checkState() == QtCore.Qt.Checked:
            _reset = True
        else:
            _reset = False

        self.setEnabled(False)
        _message = self.parent.db_scisola.importFromSc3(self.parent.db_sc3,
                                                        reset=_reset)

        if not _message:
            QtGui.QMessageBox.information(self, 'Saving to scisola',
                                          'Stations successfully updated.')
            self.parent.master_log.info("Stations successfully updated.")
        else:
            QtGui.QMessageBox.critical(self, 'Saving to scisola',
                                   "Could not update stations.\nError:\n" +\
                                   str(_message[:2]))
        self.setEnabled(True)


import os
from http.client import HTTPResponse
from django.shortcuts import render, redirect
from blog.models import Post, Comentario
from blog.forms import ComentarioForm
from django.views.generic import DetailView
from django.contrib import messages
from blog.forms import ComentarioForm, PostearForm
from django.views.generic import DetailView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy

# Create your views here.

def index(request):
    ls = list(Post.objects.all().order_by('-id'))
    context = {'post_list': ls,
                }
    return render(request,'index.html',context=context)

def contacto(request):
    ig = '@instagram'
    fc = 'orquestavientosdecambio'
    tw = 'twitter'
    ubi = 'Roque Sáenz Peña, Chaco, Argentina'
    telhiper = '549'+'03644574664'
    tel = '03644-574664'
    email = 'orquestaescuelavientosdecambio@gmail.com'
    return render(request,'contacto.html',context={'ig':ig,
                                                    'fc':fc,
                                                    'tw':tw,
                                                    'ubi':ubi,
                                                    'telhiper':telhiper,
                                                    'tel':tel,
                                                    'email':email,
                                                    })

def about(request):
    return render(request,'about.html')

def post_list(request):
    ls = list(Post.objects.all().order_by('-id'))
    context = {'post_list': ls,
                }
    return render(request,'test.html',context)

class DetallesPost(DetailView):

    model = Post
    template_name = 'single-post.html'

    def get_context_data(self, **kwargs):
        data = super().get_context_data(**kwargs)

        comentarios_conectados = Comentario.objects.filter(
            post=self.get_object()).order_by('-fecha_publicacion')

        data['comentarios'] = comentarios_conectados

        if self.request.user.is_authenticated:
            data['comentario_form'] = ComentarioForm(instance=self.request.user)

        return data

    def post(self, request, *args, **kwargs):
        nuevo_comentario = Comentario(texto=request.POST.get('texto'),
                                  autor=self.request.user,
                                  post=self.get_object())
        nuevo_comentario.save()
        return self.get(self, request, *args, **kwargs)

'''def single_post(request,postid,slug):

    comentario_form = ComentarioForm()

    context = {
        'post': Post.objects.get(id=postid),
        'comentario_form': comentario_form,

                }
    return render(request,'single-post.html',context)'''

'''def crear_post(request):
    if request.method == "POST":
        form = FormPost(request.POST, request.FILES)
        if form.is_valid():
            post = form.save(commit=False)
            post.author_id = request.user.id
            post.save()
            titulo = form.cleaned_data.get("title")
            messages.success(request, f"El post {titulo} se ha creado correctamente")
            return redirect("post")
        else:
            for msg in form.error_messages:
                messages.error(request, form.error_messages[msg])
    form = FormPost()
    return render(request, "crear_post.html", {"form": form})'''

class Postear(CreateView):
    model = Post
    template_name = 'crear_post.html'
    fields = ('titulo','texto')

    def get_context_data(self, **kwargs):
        data = super().get_context_data(**kwargs)

        if self.request.user.is_authenticated:
            data['post_form'] = PostearForm(instance=self.request.user)

        return data

    def post(self, request, *args, **kwargs):
            nuevo_post = Post(texto=request.POST.get('texto'),
                                    autor=self.request.user,
                                    titulo=self.request.POST.get('titulo')
                                    )
            nuevo_post.save()

            return self.get(self, request, *args, **kwargs)

class EditarPost(UpdateView):
    model = Post
    template_name = 'editar_post.html'
    fields = ('titulo','texto','imagen')

class EliminarPost(DeleteView):
    model = Post
    template_name = 'eliminar_post.html'
    success_url = reverse_lazy('home')


from django.db.models import Count, OuterRef, Subquery, F, ExpressionWrapper, IntegerField
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.functions import Coalesce
from django.shortcuts import render, redirect
from django.http import HttpResponse
from contextlib import suppress
import simplejson as json
from .models import Tag, Question, Deck, Pain, Answer, Vote

# Landing
def index(request):
      return HttpResponse("hello, world you are at card question")

def landing(request):
  all_tags = Tag.objects.all().annotate(num_questions=Count('question')).order_by('-num_questions')
  top_tags = all_tags[0:3]
  return render(request, 'flashr/landing.html', {'all_tags': all_tags, 'top_tags': top_tags})

#Questions
#show one card from anywhere to anywhere
@login_required
def card_show(request, **kwargs):
  print('is this thing ieven ariirna')
  profile = request.user.profile
  if 'idx' in kwargs:
    idx = kwargs['idx']
    deck = Deck.objects.filter(profile=profile) #grabs the subquery so only one db delve
    count = deck.count() #counts the cards obv
    card = deck.get(order_idx=idx).question #gets the single card in question
    values = {'question': card, 'tag': kwargs['tag'], 'deck_idx': idx, 'last_card': (idx == count)}
    # values['last_card'] = (idx == count)
  else:
    card = Question.objects.get(pk=kwargs['pk'])
    values = {'question': card}

  values['card_tags'] = card.tags.all()
  print('pre community gathering')
  values['community'] = Answer.objects.filter(question=card, public=True).exists()
  with suppress(ObjectDoesNotExist):
    print('no valuse')
    values['current_answer'] = Answer.objects.get(author=profile, question=card)
    print('curent answer value', values['current_answer'])
  with suppress(ObjectDoesNotExist):
    values['pain'] = Pain.objects.filter(profile=profile, question=card).latest('time_stamp')#.order_by('-time_stamp')[0:1].get()

  return render(request, 'flashr/card.html', values)

@login_required
def deck_create(request, tag):
  user = request.user
  tag.lower()
  # Delete records for any existing deck
  Deck.objects.filter(profile=user.profile).delete()
  # Find all the questions for the new deck
  deck = Question.objects.filter(tags__content=tag)

  # Note: targeting specific user part is not fully tested
  # Create a subquery that gets its id # from the calling query. Gets a users latest pain for a specific question
  # values('level')[:1] extracts just the level, trimmed to the latest one based on the order_by sort
  # Using '.latest()', '[0]', and '.level' don't work since they trigger immediate evaluation in the subquery
  pains = Pain.objects.all().prefetch_related('profile', 'question') # get once and cache in pains variable
  user_pain = pains.filter(profile=user.profile,question__id=OuterRef('pk')).order_by('-time_stamp').values('level')[:1]
  # Use the subquery to add the users latest pain to all of the questions with the deck tag.
  deck_pain = deck.annotate(user_pain=Subquery(user_pain))#.values('user_pain')
  # Now sort but user pain, descending, with nulls first
  deck_sorted = deck_pain.order_by(F('user_pain').desc(nulls_first=True))

  # for idx, card in enumerate(deck):
  #   Deck.objects.create(profile=user.profile, question=deck[idx], order_idx=(idx+1))
  for idx, card in enumerate(deck_sorted):
    Deck.objects.create(profile=user.profile, question=card, order_idx=(idx+1))

  return redirect('deck_show', tag=tag, idx=1)

@login_required
def card_community(request, **kwargs):
  profile = request.user.profile
  if 'idx' in kwargs:
    deck = Deck.objects.filter(profile=profile)
    question = deck.get(order_idx=kwargs['idx']).question #gets the single card in question
  else:
    question = Question.objects.get(pk=kwargs['pk'])

  card_tags = question.tags.all()

  # Cache all public answers for current question, and all votes for those answers
  answers = Answer.objects.filter(public=True,question=question).prefetch_related('question')
  votes = Vote.objects.filter(answer__question=question).prefetch_related('answer')
  ## get all the users votes
  user_votes = votes.filter(profile=profile)
  ## append them to the relevant answer
  user_votes = votes.filter(profile=profile,answer_id=OuterRef('pk')).values('vote')
  answers = answers.annotate(user_vote=Subquery(user_votes))

  ## Count up all the votes and sort by the top Answers
  ## Step 1: Count up the yes' and the no's
  # 1. a) Subqueries: Count up number of votes, filtered by answer and vote type. Saves it to 'yes' or 'no', and returns it.
  count_yes = votes.filter(answer__id=OuterRef('pk'),vote=1).values('vote').annotate(yes=Count('vote')).values('yes')
  count_no = votes.filter(answer__id=OuterRef('pk'),vote=-1).values('vote').annotate(no=Count('vote')).values('no')
  # 1. b) Main outer queries: Runs subquery on all answers for current question, saving returned vote counts to each answer.
  # Note: The only thing Coalesce is doing is replacing any 'none' responses with '0'
  answers = answers.annotate(yes_count=Coalesce(Subquery(count_yes, output_field=IntegerField()), 0))
  answers = answers.annotate(no_count=Coalesce(Subquery(count_no, output_field=IntegerField()), 0))
  # answers[0].yes_count
  # answers[0].no_count
  ## Step 2: Subtract no's from yes' and save as the total. (Coalesce setting 0's instead of Nones makes this work)
  annotated = answers.annotate(total_vote=ExpressionWrapper(F('yes_count') - F('no_count'), output_field=IntegerField()))
  # annotated[0].total_vote
  ## Step 3: Sort by the new total_vote field, highest count first.
  annotated_sorted = annotated.order_by(F('total_vote').desc(nulls_last=True))

  ## Notes on aggregate Sum vs annotate Count:
  # Aggregate works for individual answers, but you can't put .aggregate() inside a subquery. It evaluates right away.
  # sum_votes = Vote.objects.filter(answer__id=1).aggregate(yes=Sum('vote'))
  # So instead we need to count up all the Yes votes and all the No votes, and then subtract them

  values = {'question': question, 'card_tags': card_tags, 'answers': annotated_sorted}
  values['community'] = Answer.objects.filter(question=question, public=True).exists()
  with suppress(ObjectDoesNotExist):
    print('no valuse')
    values['current_answer'] = Answer.objects.get(author=profile, question=question)
    print('curent answer value', values['current_answer'])
  with suppress(ObjectDoesNotExist):
    values['pain'] = Pain.objects.filter(profile=profile, question=question).latest('time_stamp')#.order_by('-time_stamp')[0:1].get()
  return render(request, 'flashr/card_community.html', values)

## API Endpoints
# Send PAIN API Endpoint
def send_pain(request):
  # print(request.POST)
  if request.method == 'POST':
    profile = request.user.profile
    pain_level = request.POST['level']
    question = Question.objects.get(id=request.POST['question_id'])
    Pain.objects.create(level=pain_level, question=question, profile=profile)

    response = {}
    response['status'] = 200
    response['pain_level'] = pain_level

    return HttpResponse(json.dumps(response), content_type="application/json")
  else:
    return HttpResponse(json.dumps({"error": "wasn't a POST request"}), content_type="application/json")

# Send Answer API Endpoint
def send_answer(request):
  print(request.POST)
  if request.method == 'POST':
    profile = request.user.profile
    content = request.POST['content']
    public = ((True, False) [int(request.POST['public']) == 1 ])
    question = Question.objects.get(id=request.POST['question_id'])

    with suppress(ObjectDoesNotExist):
      Answer.objects.get(author=profile,question=question ).delete()

    Answer.objects.create(content=content, public=public, question=question, author=profile)

    response = {}
    response['status'] = 200
    response['content'] = content
    response['public'] = public

    return HttpResponse(json.dumps(response), content_type="application/json")
  else:
    return HttpResponse(json.dumps({"no info": "wasn't a POST request"}), content_type="application/json")

# Send Vote API Endpoint
def send_vote(request):
  # print(request.POST)
  if request.method == 'POST':
    profile = request.user.profile
    vote_choice = int(request.POST['vote'])
    answer = Answer.objects.get(id=request.POST['answer_id'])
    action = request.POST['action']

    # print('vote:', vote_choice)
    # print('type? ', type(vote_choice))
    # print('answer :', answer)
    response = {}
    # Validate the vote value for the expected values
    if (vote_choice == 1) or (vote_choice == -1):
      # print('yes or no vote detected...')
      # Delete any existing votes from this user for this answer
      Vote.objects.filter(profile=profile, answer=answer).delete()
      # Add the user's vote for this question
      Vote.objects.create(vote=vote_choice, answer=answer, profile=profile)
      response['status'] = 200
      response['vote'] = vote_choice
      response['a_id'] = answer.id
    elif (vote_choice == 0):
      # print('un-vote detected...')
      # 0 means they are 'un-voting', ie taking back a vote
      # Delete any existing votes from this user for this answer
      Vote.objects.filter(profile=profile, answer=answer).delete()
      response['status'] = 200
      response['vote'] = vote_choice
      response['action'] = action
      response['a_id'] = answer.id
    else:
      # print('invalid vote detected...')
      # Not a valid vote
      response['status'] = 400
      response['error'] = "Invalid vote option"

    return HttpResponse(json.dumps(response), content_type="application/json")
  else:
    return HttpResponse(json.dumps({"error": "wasn't a POST request"}), content_type="application/json")