
import  sys
import  os
import  serial
import  struct
import  serial.tools.list_ports
import  pyqtgraph   as pg
import  pandas      as pd
import  time
import  json
from    PyQt6.QtWidgets import QApplication, QVBoxLayout, QWidget, QLineEdit, QPushButton, QLabel, QHBoxLayout
from    PyQt6.QtCore    import QTimer
from    PyQt6.QtGui     import QIcon
from    PyQt6.QtCore    import QSettings
from    openpyxl        import load_workbook
from    collections     import deque

# -------------------------- Get resource path --------------------------
def resource_path(relative_path):
    if getattr(sys, 'frozen', False):
        # Running as exe
        base_path = sys._MEIPASS
    else:
        # Running as script
        base_path = os.path.dirname(os.path.abspath(__file__))
    return os.path.join(base_path, relative_path)

# -------------------------- Arduino detection --------------------------
def find_arduino():
    ports = list(serial.tools.list_ports.comports())
    print(f"Found ports: {[p.description for p in ports]}")  # DEBUG LOG
    for port in ports:
        print(f"Checking port: {port.device} - {port.description}")  # DEBUG LOG
        if any(kw in port.description for kw in ["Arduino", "CH340", "USB Serial", "FTDI"]):
            print(f"Arduino found at: {port.device}")
            return port.device
    print("No Arduino found")
    return None

arduino_port = find_arduino()
ser = None
if arduino_port:
    ser = serial.Serial(
        arduino_port,
        9600,
        timeout=0,            # Non-blocking
        write_timeout=0
    )
    ser.reset_input_buffer()
    ser.reset_output_buffer()

# --------------------------- Config JSON helpers ---------------------------
def get_presets_path():
    if getattr(sys, 'frozen', False):
        base_runtime = os.path.dirname(sys.executable)
    else:
        base_runtime = os.path.dirname(os.path.abspath(__file__))

    data_dir = os.path.join(base_runtime, "Data_log")
    os.makedirs(data_dir, exist_ok=True)

    return os.path.join(data_dir, "presets.json")

CONFIG_JSON_FILE = get_presets_path()

DEFAULT_PRESETS = {
    "Anode": {"setpoint_start": 160, "setpoint_end": 150, "setwindow": 60, "marker_interval": 10},
    "Cathode": {"setpoint_start": 160, "setpoint_end": 150, "setwindow": 60, "marker_interval": 10},
    "Coolant": {"setpoint_start": 160, "setpoint_end": 150, "setwindow": 60, "marker_interval": 10},
}

def load_presets():
    if os.path.exists(CONFIG_JSON_FILE):
        try:
            with open(CONFIG_JSON_FILE, "r") as f:
                data = json.load(f)
                return data
        except Exception as e:
            print("Failed to load preset JSON, using defaults:", e)
    return DEFAULT_PRESETS.copy()

def save_presets(presets: dict):
    try:
        with open(CONFIG_JSON_FILE, "w") as f:
            json.dump(presets, f, indent=4)
        print("Presets saved to JSON.")
    except Exception as e:
        print("Failed to save presets:", e)

