from keras.models import Model, Sequential
from keras.layers import Input, Dropout, Dense
import matplotlib.pyplot as plt
import numpy as np

from base_neural_network import BaseNeuralNetwork

class AutoencoderNetwork(BaseNeuralNetwork):
    def __init__(self, output_dir, **kwargs):
        self.num_features = kwargs['num_features']
        super(AutoencoderNetwork, self).__init__(output_dir=output_dir, **kwargs)

    def _load_model(self):
        inputs = Input((self.num_features,), name='inputs')

        dense1 = Dense(int(self.num_features / 2), activation="sigmoid")(inputs)

        dense2 = Dense(int(self.num_features / 3), activation="sigmoid")(dense1)

        dense3 = Dense(int(self.num_features / 2), activation="sigmoid")(dense2)

        dense4 = Dense(self.num_features, activation="sigmoid")(dense3)

        self.model = Model(inputs=inputs, outputs=dense4)

        self._compile()

    def train(self, training_dataset, validation_dataset):
        return super(AutoencoderNetwork, self).train((training_dataset[0],training_dataset[0]),(validation_dataset[0],validation_dataset[0]))

    def visualize(self, **kwargs):

        X_outliers = kwargs['X_outliers']
        X_validation = kwargs['X_validation']
        y_pred_outliers = kwargs['y_pred_outliers']
        y_pred_validation = kwargs['y_pred_validation']

        euclidian_distance_outliers = np.linalg.norm(X_outliers - y_pred_outliers, axis=1)
        euclidian_distance_validation = np.linalg.norm(X_validation - y_pred_validation, axis=1)

        plt.title('Euclidian distance')
        plt.ylabel('X - y_pred')
        plt.xlabel('Instance number')
        plt.plot(euclidian_distance_outliers, 'ro', euclidian_distance_validation, 'bo')

        plt.show()


import enum
from typing import NamedTuple

class AttributeType(enum.Enum):
    """DICOM Data Element attribute requirement types.

    See <https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_7.4.html>
    """

    REQUIRED = "1"
    CONDITIONALLY_REQUIRED = "1C"
    REQUIRED_EMPTY_IF_UNKNOWN = "2"
    CONDITIONALLY_REQUIRED_EMPTY_IF_UNKNOWN = "2C"
    OPTIONAL = "3"

class Usage(str, enum.Enum):
    """DICOM module usage requirements

    See the usage column within
    <https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_A.19.3.html#table_A.19.3-1>
    """

    MANDATORY = "M"
    USER_OPTIONAL = "U"

class Inheritance(str, enum.Enum):
    """Whether or not a given module should be inherited from the
    original CT series, or if it should be created fresh within the
    new RT Structure file.
    """

    CREATE = "create"
    INHERIT = "inherit"

class ModuleOptions(NamedTuple):
    """The options for a given module.

    Used to represent the following table:
    https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_A.19.3.html#table_A.19.3-1
    """

    usage: Usage
    inheritance: Inheritance

# Encoding of the RT Structure Set IOD Modules table. Original table
# available at:
# <https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_A.19.3.html#table_A.19.3-1>
RTSTRUCT_DICOM_MODULES = {
    # IE Patient
    "patient": ModuleOptions(Usage.MANDATORY, Inheritance.INHERIT),
    "clinical-trial-subject": ModuleOptions(Usage.USER_OPTIONAL, Inheritance.INHERIT),
    # IE Study
    "general-study": ModuleOptions(Usage.MANDATORY, Inheritance.INHERIT),
    "patient-study": ModuleOptions(Usage.USER_OPTIONAL, Inheritance.INHERIT),
    "clinical-trial-study": ModuleOptions(Usage.USER_OPTIONAL, Inheritance.INHERIT),
    # IE Series
    "rt-series": ModuleOptions(Usage.MANDATORY, Inheritance.CREATE),
    "clinical-trial-series": ModuleOptions(Usage.USER_OPTIONAL, Inheritance.CREATE),
    # IE Equipment
    "general-equipment": ModuleOptions(Usage.MANDATORY, Inheritance.CREATE),
    # IE Frame of Reference
    "frame-of-reference": ModuleOptions(Usage.USER_OPTIONAL, Inheritance.INHERIT),
    # IE Structure Set
    "structure-set": ModuleOptions(Usage.MANDATORY, Inheritance.CREATE),
    "roi-contour": ModuleOptions(Usage.MANDATORY, Inheritance.CREATE),
    "rt-roi-observations": ModuleOptions(Usage.MANDATORY, Inheritance.CREATE),
    "approval": ModuleOptions(Usage.USER_OPTIONAL, Inheritance.CREATE),
    "general-reference": ModuleOptions(Usage.USER_OPTIONAL, Inheritance.CREATE),
    "sop-common": ModuleOptions(Usage.MANDATORY, Inheritance.CREATE),
    "common-instance-reference": ModuleOptions(Usage.USER_OPTIONAL, Inheritance.CREATE),
}


import argparse
import json
import logging
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib import colormaps
import numpy as np
import pathlib
import shutil
import sys
import textwrap
import time
import traceback
import wp3

# Remove interaction buttons to prevent the user change the canvas accidentally.
mpl.rcParams["toolbar"] = "None"

logger = logging.getLogger("wp3_designer")

