# -*- coding: utf-8 -*-
"""
LVT TAB Reader — MapInfo TAB → Shapefile converter.

Read MapInfo TAB files (which can have mixed geometry: Point, Line, Polygon,
AND Text objects), split by geometry type, and export each as separate SHP
files with UTF-8 encoding and CRS assignment.

Text objects (labels on map) are extracted as Point features with
TEXT, ANGLE, FONT_SIZE, COLOR attributes.

Uses OGR/GDAL Python API for all reading AND writing to get full control
over Text object detection and geometry type handling.

Author: Lộc Vũ Trung (LVT)
License: GPL-3.0
"""

import os
import re
from qgis.PyQt.QtCore import Qt, QTimer
from qgis.PyQt.QtWidgets import (
    QDialog, QVBoxLayout, QHBoxLayout, QLabel,
    QPushButton, QFileDialog, QMessageBox, QGroupBox,
    QProgressBar, QApplication, QCheckBox, QLineEdit,
    QComboBox, QTreeWidget, QTreeWidgetItem, QHeaderView,
    QTextEdit,
)
from qgis.core import QgsProject, QgsVectorLayer
from osgeo import ogr, osr
from .._crs_list import CRS_LIST
from ..shared.i18n import tr
from ..shared.qt_compat import QtCompat, MsgButton

# Import TCVN3→Unicode conversion tables
from ..font_converter.dialog import _TCVN2UNI_1, _TCVN2UNI_2


# ═══════════════════════════════════════════════════════════════
# Module-level helpers
# ═══════════════════════════════════════════════════════════════

def _convert_tcvn3_to_unicode(text):
    """Convert TCVN3 encoded text to Unicode (standalone function).

    Uses the two-byte and single-byte mapping tables imported from
    font_converter.dialog to translate legacy TCVN3 (ABC/.VnTime)
    encoded strings to proper Unicode Vietnamese.

    Args:
        text: String potentially containing TCVN3 encoded characters.

    Returns:
        str: Unicode-converted string, or the original if input is empty.
    """
    if not text:
        return text
    result = []
    i = 0
    while i < len(text):
        if i + 1 < len(text):
            pair = text[i:i + 2]
            if pair in _TCVN2UNI_2:
                result.append(_TCVN2UNI_2[pair])
                i += 2
                continue
        ch = text[i]
        if ch in _TCVN2UNI_1:
            result.append(_TCVN2UNI_1[ch])
        else:
            result.append(ch)
        i += 1
    return ''.join(result)


def _find_tab_files(directory):
    """Recursively find all .tab files in a directory."""
    results = []
    for root, dirs, files in os.walk(directory):
        for f in files:
            if f.lower().endswith('.tab'):
                results.append(os.path.join(root, f))
    results.sort()
    return results


# ═══════════════════════════════════════════════════════════════
# Dialog class
# ═══════════════════════════════════════════════════════════════

