# -*- coding: utf-8 -*-
"""
CAD file scanning and level statistics for LVT4U DWG Converter.

This module reads OGR/GDAL vector layers (DXF, DGN, DWG via OGR) and
produces per-Level/Layer statistics grouped by geometry type.

Author: Lộc Vũ Trung / Slow Forest
License: GPL-3.0
"""
from __future__ import annotations
import re
from collections import Counter
from dataclasses import dataclass, field
from typing import Optional

from qgis.core import QgsVectorLayer, QgsWkbTypes


# ── Geometry type classification ──────────────────────────────────────────

GEOM_POINT = 'point'
GEOM_LINE = 'line'
GEOM_POLYGON = 'polygon'
GEOM_TEXT = 'text'    # text entities (CAD TEXT/MTEXT)
GEOM_NONE = 'none'    # no geometry

KIND_LABELS = {
    GEOM_POINT: 'Điểm',
    GEOM_LINE: 'Đường',
    GEOM_POLYGON: 'Vùng',
    GEOM_TEXT: 'Chữ',
    GEOM_NONE: 'Khác',
}

KIND_ORDER = [GEOM_POINT, GEOM_LINE, GEOM_POLYGON, GEOM_TEXT, GEOM_NONE]


def classify_geom_type(wkb_type: int) -> str:
    """Map QgsWkbTypes integer to geometry kind string."""
    flat = QgsWkbTypes.flatType(wkb_type)
    if flat in (QgsWkbTypes.Type.Point, QgsWkbTypes.Type.MultiPoint):
        return GEOM_POINT
    if flat in (
        QgsWkbTypes.Type.LineString, QgsWkbTypes.Type.MultiLineString,
        QgsWkbTypes.Type.CircularString, QgsWkbTypes.Type.CompoundCurve,
        QgsWkbTypes.Type.MultiCurve,
    ):
        return GEOM_LINE
    if flat in (
        QgsWkbTypes.Type.Polygon, QgsWkbTypes.Type.MultiPolygon,
        QgsWkbTypes.Type.CurvePolygon, QgsWkbTypes.Type.MultiSurface,
    ):
        return GEOM_POLYGON
    return GEOM_NONE


# ── Color extraction ──────────────────────────────────────────────────────

_COLOR_RE = re.compile(r'c:(#[0-9A-Fa-f]{6})', re.IGNORECASE)

# DGN standard color table (indices 0–28 most common)
_DGN_COLOR_TABLE = [
    '#FFFFFF', '#000000', '#7F7F7F', '#BFBFBF', '#FF0000', '#FF7F00',
    '#FFFF00', '#7FFF00', '#00FF00', '#00FF7F', '#00FFFF', '#007FFF',
    '#0000FF', '#7F00FF', '#FF00FF', '#FF007F', '#664C00', '#FF4C4C',
    '#FF994C', '#FFFF4C', '#99FF4C', '#4CFF4C', '#4CFF99', '#4CFFFF',
    '#4C99FF', '#4C4CFF', '#994CFF', '#FF4CFF', '#FF4C99',
]


def extract_color(feature) -> tuple[Optional[int], Optional[str]]:
    """
    Extract color from an OGR/QGIS feature.

    Sources checked in order:
    1. 'Color' or 'ColorIndex' field (DGN integer color index)
    2. StyleString 'c:#RRGGBB' (DXF/DWG style)

    Returns:
        (color_index_int_or_None, '#RRGGBB_or_None')
    """
    color_idx: Optional[int] = None
    color_hex: Optional[str] = None

    fields = feature.fields()
    field_names = [f.name() for f in fields]

    # 1. Try DGN integer color index
    for fname in ('Color', 'ColorIndex', 'color', 'colorindex'):
        if fname in field_names:
            idx = fields.indexFromName(fname)
            if idx >= 0:
                val = feature[idx]
                if val is not None and val != '' and val != 'NULL':
                    try:
                        color_idx = int(val)
                        if 0 <= color_idx < len(_DGN_COLOR_TABLE):
                            color_hex = _DGN_COLOR_TABLE[color_idx]
                        break
                    except (ValueError, TypeError):
                        pass  # nosec B110

    # 2. Try StyleString for DXF hex color
    if color_hex is None and 'StyleString' in field_names:
        idx = fields.indexFromName('StyleString')
        if idx >= 0:
            style_str = feature[idx]
            if style_str:
                m = _COLOR_RE.search(str(style_str))
                if m:
                    color_hex = m.group(1).upper()

    return color_idx, color_hex


# ── Text extraction ───────────────────────────────────────────────────────

def extract_text(feature) -> Optional[str]:
    """
    Extract text content from a CAD text entity feature.
    Checks: 'Text', 'Label', 'text', 'TEXTE', 'STRING' field names.
    Returns the text string, or None if not found / empty.
    """
    fields = feature.fields()
    field_names = [f.name() for f in fields]
    for fname in ('Text', 'text', 'Label', 'label', 'TEXTE', 'STRING', 'TEXTSTRING'):
        if fname in field_names:
            idx = fields.indexFromName(fname)
            if idx >= 0:
                val = feature[idx]
                if val and str(val).strip() and str(val).strip() not in ('NULL', 'None'):
                    return str(val).strip()
    return None


# ── LevelStat dataclass ───────────────────────────────────────────────────