def main():
    # Get configuration file and working directory.
    output_dir, settings = wp3.open_project()
    logger.info(f"Working on project '{output_dir.name}'.")

    # Extract a list of materials.
    materials_list = wp3.load_materials(settings)

    # Load a design from the settings, if this is available.
    initial_tiling = settings["panels"].get("initial_tiling")
    logger.info(
        "Loaded initial tiling from 'config.yaml'."
        if initial_tiling is not None
        else "No initial tiling found in loaded settings."
    )

    # Load the requested Tile sub-class.
    TileClass = wp3.Tile.load_tile_type(settings["panels"]["tiles"])

    # Create a 'cad' directory within the project and export STLs inside it.
    cad_save_path = output_dir.joinpath("cad")
    logger.debug(f"Creating directory '{cad_save_path}'.")
    cad_save_path.mkdir(parents=True, exist_ok=True)
    stl_exported = TileClass(
        0,
        0,
        settings["panels"]["spacing"],
        settings["panels"]["tiles"].get("variant", 0),
    ).export_stl(cad_save_path, **settings.get("freecad", {}))
    logger.info(f"STL export: {'success' if stl_exported else 'failure'}.")

    # Fill the available design space with the chosen tiles.
    tiles = []
    for row in range(settings["panels"]["rows"]):
        for col in range(settings["panels"]["columns"]):
            tile = TileClass(
                row,
                col,
                settings["panels"]["spacing"],
                settings["panels"]["tiles"].get("variant", 0),
            )
            tiles.append(tile)
            logger.debug(
                f"Added new tile in (row, col) = ({row}, {col}). "
                f"Cartesian coordinates: ({tile.x}, {tile.y})."
            )
            # If an initial design is provided and this tile is not part of it,
            # make sure to hide it.
            if initial_tiling is not None and [row, col] not in initial_tiling:
                logger.debug(
                    "Hiding this tile as it is not part of the initial tiling."
                )
                tile.set_visible(False)

    # Calculate the dimensions of the canvas.
    canvas_width, canvas_height = wp3.get_bounding_box_size(tiles)
    logger.debug(f"Canva's dimensions: {canvas_width} by {canvas_height}.")

    # Create a figure to plot the tiles.
    fig, ax = wp3.tight_figure(tiles)

    # Place the tiles in the plot.
    wp3.add_tiles_to_axes(tiles, ax)

    # For each tile, create an event connection that toggles its visibility if
    # one clicks inside its patch.
    for tile in tiles:
        logger.debug(
            f"Creating mpl connection for tile ({tile.row}, {tile.col})."
        )
        fig.canvas.mpl_connect(
            "button_press_event",
            lambda event, tile=tile: wp3.toggle_tile_if_clicked(
                event, tile, ax
            ),
        )

    # Create a connection to hide, show or toggle all tile visibilities at once
    # using the keyboard.
    fig.canvas.mpl_connect(
        "key_press_event", lambda event: wp3.toggle_all_tiles(event, tiles, ax)
    )

    # Detect when a figure is closed using an event connection.
    exit_helper = wp3.Struct(keep_running=True)
    fig.canvas.mpl_connect(
        "close_event", lambda event: setattr(exit_helper, "keep_running", False)
    )

    # Period and shift factor to color all tiles using a HSV wave.
    period = 4.0
    velocity = 0.005

    # Loop that changes the color of each tile and then updates the plot.
    # The loop will stop once the figure is closed.
    logger.debug("Entering main loop to select the tiles.")
    while exit_helper.keep_running:
        for i, tile in enumerate(tiles):
            tile.patch.set_color(
                colormaps["hsv"](((time.time() / period - i * velocity) % 1))
            )
        plt.pause(0.05)
    logger.debug("Designer window was closed by the user.")

    # Collect all tiles (and their coordinates) in the chosen design.
    visible_tiles = []
    tiling_coordinates = []
    for tile in tiles:
        if tile.is_visible():
            visible_tiles.append(tile)
            tiling_coordinates.append([tile.row, tile.col])
    logger.debug(
        "Initial tiling array (to be stored in 'config.yaml'):"
        f" {tiling_coordinates}."
    )

    # Store the current design in the YAML configuration file.
    wp3.update_initial_tiling(output_dir, settings, tiling_coordinates)

    # Read how many segments should be generated in the routing problem.
    segments = 1
    if settings.has("routing", "segments") and settings.has(
        "routing", "tiles_per_segment"
    ):
        raise RuntimeError(
            "Sorry, but the parameters 'routing/segments' and "
            "'routing/tiles_per_segment' should not be given at the same "
            "time. Please, update 'config.yaml' and re-launch the designer."
        )
    if settings.has("routing", "segments"):
        segments = settings["routing"]["segments"]
        logger.debug(f"Found parameter 'routing/segments'; value: {segments}.")
    elif settings.has("routing", "tiles_per_segment"):
        tiles_per_segment = settings["routing"]["tiles_per_segment"]
        segments = np.ceil(len(visible_tiles) / tiles_per_segment).astype(int)
        logger.debug(
            "Found parameter 'routing/tiles_per_segment'; value:"
            f" {tiles_per_segment}."
        )

    # Setup the routing problem and generate an initial solution, either from
    # a pre-computed one or at random.
    logger.debug(f"Creating routing problem with {segments} segments.")
    routing = wp3.Routing(visible_tiles, segments=segments)
    best_routing = routing.random_sample()
    if settings.has("routing", "cache"):
        cache = np.array(settings["routing"]["cache"])
        logger.debug(f"Loaded routing cache: {cache.tolist()}.")
        if (
            cache.shape == best_routing.shape
            and cache[1].max() < routing.vertices_per_tile
        ):
            best_routing = cache
        else:
            logger.info(
                "Loaded routing cache is not compatible with the "
                "current tiling. This might simply mean that routing "
                "was previously performed on a different design."
            )

    # Read routing parameters from the YAML configuration.
    routing_kwargs = settings.extract("routing", {})

    # Remove parameters that are not to be passed to Routing.optimize.
    if "cache" in routing_kwargs:
        logger.debug("Removing parameter 'cache' from 'routing_kwargs'.")
        routing_kwargs.pop("cache")
    if "segments" in routing_kwargs:
        logger.debug("Removing parameter 'segments' from 'routing_kwargs'.")
        routing_kwargs.pop("segments")
    if "tiles_per_segment" in routing_kwargs:
        logger.debug(
            "Removing parameter 'tiles_per_segment' from 'routing_kwargs'."
        )
        routing_kwargs.pop("tiles_per_segment")
    logger.debug(f"Routing keyword arguments: {routing_kwargs}.")

    # Optimize cable connections (routing). This is done in a loop that roughly
    # looks like:
    # 1. Show current routing in a figure;
    # 2. If the user presses the space bar:
    #    - Close the figure;
    #    - Improve the solution;
    #    - Go back to 1;
    # 3. If the user closes the routing window: exit.
    while True:
        # Create a figure to plot the tiles.
        fig, ax = wp3.tight_figure(visible_tiles)

        # Add visible tiles to the created Axis instance.
        wp3.add_tiles_to_axes(
            visible_tiles,
            ax,
            copy=True,
            patch_color="white",
            border_color="lightgray",
        )

        # Save the content of the figure so that one can try to manually draw a
        # routing if needed.
        design_file_name = output_dir.joinpath("wp3_design.pdf")
        logger.debug(f"Saving current design into '{design_file_name}'.")
        fig.savefig(design_file_name, bbox_inches="tight")

        # Add the proposed routing to the Axis.
        routing.plot_routing(best_routing, visible_tiles, ax)

        # Establish connections to be able to detect when the user presses the
        # space bar or closes the figure.
        logger.debug(
            "Creating connections to detect when the user presses the "
            "space bar or closes the figure."
        )
        exit_helper = wp3.Struct(keep_running=True, reroute=False)
        fig.canvas.mpl_connect(
            "key_press_event",
            lambda event: setattr(exit_helper, "reroute", event.key == " "),
        )
        fig.canvas.mpl_connect(
            "close_event",
            lambda event: setattr(exit_helper, "keep_running", False),
        )

        # Loop that waits for user interaction.
        logger.debug(
            "Waiting for the user to press the space bar or close the active"
            " figure."
        )
        while exit_helper.keep_running and not exit_helper.reroute:
            plt.pause(0.05)

        # Save the current routing into a figure, to make sure that it is safely
        # stored somewhere.
        routing_file_name = output_dir.joinpath("wp3_routing.pdf")
        logger.debug(f"Saving current routing into '{routing_file_name}'.")
        fig.savefig(routing_file_name, bbox_inches="tight", dpi=500)
        plt.close("all")

        # Decide what to do depending on the user's choice.
        if exit_helper.reroute:
            # Try to improve the routing path.
            logger.debug("Rerouting requested.")
            routing_cost_before = routing.evaluate_cost(best_routing)
            best_routing = routing.optimize(
                best_sample=best_routing, **routing_kwargs
            )
            routing_cost_after = routing.evaluate_cost(best_routing)
            print(
                "Cost decreased by",
                np.round(
                    100
                    * (routing_cost_before - routing_cost_after)
                    / routing_cost_before,
                    3,
                ),
            )
        else:
            # Routing completed!
            logger.debug("Figure closed: routing completed.")
            break

    logger.info(
        "Design and routing files have been saved into "
        f"'{design_file_name}' and '{routing_file_name}'."
    )

    # Store current routing.
    wp3.update_cached_routing(output_dir, settings, best_routing.tolist())

    # List of items to be purchased/manufactured.
    bill_of_materials = []

    # Add walls and joints to the list of materials.
    logger.info("Adding CAD components to the bill of materials.")
    for part, quantity in TileClass.count_cad_parts(visible_tiles):
        bill_of_materials.append(
            wp3.BillItem(name=part, quantity=quantity, category="3D printed")
        )

    # Process panel materials. The goal is, for each panel type, to evaluate how
    # many tiles can be inserted, or how much it costs to fill them (in case the
    # cost depends on the size).
    # TODO: it would be ideal to allow, for variable size materials, to let
    # the designer try with different sizes as well.
    panel_material_data = {}

    # Generated tiling files for all materials should be first saved into a
    # temporary file and then copied if needed.
    tiling_temp_dir = output_dir.joinpath("tiling_temp")
    logger.debug(
        f"Creating temporary directory '{tiling_temp_dir}' to store "
        "temporary tiling files."
    )
    tiling_temp_dir.mkdir(exist_ok=True)

    for layer_name, layer in materials_list["sheets"].items():
        # Get the size of the current sheet.
        width, height = layer["size"]
        logger.debug(
            f"Processing material '{layer_name}' with dimensions"
            f" {width}x{height}."
        )

        # Try to fill the given sheet with all possible variants of the same
        # tile family.
        tilings = []
        for variant in TileClass.get_variants():
            logger.debug(f"Tiling '{layer_name}' with variant {variant}.")
            tiling = TileClass.fit_in_sheet(
                len(visible_tiles),
                0,
                variant,
                width,
                height,
            )
            logger.debug(f"Tiling completed, it contains {len(tiling)} tiles.")
            if len(tiling) > 0:
                tilings.append(tiling)

        # If no tiling was possible, just skip this sheet.
        if len(tilings) == 0:
            logger.warn(f"Could not fit any tile in {layer_name}.")
            continue

        # Choose the best tiling variant. If the sheet has variable height,
        # the best variant is the one that minimizes the height of the sheet
        # itself. If the size is fixed, the best variant is the one that uses
        # the least amount of material per sheet. In both cases, evaluate the
        # height of the sheet and its cost.
        if height == np.inf:
            heights = np.array(
                [wp3.get_bounding_box_size(tiling)[1] for tiling in tilings]
            )
            idx = heights.argmin()
            bb_height = heights[idx]
            tiles_layer = tilings[idx]
            layer_cost = layer.get("cost", 0) * bb_height
            logger.debug(
                f"The sheet '{layer_name}' had variable size. After "
                f"tiling, the heights {heights} were calculated. The "
                f"optimal one (#{idx}) has height {bb_height} and "
                f"cost {layer_cost}."
            )
        else:
            areas = np.array(
                [wp3.get_bounding_box_area(tiling) for tiling in tilings]
            )
            idx = areas.argmin()
            tiles_layer = tilings[idx]
            layer_cost = layer.get("cost", 0)
            bb_height = height
            logger.debug(
                f"The sheet '{layer_name}' had fixed size. After "
                f"tiling, the covered areas {areas} were calculated. "
                f"The optimal one (#{idx}) has area {areas[idx]}."
            )

        # Store the details of this sheet for the cost optimization, performed
        # later in the script.
        panel_material_data[layer_name] = wp3.Struct(
            name=layer_name,
            value=len(tiles_layer),
            cost=layer_cost,
            variable_size=height == np.inf,
            height=bb_height,
            unit_cost=layer.get("cost", 0),
        )

        # The figure is used to show how tiles should be cut from each sheet.
        fig, ax = plt.subplots()
        ax.set_aspect("equal")
        ax.set_xlim(0, width)
        ax.set_ylim(0, bb_height)
        ax.set_title(layer_name)
        fig.tight_layout()

        # Change the visual properties of the patches edges.
        for tile in tiles_layer:
            tile.patch.set_linewidth(0.2)
            tile.patch.set_edgecolor("black")

        # Add the patches to the figure.
        wp3.add_tiles_to_axes(tiles_layer, ax, patch_color="lightgray")

        # Save the tiling into a file.
        temp_tiling_file_name = tiling_temp_dir.joinpath(
            f"tiling_{layer_name}.pdf"
        )
        logger.debug(f"Saving temporary tiling in '{temp_tiling_file_name}'.")
        fig.savefig(temp_tiling_file_name, bbox_inches="tight")
        plt.close("all")

    if settings.has("assembly", "sheets"):
        # For each group of materials that can be used to manufacture the tiles,
        # evaluate the cheapest combination of articles that can be purchased.
        for i, materials in enumerate(settings["assembly"]["sheets"]):
            logger.debug(f"Processing sheets assembly #{i}.")

            # Make sure that the list of sheets is not empty.
            if len(materials) == 0:
                raise RuntimeError(
                    f"Error in assembly list 'sheets/{i}': no materials"
                    " specified."
                )

            # Get the list of sheets that have to be purchased, and their
            # quantity.
            components, cost, _ = wp3.named_tree_search(
                [panel_material_data[m] for m in materials], len(visible_tiles)
            )
            logger.debug(
                f"Optimal components in assembly: {components}. Total cost:"
                f" {cost}."
            )

            # For each sheet type to be purchased, add a line in the bill of
            # materials that specifies how many sheets to buy (or their length
            # in the case of variable-length sheets).
            for component, quantity in components:
                logger.debug(
                    f"Adding {quantity} units of '{component}' to the "
                    "bill of materials."
                )
                url = materials_list["sheets"][component.name].get("url")
                url_md = f"[url link]({url})" if url is not None else ""
                if component.variable_size:
                    quantity = np.round(component.height, 3)
                    logger.debug(
                        f"Changed quantity to {quantity} for "
                        f"variable-sized component '{component}'."
                    )
                bill_of_materials.append(
                    wp3.BillItem(
                        name=component.name,
                        quantity=quantity,
                        cost=component.unit_cost,
                        category=f"sheets-{i}",
                        notes=url_md,
                    )
                )

                # Since this material has to be purchased, copy its tiling
                # scheme from the temporary folder to the project.
                tiling_file_name = tiling_temp_dir.joinpath(
                    f"tiling_{component.name}.pdf"
                )
                logger.info(
                    f"Saving tiling for '{component.name}' as "
                    f"'{tiling_file_name.name}'."
                )
                shutil.copy(tiling_file_name, output_dir)
    else:
        logger.info(
            "Parameter 'assembly/sheets' not found. You can add "
            "assemblies following the instructions available at "
            "https://github.com/francofusco/wp3#assembly-settings."
        )

    logger.debug(f"Removing temporary folder '{tiling_temp_dir}'.")
    shutil.rmtree(tiling_temp_dir)

    if settings.has("assembly", "leds"):
        # Process LED materials: evaluate how many LED strips have to be
        # purchased.
        for i, materials in enumerate(settings["assembly"]["leds"]):
            logger.debug(f"Processing leds assembly #{i}.")

            # Make sure that the list of strips is not empty.
            if len(materials) == 0:
                raise RuntimeError(
                    f"Error in assembly list 'leds/{i}': no materials"
                    " specified."
                )

            # Check if all strips in this assembly have the same LED density.
            led_density = materials_list["leds"][materials[0]]["leds_per_meter"]
            logger.debug(
                f"Checking LEDs densities. {materials[0]}: {led_density}."
            )
            for m in materials[:1]:
                m_density = materials_list["leds"][m]["leds_per_meter"]
                logger.debug(f"Checking LEDs densities. {m}: {m_density}.")
                if m_density != led_density:
                    raise RuntimeError(
                        f"Error in assembly list 'leds/{i}'. The components "
                        f"'{materials[0]}' and '{m}' have a different amount "
                        "of LEDs per meter."
                    )

            # Evaluate how many LEDs should be inserted in a single strip and
            # how many meters would be needed to have all tiles filled with
            # LEDs.
            leds_per_tile_float = visible_tiles[0].perimeter() * led_density
            logger.debug(f"LEDs per tile (raw): {leds_per_tile_float}.")
            if np.allclose(np.round(leds_per_tile_float), leds_per_tile_float):
                logger.debug(
                    "The number of LEDs per tile is almost equal to "
                    "its next integer value. The difference is so "
                    "small that it likely is due to rounding errors."
                )
                leds_per_tile = np.round(leds_per_tile_float).astype(int)
            else:
                logger.debug("Rounding down the number of LEDs.")
                leds_per_tile = np.floor(
                    visible_tiles[0].perimeter() * led_density
                ).astype(int)
            required_led_length = (
                len(visible_tiles) * leds_per_tile / led_density
            )
            logger.debug(
                f"LEDs per tile: {leds_per_tile}; required strip "
                f"length: {required_led_length}."
            )

            # Get the list of strips that have to be purchased, and their
            # quantity.
            components, cost, _ = wp3.named_tree_search(
                [
                    wp3.Struct(
                        name=m,
                        value=materials_list["leds"][m]["number_of_leds"]
                        / materials_list["leds"][m]["leds_per_meter"],
                        cost=materials_list["leds"][m].get("cost", 0),
                    )
                    for m in materials
                ],
                required_led_length,
            )
            logger.debug(
                f"Optimal components in assembly: {components}. Total cost:"
                f" {cost}."
            )

            # For each strip type to be purchased, add a line in the bill of
            # materials that specifies how many to buy.
            for component, quantity in components:
                logger.debug(
                    f"Adding {quantity} units of '{component}' to the "
                    "bill of materials."
                )
                led_notes = f"Leds per tile: {leds_per_tile}."
                url = materials_list["leds"][component.name].get("url")
                if url is not None:
                    led_notes += f" [url link]({url})"
                bill_of_materials.append(
                    wp3.BillItem(
                        name=component.name,
                        quantity=quantity,
                        cost=component.cost,
                        category=f"leds-{i}",
                        notes=led_notes,
                    )
                )

            # If wattage information is provided for all strips, try to estimate
            # the total wattage required to power the LEDs and add this
            # information to the bill of materials (as a PSU item).
            watts = sum(
                n * materials_list["leds"][c.name].get("watts", np.nan)
                for c, n in components
            )
            logger.debug(f"Estimated wattage of the LED assembly: {watts}.")
            if not np.isnan(watts):
                bill_of_materials.append(
                    wp3.BillItem(
                        name=f"{watts}W Power Supply Unit",
                        category=f"leds-{i}",
                        notes=(
                            "The power has been estimated. You might need a"
                            " lower wattage."
                        ),
                    )
                )
            else:
                materials_with_no_watts = [
                    c.name
                    for c, _ in components
                    if "watts" not in materials_list["leds"][c.name]
                ]
                logger.info(
                    f"Could not estimate wattage for LED assembly #{i} "
                    "since the following materials do not have the "
                    "'watts' property: "
                    f"{', '.join(materials_with_no_watts)}."
                )

            # Knowing the number of LEDs per tile, we can provide a detailed
            # scheme of the wiring. This information is stored in a PDF document
            # that can be viewed by the user.
            fig, ax = wp3.tight_figure(visible_tiles)
            wp3.add_tiles_to_axes(
                visible_tiles,
                ax,
                copy=True,
                patch_color="white",
                border_color="lightgray",
            )
            routing.plot_detailed_routing(
                best_routing, visible_tiles, leds_per_tile, ax
            )
            detailed_routing_file_name = output_dir.joinpath(
                f"wp3_routing_{leds_per_tile}_leds_per_tile.pdf"
            )
            logger.info(
                f"Saving detailed routing for assembly #{i} into "
                f"'{detailed_routing_file_name}'."
            )
            fig.savefig(
                detailed_routing_file_name, bbox_inches="tight", dpi=500
            )
            plt.close("all")

            # Options for SignalRGB components.
            signal_rgb_settings = settings.get("signal_rgb", wp3.SettingsDict())

            # Generate a component file for SignalRGB. The component in the
            # "Layouts" page will be a rectangle with maximum dimenions equal to
            # 100 - an arbitrary number that seems reasonable on my PC.
            width, height = wp3.get_bounding_box_size(visible_tiles)
            scale = signal_rgb_settings.get("component_size", 20) / max(
                width, height
            )
            width *= scale
            height *= scale
            signal_rgb_name_prefix = signal_rgb_settings.get(
                "name_prefix", f"WP3 {output_dir.name}"
            )
            signal_rgb_data = {
                "ProductName": signal_rgb_name_prefix,
                "DisplayName": signal_rgb_name_prefix,
                "Brand": "WP3",
                "Type": "custom",
                "LedCount": int(len(visible_tiles) * leds_per_tile),
                "Width": int(width),
                "Height": int(height),
                "LedMapping": [],
                "LedCoordinates": [],
                "LedNames": [],
            }

            # Get the origin of the bounding box, to properly shift LEDs towards
            # the bottom-left corner.
            origin, _ = wp3.get_bounding_box(visible_tiles)

            # Store, for each LED in the routing path, its name and coordinates.
            for segment in routing.get_detailed_routing_points(
                best_routing, visible_tiles, leds_per_tile
            ):
                for led in segment:
                    led_idx = len(signal_rgb_data["LedMapping"])
                    signal_rgb_data["LedMapping"].append(led_idx)
                    led_coordinates = (led - origin) * scale
                    led_coordinates[1] = height - led_coordinates[1]
                    signal_rgb_data["LedCoordinates"].append(
                        led_coordinates.tolist()
                    )
                    signal_rgb_data["LedNames"].append(
                        f"Led {led_idx} (tile {led_idx//leds_per_tile})"
                    )
            logger.debug(
                "Created SignalRGB component "
                f"'{signal_rgb_data['DisplayName']}' with dimenions "
                f"{signal_rgb_data['Width']}x"
                f"{signal_rgb_data['Height']}"
                f"{len(signal_rgb_data['LedCoordinates'])} LEDs."
            )

            # Save the generated data into a JSON file that can be imported into
            # SignalRGB to define the custom LED geometry.
            signal_rgb_component_file = output_dir.joinpath(
                f"wp3_signal_rgb_{output_dir.name}_{leds_per_tile}_leds.json"
            )
            logger.info(
                "Saving SignalRGB component into"
                f" '{signal_rgb_component_file}'."
            )
            with open(signal_rgb_component_file, "w") as f:
                json.dump(signal_rgb_data, f)

            # Generate and alternative file for SignalRGB with all LEDs in the
            # centers of the tiles.
            dev_name = f"{signal_rgb_name_prefix} centered"
            signal_rgb_data["ProductName"] = dev_name
            signal_rgb_data["DisplayName"] = dev_name
            for i, t in enumerate(best_routing[0]):
                led_coordinates = (
                    (visible_tiles[t].center() - origin) * scale
                ).tolist()
                led_coordinates[1] = height - led_coordinates[1]
                for j in range(leds_per_tile):
                    signal_rgb_data["LedCoordinates"][
                        leds_per_tile * i + j
                    ] = led_coordinates
            logger.debug(
                "Created SignalRGB component "
                f"'{signal_rgb_data['DisplayName']}' with dimenions "
                f"{signal_rgb_data['Width']}x"
                f"{signal_rgb_data['Height']}"
                f"{len(signal_rgb_data['LedCoordinates'])} LEDs."
            )

            # Save the generated data into a JSON file that can be imported into
            # SignalRGB to define the custom LED geometry.
            signal_rgb_component_file = output_dir.joinpath(
                f"wp3_signal_rgb_{output_dir.name}_{leds_per_tile}_leds_centered.json"
            )
            logger.info(
                "Saving SignalRGB component into"
                f" '{signal_rgb_component_file}'."
            )
            with open(signal_rgb_component_file, "w") as f:
                json.dump(signal_rgb_data, f)

            # Generate a file for SignalRGB corresponding to a single tile.
            tile_width, tile_height = wp3.get_bounding_box_size(
                [visible_tiles[0]]
            )
            tile_scale = signal_rgb_settings.get("tile_size", 10) / max(
                tile_width, tile_height
            )
            tile_width *= tile_scale
            tile_height *= tile_scale
            tile_dev_name = f"{signal_rgb_name_prefix} tile"
            tile_signal_rgb_data = {
                "ProductName": tile_dev_name,
                "DisplayName": tile_dev_name,
                "Brand": "WP3",
                "Type": "custom",
                "LedCount": int(leds_per_tile),
                "Width": int(tile_width),
                "Height": int(tile_height),
                "LedMapping": [],
                "LedCoordinates": [],
                "LedNames": [],
            }

            # Get the origin of the bounding box, to properly shift LEDs towards
            # the bottom-left corner.
            tile_origin, _ = wp3.get_bounding_box([visible_tiles[0]])

            # Store, for each LED in the tile, its name and coordinates.
            for led in visible_tiles[0].sample_perimeter(
                leds_per_tile, 0, border=-1
            ):
                led_idx = len(tile_signal_rgb_data["LedMapping"])
                tile_signal_rgb_data["LedMapping"].append(led_idx)
                led_coordinates = (led - tile_origin) * tile_scale
                led_coordinates[1] = tile_height - led_coordinates[1]
                tile_signal_rgb_data["LedCoordinates"].append(
                    led_coordinates.tolist()
                )
                tile_signal_rgb_data["LedNames"].append(f"Led {led_idx}")
            logger.debug(
                "Created SignalRGB component "
                f"'{tile_signal_rgb_data['DisplayName']}' with "
                f"dimenions {tile_signal_rgb_data['Width']}x"
                f"{tile_signal_rgb_data['Height']}"
                f"{len(tile_signal_rgb_data['LedCoordinates'])} LEDs."
            )

            # Save the generated data into a JSON file that can be imported into
            # SignalRGB to define the custom LED geometry.
            signal_rgb_component_file = output_dir.joinpath(
                f"wp3_signal_rgb_{output_dir.name}_{leds_per_tile}_leds_tile.json"
            )
            logger.info(
                "Saving SignalRGB component into"
                f" '{signal_rgb_component_file}'."
            )
            with open(signal_rgb_component_file, "w") as f:
                json.dump(tile_signal_rgb_data, f)

            # Generate and alternative file for SignalRGB with all LEDs in the
            # centers of the tile.
            tile_dev_name = f"{signal_rgb_name_prefix} tile centered"
            tile_signal_rgb_data["ProductName"] = tile_dev_name
            tile_signal_rgb_data["DisplayName"] = tile_dev_name
            led_coordinates = (
                (visible_tiles[0].center() - tile_origin) * tile_scale
            ).tolist()
            led_coordinates[1] = tile_height - led_coordinates[1]
            for i in range(leds_per_tile):
                tile_signal_rgb_data["LedCoordinates"][i] = led_coordinates
            logger.debug(
                "Created SignalRGB component "
                f"'{tile_signal_rgb_data['DisplayName']}' with "
                f"dimenions {tile_signal_rgb_data['Width']}x"
                f"{tile_signal_rgb_data['Height']}"
                f"{len(tile_signal_rgb_data['LedCoordinates'])} LEDs."
            )

            # Save the generated data into a JSON file that can be imported into
            # SignalRGB to define the custom LED geometry.
            signal_rgb_component_file = output_dir.joinpath(
                f"wp3_signal_rgb_{output_dir.name}_{leds_per_tile}_leds_tile_centered.json"
            )
            logger.info(
                "Saving SignalRGB component into"
                f" '{signal_rgb_component_file}'."
            )
            with open(signal_rgb_component_file, "w") as f:
                json.dump(tile_signal_rgb_data, f)

            # Download the SRGBmods Pico firmware.
            logger.info("Downloading Pico firmware from SRGBmods.")
            wp3.download_pico_firmware(
                output_dir,
                f"{signal_rgb_name_prefix}.ino",
                np.array(routing.tiles_per_segment()) * leds_per_tile,
                [],
            )
    else:
        logger.info(
            "Parameter 'assembly/leds' not found. You can add "
            "assemblies following the instructions available at "
            "https://github.com/francofusco/wp3#assembly-settings."
        )

    # Add to the bill of materials one entry that corresponds to the number of
    # connectors to be purchased.
    bill_of_materials.append(
        wp3.BillItem(
            name="3 pin connectors",
            quantity=len(visible_tiles),
            category="wiring",
            notes="The quantity refers to male/female pairs.",
        )
    )

    # Print the bill of materials into a file.
    bill_of_materials_file_name = output_dir.joinpath("bill_of_materials.md")
    logger.info(
        f"Saving bill of materials into '{bill_of_materials_file_name}'."
    )
    wp3.BillItem.dump_to_markdown(
        bill_of_materials, bill_of_materials_file_name
    )

