"""Binary data parser and temperature converter for wafer data.

Mirrors the C# Form1.cs binary parsing pipeline:
  1. Read raw bytes → strip per-block overhead → 1D hex array
  2. Chunk 1D array into rows × sensors → List[List[str]] (hex values)
  3. Convert hex values → float temperatures (family-dependent)
  4. Remove trailing zero rows
  5. Save to CSV with Sensor1, Sensor2, ... headers
"""

import logging
import os
from datetime import datetime
from typing import Optional

from pygui.backend.wafer.family_spec import sensor_count_for

log = logging.getLogger(__name__)


def csv_column_count(family_code: str) -> int:
    """Return the number of columns to display for a family code."""
    return sensor_count_for(family_code)


def _hex_to_binary(hex_str: str) -> list[int]:
    """Convert a 4-char hex string to a 16-bit binary list (MSB first)."""
    value = int(hex_str, 16)
    return [(value >> (15 - i)) & 1 for i in range(16)]


def _twos_complement_excluding_msb(bits: list[int]) -> list[int]:
    """Invert bits 1-15, add 1 (2's complement excluding MSB)."""
    bits = list(bits)  # copy
    for i in range(1, len(bits)):
        bits[i] = 1 - bits[i]
    carry = 1
    for i in range(len(bits) - 1, 0, -1):
        s = bits[i] + carry
        bits[i] = s % 2
        carry = s // 2
    return bits


def _binary_subsequence_to_int(bits: list[int], start: int, end: int) -> int:
    """Convert bits[start:end+1] to integer."""
    result = 0
    for i in range(start, end + 1):
        result = (result << 1) | bits[i]
    return result


def _binary_fraction_to_double(bits: list[int], start: int) -> float:
    """Convert bits[start:] to a fractional value (0 < frac < 1)."""
    result = 0.0
    divisor = 2.0
    for i in range(start, len(bits)):
        result += bits[i] / divisor
        divisor *= 2.0
    return result


def _convert_standard(binary_bits: list[int]) -> float:
    """Convert 16-bit binary using standard formula (B/C/D/F families).

    Bit layout:
      bit 0  : sign
      bits 1-11 : integer part (11 bits, 2^10 .. 2^0)
      bits 12-13 : fractional part (2 bits, 2^-1, 2^-2)
      bits 14-15 : unused
    """
    value = 0.0
    for i in range(1, 14):
        if binary_bits[i]:
            value += 2.0 ** (11 - i)
    if binary_bits[0]:
        value = -value
    return value


def _convert_aep(binary_bits: list[int]) -> float:
    """Convert 16-bit binary using AEP formula.

    Bit layout:
      bit 0  : sign
      bits 1-8 : integer part (8 bits)
      bits 9-15 : fractional part (7 bits)
    """
    bits = binary_bits
    if bits[0] == 1:
        bits = _twos_complement_excluding_msb(bits)
    integer_part = _binary_subsequence_to_int(bits, 1, 8)
    fractional_part = _binary_fraction_to_double(bits, 9)
    result = integer_part + fractional_part
    if binary_bits[0] == 1:
        result = -result
    return result


def _convert_debug_temp(hex_str: str) -> float:
    """Convert a 4-char hex string to a debug/cold-junction temperature.

    Mirrors C#'s ConvertBinaryArrayToDebugTemp: bits 4-14 scaled by
    2**(i-8), sign bit at bit 0. No family-code branching — this formula
    applies regardless of wafer family, unlike _convert_hex_to_temp.
    """
    bits = _hex_to_binary(hex_str)
    value = 0.0
    for i in range(4, 15):
        if bits[i]:
            value += 2.0 ** (i - 8)
    if bits[0]:
        value = -value
    return round(value, 2)


