"""cssl_dtalib.py - CSSL Data Analysis & Visualization Library

v4.9.15: Builtin include for visualizing data structures using PyQt6.

Usage in CSSL:
    dta = include("cssl-dtalib");
    analys = new dta::CsslDataAnalysis(
        mode=dta::Pixel,
        max=false,
        w_h=(900, 600),
        pix_color=fmt::green
    );
    analys.add(container1);
    analys.add(container2);
    analys.add(container3);
    analys.options.showBytes = true;
    analys.show();

Modes:
    Pixel  - Stacked pixel-art columns per element/index, X axis layout
    Bar    - Classic bar chart with gradient fills, per-group coloring
    Line   - Connected line graph with filled area, multi-group overlay
    Round  - Pie/donut chart with legend and percentage labels
"""
from __future__ import annotations

import math
import random
import struct
from typing import Any, Dict, List, Optional, Tuple, Union

# ─────────────────────────────────────────────────────────────────────────────
# Color resolution — accepts fmt:: callables, hex strings, names, RGB tuples
# ─────────────────────────────────────────────────────────────────────────────

_FMT_COLOR_HEX: Dict[str, str] = {
    'red': '#E74C3C',         'green': '#2ECC71',       'blue': '#3498DB',
    'yellow': '#F1C40F',      'cyan': '#1ABC9C',         'magenta': '#9B59B6',
    'white': '#ECF0F1',       'black': '#1C2833',        'pink': '#FF69B4',
    'purple': '#8E44AD',      'orange': '#E67E22',       'gold': '#F39C12',
    'lime': '#27AE60',        'teal': '#16A085',         'navy': '#1B2631',
    'olive': '#7D6608',       'maroon': '#7B241C',       'coral': '#F1948A',
    'salmon': '#FA8072',      'turquoise': '#48C9B0',    'silver': '#BDC3C7',
    'brown': '#795548',       'gray': '#95A5A6',         'grey': '#95A5A6',
    'light_red': '#F1948A',   'dark_red': '#922B21',
    'light_green': '#82E0AA', 'dark_green': '#1E8449',
    'light_blue': '#85C1E9',  'dark_blue': '#1F618D',
    'light_cyan': '#A2D9CE',  'dark_cyan': '#0E6655',
    'light_yellow': '#F9E79F','dark_yellow': '#B7950B',
    'light_magenta': '#C39BD3','dark_magenta': '#6C3483',
    'light_gray': '#D7DBDD',  'dark_gray': '#515A5A',
    'light_grey': '#D7DBDD',  'dark_grey': '#515A5A',
    'light_pink': '#FFB6C1',  'dark_pink': '#FF1493',
    'light_orange': '#FAD7A0','dark_orange': '#CA6F1E',
}

_ANSI_CODE_TO_HEX: Dict[int, str] = {
    31: '#E74C3C', 32: '#2ECC71', 33: '#F1C40F', 34: '#3498DB',
    35: '#9B59B6', 36: '#1ABC9C', 37: '#ECF0F1', 90: '#95A5A6',
    91: '#F1948A', 92: '#82E0AA', 93: '#F9E79F', 94: '#85C1E9',
    95: '#C39BD3', 96: '#A2D9CE', 97: '#FFFFFF',
}

_ROUND_PALETTE: List[str] = [
    '#2ECC71', '#3498DB', '#E74C3C', '#F39C12', '#9B59B6',
    '#1ABC9C', '#E67E22', '#D35400', '#27AE60', '#2980B9',
    '#8E44AD', '#F1C40F', '#16A085', '#C0392B', '#7F8C8D',
    '#6C3483', '#1A5276', '#145A32', '#784212', '#1B2631',
]


def _resolve_color(val: Any, default: str = '#2ECC71') -> str:
    """Resolve any color representation to a CSS hex string."""
    if val is None:
        return default
    if isinstance(val, str):
        stripped = val.strip()
        if stripped.startswith('#'):
            return stripped
        lower = stripped.lower()
        return _FMT_COLOR_HEX.get(lower, stripped)
    if isinstance(val, (list, tuple)) and len(val) >= 3:
        return '#{:02x}{:02x}{:02x}'.format(int(val[0]) & 0xFF, int(val[1]) & 0xFF, int(val[2]) & 0xFF)
    if callable(val):
        name = getattr(val, '__name__', '') or ''
        if name in _FMT_COLOR_HEX:
            return _FMT_COLOR_HEX[name]
        # Probe via ANSI escape codes emitted by fmt methods
        try:
            import re
            result = val('')
            m = re.search(r'\x1b\[(\d+)m', str(result))
            if m:
                return _ANSI_CODE_TO_HEX.get(int(m.group(1)), default)
        except Exception:
            pass
    return default


# ─────────────────────────────────────────────────────────────────────────────
# Byte-size calculation
# ─────────────────────────────────────────────────────────────────────────────