if __name__ == "__main__":
    # Use argparse to deal with command-line arguments.
    parser = argparse.ArgumentParser(
        description=(
            "Create a custom design of A-RGB panels to be used in SignalRGB."
        )
    )
    parser.add_argument(
        "--verbose",
        "--info",
        action="store_true",
        help="Outputs additional information on the console to help debugging.",
    )
    parser.add_argument(
        "--debug",
        action="store_true",
        help=(
            "Outputs "
            "additional information on the console to help "
            "debugging. This is even more verbose than --verbose."
        ),
    )
    args = parser.parse_args()

    # Create log handler that appends all messages to a file. This allows to
    # keep a log of all runs.
    all_runs_handler = logging.FileHandler(".wp3_all_runs.log")
    all_runs_handler.setLevel(logging.DEBUG)

    # Create log handler that prints all messages to a file, but it clears its
    # content before starting. This allows to keep a long of the last run.
    last_run_handler = logging.FileHandler(".wp3_last_run.log", mode="w")
    last_run_handler.setLevel(logging.DEBUG)

    # Create a log handler that prints messages with given severity on stdout.
    stream_handler = logging.StreamHandler()
    stream_handler.setLevel(
        logging.DEBUG
        if args.debug
        else logging.INFO
        if args.verbose
        else logging.WARNING
    )

    # Setup logging.
    logging.basicConfig(
        format="%(levelname)s (%(name)s): %(message)s",
        handlers=[all_runs_handler, last_run_handler, stream_handler],
    )
    logger.setLevel(logging.DEBUG)
    logging.getLogger("wp3").setLevel(logging.DEBUG)

    # Run the main script and catch all exceptions.
    try:
        main()
    except Exception as e:
        if not isinstance(e, SystemExit):
            # Create a "fancy error message". One of the reasons for this
            # over-complicated formatting choice is that, in this way, errors
            # can be located more easily in log files.
            wraplen = 65
            msg = ["Execution stopped unexpectedly. Reason:", ""]
            msg += [
                "> " + s
                for s in textwrap.wrap(f"{type(e).__name__}: {e}", wraplen - 2)
            ]
            msg += [""]
            msg += textwrap.wrap(
                "If this is not the result of your mistake, "
                "consider opening an issue at: "
                "https://github.com/francofusco/wp3/issues/new",
                wraplen,
            )
            max_len = max(map(len, msg))
            headline = "+" + "-" * (max_len + 2) + "+"
            msg_ind = ["| " + m + " " * (max_len - len(m)) + " |" for m in msg]
            logger.exception(
                "\n".join(["\n", headline] + msg_ind + [headline, "\n"]),
                exc_info=e,
            )

        # If the script is being run from the executable generated with
        # PyInstaller, it is necessary to block the execution here. Otherwise,
        # the user would not have the time to read the error message!
        if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
            print("\nPress CTRL+C to exit, or close the terminal window.")
            try:
                while True:
                    pass
            except KeyboardInterrupt:
                pass