def _convert_hex_to_temp(hex_str: str, family_code: str) -> float:
    """Convert a singi hale 4-char hex string to a float temperature."""
    bits = _hex_to_binary(hex_str)
    if family_code in ("A", "E", "P"):
        result = _convert_aep(bits)
    elif family_code in ("B", "C", "D"):
        result = _convert_standard(bits)
        if result < -2000:
            result = 0.0
    elif family_code == "X":
        # X wafer uses AEP-style conversion
        result = _convert_aep(bits)
    else:
        # Unknown family — try standard
        result = _convert_standard(bits)
    return round(result, 2)


def parse_binary_data(data_bytes: bytes, family_code: str) -> Optional[list[list[str]]]:
    """Parse raw wafer bytes into a 2D array of hex strings.

    Strips per-block overhead bytes (12 for P, 14 for X) and chunks
    the remaining readings into rows.

    Args:
        data_bytes: Raw binary data from the wafer.
        family_code: Wafer family code ("P", "X", "A", "B", "C", "D", "F").

    Returns:
        List of rows, each row is a list of 4-char hex strings.
        Returns None on failure.
    """
    try:
        if family_code == "X":
            return _parse_x_binary(data_bytes)
        else:
            return _parse_p_binary(data_bytes, family_code)
    except Exception as exc:
        log.error("Binary parse failed: %s", exc)
        return None


def _parse_p_binary(data_bytes: bytes, family_code: str = "P") -> list[list[str]]:
    """Parse P-family (and A/B/C/D/E/F) binary data.

    Each block of 256 readings has 244 valid + 12 overhead.
    Valid readings are chunked into rows of the family's sensor count (244).
    """
    sensor_count = sensor_count_for(family_code or "P")
    readings: list[str] = []

    # Read 2 bytes at a time (UInt16 little-endian)
    # Each 256-word block has `sensor_count` valid readings (first N words), rest are overhead
    num_words = len(data_bytes) // 2
    for i in range(num_words):
        value = int.from_bytes(data_bytes[i * 2 : i * 2 + 2], byteorder="little")
        if i % 256 < sensor_count:
            readings.append(f"{value:04X}")

    # Chunk into rows of sensor_count
    result: list[list[str]] = []
    idx = 0
    while idx + sensor_count <= len(readings):
        result.append(readings[idx : idx + sensor_count])
        idx += sensor_count

    log.info("Parsed %s-family: %d rows × %d cols", family_code or "P", len(result), sensor_count)
    return result


def _parse_x_binary(data_bytes: bytes) -> list[list[str]]:
    """Parse X-family binary data.

    Each block of 256 readings has 80 valid + 14 overhead.
    Valid readings are chunked into rows of the family's sensor count (80).
    """
    sensor_count = sensor_count_for("X")
    readings: list[str] = []

    num_words = len(data_bytes) // 2
    for i in range(num_words):
        value = int.from_bytes(data_bytes[i * 2 : i * 2 + 2], byteorder="little")
        if i % 256 < sensor_count:
            readings.append(f"{value:04X}")

    # Chunk into rows of sensor_count
    result: list[list[str]] = []
    idx = 0
    while idx + sensor_count <= len(readings):
        result.append(readings[idx : idx + sensor_count])
        idx += sensor_count

    log.info("Parsed X-family: %d rows × %d cols", len(result), sensor_count)
    return result


def convert_to_temperatures(
    hex_data: list[list[str]], family_code: str
) -> list[list[str]]:
    """Convert hex string values to temperature strings.

    Args:
        hex_data: 2D array of 4-char hex strings from parse_binary_data.
        family_code: Wafer family code.

    Returns:
        Same structure with temperature strings (e.g. "25.37").
    """
    temp_value: list[list[str]] = []
    for row in hex_data:
        temp_row: list[str] = []
        for hex_val in row:
            temp = _convert_hex_to_temp(hex_val, family_code)
            temp_row.append(str(temp))
        temp_value.append(temp_row)
    return temp_value


def convert_to_debug_temperatures(hex_data: list[list[str]]) -> list[list[str]]:
    """Convert debug/cold-junction hex values to temperature strings.

    Same shape contract as convert_to_temperatures, but uses the
    family-independent debug formula (_convert_debug_temp).
    """
    return [[str(_convert_debug_temp(h)) for h in row] for row in hex_data]