def _byte_size(value: Any) -> int:
    """Compute the in-memory byte footprint of a CSSL / Python value."""
    if isinstance(value, bool):
        return 1
    if isinstance(value, int):
        return max(1, (value.bit_length() + 7) // 8)
    if isinstance(value, float):
        return 8
    if isinstance(value, str):
        return len(value.encode('utf-8'))
    if isinstance(value, (list, tuple)):
        return sum(_byte_size(x) for x in value)
    if isinstance(value, dict):
        return sum(_byte_size(k) + _byte_size(v) for k, v in value.items())
    try:
        import sys
        return sys.getsizeof(value)
    except Exception:
        return 0


# ─────────────────────────────────────────────────────────────────────────────
# Data element — wraps one add() call
# ─────────────────────────────────────────────────────────────────────────────

class _DataElement:
    """Internal representation of one data element added via .add()."""

    __slots__ = ('raw', 'label', 'values', 'sublabels', 'byte_size')

    def __init__(self, raw: Any, label: str = '') -> None:
        self.raw = raw
        self.label = label
        self.byte_size: int = _byte_size(raw)
        self.values: List[float] = []
        self.sublabels: List[str] = []
        self._decompose()

    def _decompose(self) -> None:
        raw = self.raw
        if isinstance(raw, (list, tuple)):
            self.values = [
                float(v) if isinstance(v, (int, float, bool)) else float(len(str(v)))
                for v in raw
            ]
            self.sublabels = [str(i) for i in range(len(raw))]
        elif isinstance(raw, dict):
            self.values = [
                float(v) if isinstance(v, (int, float, bool)) else float(len(str(v)))
                for v in raw.values()
            ]
            self.sublabels = [str(k) for k in raw.keys()]
        elif isinstance(raw, str):
            self.values = [float(len(raw))]
            self.sublabels = ['len']
        elif isinstance(raw, bool):
            self.values = [1.0 if raw else 0.0]
            self.sublabels = ['bool']
        elif isinstance(raw, (int, float)):
            self.values = [float(raw)]
            self.sublabels = [str(raw)]
        else:
            try:
                items = list(raw)
                self.values = [
                    float(v) if isinstance(v, (int, float, bool)) else float(len(str(v)))
                    for v in items
                ]
                self.sublabels = [str(i) for i in range(len(items))]
            except TypeError:
                self.values = [0.0]
                self.sublabels = ['?']

    @property
    def total_bars(self) -> int:
        return len(self.values)

    @property
    def max_value(self) -> float:
        return max(self.values) if self.values else 0.0


def _apply_max(elements: List[_DataElement], max_items: Any) -> List[_DataElement]:
    """Apply max constraint to element list."""
    if max_items is False or max_items is None:
        return elements
    if isinstance(max_items, bool):
        return elements
    if isinstance(max_items, int):
        return elements[:max_items]
    if isinstance(max_items, (tuple, list)) and len(max_items) == 2:
        max_elems = int(max_items[0])
        max_idx = int(max_items[1])
        result: List[_DataElement] = []
        for orig in elements[:max_elems]:
            el = object.__new__(_DataElement)
            el.raw = orig.raw
            el.label = orig.label
            el.byte_size = orig.byte_size
            el.values = orig.values[:max_idx]
            el.sublabels = orig.sublabels[:max_idx]
            result.append(el)
        return result
    return elements


# ─────────────────────────────────────────────────────────────────────────────
# Options object
# ─────────────────────────────────────────────────────────────────────────────

class DtaOptions:
    """Mutable runtime options for CsslDataAnalysis.

    Usage in CSSL:
        analys.options.showBytes = true;
        analys.options.grid = false;
        analys.options.labels = false;
    """

    def __init__(self) -> None:
        self.showBytes: bool = False   # Show byte-size badge above each element group
        self.grid: bool = True         # Show horizontal grid lines
        self.labels: bool = True       # Show element labels on the X axis
        self.values: bool = True       # Show value annotations on bars / points
        self.animate: bool = False     # Animate bar growth on show (future use)
        self.tooltip: bool = True      # Show hover tooltips
        self.zoom: bool = True         # Enable scroll-wheel zoom
        self.scrollable: bool = True   # Enable horizontal scroll with arrow keys


# ─────────────────────────────────────────────────────────────────────────────
# PyQt6 visualization window — built lazily at show() time
# ─────────────────────────────────────────────────────────────────────────────

def _require_qt() -> None:
    try:
        import PyQt6.QtWidgets  # noqa: F401
    except ImportError:
        raise ImportError(
            "cssl-dtalib requires PyQt6. Install with: pip install PyQt6"
        )


def _build_and_exec_window(
    elements: List[_DataElement],
    mode: str,
    w: int,
    h: int,
    pix_color: str,
    title: str,
    bg_color: str,
    grid: bool,
    labels: bool,
    values_ann: bool,
    show_bytes: bool,
    font_size: int,
    padding: int,
    bar_gap: int,
    line_width: int,
    point_size: int,
    axis_color: str,
    text_color: str,
    grid_color: str,
    round_donut: bool,
    round_labels: bool,
    round_colors: Optional[List[str]],
    zoom: bool,
    scrollable: bool,
    tooltip: bool,
) -> None:
    """Construct and exec() the frameless dark PyQt6 visualization dialog."""

    from PyQt6.QtWidgets import (
        QApplication, QDialog, QVBoxLayout, QHBoxLayout,
        QWidget, QLabel, QPushButton, QSizePolicy,
    )
    from PyQt6.QtCore import Qt, QRect, QPoint, QRectF, QPointF
    from PyQt6.QtGui import (
        QPainter, QColor, QFont, QPen, QBrush, QPainterPath,
        QLinearGradient, QFontMetrics, QCursor, QPalette,
    )

    app = QApplication.instance()
    created_app = False
    if app is None:
        import sys
        app = QApplication(sys.argv)
        created_app = True

    # Apply dark palette globally for this session
    pal = QPalette()
    pal.setColor(QPalette.ColorRole.Window,          QColor(bg_color))
    pal.setColor(QPalette.ColorRole.WindowText,      QColor(text_color))
    pal.setColor(QPalette.ColorRole.Base,            QColor(bg_color))
    pal.setColor(QPalette.ColorRole.AlternateBase,   QColor('#1a1a30'))
    pal.setColor(QPalette.ColorRole.Text,            QColor(text_color))
    pal.setColor(QPalette.ColorRole.Button,          QColor('#22223a'))
    pal.setColor(QPalette.ColorRole.ButtonText,      QColor(text_color))
    pal.setColor(QPalette.ColorRole.Highlight,       QColor(pix_color))
    pal.setColor(QPalette.ColorRole.HighlightedText, QColor('#ffffff'))
    app.setPalette(pal)

    # ── Custom title bar ─────────────────────────────────────────────────────

    class TitleBar(QWidget):
        def __init__(self, parent: QDialog) -> None:
            super().__init__(parent)
            self._drag_pos: Optional[QPoint] = None
            self.setFixedHeight(34)
            self.setStyleSheet(
                'QWidget { background-color: #12122a; border-top-left-radius: 7px;'
                '          border-top-right-radius: 7px; }'
            )
            lay = QHBoxLayout(self)
            lay.setContentsMargins(12, 0, 8, 0)
            lay.setSpacing(8)

            icon = QLabel('◈')
            icon.setStyleSheet(f'color: {pix_color}; font-size: 15px; background: transparent;')
            lay.addWidget(icon)

            ttl = QLabel(title)
            ttl.setStyleSheet(
                f'color: {text_color}; font-size: 11px; font-weight: bold; background: transparent;'
            )
            lay.addWidget(ttl)
            lay.addStretch()

            badge = QLabel(mode.upper())
            badge.setStyleSheet(
                f'color: {pix_color}; font-size: 8px; font-weight: bold; background: transparent;'
                f'padding: 1px 6px; border: 1px solid {pix_color}; border-radius: 3px;'
            )
            lay.addWidget(badge)

            close_btn = QPushButton('✕')
            close_btn.setFixedSize(24, 24)
            close_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
            close_btn.setStyleSheet(
                'QPushButton { background: #3a1a1a; color: #cc5555; border: none;'
                '              border-radius: 12px; font-size: 11px; font-weight: bold; }'
                'QPushButton:hover { background: #cc4444; color: #ffffff; }'
            )
            close_btn.clicked.connect(parent.close)
            lay.addWidget(close_btn)

        def mousePressEvent(self, ev) -> None:
            if ev.button() == Qt.MouseButton.LeftButton:
                self._drag_pos = ev.globalPosition().toPoint() - self.parent().frameGeometry().topLeft()

        def mouseMoveEvent(self, ev) -> None:
            if self._drag_pos is not None and ev.buttons() == Qt.MouseButton.LeftButton:
                self.parent().move(ev.globalPosition().toPoint() - self._drag_pos)

        def mouseReleaseEvent(self, ev) -> None:
            self._drag_pos = None

    # ── Chart canvas ─────────────────────────────────────────────────────────

    class ChartWidget(QWidget):
        def __init__(self) -> None:
            super().__init__()
            self._zoom: float = 1.0
            self._scroll: int = 0
            self._hover_text: Optional[str] = None
            self._hover_pos: Optional[QPoint] = None
            self.setMouseTracking(True)
            self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
            self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
            self.setMinimumSize(320, 180)

        def wheelEvent(self, ev) -> None:
            if zoom:
                delta = ev.angleDelta().y()
                factor = 1.12 if delta > 0 else (1.0 / 1.12)
                self._zoom = max(0.15, min(10.0, self._zoom * factor))
                self.update()

        def keyPressEvent(self, ev) -> None:
            if scrollable:
                if ev.key() == Qt.Key.Key_Left:
                    self._scroll = max(0, self._scroll - 40)
                    self.update()
                elif ev.key() == Qt.Key.Key_Right:
                    self._scroll += 40
                    self.update()
            super().keyPressEvent(ev)

        def mouseMoveEvent(self, ev) -> None:
            self._hover_pos = ev.pos()
            if tooltip:
                self._hover_text = self._hit_test(ev.pos())
            self.update()

        def leaveEvent(self, ev) -> None:
            self._hover_text = None
            self._hover_pos = None
            self.update()

        def _hit_test(self, pos: QPoint) -> Optional[str]:
            # Subclass-specific; returns tooltip string or None
            return None

        # ── shared helpers ───────────────────────────────────────────────────

        def _cr(self) -> QRect:
            """Usable chart area after padding."""
            return self.rect().adjusted(padding, padding, -padding, -padding)

        def _draw_grid(self, p: QPainter, cr: QRect, max_val: float, lines: int = 5) -> None:
            if not grid:
                return
            p.setPen(QPen(QColor(grid_color), 1, Qt.PenStyle.DashLine))
            for i in range(1, lines + 1):
                y = cr.bottom() - int(cr.height() * i / lines)
                p.drawLine(cr.left(), y, cr.right(), y)
                if values_ann and max_val > 0:
                    p.setPen(QColor(text_color))
                    p.setFont(QFont('monospace', max(6, font_size - 2)))
                    label_val = max_val * i / lines
                    p.drawText(cr.left() - padding + 2, y - 3, f'{label_val:.1f}')
                    p.setPen(QPen(QColor(grid_color), 1, Qt.PenStyle.DashLine))

        def _draw_axes(self, p: QPainter, cr: QRect) -> None:
            p.setPen(QPen(QColor(axis_color), 1))
            p.drawLine(cr.left(), cr.top(), cr.left(), cr.bottom())
            p.drawLine(cr.left(), cr.bottom(), cr.right(), cr.bottom())

        def _draw_bytes_badge(self, p: QPainter, cx: int, top_y: int, el: _DataElement) -> None:
            if not show_bytes:
                return
            txt = f'{el.byte_size}B'
            p.setFont(QFont('monospace', max(6, font_size - 3)))
            fm = QFontMetrics(p.font())
            tw = fm.horizontalAdvance(txt) + 6
            bx, by = cx - tw // 2, top_y - 18
            p.fillRect(bx, by, tw, 14, QColor('#111122'))
            p.setPen(QColor('#444466'))
            p.drawRect(bx, by, tw, 14)
            p.setPen(QColor('#8888aa'))
            p.drawText(bx + 3, by + 11, txt)

        def _draw_tooltip_box(self, p: QPainter) -> None:
            if not self._hover_text or not self._hover_pos:
                return
            text = self._hover_text
            p.setFont(QFont('monospace', font_size))
            fm = QFontMetrics(p.font())
            lines = text.split('\n')
            tw = max(fm.horizontalAdvance(l) for l in lines) + 14
            th = fm.height() * len(lines) + 10
            tx = min(self._hover_pos.x() + 12, self.width() - tw - 4)
            ty = max(self._hover_pos.y() - th - 6, 4)
            p.fillRect(tx - 1, ty - 1, tw + 2, th + 2, QColor('#0a0a1e'))
            p.setPen(QColor(pix_color))
            p.drawRect(tx, ty, tw, th)
            p.setPen(QColor(text_color))
            for i, line in enumerate(lines):
                p.drawText(tx + 7, ty + fm.height() * (i + 1) + 2, line)

        # ── paintEvent dispatcher ────────────────────────────────────────────

        def paintEvent(self, ev) -> None:
            p = QPainter(self)
            p.setRenderHint(QPainter.RenderHint.Antialiasing)
            p.fillRect(self.rect(), QColor(bg_color))
            if not elements:
                p.setPen(QColor(text_color))
                p.setFont(QFont('monospace', font_size + 2))
                p.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, 'No data added.')
                p.end()
                return
            dispatch = {
                'pixel': self._paint_pixel,
                'bar':   self._paint_bar,
                'line':  self._paint_line,
                'round': self._paint_round,
            }
            draw_fn = dispatch.get(mode, self._paint_pixel)
            draw_fn(p)
            self._draw_tooltip_box(p)
            p.end()

        # ── PIXEL mode ───────────────────────────────────────────────────────

        def _paint_pixel(self, p: QPainter) -> None:
            cr = self._cr()
            all_vals = [v for el in elements for v in el.values]
            max_val = max(all_vals) if all_vals else 1.0
            if max_val == 0:
                max_val = 1.0

            z = self._zoom
            px = max(2, int(4 * z))          # square pixel size
            bw = px + max(1, int(bar_gap * z))  # bar column width

            self._draw_grid(p, cr, max_val)
            self._draw_axes(p, cr)

            x = cr.left() - self._scroll
            base_c = QColor(pix_color)

            for el in elements:
                el_x0 = x
                for idx, val in enumerate(el.values):
                    height_px = int((val / max_val) * cr.height())
                    num_sq = max(0, height_px // px)
                    for j in range(num_sq):
                        sq_y = cr.bottom() - (j + 1) * px
                        if sq_y < cr.top():
                            break
                        # Brightness gradient: bottom dim → top full
                        t = j / max(num_sq, 1)
                        r = min(255, int(base_c.red()   * (0.35 + 0.65 * t)))
                        g = min(255, int(base_c.green() * (0.35 + 0.65 * t)))
                        b = min(255, int(base_c.blue()  * (0.35 + 0.65 * t)))
                        p.fillRect(x, sq_y, px - 1, px - 1, QColor(r, g, b))

                    # Sub-label (index)
                    if labels and len(el.sublabels) > idx:
                        p.setFont(QFont('monospace', max(6, font_size - 3)))
                        p.setPen(QColor(axis_color))
                        p.drawText(x, cr.bottom() + font_size + 2, el.sublabels[idx][:4])

                    x += bw

                # Group separator
                sep_x = x - bw // 2
                p.setPen(QPen(QColor(axis_color), 1, Qt.PenStyle.DotLine))
                p.drawLine(sep_x, cr.top(), sep_x, cr.bottom())

                # Group label
                el_cx = (el_x0 + x) // 2
                if labels and el.label:
                    p.setFont(QFont('monospace', max(7, font_size - 1)))
                    p.setPen(QColor(text_color))
                    fm = QFontMetrics(p.font())
                    lbl = el.label[:14]
                    lx = el_cx - fm.horizontalAdvance(lbl) // 2
                    p.drawText(lx, cr.bottom() + font_size * 2 + 4, lbl)

                self._draw_bytes_badge(p, el_cx, cr.top(), el)

        # ── BAR mode ─────────────────────────────────────────────────────────

        def _paint_bar(self, p: QPainter) -> None:
            cr = self._cr()
            all_vals = [v for el in elements for v in el.values]
            max_val = max(all_vals) if all_vals else 1.0
            if max_val == 0:
                max_val = 1.0

            z = self._zoom
            total_bars = sum(el.total_bars for el in elements)
            raw_bw = cr.width() / max(total_bars, 1) * z
            bw = max(4, int(raw_bw) - int(bar_gap * z))

            self._draw_grid(p, cr, max_val)
            self._draw_axes(p, cr)

            x = cr.left() - self._scroll
            for e_idx, el in enumerate(elements):
                grp_hex = _ROUND_PALETTE[e_idx % len(_ROUND_PALETTE)]
                grp_c = QColor(grp_hex)
                el_x0 = x

                for idx, val in enumerate(el.values):
                    bh = int((val / max_val) * cr.height())
                    by = cr.bottom() - bh

                    grad = QLinearGradient(QPointF(x, by), QPointF(x, cr.bottom()))
                    grad.setColorAt(0.0, grp_c.lighter(160))
                    grad.setColorAt(1.0, grp_c)
                    p.fillRect(x, by, bw, bh, grad)

                    # Value annotation inside bar
                    if values_ann and bh > font_size + 6:
                        p.setPen(QColor('#ffffff'))
                        p.setFont(QFont('monospace', max(7, font_size - 2)))
                        p.drawText(x + 2, by + font_size + 2, f'{val:.0f}')

                    # Sub-label below bar
                    if labels and len(el.sublabels) > idx:
                        p.setFont(QFont('monospace', max(6, font_size - 3)))
                        p.setPen(QColor(text_color))
                        p.drawText(x, cr.bottom() + font_size + 3, el.sublabels[idx][:5])

                    x += bw + int(bar_gap * z)

                # Group label
                el_cx = (el_x0 + x) // 2
                if labels and el.label:
                    p.setFont(QFont('monospace', font_size))
                    p.setPen(QColor(text_color))
                    fm = QFontMetrics(p.font())
                    lbl = el.label[:14]
                    lx = el_cx - fm.horizontalAdvance(lbl) // 2
                    p.drawText(lx, cr.bottom() + font_size * 2 + 4, lbl)

                self._draw_bytes_badge(p, el_cx, cr.top(), el)

        # ── LINE mode ────────────────────────────────────────────────────────

        def _paint_line(self, p: QPainter) -> None:
            cr = self._cr()
            all_vals = [v for el in elements for v in el.values]
            max_val = max(all_vals) if all_vals else 1.0
            if max_val == 0:
                max_val = 1.0

            self._draw_grid(p, cr, max_val)
            self._draw_axes(p, cr)

            z = self._zoom
            total_pts = sum(el.total_bars for el in elements)
            step = max(4, int(cr.width() / max(total_pts, 1) * z))

            x = cr.left() - self._scroll
            for e_idx, el in enumerate(elements):
                grp_c = QColor(_ROUND_PALETTE[e_idx % len(_ROUND_PALETTE)])
                pts: List[QPointF] = []

                for val in el.values:
                    py = cr.bottom() - int((val / max_val) * cr.height())
                    pts.append(QPointF(x, py))
                    x += step

                if not pts:
                    continue

                # Filled area under line
                if len(pts) >= 2:
                    path = QPainterPath()
                    path.moveTo(pts[0].x(), cr.bottom())
                    for pt in pts:
                        path.lineTo(pt)
                    path.lineTo(pts[-1].x(), cr.bottom())
                    path.closeSubpath()
                    fill = QColor(grp_c)
                    fill.setAlpha(45)
                    p.fillPath(path, fill)

                    # Line
                    lw = max(1, int(line_width * z))
                    p.setPen(QPen(grp_c, lw, Qt.PenStyle.SolidLine,
                                  Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
                    for i in range(len(pts) - 1):
                        p.drawLine(pts[i], pts[i + 1])

                # Points
                ps = max(2, int(point_size * z))
                p.setBrush(QBrush(grp_c))
                p.setPen(QPen(QColor('#ffffff'), 1))
                for i, pt in enumerate(pts):
                    p.drawEllipse(pt, ps, ps)
                    # Value annotation
                    if values_ann and len(el.values) > i:
                        p.setPen(QColor(text_color))
                        p.setFont(QFont('monospace', max(6, font_size - 2)))
                        p.drawText(int(pt.x()) + ps + 2, int(pt.y()) - 2, f'{el.values[i]:.0f}')
                        p.setPen(QPen(QColor('#ffffff'), 1))

                # Group label & bytes
                if pts:
                    el_cx = int((pts[0].x() + pts[-1].x()) / 2)
                    if labels and el.label:
                        p.setPen(QColor(text_color))
                        p.setFont(QFont('monospace', font_size))
                        fm = QFontMetrics(p.font())
                        lbl = el.label[:14]
                        p.drawText(el_cx - fm.horizontalAdvance(lbl) // 2,
                                   cr.bottom() + font_size + 4, lbl)
                    self._draw_bytes_badge(p, el_cx, cr.top(), el)

        # ── ROUND / PIE mode ─────────────────────────────────────────────────

        def _paint_round(self, p: QPainter) -> None:
            cr = self._cr()
            all_vals = [v for el in elements for v in el.values]
            total = sum(all_vals) or 1.0

            colors = round_colors if round_colors else _ROUND_PALETTE
            cx = cr.center().x()
            cy = cr.center().y()
            radius = min(cr.width(), cr.height()) // 2 - 12
            inner_r = radius // 2 if round_donut else 0

            angle_deg = -90.0   # start at top (12 o'clock)
            seg_idx = 0

            for el in elements:
                for i, val in enumerate(el.values):
                    if val <= 0:
                        seg_idx += 1
                        continue
                    span = (val / total) * 360.0
                    color = QColor(colors[seg_idx % len(colors)])

                    # Pie segment
                    p.setBrush(QBrush(color))
                    p.setPen(QPen(QColor(bg_color), 2))
                    rect = QRectF(cx - radius, cy - radius, radius * 2, radius * 2)
                    p.drawPie(rect, int(angle_deg * 16), int(-span * 16))

                    # Donut hole
                    if round_donut and inner_r > 0:
                        p.setBrush(QBrush(QColor(bg_color)))
                        p.setPen(Qt.PenStyle.NoPen)
                        p.drawEllipse(QRectF(cx - inner_r, cy - inner_r,
                                             inner_r * 2, inner_r * 2))

                    # Segment label
                    if round_labels and span > 8:
                        mid_rad = math.radians(angle_deg - span / 2)
                        lr = (radius * 0.68 + inner_r * 0.25)
                        lx = cx + lr * math.cos(mid_rad)
                        ly = cy - lr * math.sin(mid_rad)
                        p.setPen(QColor('#ffffff'))
                        p.setFont(QFont('monospace', max(7, font_size - 1)))
                        pct = val / total * 100
                        sub = el.sublabels[i] if i < len(el.sublabels) else str(i)
                        for li, line in enumerate([sub, f'{pct:.1f}%']):
                            p.drawText(int(lx) - 18, int(ly) + li * (font_size + 2), line)

                    angle_deg -= span
                    seg_idx += 1

            # Donut center text
            if round_donut and inner_r > 0:
                p.setPen(QColor(text_color))
                p.setFont(QFont('monospace', max(8, font_size), QFont.Weight.Bold))
                center_rect = QRect(int(cx - inner_r), int(cy - inner_r),
                                    inner_r * 2, inner_r * 2)
                p.drawText(center_rect, Qt.AlignmentFlag.AlignCenter,
                           f'{len(all_vals)}\nelems')

            # Legend
            legend_x = cr.right() - 130
            legend_y = cr.top() + 8
            seg_idx = 0
            for el in elements:
                for i, val in enumerate(el.values):
                    color = QColor(colors[seg_idx % len(colors)])
                    p.fillRect(legend_x, legend_y, 12, 12, color)
                    p.setPen(QColor(text_color))
                    p.setFont(QFont('monospace', max(7, font_size - 1)))
                    sub = el.sublabels[i] if i < len(el.sublabels) else str(i)
                    lbl = (f'{el.label}.{sub}' if el.label else sub)[:20]
                    p.drawText(legend_x + 16, legend_y + 10, lbl)
                    legend_y += 16
                    seg_idx += 1

    # ── Dialog container ──────────────────────────────────────────────────────

    class DtaWindow(QDialog):
        def __init__(self) -> None:
            super().__init__(None)
            self.setWindowFlags(
                Qt.WindowType.FramelessWindowHint | Qt.WindowType.Dialog
            )
            self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
            self.setFixedSize(w, h)
            self.setStyleSheet(
                f'QDialog {{ background-color: {bg_color};'
                f'border: 1px solid #252540; border-radius: 7px; }}'
            )

            vbox = QVBoxLayout(self)
            vbox.setContentsMargins(0, 0, 0, 0)
            vbox.setSpacing(0)

            vbox.addWidget(TitleBar(self))

            chart = ChartWidget()
            vbox.addWidget(chart, 1)

            # Bottom byte-summary bar
            if show_bytes:
                parts = [
                    f'{el.label or f"[{i}]"}: {el.byte_size}B'
                    for i, el in enumerate(elements)
                ]
                summary = QLabel('  '.join(parts))
                summary.setStyleSheet(
                    f'color: #7777aa; font-size: 8px; font-family: monospace;'
                    f'background: #0e0e20; padding: 3px 14px;'
                    f'border-top: 1px solid #252540;'
                )
                summary.setWordWrap(True)
                vbox.addWidget(summary)

    win = DtaWindow()
    win.exec()


# ─────────────────────────────────────────────────────────────────────────────
# Main public API class
# ─────────────────────────────────────────────────────────────────────────────

class CsslDataAnalysis:
    """CSSL Data Analysis & Visualization.

    Opens a frameless dark PyQt6 window that renders any CSSL data structure
    as a Pixel, Bar, Line, or Round (pie/donut) chart.

    Constructor parameters
    ──────────────────────
    mode (str)             : 'pixel' | 'bar' | 'line' | 'round'  [default: 'pixel']
    max (any)              : False = no limit | int = max elements |
                             (max_elems, max_idx) = element + index cap
    w_h (tuple)            : (width, height) of the window           [default: (800, 600)]
    pix_color (any)        : Primary color — fmt:: callable, hex, name, or (r,g,b) tuple
    title (str)            : Window title bar text
    bg_color (str)         : Background hex color                    [default: '#0d0d1a']
    grid (bool)            : Horizontal grid lines                   [default: True]
    labels (bool)          : Element labels on X axis                [default: True]
    values (bool)          : Value annotations on bars/points        [default: True]
    font_size (int)        : Base font size for all labels            [default: 10]
    padding (int)          : Padding around chart area in px          [default: 40]
    bar_gap (int)          : Gap between bars (Pixel / Bar modes)     [default: 4]
    line_width (int)       : Line stroke width (Line mode)            [default: 2]
    point_size (int)       : Point dot radius (Line mode)             [default: 5]
    axis_color (str)       : Axis / separator line hex color
    text_color (str)       : Label text hex color
    grid_color (str)       : Grid line hex color
    round_donut (bool)     : Round mode — hollow donut style          [default: False]
    round_labels (bool)    : Round mode — % labels on segments        [default: True]
    round_colors (list)    : Round mode — explicit color list, None = auto
    zoom (bool)            : Scroll-wheel zoom                        [default: True]
    scrollable (bool)      : Horizontal scroll via arrow keys         [default: True]
    tooltip (bool)         : Hover tooltips                           [default: True]

    Runtime options (via .options.*):
        showBytes   — byte-size badge above each element group
        grid, labels, values, tooltip, zoom, scrollable — mirror ctor params

    Usage:
        analys = new dta::CsslDataAnalysis(mode=dta::Bar, w_h=(1000, 700));
        analys.add(myVec);
        analys.add(someInt);
        analys.options.showBytes = true;
        analys.show();
    """

    def __init__(
        self,
        mode: str = 'pixel',
        max: Any = False,
        w_h: Tuple[int, int] = (800, 600),
        pix_color: Any = '#2ECC71',
        title: str = 'CSSL Data Analysis',
        bg_color: str = '#0d0d1a',
        grid: bool = True,
        labels: bool = True,
        values: bool = True,
        font_size: int = 10,
        padding: int = 40,
        bar_gap: int = 4,
        line_width: int = 2,
        point_size: int = 5,
        axis_color: str = '#3a3a5a',
        text_color: str = '#c8c8e0',
        grid_color: str = '#1a1a32',
        round_donut: bool = False,
        round_labels: bool = True,
        round_colors: Optional[List[str]] = None,
        zoom: bool = True,
        scrollable: bool = True,
        tooltip: bool = True,
    ) -> None:
        m = str(mode).lower().strip()
        if m not in ('pixel', 'bar', 'line', 'round'):
            raise ValueError(f"Unknown mode '{mode}'. Use: pixel, bar, line, round")
        self._mode = m
        self._max = max
        self._w = int(w_h[0]) if isinstance(w_h, (tuple, list)) else 800
        self._h = int(w_h[1]) if isinstance(w_h, (tuple, list)) else 600
        self._pix_color = _resolve_color(pix_color)
        self._title = str(title)
        self._bg_color = str(bg_color)
        self._grid = bool(grid)
        self._labels = bool(labels)
        self._values = bool(values)
        self._font_size = int(font_size)
        self._padding = int(padding)
        self._bar_gap = int(bar_gap)
        self._line_width = int(line_width)
        self._point_size = int(point_size)
        self._axis_color = str(axis_color)
        self._text_color = str(text_color)
        self._grid_color = str(grid_color)
        self._round_donut = bool(round_donut)
        self._round_labels = bool(round_labels)
        self._round_colors = list(round_colors) if round_colors else None
        self._zoom = bool(zoom)
        self._scrollable = bool(scrollable)
        self._tooltip = bool(tooltip)
        self._elements: List[_DataElement] = []
        self._ctr = 0

        self.options = DtaOptions()

    def add(self, value: Any, label: str = '') -> 'CsslDataAnalysis':
        """Add a data element to be visualized.

        Args:
            value : Any CSSL value (int, float, string, list, vector, dict …)
            label : Optional display label (auto-indexed if empty)

        Returns:
            self — allows chaining
        """
        if not label:
            label = f'[{self._ctr}]'
        self._ctr += 1
        self._elements.append(_DataElement(value, label))
        return self

    def show(self) -> None:
        """Open the visualization window. Blocks until the user closes it with X."""
        _require_qt()
        visible = _apply_max(self._elements, self._max)
        _build_and_exec_window(
            elements=visible,
            mode=self._mode,
            w=self._w,
            h=self._h,
            pix_color=self._pix_color,
            title=self._title,
            bg_color=self._bg_color,
            grid=self._grid and self.options.grid,
            labels=self._labels and self.options.labels,
            values_ann=self._values and self.options.values,
            show_bytes=self.options.showBytes,
            font_size=self._font_size,
            padding=self._padding,
            bar_gap=self._bar_gap,
            line_width=self._line_width,
            point_size=self._point_size,
            axis_color=self._axis_color,
            text_color=self._text_color,
            grid_color=self._grid_color,
            round_donut=self._round_donut,
            round_labels=self._round_labels,
            round_colors=self._round_colors,
            zoom=self._zoom and self.options.zoom,
            scrollable=self._scrollable and self.options.scrollable,
            tooltip=self._tooltip and self.options.tooltip,
        )

    def clear(self) -> 'CsslDataAnalysis':
        """Remove all added elements and reset the counter."""
        self._elements.clear()
        self._ctr = 0
        return self

    def set_mode(self, mode: str) -> 'CsslDataAnalysis':
        """Change display mode after construction."""
        m = str(mode).lower().strip()
        if m not in ('pixel', 'bar', 'line', 'round'):
            raise ValueError(f"Unknown mode '{mode}'")
        self._mode = m
        return self

    def set_color(self, color: Any) -> 'CsslDataAnalysis':
        """Change the primary color after construction."""
        self._pix_color = _resolve_color(color)
        return self

    def set_size(self, w: int, h: int) -> 'CsslDataAnalysis':
        """Change the window dimensions after construction."""
        self._w = int(w)
        self._h = int(h)
        return self

    def set_title(self, title: str) -> 'CsslDataAnalysis':
        """Change the window title after construction."""
        self._title = str(title)
        return self


# ─────────────────────────────────────────────────────────────────────────────
# Module object — returned by include("cssl-dtalib")
# ─────────────────────────────────────────────────────────────────────────────

class CsslDtalibModule:
    """CSSL Data Analysis Library module.

    Returned by include("cssl-dtalib"). Provides the CsslDataAnalysis class
    and mode constants accessible via the dta:: namespace.

    Usage:
        dta = include("cssl-dtalib");
        analys = new dta::CsslDataAnalysis(mode=dta::Pixel, w_h=(900, 600));
        analys.add(myData);
        analys.show();

    Mode constants:
        dta::Pixel  → 'pixel'
        dta::Bar    → 'bar'
        dta::Line   → 'line'
        dta::Round  → 'round'
    """

    # Class exposed to CSSL via dta::CsslDataAnalysis
    CsslDataAnalysis = CsslDataAnalysis

    # Mode constants
    Pixel = 'pixel'
    Bar   = 'bar'
    Line  = 'line'
    Round = 'round'

    def __init__(self) -> None:
        self._runtime = None

    def __getattr__(self, name: str) -> Any:
        _map: Dict[str, Any] = {
            'CsslDataAnalysis': CsslDataAnalysis,
            'Pixel': 'pixel',
            'Bar':   'bar',
            'Line':  'line',
            'Round': 'round',
        }
        if name in _map:
            return _map[name]
        raise AttributeError(f"'cssl-dtalib' module has no attribute '{name}'")


_dtalib_module_instance: Optional[CsslDtalibModule] = None


def get_dtalib_module() -> CsslDtalibModule:
    """Return the singleton CsslDtalibModule instance."""
    global _dtalib_module_instance
    if _dtalib_module_instance is None:
        _dtalib_module_instance = CsslDtalibModule()
    return _dtalib_module_instance