#!/usr/bin/python3
"""This module defines a class to manage data base storage for hbnb clone"""
from models.base_model import BaseModel, Base
from sqlalchemy import (create_engine)
from sqlalchemy.orm import sessionmaker
from models.amenity import Amenity
from models.city import City
from models.place import Place
from models.review import Review
from models.state import State
from models.user import User
from os import getenv
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker

classes = {
    'BaseModel': BaseModel, 'User': User, 'Place': Place,
    'State': State, 'City': City, 'Amenity': Amenity,
    'Review': Review
}

class DBStorage:
    __engine = None
    __session = None

    def __init__(self):
        """Instatntiates the database storage to create the engine"""
        self.__engine = create_engine('mysql+mysqldb://{}:{}@{}/{}'.
                                      format(getenv("HBNB_MYSQL_USER"),
                                             getenv("HBNB_MYSQL_PWD"),
                                             getenv("HBNB_MYSQL_HOST"),
                                             getenv("HBNB_MYSQL_DB"),
                                             pool_pre_ping=True))

        if getenv("HBNB_ENV ") == 'test':
            Base.metadata.drop_all(self.__engine)

    def all(self, cls=None):
        """query on the current database session"""
        dic_of_objects = {}
        if cls and cls in classes.values():
            all_objetcs = self.__session.query(cls).all()
            for obj in all_objetcs:
                key = "{}.{}".format(obj.__class__.__name__, obj.id)
                value = obj
                dic_of_objects[key] = value
        elif cls is None:
            # acá guarda clase por clase todas sus filas en caso tal de que
            # la clase No esté vacía.
            for cls in classes.values():
                all_objetcs = self.__session.query(cls).all()
                for obj in all_objetcs:
                    key = "{}.{}".format(obj.__class__.__name__, obj.id)
                    value = obj
                    dic_of_objects[key] = value
        return dic_of_objects

    def new(self, obj):
        """Method to add the object to the current database session"""
        if obj:
            self.__session.add(obj)

    def save(self):
        """Method to commit all changes of the current database session"""
        self.__session.commit()

    def delete(self, obj=None):
        """Method delete from the current database session obj if not None"""
        # obj = cls.id, dentro de una clae, sería una fila de esa clase
        if obj:
            self.__session.delete((obj))

    def reload(self):
        """create all tables in the database"""
        Base.metadata.create_all(self.__engine)
        session_factory = sessionmaker(
            bind=self.__engine, expire_on_commit=False)
        Session = scoped_session(session_factory)
        self.__session = Session()

    def close(self):
        """ Call close() on the class Session
        """
        return self.__session.close()