def remove_trailing_zeros(data: list[list[str]]) -> None:
    """Remove rows of all-zero values from the end of data (in-place).

    A value is considered zero if its float representation is < 0.01.
    """
    while data:
        last_row = data[-1]
        is_all_zero = True
        for val in last_row:
            try:
                fval = float(val)
            except ValueError:
                fval = 99.9
            if abs(fval) >= 0.01:
                is_all_zero = False
                break
        if is_all_zero:
            data.pop()
        else:
            break


def save_to_csv(
    data: list[list[str]],
    family_code: str,
    serial_number: str,
    output_dir: str,
) -> Optional[str]:
    """Save parsed temperature data to a CSV file.

    Args:
        data: 2D array of temperature strings.
        family_code: Wafer family code.
        serial_number: Wafer serial number (e.g. "P00001").
        output_dir: Directory to save the CSV file.

    Returns:
        Full file path on success, None on failure.
    """
    try:
        os.makedirs(output_dir, exist_ok=True)
        # Build filename: P00001-20260505_133045.csv

        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"{serial_number}-{timestamp}.csv"
        filepath = os.path.join(output_dir, filename)

        # Header count = max of display column count and actual data width.
        # csv_column_count gives the display sensor count (e.g. 48 for P),
        # but the binary parser may yield more readings (244 for P). Use the
        # actual data width so no sensor values are silently dropped.
        display_cols = csv_column_count(family_code)
        data_width = max((len(r) for r in data), default=0)
        num_cols = max(display_cols, data_width)
        with open(filepath, "w", encoding="utf-8") as f:
            # Header row: Sensor1,Sensor2,...
            headers = [f"Sensor{j + 1}" for j in range(num_cols)]
            f.write(",".join(headers) + "\n")
            # Data rows
            for row in data:
                # Pad or trim to match header count
                padded = row[:num_cols] + ["0"] * max(0, num_cols - len(row))
                f.write(",".join(padded) + "\n")

        log.info("Saved %d rows × %d cols to %s", len(data), num_cols, filepath)
        return filepath
    except Exception as exc:
        log.error("CSV save failed: %s", exc)
        return None


def save_debug_csv(
    temp_data: list[list[str]],
    debug_data: list[list[str]],
    output_dir: str,
) -> Optional[str]:
    """Save sensor + debug temperatures side-by-side to debug_info.csv.

    Mirrors C#'s writeDebugCSV: alternating Sensor{j+1},Debug{j+1} columns,
    truncated to the shorter of the two row/column counts. Fixed filename
    (not timestamped), unlike save_to_csv.

    Args:
        temp_data: 2D array of sensor temperature strings (D1, decoded via
            convert_to_temperatures).
        debug_data: 2D array of debug temperature strings (F1, decoded via
            convert_to_debug_temperatures).
        output_dir: Directory to save the CSV file.

    Returns:
        Full file path on success, None on empty input or write failure.
    """
    if not temp_data or not debug_data:
        log.error("Debug CSV save failed: temp or debug data is empty")
        return None
    try:
        os.makedirs(output_dir, exist_ok=True)
        num_cols = min(len(temp_data[0]), len(debug_data[0]))
        num_rows = min(len(temp_data), len(debug_data))
        filepath = os.path.join(output_dir, "debug_info.csv")
        with open(filepath, "w", encoding="utf-8") as f:
            headers = [f"Sensor{j + 1},Debug{j + 1}" for j in range(num_cols)]
            f.write(",".join(headers) + "\n")
            for i in range(num_rows):
                row = [f"{temp_data[i][j]},{debug_data[i][j]}" for j in range(num_cols)]
                f.write(",".join(row) + "\n")

        log.info("Saved debug CSV: %d rows × %d cols to %s", num_rows, num_cols, filepath)
        return filepath
    except Exception as exc:
        log.error("Debug CSV save failed: %s", exc)
        return None
