import sys
import os
import re
import cv2
import numpy as np
import pandas as pd
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 
                             QLabel, QLineEdit, QPushButton, QFileDialog, QTabWidget, 
                             QSlider, QDoubleSpinBox, QProgressBar, QComboBox, QGroupBox, QFormLayout,
                             QSpinBox)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt5.QtGui import QImage, QPixmap
from scipy.spatial.transform import Rotation as R
from pyproj import Transformer

# =================================================================
# [NEXT GEN] advanced_tuner_v2.py: Streamlined GIS GUI Tuner
# 설계 원칙:
# 1. MP4 + SRT + CSV (WGS84) 워크플로우 통합
# 2. 실시간 좌표 변환 (Lat/Lon -> EPSG:5186)
# 3. 사용자 친화적 튜닝 인터페이스 제공
# =================================================================

class SRTParser:
    @staticmethod
    def parse(srt_path):
        data_dict = {}
        if not os.path.exists(srt_path): return data_dict
        try:
            with open(srt_path, 'r', encoding='utf-8', errors='ignore') as f:
                content = f.read()
            if "latitude" not in content.lower():
                with open(srt_path, 'r', encoding='cp949', errors='ignore') as f:
                    content = f.read()
            
            blocks = content.split('\n\n')
            for block in blocks:
                f_match = re.search(r'FrameCnt:\s*(\d+)', block)
                if not f_match: continue
                idx = int(f_match.group(1))
                
                lat = float(re.search(r'latitude:\s*([\d\.-]+)', block).group(1))
                lon = float(re.search(r'longitude:\s*([\d\.-]+)', block).group(1))
                alt = float(re.search(r'abs_alt:\s*([\d\.-]+)', block).group(1))
                yaw = float(re.search(r'gb_yaw:\s*([\d\.-]+)', block).group(1))
                pitch = float(re.search(r'gb_pitch:\s*([\d\.-]+)', block).group(1))
                roll = float(re.search(r'gb_roll:\s*([\d\.-]+)', block).group(1))
                focal = float(re.search(r'focal_len:\s*([\d\.-]+)', block).group(1))
                
                data_dict[idx] = {
                    'lat': lat, 'lon': lon, 'alt': alt,
                    'yaw': yaw, 'pitch': pitch, 'roll': roll,
                    'focal': focal
                }
            # Post-process: Filling gaps and Smoothing
            if not data_dict: return {}
            
            indices = sorted(data_dict.keys())
            min_idx, max_idx = indices[0], indices[-1]
            
            # 1. Interpolate Gaps
            all_indices = np.arange(min_idx, max_idx + 1)
            new_data = {}
            
            fields = ['lat', 'lon', 'alt', 'yaw', 'pitch', 'roll', 'focal']
            temp_arrays = {f: [] for f in fields}
            for idx in indices:
                for f in fields:
                    temp_arrays[f].append(data_dict[idx][f])
            
            interp_arrays = {}
            for f in fields:
                # Handle Yaw wrapping for smoother rotation (Optional but safer)
                if f == 'yaw':
                    y_arr = np.array(temp_arrays[f])
                    # Ensure continuous angles
                    y_arr = np.unwrap(np.radians(y_arr))
                    interp_y = np.interp(all_indices, indices, y_arr)
                    # 2. Smooth (Moving Average)
                    smooth_y = np.convolve(interp_y, np.ones(5)/5, mode='same')
                    interp_arrays[f] = np.degrees(smooth_y)
                else:
                    interp_f = np.interp(all_indices, indices, temp_arrays[f])
                    # 2. Smooth (Moving Average)
                    interp_arrays[f] = np.convolve(interp_f, np.ones(5)/5, mode='same')

            for i, idx in enumerate(all_indices):
                new_data[int(idx)] = {f: interp_arrays[f][i] for f in fields}
                
            return new_data
        except Exception as e:
            print(f"SRT Error: {e}")
            return {}