# from pudb import set_trace; set_trace()
from typing import List

# Definition for a binary tree node.
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

class Solution1:
    def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        """LeetCode 144

        32 ms, 68% ranking.
        """
        if not root:
            return []
        left = self.preorderTraversal(root.left)
        right = self.preorderTraversal(root.right)
        return [root.val] + left + right

class Solution2:
    def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        """
        28 ms, 87% ranking.
        """
        res = []

        def dfs(node: Optional[TreeNode]) -> None:
            if node:
                res.append(node.val)
                dfs(node.left)
                dfs(node.right)

        dfs(root)
        return res

# sol = Solution3()
# tests = [
#     ('abab', True),
#     ('aba', False),
#     ('abcabcabcabc', True),
#     ('abcabcababcabcab', True),
#     ('abcbac', False),
#     ('aabaabaab', True),
#     ('a', False),
#     ('aaaaaaa', True),
#     ('aaaaab', False),
# ]

# for i, (s, ans) in enumerate(tests):
#     res = sol.repeatedSubstringPattern(s)
#     if res == ans:
#         print(f'Test {i}: PASS')
#     else:
#         print(f'Test {i}; Fail. Ans: {ans}, Res: {res}')


import torch
import torchvision
import torchvision.transforms as transforms
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import matplotlib.pyplot as plt
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor
from torchsummary import summary
from MY_DLIP import *