@dataclass
class LevelStat:
    """
    Statistics for one CAD Level/Layer.

    Attributes:
        name: Level/Layer name from the CAD file
        counts: dict mapping geometry kind → feature count
        colors: list of '#RRGGBB' strings collected from features
        text_samples: up to 5 text values for encoding preview
    """
    name: str
    counts: dict = field(default_factory=lambda: {
        GEOM_POINT: 0, GEOM_LINE: 0, GEOM_POLYGON: 0,
        GEOM_TEXT: 0, GEOM_NONE: 0
    })
    colors: list = field(default_factory=list)
    text_samples: list = field(default_factory=list)

    @property
    def total(self) -> int:
        return sum(self.counts.values())

    @property
    def dominant_color(self) -> Optional[str]:
        """Most frequently occurring color in this level, or None."""
        if not self.colors:
            return None
        return Counter(self.colors).most_common(1)[0][0]

    def has_kind(self, kind: str) -> bool:
        return self.counts.get(kind, 0) > 0


# ── Main scanning function ─────────────────────────────────────────────────

def scan_ogr_layer(vlayer: QgsVectorLayer) -> dict[str, LevelStat]:
    """
    Scan a QgsVectorLayer (loaded from DXF/DGN/OGR) and build per-Level stats.

    Groups features by 'Layer' or 'Level' field value.
    For each group, counts features by geometry type and collects colors.

    Args:
        vlayer: A valid QgsVectorLayer opened from a CAD file.

    Returns:
        dict mapping level_name → LevelStat, sorted by level name.
    """
    stats: dict[str, LevelStat] = {}
    fields = vlayer.fields()

    # Detect level field name
    level_field: Optional[str] = None
    for fname in ('Layer', 'Level', 'layer', 'level', 'LAYER', 'LEVEL'):
        if fields.indexFromName(fname) >= 0:
            level_field = fname
            break

    for feature in vlayer.getFeatures():
        # Get level name
        if level_field:
            level_val = feature[level_field]
            level_name = str(level_val) if level_val is not None else '0'
        else:
            level_name = '0'

        if not level_name or level_name in ('NULL', 'None', ''):
            level_name = '0'

        if level_name not in stats:
            stats[level_name] = LevelStat(name=level_name)

        stat = stats[level_name]

        # Classify geometry type
        geom = feature.geometry()
        text_val = extract_text(feature)

        if text_val is not None:
            kind = GEOM_TEXT
            if len(stat.text_samples) < 5:
                stat.text_samples.append(text_val)
        elif geom is None or geom.isNull() or geom.isEmpty():
            kind = GEOM_NONE
        else:
            kind = classify_geom_type(geom.wkbType())

        stat.counts[kind] = stat.counts.get(kind, 0) + 1

        # Extract color
        _, color_hex = extract_color(feature)
        if color_hex:
            stat.colors.append(color_hex)

    # Sort by level name (try numeric sort, fall back to string)
    def sort_key(name: str):
        try:
            return (0, int(name), '')
        except ValueError:
            return (1, 0, name.lower())

    return dict(sorted(stats.items(), key=lambda x: sort_key(x[0])))


def scan_sublayers(file_path: str) -> dict[str, LevelStat]:
    """
    Open a CAD file via OGR and scan all features for level statistics.
    Handles DXF, DGN with multiple sublayers by merging all into one scan.

    Args:
        file_path: Absolute path to the DXF/DGN/DWG/GPKG file.

    Returns:
        dict mapping level_name → LevelStat
    """
    merged_stats: dict[str, LevelStat] = {}

    # Try opening the file as a QGIS vector layer
    vlayer = QgsVectorLayer(file_path, 'cad_scan', 'ogr')

    if not vlayer.isValid():
        # Try with encoding hint for DXF files
        vlayer = QgsVectorLayer(
            f'{file_path}|option:DXF_ENCODING=UTF-8', 'cad_scan', 'ogr')

    if not vlayer.isValid():
        return {}

    # Check for sublayers
    sub_layers = vlayer.dataProvider().subLayers()
    if sub_layers:
        del vlayer
        for sub in sub_layers:
            parts = sub.split('!!::!!')
            if len(parts) < 4:
                continue
            layer_name = parts[1]
            geom_type_str = parts[3] if len(parts) > 3 else ''
            if geom_type_str in ('No Geometry', 'Unknown', ''):
                continue
            # Open this sublayer
            uri = f'{file_path}|layername={layer_name}'
            sub_vlayer = QgsVectorLayer(uri, f'scan_{layer_name}', 'ogr')
            if sub_vlayer.isValid():
                sub_stats = scan_ogr_layer(sub_vlayer)
                # Merge into master dict
                for level, stat in sub_stats.items():
                    if level not in merged_stats:
                        merged_stats[level] = LevelStat(name=level)
                    ms = merged_stats[level]
                    for kind, cnt in stat.counts.items():
                        ms.counts[kind] = ms.counts.get(kind, 0) + cnt
                    ms.colors.extend(stat.colors)
                    # Keep up to 5 text samples total
                    remaining = 5 - len(ms.text_samples)
                    if remaining > 0:
                        ms.text_samples.extend(stat.text_samples[:remaining])
                del sub_vlayer
    else:
        merged_stats = scan_ogr_layer(vlayer)
        del vlayer

    return merged_stats