class RenderWorker(QThread):
    progress = pyqtSignal(int)
    finished = pyqtSignal(str)

    def __init__(self, params):
        super().__init__()
        self.p = params
        self.is_running = True

    def stop(self):
        self.is_running = False

    def run(self):
        try:
            cap = cv2.VideoCapture(self.p['video_path'])
            original_fps = cap.get(cv2.CAP_PROP_FPS)
            cap.set(cv2.CAP_PROP_POS_FRAMES, self.p['range'][0])
            
            target_w, target_h = self.p['resolution']
            target_fps = self.p['fps'] if self.p['fps'] > 0 else original_fps
            start_f, end_f = self.p['range']
            total_work = end_f - start_f + 1

            fourcc = cv2.VideoWriter_fourcc(*self.p['codec'])
            out = cv2.VideoWriter(self.p['output_path'], fourcc, target_fps, (target_w, target_h))

            transformer = Transformer.from_crs("epsg:4326", "epsg:5186")
            
            # Pre-swap line points if needed
            eff_line_pts = self.p['line_pts']
            if self.p['swap_xy']:
                eff_line_pts = eff_line_pts.copy()
                eff_line_pts[:, [0, 1]] = eff_line_pts[:, [1, 0]]

            for f_idx in range(start_f, end_f + 1):
                if not self.is_running: break
                ret, frame = cap.read()
                if not ret: break
                
                # Check for Sync
                if f_idx in self.p['srt_data']:
                    meta = self.p['srt_data'][f_idx]
                    dx, dy = transformer.transform(meta['lat'], meta['lon'])
                    
                    if self.p['swap_xy']:
                        dx, dy = dy, dx

                    # Resize frame if needed
                    original_h, original_w = frame.shape[:2]
                    if (target_w, target_h) != (original_w, original_h):
                        draw_frame = cv2.resize(frame, (target_w, target_h))
                    else:
                        draw_frame = frame.copy()

                    drone_pos = np.array([dx + self.p['off_x'], dy + self.p['off_y'], meta['alt'] + self.p['off_z']])
                    rvec, tvec, K = self.get_proj(meta, drone_pos, target_w, target_h)
                    R_w2c, _ = cv2.Rodrigues(rvec)
                    pts_cam = (eff_line_pts @ R_w2c.T) + tvec.T
                    
                    for i in range(0, len(pts_cam), 2):
                        p1c, p2c = pts_cam[i], pts_cam[i+1]
                        if p1c[2] < 0.1 and p2c[2] < 0.1: continue
                        if p1c[2] < 0.1 or p2c[2] < 0.1:
                            t = (0.1 - p1c[2]) / (p2c[2] - p1c[2])
                            p_mid = p1c + t * (p2c - p1c)
                            if p1c[2] < 0.1: p1c = p_mid
                            else: p2c = p_mid
                        
                        u1 = int(K[0, 0] * (p1c[0] / p1c[2]) + K[0, 2])
                        v1 = int(K[1, 1] * (p1c[1] / p1c[2]) + K[1, 2])
                        u2 = int(K[0, 0] * (p2c[0] / p2c[2]) + K[0, 2])
                        v2 = int(K[1, 1] * (p2c[1] / p2c[2]) + K[1, 2])
                        
                        ret_cli, pt1, pt2 = cv2.clipLine((0, 0, target_w, target_h), (u1, v1), (u2, v2))
                        if ret_cli:
                            cv2.line(draw_frame, pt1, pt2, (0, 0, 255), 10)
                    out.write(draw_frame)
                else:
                    if (target_w, target_h) != (frame.shape[1], frame.shape[0]):
                        out.write(cv2.resize(frame, (target_w, target_h)))
                    else:
                        out.write(frame)

                self.progress.emit(int(((f_idx - start_f + 1) / total_work) * 100))


            cap.release()
            out.release()
            self.finished.emit(self.p['output_path'])
        except Exception as e:
            self.finished.emit(f"Error: {e}")

    def get_proj(self, meta, drone_pos, w, h):
        yaw = np.radians(meta['yaw'] + self.p['off_yaw'])
        pitch = np.radians(meta['pitch'] + self.p['off_pitch'])
        roll = np.radians(meta['roll'] + self.p['off_roll'])
        
        R_b2w = (R.from_euler('z', -yaw) * R.from_euler('x', pitch) * R.from_euler('y', roll)).as_matrix()
        R_w2c = np.array([[1, 0, 0], [0, 0, -1], [0, 1, 0]]) @ R_b2w.T
        rvec, _ = cv2.Rodrigues(R_w2c)
        tvec = -R_w2c @ drone_pos
        
        f_px = (self.p['focal'] / self.p['sensor_w']) * w
        K = np.array([[f_px, 0, w/2], [0, f_px, h/2], [0, 0, 1]])
        return rvec, tvec, K

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Advanced GIS Tuner v2 (Streamlined)")
        self.resize(1280, 850)
        
        self.srt_data = {}
        self.line_points = np.array([])
        self.transformer = Transformer.from_crs("epsg:4326", "epsg:5186")
        
        self.init_ui()

    def init_ui(self):
        tabs = QTabWidget()
        self.setCentralWidget(tabs)
        
        # --- Tab 1: Data Input ---
        tab1 = QWidget()
        layout1 = QVBoxLayout()
        form1 = QFormLayout()
        
        self.btn_video = QPushButton("Find MP4")
        self.txt_video = QLineEdit("하행)회덕-대전조차장.MP4")
        self.btn_video.clicked.connect(lambda: self.find_file(self.txt_video, "Video (*.mp4)"))
        form1.addRow(self.btn_video, self.txt_video)
        
        self.btn_srt = QPushButton("Find SRT")
        self.txt_srt = QLineEdit("하행)회덕-대전조차장.srt")
        self.btn_srt.clicked.connect(lambda: self.find_file(self.txt_srt, "SRT (*.srt)"))
        form1.addRow(self.btn_srt, self.txt_srt)
        
        self.btn_csv = QPushButton("Find CSV (Lat/Lon/H)")
        self.txt_csv = QLineEdit("center.csv")
        self.btn_csv.clicked.connect(lambda: self.find_file(self.txt_csv, "CSV (*.csv)"))
        form1.addRow(self.btn_csv, self.txt_csv)
        
        layout1.addLayout(form1)
        self.btn_load = QPushButton("LOAD ALL DATA")
        self.btn_load.setStyleSheet("background-color: #4CAF50; color: white; height: 50px; font-weight: bold;")
        self.btn_load.clicked.connect(self.load_all_data)
        layout1.addWidget(self.btn_load)
        layout1.addStretch()
        tab1.setLayout(layout1)
        tabs.addTab(tab1, "1. 자료입력")

        # --- Tab 2: Tuner ---
        tab2 = QWidget()
        hbox2 = QHBoxLayout()
        
        # Left: Preview
        vbox_prev = QVBoxLayout()
        self.lbl_preview = QLabel("Video Preview (Load Data First)")
        self.lbl_preview.setAlignment(Qt.AlignCenter)
        self.lbl_preview.setStyleSheet("border: 2px solid gray; background-color: black;")
        self.lbl_preview.setMinimumSize(800, 450)
        vbox_prev.addWidget(self.lbl_preview)
        
        self.sld_frame = QSlider(Qt.Horizontal)
        self.sld_frame.valueChanged.connect(self.update_sync_frame)
        self.spn_frame = QDoubleSpinBox()
        self.spn_frame.setDecimals(0)
        self.spn_frame.setSingleStep(1)
        self.spn_frame.valueChanged.connect(self.update_sync_frame)
        
        self.btn_play = QPushButton("▶ PLAY")
        self.btn_play.setCheckable(True)
        self.btn_play.clicked.connect(self.toggle_play)
        
        self.play_timer = QTimer()
        self.play_timer.timeout.connect(self.next_frame)
        
        frame_hbox = QHBoxLayout()
        frame_hbox.addWidget(self.btn_play)
        frame_hbox.addWidget(QLabel("Frame:"))
        frame_hbox.addWidget(self.spn_frame)
        frame_hbox.addWidget(self.sld_frame, 10)
        vbox_prev.addLayout(frame_hbox)
        
        hbox2.addLayout(vbox_prev, 7)
        
        # Right: Controls
        vbox_ctrl = QVBoxLayout()
        group_offsets = QGroupBox("Offsets (Yaw/Pitch/Roll/XYZ)")
        form2 = QFormLayout()
        
        self.chk_swap_xy = QPushButton("Swap XY Coordinates: OFF")
        self.chk_swap_xy.setCheckable(True)
        self.chk_swap_xy.clicked.connect(self.toggle_swap_xy)
        form2.addRow("Axis:", self.chk_swap_xy)
        
        self.spn_yaw = self.create_spinbox(-180, 180, 0, form2, "Yaw Off")
        self.spn_pitch = self.create_spinbox(-180, 180, 0, form2, "Pitch Off")
        self.spn_roll = self.create_spinbox(-180, 180, 0, form2, "Roll Off")
        form2.addRow(QLabel("<hr>"))
        self.spn_off_x = self.create_spinbox(-1000, 1000, 0, form2, "Pos X Off (m)")
        self.spn_off_y = self.create_spinbox(-1000, 1000, 0, form2, "Pos Y Off (m)")
        self.spn_off_z = self.create_spinbox(-1000, 1000, 0, form2, "Pos Z Off (m)")
        form2.addRow(QLabel("<hr>"))
        self.spn_focal = self.create_spinbox(1, 200, 24, form2, "Focal (mm)")
        self.spn_sensor = self.create_spinbox(1, 100, 36, form2, "Sensor W (mm)")
        
        group_offsets.setLayout(form2)
        vbox_ctrl.addWidget(group_offsets)
        vbox_ctrl.addStretch()
        hbox2.addLayout(vbox_ctrl, 3)
        
        tab2.setLayout(hbox2)
        tabs.addTab(tab2, "2. 튜너")

        # --- Tab 3: Export ---
        tab3 = QWidget()
        vbox3 = QVBoxLayout()
        form3 = QFormLayout()
        
        self.cmb_res = QComboBox()
        self.cmb_res.addItems(["Original", "4K (3840x2160)", "2K (2560x1440)", "1080p (1920x1080)"])
        form3.addRow("Resolution", self.cmb_res)
        
        self.cmb_fps = QComboBox()
        self.cmb_fps.addItems(["Original", "60", "30"])
        form3.addRow("FPS", self.cmb_fps)
        
        self.cmb_codec = QComboBox()
        self.cmb_codec.addItems(["H.264 (avc1)", "MPEG-4 (mp4v)"])
        form3.addRow("Codec", self.cmb_codec)
        
        form3.addRow(QLabel("<hr>"))
        self.spn_start = QSpinBox()
        self.spn_start.setRange(0, 999999)
        form3.addRow("Start Frame", self.spn_start)
        
        self.spn_end = QSpinBox()
        self.spn_end.setRange(0, 999999)
        form3.addRow("End Frame", self.spn_end)
        
        vbox3.addLayout(form3)
        self.btn_export = QPushButton("START EXPORT")
        self.btn_export.setStyleSheet("height: 60px; font-weight: bold; background-color: #f44336; color: white;")
        self.btn_export.clicked.connect(self.start_export)
        vbox3.addWidget(self.btn_export)
        
        self.pbar = QProgressBar()
        vbox3.addWidget(self.pbar)
        vbox3.addStretch()
        tab3.setLayout(vbox3)
        tabs.addTab(tab3, "3. 출력")

    def create_spinbox(self, min_v, max_v, def_v, layout, label):
        sb = QDoubleSpinBox()
        sb.setRange(min_v, max_v)
        sb.setValue(def_v)
        sb.setDecimals(2)
        sb.setSingleStep(0.1)
        sb.valueChanged.connect(self.update_preview)
        layout.addRow(label, sb)
        return sb

    def toggle_play(self):
        if self.btn_play.isChecked():
            self.btn_play.setText("■ PAUSE")
            # Calculate interval based on FPS (or default 33ms for 30fps)
            cap = cv2.VideoCapture(self.txt_video.text())
            fps = cap.get(cv2.CAP_PROP_FPS)
            cap.release()
            interval = int(1000 / fps) if fps > 0 else 33
            self.play_timer.start(interval)
        else:
            self.btn_play.setText("▶ PLAY")
            self.play_timer.stop()

    def next_frame(self):
        curr = self.sld_frame.value()
        if curr < self.total_frames - 1:
            self.sld_frame.setValue(curr + 1)
        else:
            self.btn_play.setChecked(False)
            self.toggle_play()

    def toggle_swap_xy(self):
        if self.chk_swap_xy.isChecked():
            self.chk_swap_xy.setText("Swap XY Coordinates: ON")
        else:
            self.chk_swap_xy.setText("Swap XY Coordinates: OFF")
        self.update_preview()

    def update_sync_frame(self, val):
        # 도우미: 슬라이더와 스핀박스 동기화
        sender = self.sender()
        if sender == self.sld_frame:
            self.spn_frame.blockSignals(True)
            self.spn_frame.setValue(val)
            self.spn_frame.blockSignals(False)
        else:
            self.sld_frame.blockSignals(True)
            self.sld_frame.setValue(int(val))
            self.sld_frame.blockSignals(False)
        self.update_preview()

    def find_file(self, line_edit, filt):
        path, _ = QFileDialog.getOpenFileName(self, "Select File", "", filt)
        if path: line_edit.setText(path)

    def load_all_data(self):
        print("Loading...")
        self.srt_data = SRTParser.parse(self.txt_srt.text())
        
        # Load CSV
        try:
            df = pd.read_csv(self.txt_csv.text(), encoding='cp949')
            # lat, lon, 타원체고(h)
            raw_pts = []
            for _, row in df.iterrows():
                x, y = self.transformer.transform(row['lat'], row['lon'])
                raw_pts.append([x, y, row['타원체고(h)']])
            
            # Reconstruct segments: 0-1, 1-2, 2-3...
            seg_pts = []
            for i in range(len(raw_pts)-1):
                seg_pts.extend([raw_pts[i], raw_pts[i+1]])
                
            self.line_points = np.array(seg_pts, dtype=np.float64)
            print(f"Loaded {len(self.line_points)//2} segments.")
        except Exception as e:
            print(f"CSV Load Error: {e}")

        # Video Info
        cap = cv2.VideoCapture(self.txt_video.text())
        self.total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
        self.video_fps = cap.get(cv2.CAP_PROP_FPS)
        
        self.sld_frame.setRange(0, self.total_frames - 1)
        self.spn_frame.setRange(0, self.total_frames - 1)
        self.spn_start.setRange(0, self.total_frames - 1)
        self.spn_end.setRange(0, self.total_frames - 1)
        self.spn_end.setValue(self.total_frames - 1)
        cap.release()
        
        self.update_preview()

    def update_preview(self):
        if not self.srt_data or self.line_points.size == 0: return
        
        f_idx = self.sld_frame.value()
        cap = cv2.VideoCapture(self.txt_video.text())
        cap.set(cv2.CAP_PROP_POS_FRAMES, f_idx)
        ret, frame = cap.read()
        cap.release()
        
        if not ret: return
        
        h, w = frame.shape[:2]
        if f_idx in self.srt_data:
            meta = self.srt_data[f_idx]
            
            dx, dy = self.transformer.transform(meta['lat'], meta['lon'])
            
            # Swap Logic
            if self.chk_swap_xy.isChecked():
                dx, dy = dy, dx
                line_pts_eff = self.line_points.copy()
                line_pts_eff[:, [0, 1]] = line_pts_eff[:, [1, 0]]
            else:
                line_pts_eff = self.line_points

            drone_pos = np.array([dx + self.spn_off_x.value(), 
                                  dy + self.spn_off_y.value(), 
                                  meta['alt'] + self.spn_off_z.value()])
            
            # 실시간 디버그 로그 (콘솔 출력)
            print(f"[Frame {f_idx}] XY_Order: {'Y,X' if self.chk_swap_xy.isChecked() else 'X,Y'} | Drone X={drone_pos[0]:.2f}, Y={drone_pos[1]:.2f}")
            
            # Projection
            yaw = np.radians(meta['yaw'] + self.spn_yaw.value())
            pitch = np.radians(meta['pitch'] + self.spn_pitch.value())
            roll = np.radians(meta['roll'] + self.spn_roll.value())
            
            r_yaw = R.from_euler('z', -yaw)
            r_pitch = R.from_euler('x', pitch)
            r_roll = R.from_euler('y', roll)
            
            R_b2w = (r_yaw * r_pitch * r_roll).as_matrix()
            R_align = np.array([[1, 0, 0], [0, 0, -1], [0, 1, 0]])
            R_w2c = R_align @ R_b2w.T
            
            rvec, _ = cv2.Rodrigues(R_w2c)
            tvec = -R_w2c @ drone_pos
            
            f_px = (self.spn_focal.value() / self.spn_sensor.value()) * w
            K = np.array([[f_px, 0, w/2], [0, f_px, h/2], [0, 0, 1]])
            
            pts_cam = (line_pts_eff @ R_w2c.T) + tvec.T
            for i in range(0, len(pts_cam), 2):
                p1c, p2c = pts_cam[i], pts_cam[i+1]
                if p1c[2] < 0.1 and p2c[2] < 0.1: continue
                if p1c[2] < 0.1 or p2c[2] < 0.1:
                    t = (0.1 - p1c[2]) / (p2c[2] - p1c[2])
                    p_mid = p1c + t * (p2c - p1c)
                    if p1c[2] < 0.1: p1c = p_mid
                    else: p2c = p_mid
                
                u1 = int(K[0, 0] * (p1c[0] / p1c[2]) + K[0, 2])
                v1 = int(K[1, 1] * (p1c[1] / p1c[2]) + K[1, 2])
                u2 = int(K[0, 0] * (p2c[0] / p2c[2]) + K[0, 2])
                v2 = int(K[1, 1] * (p2c[1] / p2c[2]) + K[1, 2])
                
                ret_cli, pt1, pt2 = cv2.clipLine((0, 0, w, h), (u1, v1), (u2, v2))
                if ret_cli:
                    cv2.line(frame, pt1, pt2, (0, 0, 255), 10)

        # Convert to QPixmap
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        img = QImage(frame.data, w, h, w*3, QImage.Format_RGB888)
        pixmap = QPixmap.fromImage(img).scaled(self.lbl_preview.width(), self.lbl_preview.height(), Qt.KeepAspectRatio)
        self.lbl_preview.setPixmap(pixmap)

    def start_export(self):
        res_map = {"Original": (0, 0), "4K (3840x2160)": (3840, 2160), "2K (2560x1440)": (2560, 1440), "1080p (1920x1080)": (1920, 1080)}
        codec_map = {"H.264 (avc1)": "avc1", "MPEG-4 (mp4v)": "mp4v"}
        
        target_res = res_map[self.cmb_res.currentText()]
        if target_res == (0, 0):
            cap = cv2.VideoCapture(self.txt_video.text())
            target_res = (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
            cap.release()
            
        params = {
            'video_path': self.txt_video.text(),
            'srt_data': self.srt_data,
            'line_pts': self.line_points,
            'off_yaw': self.spn_yaw.value(),
            'off_pitch': self.spn_pitch.value(),
            'off_roll': self.spn_roll.value(),
            'off_x': self.spn_off_x.value(),
            'off_y': self.spn_off_y.value(),
            'off_z': self.spn_off_z.value(),
            'focal': self.spn_focal.value(),
            'sensor_w': self.spn_sensor.value(),
            'resolution': target_res,
            'fps': int(self.cmb_fps.currentText()) if self.cmb_fps.currentText() != "Original" else 0,
            'range': (self.spn_start.value(), self.spn_end.value()),
            'swap_xy': self.chk_swap_xy.isChecked(),
            'codec': codec_map[self.cmb_codec.currentText()],
            'output_path': "output_tuner_v2.mp4"
        }
        
        self.btn_export.setEnabled(False)
        self.worker = RenderWorker(params)
        self.worker.progress.connect(self.pbar.setValue)
        self.worker.finished.connect(self.export_done)
        self.worker.start()

    def export_done(self, msg):
        self.btn_export.setEnabled(True)
        print(f"Export Result: {msg}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())