# Get cpu or gpu device for training.
device = "cuda" if torch.cuda.is_available() else "cpu"

print(f"Using {device} device")
if torch.cuda.is_available(): print(f'Device name: {torch.cuda.get_device_name(0)}')

# torchvision 을 통한 CIFAR10 다운로드
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
])

training_data = datasets.CIFAR10(
    root="data",
    train=True,
    download=True,
    transform=transform,   #converts 0~255 value to 0~1 value.
)

# Download test data from open datasets.
test_data = datasets.CIFAR10(
    root="data",
    train=False,
    download=True,
    transform=transform,
)

print(f"train dataset length = {len(training_data)}")
print(f"test  dataset length = {len(test_data)}")

batch_size = 64

# Create data loaders.
train_dataloader = DataLoader(training_data, batch_size=batch_size, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=batch_size, shuffle=True)

for X, y in test_dataloader:
    print(f"Shape of X [N, C, H, W]: {X.shape} {y.dtype}")
    print(f"Shape of y: {y.shape} {y.dtype}")
    break

classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

import matplotlib.pyplot as plt

dataiter = iter(train_dataloader)
images, labels = next(dataiter)

figure = plt.figure()
num_of_images = 9
for index in range(num_of_images):
    plt.subplot(3, 3, index+1)
    plt.axis('off')
    plt.title(f"Ground Truth: {classes[labels[index]]}")
    plt.imshow(np.transpose((images[index] * 0.5 + 0.5).numpy(), (1, 2, 0)))  # 출력을 위한 차원변환 (channels*rows*cols) -> (rows*cols*channels)

# model = LeNet5().to(device)
model = torch.load('trained_Lenet5(CIFAR10).pth')

summary(model, (3,32,32))

loss_fn = nn.CrossEntropyLoss()

epochs = 2
for t in range(epochs):
    print(f"Epoch {t+1}\n-------------------------------")
    test(test_dataloader, model, loss_fn, device)
print("Done!")

# Get some random test  images // BatchSize at a time
dataiter = iter(test_dataloader)
images, labels = dataiter.next()

images = images.to(device)
labels = labels.to(device)
pred = model(images)
predicted=pred.argmax(1);

figure = plt.figure()
num_of_images = 9

for index in range(num_of_images):
    plt.subplot(3, 3, index+1)
    plt.axis('off')
    plt.title(f"Ground Truth: {classes[labels[index]]}")
    plt.title(f"{classes[predicted[index].item()]} (true:{classes[labels[index]]})")
    plt.imshow(np.transpose((images[index] * 0.5 + 0.5).cpu().numpy().squeeze(), (1, 2, 0)))  # 출력을 위한 차원변환 (channels*rows*cols) -> (rows*cols*channels)


import time
import tkinter as tk
import os
from PIL import Image,ImageTk,ImageFont
import tkinter.font as tkFont
import datetime
import matplotlib.pyplot as plt
import subprocess

root = tk.Tk()
selectedTest=None
test_buttons={}
compareStatus={"test1_s":0,"test1_m":0,"test1_sol1":0,"test1_sol2":0,"test2_s":0,"test2_m":0,"test2_sol1":0,"test2_sol2":0}
equivalences={"test1_s":"benchmarkOne","test1_m":"benchmarkOne","test1_sol1":"benchmarkOneS1","test1_sol2":"benchmarkOneS2","test2_s":"benchmarkTwo","test2_m":"benchmarkTwo","test2_sol1":"benchmarkTwoS1","test2_sol2":"benchmarkTwoS2"}
compareButtons=[]
def setCompare(i):

    global compareStatus

    colors=["#1f1d20","#47434c"]

    tests=available_for_compare()

    test=tests[i]

    compareStatus[test]=not compareStatus[test]
    compare_button=compareButtons[i]
    compare_button.config(bg=colors[i%2])
    if(compareStatus[test]):

        compare_button.config(bg="#062239")

def doCompare():

    testCompare=[]

    plt.clf()

    for key in list(compareStatus.keys()):

        if(compareStatus[key]):

            testCompare+=[key]

    plt.subplot(3, 1, 1)

    for test in testCompare:

        inputs = parseResults("output_" + test + ".txt")

        iterations = range(len(inputs[list(inputs.keys())[0]]))

        plt.subplot(3, 1, 1)
        plt.plot(iterations, inputs["L1-dcache-load-misses"], label=test)
        plt.title("Iterations Vs Cache Misses")

        plt.subplot(3, 1, 2)
        plt.plot(iterations, inputs["L1-dcache-loads"],label=test)
        plt.title("Iterations Vs Cache Loads")

        plt.subplot(3, 1, 3)
        plt.plot(iterations, inputs["time"],label=test)
        plt.title("Iterations Vs Time per Iteration")

    plt.tight_layout()
    plt.legend()
    plt.savefig("comparison.png")

    compare_window = tk.Toplevel(root)

    compare_window.title("False Sharing Analysis Tool")
    compare_window.geometry("700x565")
    compare_window.configure(bg="#28262b")
    compare_window.resizable(width=False, height=False)
    # compare_window.wm_attributes('-type', 'splash')
    compare_window.configure(borderwidth=2, relief="solid")

    window_tk_font = tk.font.Font(family="Share Tech Mono", size=12)

    benchmark_label = tk.Label(compare_window, text="Comparison Results", font=window_tk_font, fg="#55ed64",bg="#28262b")
    benchmark_label.place(x=0, y=1)

    graph_panel = tk.Frame(compare_window, bg="#353238", width=696, height=536)
    graph_panel.place(x=0, y=25)

    image = Image.open("comparison.png")
    graph_image = ImageTk.PhotoImage(image)

    graph_image_label = tk.Label(graph_panel, image=graph_image, width=640, height=480)
    graph_image_label.image = graph_image
    graph_image_label.place(x=30, y=30)

def kill_compare():
    global compare_window,compareButtons

    for key in list(compareStatus.keys()):
        compareStatus[key]=0

    compareButtons = []
    compare_window.destroy()

def setTest(test):

    global execute_button,selectedTest,test_results_button

    selectedTest=test

    if(test in list(compareStatus.keys())):

        execute_button.config(state="normal")

    else:

        execute_button.config(state="disabled")

    for key in list(test_buttons.keys()):

        if(key == test):

            test_buttons[key].config(bg="#55ed64", fg="#062239")
        else:

            test_buttons[key].config(fg="#55ed64", bg="#062239")

    exists_data=parseResults("output_"+test+".txt")

    if(exists_data!=-1):

        test_results_button.config(state="normal")

    else:

        test_results_button.config(state="disabled")

def available_for_compare():

    available=[]

    for test in equivalences.keys():

        exists_data = parseResults("output_" + test + ".txt")

        if (exists_data != -1):

            available+=[test]

    return available

def getSpecs():

    global wait_window

    specs_path="specs.txt"

    showWaitWindow("Fetching system information, please wait")

    try:
        os.remove(specs_path)
    except:

        None

    file = open(specs_path, "w")
    file.close()

    os.system(f"./systemInfo t >> specs.txt")
    initial_time=datetime.datetime.now()
    fail=False

    while(True):

        current_time=datetime.datetime.now()

        seconds=(current_time-initial_time).seconds

        if (os.path.exists(specs_path)):
            break

        if(seconds>=60):

            fail=True
            break
    if(fail):

        destroyWaitWindow()
        showWaitWindow("Failed to retrieve information")
        time.sleep(2)
        destroyWaitWindow()
        test_window.deiconify()
    else:

        destroyWaitWindow()
        showWaitWindow("Spec can be found at: "+specs_path)
        time.sleep(2)
        destroyWaitWindow()
        os.system(f"gedit specs.txt")
        test_window.deiconify()

def getThreadsPerCore():
    # Run the lscpu command and capture its output
    command = "lscpu | grep 'Thread(s) per core'"
    output = subprocess.check_output(command, shell=True, text=True)

    # Parse the value from the output
    lines = output.strip().split('\n')
    if lines:
        line = lines[0]
        parts = line.split(':')
        if len(parts) == 2:
            threads_per_core = int(parts[1].strip())
            #print("Threads per core:", threads_per_core)
            return threads_per_core
    return None