class TabReaderDialog(QDialog):
    """MapInfo TAB → SHP converter dialog.

    Reads MapInfo TAB files, analyses geometry types (Point, Line,
    Polygon, Text), and exports each type as a separate Shapefile
    with UTF-8 encoding, CRS assignment, and optional TCVN3→Unicode
    font conversion.
    """

    def __init__(self, iface, parent=None):
        super().__init__(parent or iface.mainWindow())
        self.iface = iface
        self.setWindowTitle("MapInfo TAB → SHP")
        self.setMinimumSize(650, 620)
        self.setWindowFlags(
            self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
        self._input_files = []
        self._analysis = {}  # {filepath: {'Point': count, 'Line': count, ...}}
        self._analyze_timer = None  # Debounce timer for auto-analyze
        self._build_ui()

    # ───────────────────────────────────────────────────────────
    # UI construction
    # ───────────────────────────────────────────────────────────

    def _build_ui(self):
        """Build the complete dialog UI."""
        ly = QVBoxLayout(self)

        # Header
        hdr = QLabel("<h3>📋 MapInfo TAB → SHP</h3>")
        ly.addWidget(hdr)

        # ── Input GroupBox ──
        grp_input = QGroupBox(tr('Input TAB Files'))
        vl_input = QVBoxLayout(grp_input)

        # Row 1: Buttons
        hl_browse = QHBoxLayout()
        self.btn_browse_folder = QPushButton("📂 " + tr('Folder...'))
        self.btn_browse_folder.setToolTip(
            tr('Select a parent folder — subfolders with TAB files will be listed'))
        self.btn_browse_folder.clicked.connect(self._browse_parent_folder)

        self.btn_browse_files = QPushButton("📄 " + tr('Files...'))
        self.btn_browse_files.setToolTip(
            tr('Select individual TAB files'))
        self.btn_browse_files.clicked.connect(self._browse_files)

        self.btn_clear = QPushButton(tr('Clear'))
        self.btn_clear.clicked.connect(self._clear_files)

        self.lbl_count = QLabel("0 file(s)")

        hl_browse.addWidget(self.btn_browse_folder)
        hl_browse.addWidget(self.btn_browse_files)
        hl_browse.addWidget(self.btn_clear)
        hl_browse.addStretch()
        hl_browse.addWidget(self.lbl_count)
        vl_input.addLayout(hl_browse)

        # Tree widget for folder/file display with checkboxes
        self.tree_input = QTreeWidget()
        self.tree_input.setHeaderLabels([
            tr('Folder / File'),
            tr('TAB files'),
        ])
        self.tree_input.setColumnCount(2)
        header_in = self.tree_input.header()
        header_in.setStretchLastSection(False)
        header_in.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
        header_in.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
        self.tree_input.setMinimumHeight(100)
        self.tree_input.setMaximumHeight(160)
        self.tree_input.itemChanged.connect(self._on_input_tree_changed)
        vl_input.addWidget(self.tree_input)
        ly.addWidget(grp_input)

        # ── Analysis TreeWidget (auto-populated, no button) ──
        grp_analysis = QGroupBox(tr('Analysis') + '  (auto)')
        vl_analysis = QVBoxLayout(grp_analysis)

        self.tree = QTreeWidget()
        self.tree.setHeaderLabels([
            tr('File / Geometry Type'),
            tr('Features'),
            tr('Export'),
        ])
        self.tree.setColumnCount(3)
        header = self.tree.header()
        header.setStretchLastSection(False)
        header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
        header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
        header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
        self.tree.setMinimumHeight(100)
        vl_analysis.addWidget(self.tree)
        ly.addWidget(grp_analysis)

        # ── Output GroupBox ──
        grp_output = QGroupBox(tr('Output'))
        vl_output = QVBoxLayout(grp_output)

        # Folder picker
        hl_folder = QHBoxLayout()
        hl_folder.addWidget(QLabel(tr('Folder:')))
        self.txt_folder = QLineEdit()
        self.txt_folder.setPlaceholderText(tr('Select output folder'))
        hl_folder.addWidget(self.txt_folder)
        self.btn_folder = QPushButton("📁")
        self.btn_folder.setFixedWidth(32)
        self.btn_folder.clicked.connect(self._browse_folder)
        hl_folder.addWidget(self.btn_folder)
        vl_output.addLayout(hl_folder)

        # CRS combo
        hl_crs = QHBoxLayout()
        hl_crs.addWidget(QLabel("CRS:"))
        self.cmb_crs = QComboBox()
        for label, _ in CRS_LIST:
            self.cmb_crs.addItem(label)
        # Disable separator items (items with empty code)
        for i, (_, code) in enumerate(CRS_LIST):
            if not code:
                self.cmb_crs.model().item(i).setEnabled(False)
        hl_crs.addWidget(self.cmb_crs)
        vl_output.addLayout(hl_crs)

        # Checkboxes row
        hl_opts = QHBoxLayout()
        self.chk_tcvn = QCheckBox(tr('Convert TCVN3 → Unicode'))
        self.chk_tcvn.setChecked(True)
        self.chk_add = QCheckBox(tr('Add to project'))
        self.chk_add.setChecked(True)
        self.chk_merge = QCheckBox(tr('Merge results'))
        self.chk_merge.setToolTip(
            tr('Combine all TAB files into a single SHP per geometry type'))
        self.chk_merge.setChecked(False)
        hl_opts.addWidget(self.chk_tcvn)
        hl_opts.addWidget(self.chk_add)
        hl_opts.addWidget(self.chk_merge)
        hl_opts.addStretch()
        vl_output.addLayout(hl_opts)

        ly.addWidget(grp_output)

        # ── Convert button ──
        self.btn_convert = QPushButton("🔄 " + tr('Convert'))
        self.btn_convert.setMinimumHeight(38)
        self.btn_convert.setStyleSheet(
            "QPushButton { font-size: 14px; font-weight: bold; "
            "background-color: #2e7d32; color: white; border-radius: 4px; }"
            "QPushButton:hover { background-color: #388e3c; }"
            "QPushButton:disabled { background-color: #9e9e9e; }"
        )
        self.btn_convert.clicked.connect(self._convert)
        ly.addWidget(self.btn_convert)

        # ── Progress bar (hidden initially) ──
        self.progress = QProgressBar()
        self.progress.setVisible(False)
        self.progress.setRange(0, 100)
        ly.addWidget(self.progress)

        # ── Log output (hidden initially) ──
        self.log = QTextEdit()
        self.log.setVisible(False)
        self.log.setMaximumHeight(140)
        self.log.setReadOnly(True)
        self.log.setStyleSheet(
            "QTextEdit { font-family: Consolas, monospace; font-size: 11px; "
            "background-color: #1e1e1e; color: #d4d4d4; }"
        )
        ly.addWidget(self.log)

    # ───────────────────────────────────────────────────────────
    # Plugin pattern required method
    # ───────────────────────────────────────────────────────────

    def refresh_layers(self):
        """Required by the LVT plugin pattern — no-op for this dialog."""

    # ───────────────────────────────────────────────────────────
    # Input: Browse parent folder → show subfolders with checkboxes
    # ───────────────────────────────────────────────────────────

    def _browse_parent_folder(self):
        """Select a parent folder and list subfolders containing TAB files."""
        folder = QFileDialog.getExistingDirectory(
            self, tr('Select parent folder containing TAB files'))
        if not folder:
            return

        self.tree_input.blockSignals(True)

        # Find TAB files in the root folder AND subfolders
        root_tabs = []
        sub_map = {}  # {subfolder_name: [tab_files]}

        for entry in sorted(os.listdir(folder)):
            full = os.path.join(folder, entry)
            if os.path.isfile(full) and entry.lower().endswith('.tab'):
                root_tabs.append(full)
            elif os.path.isdir(full):
                tabs = _find_tab_files(full)
                if tabs:
                    sub_map[entry] = tabs

        if not root_tabs and not sub_map:
            QMessageBox.information(
                self, 'LVT',
                tr('No TAB files found in this folder or subfolders.'))
            self.tree_input.blockSignals(False)
            return

        # Build tree: parent folder as root
        parent_item = QTreeWidgetItem(self.tree_input)
        parent_item.setText(0, f"📂 {os.path.basename(folder)}")
        parent_item.setData(0, QtCompat.UserRole, folder)
        parent_item.setFlags(
            parent_item.flags() | QtCompat.ItemIsUserCheckable | QtCompat.ItemIsAutoTristate)
        parent_item.setCheckState(0, QtCompat.Checked)
        parent_item.setExpanded(True)

        # Root-level TAB files
        for tab in root_tabs:
            child = QTreeWidgetItem(parent_item)
            child.setText(0, f"📄 {os.path.basename(tab)}")
            child.setText(1, "")
            child.setData(0, QtCompat.UserRole, tab)
            child.setFlags(child.flags() | QtCompat.ItemIsUserCheckable)
            child.setCheckState(0, QtCompat.Checked)

        # Subfolders with their TAB files
        for sub_name, tabs in sorted(sub_map.items()):
            sub_item = QTreeWidgetItem(parent_item)
            sub_item.setText(0, f"📁 {sub_name}")
            sub_item.setText(1, f"{len(tabs)}")
            sub_item.setData(0, QtCompat.UserRole, os.path.join(folder, sub_name))
            sub_item.setFlags(
                sub_item.flags() | QtCompat.ItemIsUserCheckable | QtCompat.ItemIsAutoTristate)
            sub_item.setCheckState(0, QtCompat.Checked)

            for tab in tabs:
                file_item = QTreeWidgetItem(sub_item)
                rel = os.path.relpath(tab, os.path.join(folder, sub_name))
                file_item.setText(0, f"📄 {rel}")
                file_item.setData(0, QtCompat.UserRole, tab)
                file_item.setFlags(file_item.flags() | QtCompat.ItemIsUserCheckable)
                file_item.setCheckState(0, QtCompat.Checked)

        self.tree_input.expandAll()
        self.tree_input.blockSignals(False)

        # Auto-set output folder
        if not self.txt_folder.text().strip():
            self.txt_folder.setText(folder)

        # Trigger file collection + auto-analyze
        self._collect_checked_files()

    def _browse_files(self):
        """Open file dialog to select one or more individual TAB files."""
        files, _ = QFileDialog.getOpenFileNames(
            self,
            tr('Select TAB files'),
            '',
            'MapInfo TAB (*.tab *.TAB);;All Files (*)',
        )
        if not files:
            return

        self.tree_input.blockSignals(True)

        for f in files:
            if f in self._input_files:
                continue
            # Add as top-level item
            item = QTreeWidgetItem(self.tree_input)
            item.setText(0, f"📄 {os.path.basename(f)}")
            item.setData(0, QtCompat.UserRole, f)
            item.setFlags(item.flags() | QtCompat.ItemIsUserCheckable)
            item.setCheckState(0, QtCompat.Checked)

        self.tree_input.blockSignals(False)
        self._collect_checked_files()

    def _on_input_tree_changed(self, item, column):
        """Called when checkbox state changes in input tree."""
        if column == 0:
            self._collect_checked_files()

    def _collect_checked_files(self):
        """Walk the input tree, collect all checked TAB file paths,
        and trigger auto-analysis with a short debounce."""
        files = []

        def _walk(item):
            path = item.data(0, QtCompat.UserRole)
            if path and os.path.isfile(path) and path.lower().endswith('.tab'):
                if item.checkState(0) == QtCompat.Checked:
                    files.append(path)
            for i in range(item.childCount()):
                _walk(item.child(i))

        for i in range(self.tree_input.topLevelItemCount()):
            _walk(self.tree_input.topLevelItem(i))

        self._input_files = files
        self.lbl_count.setText(f"{len(files)} file(s)")

        # Debounce auto-analyze (300ms)
        if self._analyze_timer:
            self._analyze_timer.stop()
        self._analyze_timer = QTimer(self)
        self._analyze_timer.setSingleShot(True)
        self._analyze_timer.timeout.connect(self._auto_analyze)
        self._analyze_timer.start(300)

    def _clear_files(self):
        """Clear all selected input files and reset analysis."""
        self._input_files.clear()
        self._analysis.clear()
        self.tree_input.clear()
        self.tree.clear()
        self.lbl_count.setText("0 file(s)")

    def _browse_folder(self):
        """Select the output folder."""
        folder = QFileDialog.getExistingDirectory(
            self, tr('Select output folder'))
        if folder:
            self.txt_folder.setText(folder)

    # ───────────────────────────────────────────────────────────
    # Auto analysis (runs silently in background)
    # ───────────────────────────────────────────────────────────

    def _auto_analyze(self):
        """Auto-analyze all selected files silently (no user interaction)."""
        if not self._input_files:
            self.tree.clear()
            self._analysis.clear()
            return
        self._analyze()

    def _analyze(self):
        """Analyse all input TAB files to count features by geometry type.

        Uses OGR to iterate every feature, classifying each as Point,
        Line, Polygon, or Text (identified by LABEL style string).
        Populates the tree widget with results and checkboxes.
        """
        self._analysis.clear()
        self.tree.clear()

        for fpath in self._input_files:
            ds = None
            try:
                ds = ogr.Open(fpath, 0)
                if not ds:
                    self._analysis[fpath] = {}
                    item = QTreeWidgetItem(self.tree)
                    item.setText(0, os.path.basename(fpath))
                    item.setText(1, '❌ Cannot open')
                    continue

                lyr = ds.GetLayer(0)
                if not lyr:
                    self._analysis[fpath] = {}
                    item = QTreeWidgetItem(self.tree)
                    item.setText(0, os.path.basename(fpath))
                    item.setText(1, '❌ No layer')
                    continue

                counts = {'Point': 0, 'Line': 0, 'Polygon': 0, 'Text': 0}

                lyr.ResetReading()
                for feat in lyr:
                    style = feat.GetStyleString() or ''
                    geom = feat.GetGeometryRef()

                    if style.startswith('LABEL'):
                        counts['Text'] += 1
                    elif geom is not None:
                        gtype = ogr.GT_Flatten(geom.GetGeometryType())
                        if gtype in (1, 4):      # wkbPoint, wkbMultiPoint
                            counts['Point'] += 1
                        elif gtype in (2, 5):    # wkbLineString, wkbMultiLineString
                            counts['Line'] += 1
                        elif gtype in (3, 6):    # wkbPolygon, wkbMultiPolygon
                            counts['Polygon'] += 1

                # Keep only types with features > 0
                active = {k: v for k, v in counts.items() if v > 0}
                self._analysis[fpath] = active

                # Build tree items
                total = sum(active.values())
                parent_item = QTreeWidgetItem(self.tree)
                parent_item.setText(0, os.path.basename(fpath))
                parent_item.setText(1, str(total))
                parent_item.setExpanded(True)

                for gtype, count in active.items():
                    child = QTreeWidgetItem(parent_item)
                    child.setText(0, f"  {gtype}")
                    child.setText(1, str(count))
                    # Add export checkbox
                    chk = QCheckBox()
                    chk.setChecked(True)
                    self.tree.setItemWidget(child, 2, chk)

            except Exception as e:
                self._analysis[fpath] = {}
                item = QTreeWidgetItem(self.tree)
                item.setText(0, os.path.basename(fpath))
                item.setText(1, f'❌ {e}')
            finally:
                ds = None  # Close OGR data source

        self.tree.expandAll()
        QApplication.processEvents()

    # ───────────────────────────────────────────────────────────
    # Label style parsing
    # ───────────────────────────────────────────────────────────

    @staticmethod
    def _parse_label_style(style_str):
        """Parse OGR LABEL style string to extract text properties.

        Extracts text content, rotation angle, font size, and color
        from an OGR style string like:
            LABEL(t:"Some text",a:45.0,s:12g,c:#FF0000)

        Args:
            style_str: OGR style string (e.g. from feat.GetStyleString()).

        Returns:
            dict: Keys 'text', 'angle', 'size', 'color' with parsed values.
        """
        result = {'text': '', 'angle': 0.0, 'size': 10.0, 'color': '#000000'}
        if not style_str or not style_str.startswith('LABEL'):
            return result

        # Extract text: t:"..." or t:'...'
        m = re.search(r't:"([^"]*)"', style_str)
        if not m:
            m = re.search(r"t:'([^']*)'", style_str)
        if m:
            result['text'] = m.group(1)

        # Angle: a:45.0
        m = re.search(r'a:([0-9.+-]+)', style_str)
        if m:
            try:
                result['angle'] = float(m.group(1))
            except ValueError:
                pass  # nosec B110

        # Size: s:10g or s:10
        m = re.search(r's:([0-9.]+)', style_str)
        if m:
            try:
                result['size'] = float(m.group(1))
            except ValueError:
                pass  # nosec B110

        # Color: c:#RRGGBB
        m = re.search(r'c:(#[0-9a-fA-F]{6})', style_str)
        if m:
            result['color'] = m.group(1)

        return result

    # ───────────────────────────────────────────────────────────
    # SHP writer
    # ───────────────────────────────────────────────────────────

    def _write_shp(self, out_path, features, src_layer_defn, geom_type_ogr,
                   srs, extra_fields=None, do_tcvn=False):
        """Write features to a new SHP file using OGR.

        Args:
            out_path: Output .shp file path.
            features: List of tuples — either (geom_wkt, attrs_dict) for
                normal features, or (point_xy, attrs_dict, label_info)
                for Text features.
            src_layer_defn: OGR LayerDefn from source (for field definitions).
            geom_type_ogr: OGR geometry type constant (e.g. ogr.wkbPoint).
            srs: osr.SpatialReference for the output CRS.
            extra_fields: Optional list of (name, ogr_type, width) for
                additional fields (used for Text type).
            do_tcvn: Whether to apply TCVN3→Unicode conversion on strings.
        """
        driver = ogr.GetDriverByName('ESRI Shapefile')
        if os.path.exists(out_path):
            driver.DeleteDataSource(out_path)

        ds = driver.CreateDataSource(out_path)
        layer = ds.CreateLayer('features', srs, geom_type_ogr,
                               options=['ENCODING=UTF-8'])

        # Copy field definitions from source
        for i in range(src_layer_defn.GetFieldCount()):
            fd = src_layer_defn.GetFieldDefn(i)
            new_fd = ogr.FieldDefn(fd.GetName(), fd.GetType())
            new_fd.SetWidth(max(fd.GetWidth(), 254))
            layer.CreateField(new_fd)

        # Add extra fields (for Text type)
        if extra_fields:
            for fname, ftype, fwidth in extra_fields:
                fd = ogr.FieldDefn(fname, ftype)
                fd.SetWidth(fwidth)
                layer.CreateField(fd)

        # Write features
        layer_defn = layer.GetLayerDefn()
        for feat_data in features:
            feat = ogr.Feature(layer_defn)

            if extra_fields:  # Text type: (point_xy, attrs, label_info)
                pt_xy, attrs, label = feat_data
                # Set original attributes
                for fn, val in attrs.items():
                    fi = feat.GetFieldIndex(fn)
                    if fi >= 0 and val is not None:
                        s = str(val)
                        if do_tcvn:
                            s = _convert_tcvn3_to_unicode(s)
                        feat.SetField(fi, s)
                # Set extra text attributes
                text = label.get('text', '')
                if do_tcvn:
                    text = _convert_tcvn3_to_unicode(text)
                feat.SetField('TEXT', text)
                feat.SetField('ANGLE', label.get('angle', 0.0))
                feat.SetField('FONT_SIZE', label.get('size', 10.0))
                feat.SetField('COLOR', label.get('color', '#000000'))
                # Set geometry
                if pt_xy:
                    pt = ogr.Geometry(ogr.wkbPoint)
                    pt.AddPoint(pt_xy[0], pt_xy[1])
                    feat.SetGeometry(pt)
            else:  # Normal geometry: (geom_wkt, attrs)
                wkt, attrs = feat_data
                for fn, val in attrs.items():
                    fi = feat.GetFieldIndex(fn)
                    if fi >= 0 and val is not None:
                        s = str(val)
                        if do_tcvn:
                            s = _convert_tcvn3_to_unicode(s)
                        feat.SetField(fi, s)
                geom = ogr.CreateGeometryFromWkt(wkt)
                if geom:
                    feat.SetGeometry(geom)

            layer.CreateFeature(feat)

        ds = None  # Flush & close

        # Write .cpg file for UTF-8 encoding declaration
        cpg = os.path.splitext(out_path)[0] + '.cpg'
        with open(cpg, 'w', encoding='utf-8') as f:
            f.write('UTF-8')

    # ───────────────────────────────────────────────────────────
    # Main conversion
    # ───────────────────────────────────────────────────────────

    def _convert(self):
        """Execute the TAB → SHP conversion for all analysed files.

        For each input TAB file, groups features by geometry type,
        then writes separate SHP files for each type that the user
        has selected via the tree checkboxes.

        If "Merge results" is checked, all files are combined into
        a single SHP per geometry type.
        """
        # Auto-analyze if not done yet
        if not self._analysis and self._input_files:
            self._analyze()

        if not self._analysis:
            QMessageBox.warning(
                self, 'LVT',
                tr('Please select TAB files first.'))
            return

        folder = self.txt_folder.text().strip()
        if not folder or not os.path.isdir(folder):
            QMessageBox.warning(
                self, 'LVT', tr('Please select a valid output folder.'))
            return

        # Build target SRS from CRS combo
        crs_idx = self.cmb_crs.currentIndex()
        crs_code = CRS_LIST[crs_idx][1]
        srs = osr.SpatialReference()
        if crs_code.startswith('EPSG:'):
            epsg = int(crs_code.split(':')[1])
            srs.ImportFromEPSG(epsg)
        elif crs_code:
            srs.ImportFromProj4(crs_code)
        else:
            srs.ImportFromEPSG(4326)
        srs.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER)

        do_tcvn = self.chk_tcvn.isChecked()
        do_add = self.chk_add.isChecked()
        do_merge = self.chk_merge.isChecked()

        # Show log & progress
        self.log.setVisible(True)
        self.log.clear()
        self.progress.setVisible(True)
        self.btn_convert.setEnabled(False)
        QApplication.processEvents()

        if do_merge:
            self._convert_merged(folder, srs, do_tcvn, do_add)
        else:
            self._convert_individual(folder, srs, do_tcvn, do_add)

        # Completion
        self.progress.setValue(100)
        self.btn_convert.setEnabled(True)

    def _convert_individual(self, folder, srs, do_tcvn, do_add):
        """Convert each TAB file individually (original behavior)."""
        total_files = len(self._analysis)
        success_count = 0

        for fi, (fpath, geom_counts) in enumerate(self._analysis.items()):
            base = os.path.splitext(os.path.basename(fpath))[0]
            self.log.append(f"\n{'=' * 50}")
            self.log.append(f"📄 {os.path.basename(fpath)}")
            self.progress.setValue(int((fi / total_files) * 100))
            QApplication.processEvents()

            # Determine which geometry types are checked in tree
            checked_types = set()
            for ti in range(self.tree.topLevelItemCount()):
                item = self.tree.topLevelItem(ti)
                if item.text(0) == os.path.basename(fpath):
                    for ci in range(item.childCount()):
                        child = item.child(ci)
                        chk = self.tree.itemWidget(child, 2)
                        if chk and chk.isChecked():
                            checked_types.add(child.text(0).strip())
                    break

            if not checked_types:
                self.log.append('  ⏭ No types selected, skipping')
                continue

            # Open source TAB file
            ds = None
            try:
                ds = ogr.Open(fpath, 0)
                if not ds:
                    self.log.append('  ❌ Cannot open file')
                    continue
                lyr = ds.GetLayer(0)
                if not lyr:
                    self.log.append('  ❌ No layer found')
                    continue

                layer_defn = lyr.GetLayerDefn()
                field_names = [
                    layer_defn.GetFieldDefn(i).GetName()
                    for i in range(layer_defn.GetFieldCount())
                ]

                # Group features by type
                groups = {
                    'Point': [], 'Line': [], 'Polygon': [], 'Text': [],
                }
                lyr.ResetReading()
                for feat in lyr:
                    style = feat.GetStyleString() or ''
                    geom = feat.GetGeometryRef()

                    # Collect attributes
                    attrs = {}
                    for fn in field_names:
                        attrs[fn] = feat.GetField(fn)

                    if style.startswith('LABEL'):
                        if 'Text' in checked_types:
                            label = self._parse_label_style(style)
                            pt = None
                            if geom:
                                pt = (geom.GetX(), geom.GetY())
                            groups['Text'].append((pt, attrs, label))
                    elif geom is not None:
                        gtype = ogr.GT_Flatten(geom.GetGeometryType())
                        wkt = geom.ExportToWkt()
                        if gtype in (1, 4) and 'Point' in checked_types:
                            groups['Point'].append((wkt, attrs))
                        elif gtype in (2, 5) and 'Line' in checked_types:
                            groups['Line'].append((wkt, attrs))
                        elif gtype in (3, 6) and 'Polygon' in checked_types:
                            groups['Polygon'].append((wkt, attrs))

                # Export each group to SHP
                ogr_geom_map = {
                    'Point': ogr.wkbPoint,
                    'Line': ogr.wkbLineString,
                    'Polygon': ogr.wkbPolygon,
                }

                for gtype, features in groups.items():
                    if not features:
                        continue

                    suffix = f"_{gtype.lower()}"
                    out_path = os.path.join(folder, f"{base}{suffix}.shp")

                    try:
                        if gtype == 'Text':
                            extra = [
                                ('TEXT', ogr.OFTString, 500),
                                ('ANGLE', ogr.OFTReal, 10),
                                ('FONT_SIZE', ogr.OFTReal, 10),
                                ('COLOR', ogr.OFTString, 20),
                            ]
                            self._write_shp(
                                out_path, features, layer_defn,
                                ogr.wkbPoint, srs, extra, do_tcvn)
                        else:
                            ogr_type = ogr_geom_map.get(
                                gtype, ogr.wkbUnknown)
                            self._write_shp(
                                out_path, features, layer_defn,
                                ogr_type, srs, None, do_tcvn)

                        success_count += 1
                        self.log.append(
                            f"  ✅ {gtype}: {len(features)}"
                            f" → {os.path.basename(out_path)}")
                        QApplication.processEvents()

                        # Add to QGIS project
                        if do_add:
                            name = os.path.splitext(
                                os.path.basename(out_path))[0]
                            result = QgsVectorLayer(out_path, name, 'ogr')
                            if result.isValid():
                                result.setProviderEncoding('UTF-8')
                                QgsProject.instance().addMapLayer(result)

                    except Exception as e:
                        self.log.append(
                            f"  ❌ {gtype}: Error — {e}")
                        QApplication.processEvents()

            except Exception as e:
                self.log.append(f"  ❌ Error processing file: {e}")
                QApplication.processEvents()
            finally:
                ds = None  # Close OGR data source

        self.log.append(
            f"\n✅ HOÀN THÀNH! {success_count} file(s) → {folder}")
        QMessageBox.information(
            self, '✅ Done',
            f'Exported {success_count} file(s) to:\n{folder}')

    def _convert_merged(self, folder, srs, do_tcvn, do_add):
        """Merge ALL TAB files into a single SHP per geometry type."""
        self.log.append(f"\n{'=' * 50}")
        self.log.append("🔀 MERGE MODE — Combining all files")

        # Collect ALL checked geometry types across all files
        all_checked = set()
        for ti in range(self.tree.topLevelItemCount()):
            item = self.tree.topLevelItem(ti)
            for ci in range(item.childCount()):
                child = item.child(ci)
                chk = self.tree.itemWidget(child, 2)
                if chk and chk.isChecked():
                    all_checked.add(child.text(0).strip())

        if not all_checked:
            self.log.append("  ⏭ No geometry types selected")
            return

        # Merged groups
        merged = {'Point': [], 'Line': [], 'Polygon': [], 'Text': []}
        first_layer_defn = None
        total = len(self._analysis)
        success_count = 0

        for fi, (fpath, geom_counts) in enumerate(self._analysis.items()):
            self.progress.setValue(int((fi / total) * 100))
            self.log.append(f"  📄 Reading {os.path.basename(fpath)}")
            QApplication.processEvents()

            ds = None
            try:
                ds = ogr.Open(fpath, 0)
                if not ds:
                    continue
                lyr = ds.GetLayer(0)
                if not lyr:
                    continue

                layer_defn = lyr.GetLayerDefn()
                if first_layer_defn is None:
                    first_layer_defn = layer_defn
                    # Need to keep ds open for first_layer_defn reference
                    ds = None  # Don't close first one

                field_names = [
                    layer_defn.GetFieldDefn(i).GetName()
                    for i in range(layer_defn.GetFieldCount())
                ]

                lyr.ResetReading()
                for feat in lyr:
                    style = feat.GetStyleString() or ''
                    geom = feat.GetGeometryRef()
                    attrs = {}
                    for fn in field_names:
                        attrs[fn] = feat.GetField(fn)

                    if style.startswith('LABEL'):
                        if 'Text' in all_checked:
                            label = self._parse_label_style(style)
                            pt = None
                            if geom:
                                pt = (geom.GetX(), geom.GetY())
                            merged['Text'].append((pt, attrs, label))
                    elif geom is not None:
                        gtype_val = ogr.GT_Flatten(geom.GetGeometryType())
                        wkt = geom.ExportToWkt()
                        if gtype_val in (1, 4) and 'Point' in all_checked:
                            merged['Point'].append((wkt, attrs))
                        elif gtype_val in (2, 5) and 'Line' in all_checked:
                            merged['Line'].append((wkt, attrs))
                        elif gtype_val in (3, 6) and 'Polygon' in all_checked:
                            merged['Polygon'].append((wkt, attrs))

            except Exception as e:
                self.log.append(f"  ❌ {os.path.basename(fpath)}: {e}")
            finally:
                if ds is not None:
                    ds = None

        if first_layer_defn is None:
            self.log.append("  ❌ No valid files to merge")
            return

        # Write merged SHP files
        ogr_geom_map = {
            'Point': ogr.wkbPoint,
            'Line': ogr.wkbLineString,
            'Polygon': ogr.wkbPolygon,
        }

        for gtype, features in merged.items():
            if not features:
                continue

            out_path = os.path.join(folder, f"merged_{gtype.lower()}.shp")

            try:
                if gtype == 'Text':
                    extra = [
                        ('TEXT', ogr.OFTString, 500),
                        ('ANGLE', ogr.OFTReal, 10),
                        ('FONT_SIZE', ogr.OFTReal, 10),
                        ('COLOR', ogr.OFTString, 20),
                    ]
                    self._write_shp(
                        out_path, features, first_layer_defn,
                        ogr.wkbPoint, srs, extra, do_tcvn)
                else:
                    ogr_type = ogr_geom_map.get(gtype, ogr.wkbUnknown)
                    self._write_shp(
                        out_path, features, first_layer_defn,
                        ogr_type, srs, None, do_tcvn)

                success_count += 1
                self.log.append(
                    f"  ✅ {gtype}: {len(features)} features"
                    f" → {os.path.basename(out_path)}")

                if do_add:
                    name = os.path.splitext(os.path.basename(out_path))[0]
                    result = QgsVectorLayer(out_path, name, 'ogr')
                    if result.isValid():
                        result.setProviderEncoding('UTF-8')
                        QgsProject.instance().addMapLayer(result)

            except Exception as e:
                self.log.append(f"  ❌ {gtype}: Error — {e}")

        # Close first datasource

        self.log.append(
            f"\n✅ MERGED! {success_count} file(s) → {folder}")
        QMessageBox.information(
            self, '✅ Done',
            f'Merged into {success_count} file(s) at:\n{folder}')