# -------------------------- HMI Class --------------------------
class HMI(QWidget):
    def __init__(self):
        super().__init__()

        # -------------------------- Data folder --------------------------
        if getattr(sys, 'frozen', False):
            base_runtime = os.path.dirname(sys.executable)
        else:
            base_runtime = os.path.dirname(os.path.abspath(__file__))

        self.data_dir = os.path.join(base_runtime, "Data_log")
        os.makedirs(self.data_dir, exist_ok=True)


        # -------------------------- Window Setup --------------------------
        self.setWindowTitle("Leak Test")
        icon_path = resource_path("assets/icon.ico")
        self.setWindowIcon(QIcon(icon_path))
        self.setGeometry(100, 100, 900, 550)

        self.settings = QSettings("EHG", "LeakTestHMI")
        self.current_preset = None  # Track the currently selected preset

        # -------------------------- Data Storage --------------------------
        self.start_time = time.time()
        self.marker_items = []
        self.time_data = deque(maxlen=5000)
        self.pressure_data = deque(maxlen=5000)
        self.serial_buffer = bytearray()

        # -------------------------- Default / Saved Configurations --------------------------
        self.setpoint_start = float(self.settings.value("setpoint_start", 25))
        self.setpoint_end = float(self.settings.value("setpoint_end", 12))
        self.setwindow = float(self.settings.value("setwindow", 900))
        self.marker_interval = float(self.settings.value("marker_interval", 20))

        # Load presets from JSON file
        self.presets = load_presets()

        # -------------------------- Layouts --------------------------
        main_layout = QVBoxLayout()
        self.setLayout(main_layout)

        # Date display
        self.date_label = QLabel()
        self.date_label.setStyleSheet("font-size: 12px; color: gray;")
        self.update_date_label()
        date_layout = QHBoxLayout()
        date_layout.addStretch()
        date_layout.addWidget(self.date_label)
        main_layout.addLayout(date_layout)
        self.datetime_timer = QTimer()
        self.datetime_timer.timeout.connect(self.update_date_label)
        self.datetime_timer.start(1000)

        # Plot Widget
        self.plot_widget = pg.PlotWidget()
        main_layout.addWidget(self.plot_widget)
        self.plot_widget.setTitle("Pressure")
        self.plot_widget.setLabel("left", "Pressure [kPa]")
        self.plot_widget.setLabel("bottom", "Time [s]")
        self.plot_widget.setBackground("w")
        self.plot = self.plot_widget.plot([], [], pen=pg.mkPen(color="k", width=2))

        # Setpoint Lines
        self.setpoint_start_line = pg.InfiniteLine(pos=self.setpoint_start, angle=0, pen=pg.mkPen(color="r", width=2, style=pg.QtCore.Qt.PenStyle.DashLine))
        self.setpoint_end_line = pg.InfiniteLine(pos=self.setpoint_end, angle=0, pen=pg.mkPen(color="b", width=2, style=pg.QtCore.Qt.PenStyle.DashLine))
        self.plot_widget.addItem(self.setpoint_start_line)
        self.plot_widget.addItem(self.setpoint_end_line)

        # Cursors
        self.cursor_start = pg.InfiniteLine(pos=10, angle=90, pen=pg.mkPen(color="g", width=2, style=pg.QtCore.Qt.PenStyle.SolidLine), movable=True)
        self.cursor_end = pg.InfiniteLine(pos=910, angle=90, pen=pg.mkPen(color="g", width=2, style=pg.QtCore.Qt.PenStyle.SolidLine), movable=True)
        self.plot_widget.addItem(self.cursor_start)
        self.plot_widget.addItem(self.cursor_end)
        self.cursor_start.sigPositionChanged.connect(lambda: self.sync_cursors(self.cursor_start, self.cursor_end))
        self.cursor_end.sigPositionChanged.connect(lambda: self.sync_cursors(self.cursor_end, self.cursor_start))

        # -------------------------- Controls --------------------------
        control_layout = QHBoxLayout()
        # Setpoints and dT
        self.setpoint_start_input = QLineEdit(str(self.setpoint_start))
        self.setpoint_start_input.setFixedWidth(50)
        self.setpoint_start_input.editingFinished.connect(self.update_setpoint_start)

        self.setpoint_end_input = QLineEdit(str(self.setpoint_end))
        self.setpoint_end_input.setFixedWidth(50)
        self.setpoint_end_input.editingFinished.connect(self.update_setpoint_end)

        self.setwindow_input = QLineEdit(str(self.setwindow))
        self.setwindow_input.setFixedWidth(50)
        self.setwindow_input.editingFinished.connect(self.update_setwindow)

        control_layout.addWidget(QLabel("Setpoint_start [kPa]:"))
        control_layout.addWidget(self.setpoint_start_input)
        control_layout.addWidget(QLabel("Setpoint_end [kPa]:"))
        control_layout.addWidget(self.setpoint_end_input)
        control_layout.addWidget(QLabel("dT [sec]:"))
        control_layout.addWidget(self.setwindow_input)

        # Marker Interval
        self.marker_input = QLineEdit(str(self.marker_interval))
        self.marker_input.setFixedWidth(50)
        self.marker_input.setPlaceholderText("Sample Time [sec]")
        self.marker_input.editingFinished.connect(self.update_marker_interval)
        control_layout.addWidget(QLabel("Sample Time [sec]:"))
        control_layout.addWidget(self.marker_input)

        # Save Configuration Button
        self.save_config_button = QPushButton("Save Configuration")
        self.save_config_button.clicked.connect(self.save_configuration)
        control_layout.addWidget(self.save_config_button)

        # Analyze Button
        self.save_button = QPushButton("Analyze")
        self.save_button.clicked.connect(self.analyze_and_save)
        control_layout.addWidget(self.save_button)

        main_layout.addLayout(control_layout)

        # Stack, Cell & Comments text fields
        stack_layout = QHBoxLayout()

        stack_layout.addWidget(QLabel("Stack No [ST#]:"))
        self.stack_input = QLineEdit("xx")
        self.stack_input.setFixedWidth(80)
        stack_layout.addWidget(self.stack_input)

        stack_layout.addWidget(QLabel("Cell No [C#]:"))
        self.cell_input = QLineEdit("1")
        self.cell_input.setFixedWidth(80)
        stack_layout.addWidget(self.cell_input)

        stack_layout.addWidget(QLabel("Config:"))
        self.config_input = QLineEdit("")
        self.config_input.setFixedWidth(100)
        #self.config_input.setReadOnly(True)  # auto-filled from preset
        stack_layout.addWidget(self.config_input)

        stack_layout.addWidget(QLabel("Comments:"))
        self.comments_input = QLineEdit("")
        self.comments_input.setFixedWidth(260)
        stack_layout.addWidget(self.comments_input)

        stack_layout.addStretch()
        main_layout.addLayout(stack_layout)


        # Preset Buttons
        preset_layout = QHBoxLayout()
        for name in self.presets:
            btn = QPushButton(f"Config {name}")
            btn.clicked.connect(lambda checked=False, n=name: self.reset_with_preset(n))
            preset_layout.addWidget(btn)
        preset_layout.addStretch()
        main_layout.addLayout(preset_layout)

        # Current Configuration Label
        self.current_config_label = QLabel("Current Config: None")
        self.current_config_label.setStyleSheet("font-size: 14px; color: blue;")
        main_layout.addWidget(self.current_config_label)

        # Last Value Display
        self.actual_value_label = pg.TextItem("", anchor=(0, 1), color="black", fill=pg.mkBrush(255, 255, 255, 150))
        self.plot_widget.addItem(self.actual_value_label)

        # Test Result Label
        self.result_label = QLabel("Test Result: N/A")
        self.result_label.setStyleSheet("font-size: 16px; font-weight: bold; color: black;")
        main_layout.addWidget(self.result_label)

        # Test Count Label
        self.test_count_label = QLabel("Test: -")
        self.test_count_label.setStyleSheet("font-weight: bold; font-size: 14px;")
        main_layout.addWidget(self.test_count_label)

        # -------------------------- Timers --------------------------
        self.marker_timer = QTimer()
        self.marker_timer.timeout.connect(self.add_marker)
        self.marker_timer.start(int(self.marker_interval * 1000))

        self.timer = QTimer()
        self.timer.timeout.connect(self.update_plot)
        self.timer.start(500)

    # -------------------------- Update Methods --------------------------
    def update_date_label(self):
        self.date_label.setText(time.strftime("%Y-%m-%d %H:%M:%S"))

    def moving_average(self, data, window_size=5):
        if len(data) < window_size:
            return sum(data) / len(data)
        return sum(data[-window_size:]) / window_size

    def update_setpoint_start(self):
        try:
            self.setpoint_start = float(self.setpoint_start_input.text())
            self.setpoint_start_line.setValue(self.setpoint_start)
        except ValueError:
            self.setpoint_start_input.setText("")

    def update_setpoint_end(self):
        try:
            self.setpoint_end = float(self.setpoint_end_input.text())
            self.setpoint_end_line.setValue(self.setpoint_end)
        except ValueError:
            self.setpoint_end_input.setText("")

    def update_setwindow(self):
        try:
            self.setwindow = float(self.setwindow_input.text())
            self.sync_cursors(self.cursor_start, self.cursor_end)
        except ValueError:
            self.setwindow_input.setText("")

    def update_marker_interval(self):
        try:
            self.marker_interval = float(self.marker_input.text())
            self.marker_timer.setInterval(int(self.marker_interval * 1000))
        except ValueError:
            self.marker_input.setText("")

    # -------------------------- Plot & Cursor Methods --------------------------
    def moving_average_fast(self, window=5):
        data = list(self.pressure_data)
        if len(data) < window:
            return data

        result = []
        running_sum = 0

        for i in range(len(data)):
            running_sum += data[i]

            if i >= window:
                running_sum -= data[i - window]

            if i >= window - 1:
                result.append(running_sum / window)
            else:
                result.append(data[i])

        return result
    
    def update_plot(self):
        if not ser or not ser.is_open:
            return
        try:
            # Read ALL available bytes
            bytes_available = ser.in_waiting
            if bytes_available:
                new_data = ser.read(bytes_available)
                self.serial_buffer.extend(new_data)

            # Process complete 2-byte packets
            while len(self.serial_buffer) >= 2:
                packet = self.serial_buffer[:2]
                del self.serial_buffer[:2]

                raw_pressure = struct.unpack('<H', packet)[0]
                eng_pressure = raw_pressure * 1.219 - 246.676

                elapsed_time = time.time() - self.start_time

                self.time_data.append(elapsed_time)
                self.pressure_data.append(eng_pressure)

            # Update plot only if new data exists
            if self.time_data:
                smoothed = self.moving_average_fast()
                self.plot.setData(list(self.time_data), smoothed)

                self.actual_value_label.setText(
                    f"{self.pressure_data[-1]:.2f} kPa"
                )
                self.actual_value_label.setPos(
                    self.time_data[-1],
                    self.pressure_data[-1]
                )

        except Exception as e:
            print("Serial read error:", e)

    def sync_cursors(self, moved_cursor, other_cursor):
        new_pos = moved_cursor.value() + self.setwindow if moved_cursor is self.cursor_start else moved_cursor.value() - self.setwindow
        other_cursor.blockSignals(True)
        other_cursor.setValue(new_pos)
        other_cursor.blockSignals(False)

    def add_marker(self):
        t = time.time() - self.start_time
        marker = pg.InfiniteLine(
            pos=t,
            angle=90,
            pen=pg.mkPen("m", width=1, style=pg.QtCore.Qt.PenStyle.DashLine)
        )
        self.plot_widget.addItem(marker)
        self.marker_items.append(marker)

    def clear_markers(self):
        for marker in self.marker_items:
            try:
                self.plot_widget.removeItem(marker)
            except Exception:
                pass
        self.marker_items.clear()


    def autorange_plot(self):
        vb = self.plot_widget.getViewBox()
        vb.enableAutoRange(axis=pg.ViewBox.XYAxes, enable=True)

    # -------------------------- Preset & Configuration --------------------------
    def reset_with_preset(self, preset_name):
        self.current_preset = preset_name
        self.current_config_label.setText(f"Current Config: {preset_name}")
        self.config_input.setText(preset_name)

        preset = self.presets[preset_name]
        self.setpoint_start_input.setText(str(preset["setpoint_start"]))
        self.setpoint_end_input.setText(str(preset["setpoint_end"]))
        self.setwindow_input.setText(str(preset["setwindow"]))
        self.marker_input.setText(str(preset["marker_interval"]))

        self.update_setpoint_start()
        self.update_setpoint_end()
        self.update_setwindow()
        self.update_marker_interval()
        self.reset_hmi()

    def save_configuration(self):
        try:
            if self.current_preset is None:
                print("No preset selected, nothing saved.")
                return

            # update preset values
            self.presets[self.current_preset]["setpoint_start"] = float(self.setpoint_start_input.text())
            self.presets[self.current_preset]["setpoint_end"] = float(self.setpoint_end_input.text())
            self.presets[self.current_preset]["setwindow"] = float(self.setwindow_input.text())
            self.presets[self.current_preset]["marker_interval"] = float(self.marker_input.text())

            # save to JSON file
            save_presets(self.presets)
            self.current_config_label.setText(f"Current Config: {self.current_preset} (saved)")
            print(f"Preset '{self.current_preset}' saved.")
        except ValueError:
            print("Invalid configuration values (not saved).")
        
    # -------------------------- Reset HMI --------------------------
    def reset_hmi(self):
        self.start_time = time.time()
        self.time_data.clear()
        self.pressure_data.clear()
        self.plot.clear()
        self.cursor_start.setValue(0)
        self.cursor_end.setValue(self.setwindow)
        self.result_label.setText("Test Result: N/A")
        self.result_label.setStyleSheet("color: black;")
        self.autorange_plot()
        self.clear_markers()


    # -------------------------- Analyze & Save --------------------------
    def analyze_and_save(self):
        try:
            start_time = self.cursor_start.value()
            end_time = self.cursor_end.value()
            if start_time >= end_time:
                self.result_label.setText("Invalid dT")
                return

            # Sample marker data
            marker_times = []
            t = start_time
            while t <= end_time:
                marker_times.append(t)
                t += self.marker_interval

            sampled_times, sampled_pressures = [], []
            for mt in marker_times:
                if not self.time_data:
                    continue
                idx = min(range(len(self.time_data)), key=lambda i: abs(self.time_data[i] - mt))
                sampled_times.append(self.time_data[idx])
                sampled_pressures.append(self.pressure_data[idx])

            if not sampled_pressures:
                self.result_label.setText("No data at marker points")
                return

            start_pressure = sampled_pressures[0]
            end_pressure = sampled_pressures[-1]
            pressure_drop = start_pressure - end_pressure
            time_interval = end_time - start_time
            drop_rate = pressure_drop / time_interval
            threshold = (self.setpoint_start - self.setpoint_end) / time_interval
            test_result = "PASS" if drop_rate <= threshold else "FAIL"
            self.result_label.setText(f"Test Result: {test_result} (Drop Rate: {drop_rate:.2f} kPa/s)")
            self.result_label.setStyleSheet("color: green;" if test_result == "PASS" else "color: red;")

            stack = self.stack_input.text().strip()
            cell = self.cell_input.text().strip()
            config = self.config_input.text().strip() or "NA"

            timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
            summary_df = pd.DataFrame([[
                timestamp,
                round(start_pressure, 1),
                round(end_pressure, 1),
                round(drop_rate, 3),
                test_result,
                stack,
                cell,
                config,
                self.comments_input.text()
            ]], columns=[
                "Time",
                "Start Pressure",
                "End Pressure",
                "Drop Rate (kPa/s)",
                "Result",
                "Stack",
                "Cell",
                "Config",
                "Comments"
            ])

            summary_path = os.path.join(self.data_dir, "leak_test_summary.csv")
            summary_df.to_csv(summary_path, mode="a", header=not os.path.exists(summary_path), index=False)

           
            
            detailed_df = pd.DataFrame({"Time (s)": [round(t, 1) for t in sampled_times],
                                        "Pressure (kPa)": [round(p, 1) for p in sampled_pressures]})
            detailed_filename = (
                f"leak_test_data_"
                f"ST{self.stack_input.text()}_"
                f"CELL{self.cell_input.text()}_"
                f"CFG{self.current_preset}_"
                f"{timestamp.replace(':', '-').replace(' ', '_')}.csv"
            )

            #*-----------------Uncomment the line below to record the acquired data samples within the interval
            '''
            detailed_path = os.path.join(self.data_dir, detailed_filename)
            detailed_df.to_csv(detailed_path, index=False)
            '''
            screenshot = self.grab()
            sc_filename = f"screenshot_{os.path.splitext(detailed_filename)[0]}.png"
            sc_path = os.path.join(self.data_dir, sc_filename)
            screenshot.save(sc_path)

            print(f"Screenshot saved to {sc_filename}")
            print(f"Saved test summary and sampled data.")

            # ---------------------------------------------------------
            # WRITE RESULTS INTO EXCEL TEMPLATE (HARD CODED CELLS)
            # ---------------------------------------------------------
            try:
                # ---------------------- Template paths ----------------------
                template_file = resource_path(os.path.join("assets", "Leaktest_Template.xlsx"))

                if not os.path.exists(template_file):
                    self.result_label.setText(f"Template not found: {template_file}")
                    return

                # ---------------------- Stack file path ----------------------
                stack_file = os.path.join(self.data_dir, f"Stack_{stack}.xlsx")

                # If stack file doesn't exist → copy template
                if not os.path.exists(stack_file):
                    from shutil import copyfile
                    copyfile(template_file, stack_file)
                    print(f"Created new stack file from template: {stack_file}")

                # Open stack file (NOT template directly)
                wb = load_workbook(stack_file)
                ws = wb.active

                if not cell.isdigit():
                    self.result_label.setText("Invalid Cell Number")
                    return

                cell_number = int(cell)

                row = cell_number + 2  # Row 3 = Cell 1
                # Write cell number in first column
                ws.cell(row, 1, cell_number)

                config_lower = config.lower()

                # ---------------- COLUMN MAPPING ----------------
                if config_lower == "anode":
                    base_col = 2
                elif config_lower == "cathode":
                    base_col = 10
                elif config_lower == "coolant":
                    base_col = 18
                else:
                    print("Unknown config for Excel writing")
                    return

                COL_CELL    = 1
                COL_START   = base_col
                COL_END     = base_col + 1
                COL_DT      = base_col + 2
                COL_DP      = base_col + 3
                COL_THR     = base_col + 4
                COL_DR1     = base_col + 5
                COL_DR2     = base_col + 6
                COL_DR3     = base_col + 7
                COL_COMMENT = 26

                # Write fixed values (overwrite)
                ws.cell(row, COL_CELL, cell_number)
                ws.cell(row, COL_START, round(start_pressure, 2))
                ws.cell(row, COL_END, round(end_pressure, 2))
                ws.cell(row, COL_DT, round(time_interval, 2))
                ws.cell(row, COL_DP, round(pressure_drop, 2))
                ws.cell(row, COL_THR, round(threshold, 2))

                # ---------------- Drop rate repetition logic ----------------
                if ws.cell(row, COL_DR1).value in (None, ""):
                    ws.cell(row, COL_DR1, round(drop_rate, 3))
                    test_number = 1
                elif ws.cell(row, COL_DR2).value in (None, ""):
                    ws.cell(row, COL_DR2, round(drop_rate, 3))
                    test_number = 2
                else:
                    ws.cell(row, COL_DR3, round(drop_rate, 3))
                    test_number = 3

                # Write comment
                ws.cell(row, COL_COMMENT, self.comments_input.text())

                wb.save(stack_file)

                # ---------------- Update HMI ----------------
                self.test_count_label.setText(
                    f"{config.upper()}  C{cell}  Test {test_number}"
                )

                print(f"Excel updated → Stack {stack}, Cell {cell}, {config}, Test {test_number}")

            except Exception as e:
                print("Excel writing failed:", e)

        except ValueError:
            self.result_label.setText("Invalid Criteria Input")

    # -------------------------- Close Event --------------------------
    def closeEvent(self, event):
        if ser and ser.is_open:
            ser.close()
            print("Serial connection closed.")
        self.datetime_timer.stop()
        self.timer.stop()
        self.marker_timer.stop()
        print("Application closed.")
        event.accept()


# -------------------------- Run Application --------------------------
if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = HMI()
    window.show()
    sys.exit(app.exec())