def getCoresPerSocket():
    # Run the lscpu command and capture its output
    command = "lscpu | grep 'Core(s) per socket'"
    output = subprocess.check_output(command, shell=True, text=True)

    # Parse the value from the output
    lines = output.strip().split('\n')
    if lines:
        line = lines[0]
        parts = line.split(':')
        if len(parts) == 2:
            cores_per_socket = int(parts[1].strip())
            #print("Cores per socket:", cores_per_socket)
            return cores_per_socket
    return None

def executeBench():
    test_equivalence=equivalences[selectedTest]
    cores=1
    if(selectedTest in ["test1_m","test1_sol1","test1_sol2","test2_m","test2_sol1","test2_sol2"]):

        threadsPerCore = getThreadsPerCore()
        coresPerSocket = getCoresPerSocket()
        totalThreads = threadsPerCore * coresPerSocket

        cores=totalThreads

    output_path=f"output_{selectedTest}.txt"
    if not os.path.exists(output_path):
        file = open(output_path, "w")
        file.close()

    result=-1
    fail=False
    showWaitWindow("Executing Benchmark, please wait")

    initial_time=datetime.datetime.now()
    result=os.system(f"./controller {test_equivalence} {cores} {selectedTest}")

    while(result==-1):

        current_time = datetime.datetime.now()

        seconds = (current_time - initial_time).seconds

        if (seconds >= 60):
            fail = True
            break

    if (fail):

        destroyWaitWindow()
        showWaitWindow("Failed to execute benchmark")

    else:

        destroyWaitWindow()
        showWaitWindow("Test executed successfully")

    time.sleep(2)
    destroyWaitWindow()
    test_window.deiconify()

def clearOutput():

    os.system("find -type f -name '*output*' -exec rm {} \;")

def init():

    # Customize the window
    root.title("False Sharing Analysis Tool")
    root.geometry("800x300")
    root.configure(bg="#353238")
    root.resizable(width=False, height=False)
    #root.wm_attributes('-type', 'splash')
    root.configure(borderwidth=2, relief="solid")

    left_image = Image.open("Assets/Images/circuitry_left.jpg")  # Replace with your image file path
    left_panel_image = ImageTk.PhotoImage(left_image)

    left_panel = tk.Frame(root, bg="blue", width=150, height=290)
    left_panel.place(x=0,y=0)

    left_line = tk.Frame(root, bg="black", width=2, height=300)
    left_line.place(x=145, y=0)

    left_image_label = tk.Label(left_panel, image=left_panel_image, width=145, height=294)
    left_image_label.image = left_panel_image  # Keep a reference to the image to prevent it from being garbage collected
    left_image_label.pack()

    right_image = Image.open("Assets/Images/circuitry_right.jpg")  # Replace with your image file path
    right_panel_image = ImageTk.PhotoImage(right_image)

    right_panel = tk.Frame(root, bg="blue", width=150, height=290)
    right_panel.place(x=640, y=0)

    right_line = tk.Frame(root, bg="black", width=2, height=300)
    right_line.place(x=640, y=0)

    right_image_label = tk.Label(right_panel, image=right_panel_image, width=154, height=294)
    right_image_label.image = right_panel_image  # Keep a reference to the image to prevent it from being garbage collected
    right_image_label.pack()

    title_tk_font = tkFont.Font(family="Share Tech Mono", size=20)

    title_label_mp=tk.Label(root, text="MP Programming",font=title_tk_font, fg="#55ed64", bg="#353238")
    title_label_mp.place(x=290,y=20)

    title_label_and = tk.Label(root, text="&", font=title_tk_font,fg="#55ed64", bg="#353238")
    title_label_and.place(x=390, y=70)

    title_label_fs = tk.Label(root, text="False Sharing Analysis", font=title_tk_font,fg="#55ed64", bg="#353238")
    title_label_fs.place(x=240, y=120)

    beg_button=tk.Button(root, text="Begin", bg="#062239",borderwidth=1, relief="solid",font=title_tk_font,fg="#55ed64",highlightthickness=0, command=showTestsWindow)
    beg_button.place(x=340, y=190)

    # Start the Tkinter main event loop
    root.mainloop()

def showWaitWindow(message):

    global wait_window,test_window

    root.withdraw()
    test_window.withdraw()

    regular_tk_font = tk.font.Font(family="Share Tech Mono", size=12)

    wait_window = tk.Toplevel(root)
    wait_window.title("False Sharing Analysis Tool")
    wait_window.geometry("500x75")
    wait_window.configure(bg="#062239")
    wait_window.resizable(width=False, height=False)
    wait_window.wm_attributes('-type', 'splash')
    wait_window.configure(borderwidth=2, relief="solid")

    message_label = tk.Label(wait_window, text=message, font=regular_tk_font, fg="#55ed64", bg="#062239")
    message_label.pack(padx=10, pady=20)

    wait_window.lift()
    wait_window.update()

def destroyWaitWindow():

    global wait_window

    wait_window.destroy()
def showTestsWindow():

    global test_window,execute_button,test_results_button

    test_window = tk.Toplevel(root)
    root.withdraw()
    test_window.title("False Sharing Analysis Tool")
    test_window.geometry("800x400")
    test_window.configure(bg="#353238")
    test_window.resizable(width=False, height=False)
    #test_window.wm_attributes('-type', 'splash')
    test_window.configure(borderwidth=2, relief="solid")
    test_window.protocol("WM_DELETE_WINDOW", root.destroy)

    upper_bar = tk.Frame(test_window, bg="#28262b", width=796, height=25)
    upper_bar.place(x=0, y=0)

    left_panel = tk.Frame(test_window, bg="#1f1d20", width=220, height=365)
    left_panel.place(x=0, y=25)

    left_line = tk.Frame(test_window, bg="#0a0a0b", width=2, height=370)
    left_line.place(x=220, y=25)

    l_left_panel = tk.Frame(test_window, bg="#353238", width=220, height=90)
    l_left_panel.place(x=0, y=300)

    l_right_panel = tk.Frame(test_window, bg="#1f1d20", width=574, height=96)
    l_right_panel.place(x=222, y=300)

    image = Image.open("Assets/Images/circuitry_left.jpg")  # Replace with your image file path
    panel_image = ImageTk.PhotoImage(image)

    panel_image_label = tk.Label(l_left_panel, image=panel_image, width=218, height=94)
    panel_image_label.image = panel_image  # Keep a reference to the image to prevent it from being garbage collected
    panel_image_label.pack()

    panel_line = tk.Frame(test_window, bg="#0a0a0b", width=795, height=2)
    panel_line.place(x=0, y=300)

    window_tk_font = tk.font.Font(family="Share Tech Mono", size=12)
    regular_tk_font = tk.font.Font(family="Share Tech Mono", size=14)

    benchmark_label = tk.Label(test_window, text="Benchmarks", font=window_tk_font, fg="#55ed64", bg="#28262b")
    benchmark_label.place(x=0,y=1)

    single_thread_label = tk.Label(test_window, text="> Benchmark 1 (C)", font=regular_tk_font, fg="#55ed64", bg="#1f1d20")
    single_thread_label.place(x=20, y=85)

    multi_thread_label = tk.Label(test_window, text="> Benchmark 2 (C++)", font=regular_tk_font, fg="#55ed64",bg="#1f1d20")
    multi_thread_label.place(x=20, y=205)

    image = Image.open("Assets/Images/compare.png")  # Replace with your image file path
    compare_image = ImageTk.PhotoImage(image)
    compare_button = tk.Button(test_window, image=compare_image, bg="#1f1d20", borderwidth=0,relief="solid", highlightthickness=0, command=showCompare)
    compare_button.photo = compare_image
    compare_button.place(x=694, y=335)

    clear_button = tk.Button(test_window, text="Clear Output", bg="#062239", borderwidth=1, relief="solid", font=regular_tk_font,
                             fg="#55ed64", highlightthickness=0, command=clearOutput)
    clear_button.place(x=230, y=330)

    specs_button = tk.Button(test_window, text="System Specs", bg="#062239", borderwidth=1, relief="solid",
                             font=regular_tk_font,
                             fg="#55ed64", highlightthickness=0, command=getSpecs)
    specs_button.place(x=380, y=330)

    execute_button=tk.Button(test_window, text="Execute", bg="#55ed64", fg="#062239",borderwidth=1,state="disabled" ,relief="solid", font=regular_tk_font, highlightthickness=0, command=executeBench)
    execute_button.place(x=530, y=330)

    image = Image.open("Assets/Images/examen.png")  # Replace with your image file path
    results_image = ImageTk.PhotoImage(image)
    test_results_button = tk.Button(test_window, image=results_image, bg="#1f1d20", borderwidth=0, state="disabled",relief="solid", highlightthickness=0, command=showResults)
    test_results_button.photo = results_image
    test_results_button.place(x=650, y=335)

    test1_panel = tk.Frame(test_window, bg="#47434c", width=574, height=42)
    test1_panel.place(x=222, y=80)

    test2_panel = tk.Frame(test_window, bg="#47434c", width=574, height=42)
    test2_panel.place(x=222, y=200)

    test1_single_button = tk.Button(test_window, text="> Single T.", bg="#062239", borderwidth=1, relief="solid",font=window_tk_font,fg="#55ed64", highlightthickness=0, command=lambda:setTest("test1_s"))
    test1_single_button.place(x=230, y=85)
    test_buttons["test1_s"]=test1_single_button

    test1_multi_button = tk.Button(test_window, text="> Multi T.", bg="#062239", borderwidth=1, relief="solid",font=window_tk_font,fg="#55ed64", highlightthickness=0, command=lambda:setTest("test1_m"))
    test1_multi_button.place(x=360, y=85)
    test_buttons["test1_m"] = test1_multi_button

    test1_sol1_button = tk.Button(test_window, text="> Sol. 1", bg="#062239", borderwidth=1, relief="solid",font=window_tk_font,fg="#55ed64", highlightthickness=0, command=lambda:setTest("test1_sol1"))
    test1_sol1_button.place(x=480, y=85)
    test_buttons["test1_sol1"] = test1_sol1_button

    test1_sol2_button = tk.Button(test_window, text="> Sol. 2", bg="#062239", borderwidth=1, relief="solid",font=window_tk_font,fg="#55ed64", highlightthickness=0, command=lambda:setTest("test1_sol2"))
    test1_sol2_button.place(x=582, y=85)
    test_buttons["test1_sol2"] = test1_sol2_button

    test2_single_button = tk.Button(test_window, text="> Single T.", bg="#062239", borderwidth=1, relief="solid",font=window_tk_font,fg="#55ed64", highlightthickness=0, command=lambda:setTest("test2_s"))
    test2_single_button.place(x=230, y=205)
    test_buttons["test2_s"] = test2_single_button

    test2_multi_button = tk.Button(test_window, text="> Multi T.", bg="#062239", borderwidth=1, relief="solid",font=window_tk_font,fg="#55ed64", highlightthickness=0, command=lambda:setTest("test2_m"))
    test2_multi_button.place(x=360, y=205)
    test_buttons["test2_m"] = test2_multi_button

    test2_sol1_button = tk.Button(test_window, text="> Sol. 1", bg="#062239", borderwidth=1, relief="solid",font=window_tk_font,fg="#55ed64", highlightthickness=0, command=lambda:setTest("test2_sol1"))
    test2_sol1_button.place(x=480, y=205)
    test_buttons["test2_sol1"] = test2_sol1_button

    test2_sol2_button = tk.Button(test_window, text="> Sol. 2", bg="#062239", borderwidth=1, relief="solid",font=window_tk_font,fg="#55ed64", highlightthickness=0, command=lambda:setTest("test2_sol2"))
    test2_sol2_button.place(x=582, y=205)
    test_buttons["test2_sol2"] = test2_sol2_button

    os.chdir("Benchmark_Scripts/")

def showResults():

    global test_window,results_window

    results_window = tk.Toplevel(root)

    results_window.title("False Sharing Analysis Tool")
    results_window.geometry("700x565")
    results_window.configure(bg="#28262b")
    results_window.resizable(width=False, height=False)
    #results_window.wm_attributes('-type', 'splash')
    results_window.configure(borderwidth=2, relief="solid")

    window_tk_font = tk.font.Font(family="Share Tech Mono", size=12)

    benchmark_label = tk.Label(results_window, text=selectedTest+" results", font=window_tk_font, fg="#55ed64", bg="#28262b")
    benchmark_label.place(x=0, y=1)

    graph_panel = tk.Frame(results_window, bg="#353238", width=696, height=536)
    graph_panel.place(x=0, y=25)

    doGraphs(parseResults("output_"+selectedTest+".txt"), "output_"+selectedTest)

    image = Image.open("output_"+selectedTest+".png")  # Replace with your image file path
    graph_image = ImageTk.PhotoImage(image)

    graph_image_label = tk.Label(graph_panel, image=graph_image, width=640, height=480)
    graph_image_label.image = graph_image  # Keep a reference to the image to prevent it from being garbage collected
    graph_image_label.place(x=30, y=30)

def showCompare():

    global test_window, results_window,compare_window

    compare_window = tk.Toplevel(root)
    compare_window.title("False Sharing Analysis Tool")

    window_tk_font = tk.font.Font(family="Share Tech Mono", size=12)

    tests=available_for_compare()

    height=len(tests)*25

    compare_window.geometry("559x100")
    if(len(tests)>0):
        compare_window.geometry("559x"+str(height+25+8+50))
        test_panel = tk.Frame(compare_window, bg="#353238", width=555, height=len(tests)*25+3)
        test_panel.place(x=0, y=25)

    compare_window.configure(bg="#28262b")
    compare_window.resizable(width=False, height=False)
    compare_window.configure(borderwidth=2, relief="solid")
    compare_window.protocol("WM_DELETE_WINDOW", kill_compare)

    available_label = tk.Label(compare_window ,text="Available Tests Results for Comparison:", font=window_tk_font, fg="#55ed64",bg="#28262b")
    available_label.place(x=0, y=2)

    if(len(tests)==0):
        benchmark_label = tk.Label(compare_window ,text="> No test results available to compare", font=window_tk_font, fg="#55ed64",bg="#28262b")
        benchmark_label.place(x=0, y=30)

    else:

        colors=["#1f1d20","#47434c"]

        for i in range(len(tests)):
            compare_button = tk.Button(compare_window,text=tests[i],font=window_tk_font, fg="#55ed64",bg=colors[i%2],borderwidth=0, relief="solid",highlightthickness=0,anchor='w', command=lambda p=i: setCompare(p))
            compare_button.place(x=0, y=25+i*25)
            compare_button.config(width=59)
            compareButtons.append(compare_button)

        compare_button = tk.Button(compare_window, text="Compare", font=window_tk_font, fg="#55ed64", bg=colors[i % 2],borderwidth=1, relief="solid", highlightthickness=0, anchor='w', command=doCompare)
        compare_button.place(x=450, y= len(tests)*25+3+37)

def parseResults(filename):

    file_path = filename  # Replace with your file path

    try:
        with open(file_path, "r") as file:

            inputs=file.read().split("$")

            for j in range(len(inputs)):
                input = inputs[j]
                lines=input.split("\n")
                components=[]
                for line in lines:
                    components+=line.split(" ")

                components = [component for component in components if component != '']
                output={}
                for i in range(len(components)):

                    component = components[i].replace('\u202f',"")
                    components[i]=component

                    if(component == 'L1-dcache-loads'):

                        output[component]=float(components[i-1])

                    elif(component == 'L1-dcache-load-misses'):

                        output[component] = float(components[i - 1])

                    elif (component == 'time'):

                        output[component] = float(components[i - 2].replace(",","."))

                inputs[j]=output

            output={"L1-dcache-load-misses":[],"L1-dcache-loads":[],"time":[]}

            for input in inputs:

                for key in input.keys():

                    output[key]=output[key]+[input[key]]

            return output

    except:

        return -1

def doGraphs(inputs,outputFile):

    plt.clf()

    iterations=range(len(inputs[list(inputs.keys())[0]]))

    plt.subplot(3,1,1)
    plt.plot(iterations,inputs["L1-dcache-load-misses"])
    plt.title("Iterations Vs Cache Misses")

    plt.subplot(3, 1, 2)
    plt.plot(iterations, inputs["L1-dcache-loads"])
    plt.title("Iterations Vs Cache Loads")

    plt.subplot(3, 1, 3)
    plt.plot(iterations, inputs["time"])
    plt.title("Iterations Vs Time per Iteration")

    plt.tight_layout()

    plt.savefig(outputFile)

init()
#doGraphs(parseResults("output_test1_s.txt"),"output_test1_s")