import os
import webbrowser
import customtkinter as ctk
from typing import Callable, Tuple
import cv2
from PIL import Image, ImageOps
import tkinterdnd2 as tkdnd
import time
import modules.globals
import modules.metadata
from modules.face_analyser import (
    get_one_face,
    get_unique_faces_from_target_image,
    get_unique_faces_from_target_video,
    add_blank_map,
    has_valid_map,
    simplify_maps,
)
from modules.capturer import get_video_frame, get_video_frame_total
from modules.processors.frame.core import get_frame_processors_modules
from modules.utilities import (
    is_image,
    is_video,
    resolve_relative_path,
    has_image_extension,
)

modules.globals.face_opacity = 100
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
os.environ["QT_SCREEN_SCALE_FACTORS"] = "1"
os.environ["QT_SCALE_FACTOR"] = "1"

ROOT = None
POPUP = None
POPUP_LIVE = None
ROOT_HEIGHT = 800
ROOT_WIDTH = 1000

PREVIEW = None
PREVIEW_MAX_HEIGHT = 800
PREVIEW_MAX_WIDTH = 1400
PREVIEW_DEFAULT_WIDTH = 1280
PREVIEW_DEFAULT_HEIGHT = 720

POPUP_WIDTH = 700
POPUP_HEIGHT = 800
POPUP_SCROLL_WIDTH = 680
POPUP_SCROLL_HEIGHT = 600

POPUP_LIVE_WIDTH = 850
POPUP_LIVE_HEIGHT = 700
POPUP_LIVE_SCROLL_WIDTH = 830
POPUP_LIVE_SCROLL_HEIGHT = 600

MAPPER_PREVIEW_MAX_HEIGHT = 120
MAPPER_PREVIEW_MAX_WIDTH = 120

DEFAULT_BUTTON_WIDTH = 200
DEFAULT_BUTTON_HEIGHT = 40

RECENT_DIRECTORY_SOURCE = None
RECENT_DIRECTORY_TARGET = None
RECENT_DIRECTORY_OUTPUT = None

preview_label = None
preview_slider = None
source_label = None
target_label = None
status_label = None
popup_status_label = None
popup_status_label_live = None
source_label_dict = {}
source_label_dict_live = {}
target_label_dict_live = {}

img_ft, vid_ft = modules.globals.file_types


class ModernButton(ctk.CTkButton):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)
        self.configure(
            font=("Roboto", 16, "bold"),
            corner_radius=15,
            border_width=2,
            border_color="#3a7ebf",
            hover_color="#2b5d8b",
            fg_color="#3a7ebf",
            text_color="white",
        )


class DragDropButton(ModernButton):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)
        self.drop_target_register(tkdnd.DND_FILES)
        self.dnd_bind("<<Drop>>", self.drop)

    def drop(self, event):
        file_path = event.data
        if file_path.startswith("{"):
            file_path = file_path[1:-1]
        self.handle_drop(file_path)

    def handle_drop(self, file_path):
        pass


class SourceButton(DragDropButton):
    def handle_drop(self, file_path):
        if is_image(file_path):
            modules.globals.source_path = file_path
            global RECENT_DIRECTORY_SOURCE
            RECENT_DIRECTORY_SOURCE = os.path.dirname(modules.globals.source_path)
            image = render_image_preview(modules.globals.source_path, (250, 250))
            source_label.configure(image=image)
            source_label.configure(text="")


class SourceMapperButton(DragDropButton):
    def __init__(self, master, map, button_num, **kwargs):
        super().__init__(master, **kwargs)
        self.map = map
        self.button_num = button_num

    def handle_drop(self, file_path):
        if is_image(file_path):
            update_popup_source(
                self.master.master, self.map, self.button_num, file_path
            )


class TargetButton(DragDropButton):
    def handle_drop(self, file_path):
        global RECENT_DIRECTORY_TARGET
        if is_image(file_path) or is_video(file_path):
            modules.globals.target_path = file_path
            RECENT_DIRECTORY_TARGET = os.path.dirname(modules.globals.target_path)
            if is_image(file_path):
                image = render_image_preview(modules.globals.target_path, (250, 250))
                target_label.configure(image=image)
                target_label.configure(text="")
            elif is_video(file_path):
                video_frame = render_video_preview(file_path, (250, 250))
                target_label.configure(image=video_frame)
                target_label.configure(text="")


class ModernLabel(ctk.CTkLabel):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)
        self.configure(
            font=("Roboto", 16),
            corner_radius=10,
            fg_color="#2a2d2e",
            text_color="white",
        )


class DragDropLabel(ModernLabel):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)
        self.drop_target_register(tkdnd.DND_FILES)
        self.dnd_bind("<<Drop>>", self.drop)

    def drop(self, event):
        file_path = event.data
        if file_path.startswith("{"):
            file_path = file_path[1:-1]
        self.handle_drop(file_path)

    def handle_drop(self, file_path):
        pass


class SourceLabel(DragDropLabel):
    def handle_drop(self, file_path):
        if is_image(file_path):
            modules.globals.source_path = file_path
            global RECENT_DIRECTORY_SOURCE
            RECENT_DIRECTORY_SOURCE = os.path.dirname(modules.globals.source_path)
            image = render_image_preview(modules.globals.source_path, (250, 250))
            source_label.configure(image=image)
            source_label.configure(text="")


class TargetLabel(DragDropLabel):
    def handle_drop(self, file_path):
        global RECENT_DIRECTORY_TARGET
        if is_image(file_path) or is_video(file_path):
            modules.globals.target_path = file_path
            RECENT_DIRECTORY_TARGET = os.path.dirname(modules.globals.target_path)
            if is_image(file_path):
                image = render_image_preview(modules.globals.target_path, (250, 250))
                target_label.configure(image=image)
                target_label.configure(text="")
            elif is_video(file_path):
                video_frame = render_video_preview(file_path, (250, 250))
                target_label.configure(image=video_frame)
                target_label.configure(text="")


def init(start: Callable[[], None], destroy: Callable[[], None]) -> tkdnd.TkinterDnD.Tk:
    global ROOT, PREVIEW

    ROOT = create_root(start, destroy)
    PREVIEW = create_preview(ROOT)

    return ROOT


def create_root(
    start: Callable[[], None], destroy: Callable[[], None]
) -> tkdnd.TkinterDnD.Tk:
    global source_label, target_label, status_label

    ctk.set_appearance_mode("dark")
    ctk.set_default_color_theme("blue")

    root = tkdnd.TkinterDnD.Tk()
    root.title(
        f"{modules.metadata.name} {modules.metadata.version} {modules.metadata.edition}"
    )
    root.configure(bg="#1a1a1a")
    root.protocol("WM_DELETE_WINDOW", lambda: destroy())
    root.resizable(True, True)
    root.attributes("-alpha", 1.0)  # Set window opacity to fully opaque

    main_frame = ctk.CTkFrame(root, fg_color="#1a1a1a")
    main_frame.pack(fill="both", expand=True, padx=20, pady=20)

    # Create two vertical frames for source and target
    source_frame = ctk.CTkFrame(main_frame, fg_color="#2a2d2e", corner_radius=15)
    source_frame.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")

    target_frame = ctk.CTkFrame(main_frame, fg_color="#2a2d2e", corner_radius=15)
    target_frame.grid(row=0, column=2, padx=10, pady=10, sticky="nsew")

    # Create a middle frame for swap button
    middle_frame = ctk.CTkFrame(main_frame, fg_color="#1a1a1a")
    middle_frame.grid(row=0, column=1, padx=5, pady=10, sticky="ns")

    source_label = SourceLabel(
        source_frame,
        text="Drag & Drop\nSource Image Here",
        justify="center",
        width=250,
        height=250,
    )
    source_label.pack(pady=(20, 10))

    target_label = TargetLabel(
        target_frame,
        text="Drag & Drop\nTarget Image/Video Here",
        justify="center",
        width=250,
        height=250,
    )
    target_label.pack(pady=(20, 10))

    select_face_button = SourceButton(
        source_frame,
        text="Select a face",
        cursor="hand2",
        command=lambda: select_source_path(),
    )
    select_face_button.pack(pady=10)

    select_target_button = TargetButton(
        target_frame,
        text="Select a target",
        cursor="hand2",
        command=lambda: select_target_path(),
    )
    select_target_button.pack(pady=10)

    swap_faces_button = ModernButton(
        middle_frame,
        text="↔",
        cursor="hand2",
        command=lambda: swap_faces_paths(),
        width=50,
        height=50,
    )
    swap_faces_button.pack(expand=True)

    options_frame = ctk.CTkFrame(main_frame, fg_color="#2a2d2e", corner_radius=15)
    options_frame.grid(row=1, column=0, columnspan=3, padx=10, pady=10, sticky="nsew")

    # Create a single column for options, centered
    options_column = ctk.CTkFrame(options_frame, fg_color="#2a2d2e")
    options_column.pack(expand=True)

    # Switches
    keep_fps_value = ctk.BooleanVar(value=modules.globals.keep_fps)
    keep_fps_checkbox = ctk.CTkSwitch(
        options_column,
        text="Keep fps",
        variable=keep_fps_value,
        cursor="hand2",
        command=lambda: setattr(
            modules.globals, "keep_fps", not modules.globals.keep_fps
        ),
        progress_color="#3a7ebf",
        font=("Roboto", 14, "bold"),
    )
    keep_fps_checkbox.pack(pady=5, anchor="w")

    keep_frames_value = ctk.BooleanVar(value=modules.globals.keep_frames)
    keep_frames_switch = ctk.CTkSwitch(
        options_column,
        text="Keep frames",
        variable=keep_frames_value,
        cursor="hand2",
        command=lambda: setattr(
            modules.globals, "keep_frames", keep_frames_value.get()
        ),
        progress_color="#3a7ebf",
        font=("Roboto", 14, "bold"),
    )
    keep_frames_switch.pack(pady=5, anchor="w")

    enhancer_value = ctk.BooleanVar(value=modules.globals.fp_ui["face_enhancer"])
    enhancer_switch = ctk.CTkSwitch(
        options_column,
        text="Face Enhancer",
        variable=enhancer_value,
        cursor="hand2",
        command=lambda: update_tumbler("face_enhancer", enhancer_value.get()),
        progress_color="#3a7ebf",
        font=("Roboto", 14, "bold"),
    )
    enhancer_switch.pack(pady=5, anchor="w")

    keep_audio_value = ctk.BooleanVar(value=modules.globals.keep_audio)
    keep_audio_switch = ctk.CTkSwitch(
        options_column,
        text="Keep audio",
        variable=keep_audio_value,
        cursor="hand2",
        command=lambda: setattr(modules.globals, "keep_audio", keep_audio_value.get()),
        progress_color="#3a7ebf",
        font=("Roboto", 14, "bold"),
    )
    keep_audio_switch.pack(pady=5, anchor="w")

    many_faces_value = ctk.BooleanVar(value=modules.globals.many_faces)
    many_faces_switch = ctk.CTkSwitch(
        options_column,
        text="Many faces",
        variable=many_faces_value,
        cursor="hand2",
        command=lambda: setattr(modules.globals, "many_faces", many_faces_value.get()),
        progress_color="#3a7ebf",
        font=("Roboto", 14, "bold"),
    )
    many_faces_switch.pack(pady=5, anchor="w")

    color_correction_value = ctk.BooleanVar(value=modules.globals.color_correction)
    color_correction_switch = ctk.CTkSwitch(
        options_column,
        text="Fix Blueish Cam",
        variable=color_correction_value,
        cursor="hand2",
        command=lambda: setattr(
            modules.globals, "color_correction", color_correction_value.get()
        ),
        progress_color="#3a7ebf",
        font=("Roboto", 14, "bold"),
    )
    color_correction_switch.pack(pady=5, anchor="w")

    map_faces = ctk.BooleanVar(value=modules.globals.map_faces)
    map_faces_switch = ctk.CTkSwitch(
        options_column,
        text="Map faces",
        variable=map_faces,
        cursor="hand2",
        command=lambda: setattr(modules.globals, "map_faces", map_faces.get()),
        progress_color="#3a7ebf",
        font=("Roboto", 14, "bold"),
    )
    map_faces_switch.pack(pady=5, anchor="w")

    button_frame = ctk.CTkFrame(main_frame, fg_color="#1a1a1a")
    button_frame.grid(row=2, column=0, columnspan=3, padx=10, pady=10, sticky="nsew")

    start_button = ModernButton(
        button_frame,
        text="Start",
        cursor="hand2",
        command=lambda: analyze_target(start, root),
        fg_color="#4CAF50",
        hover_color="#45a049",
    )
    start_button.pack(side="left", padx=10, expand=True)

    preview_button = ModernButton(
        button_frame,
        text="Preview",
        cursor="hand2",
        command=lambda: toggle_preview(),
    )
    preview_button.pack(side="left", padx=10, expand=True)

    live_button = ModernButton(
        button_frame,
        text="Live",
        cursor="hand2",
        command=lambda: webcam_preview(root),
    )
    live_button.pack(side="left", padx=10, expand=True)

    stop_button = ModernButton(
        button_frame,
        text="Destroy",
        cursor="hand2",
        command=lambda: destroy(),
        fg_color="#f44336",
        hover_color="#d32f2f",
    )
    stop_button.pack(side="left", padx=10, expand=True)

    status_label = ModernLabel(
        main_frame, text=None, justify="center", fg_color="#1a1a1a"
    )
    status_label.grid(row=3, column=0, columnspan=3, pady=10, sticky="ew")

    donate_frame = ctk.CTkFrame(main_frame, fg_color="#1a1a1a")
    donate_frame.grid(row=4, column=0, columnspan=3, pady=5, sticky="ew")

    donate_label = ModernLabel(
        donate_frame,
        text="Donate",
        justify="center",
        cursor="hand2",
        fg_color="#1870c4",
        text_color="#1870c4",
    )
    donate_label.pack(side="left", expand=True)

    donate_label.bind(
        "<Button>", lambda event: webbrowser.open("https://paypal.me/hacksider")
    )

    remove_donate_button = ModernButton(
        donate_frame,
        text="X",
        cursor="hand2",
        command=lambda: donate_frame.destroy(),
        width=30,
        height=30,
        fg_color="#f44336",
        hover_color="#d32f2f",
    )
    remove_donate_button.pack(side="right", padx=(10, 0))

    # Add opacity slider
    opacity_frame = ctk.CTkFrame(options_column, fg_color="#2a2d2e")
    opacity_frame.pack(pady=5, anchor="w", fill="x")

    opacity_label = ctk.CTkLabel(
        opacity_frame, text="Face Opacity:", font=("Roboto", 14, "bold")
    )
    opacity_label.pack(side="left", padx=(0, 10))

    opacity_slider = ctk.CTkSlider(
        opacity_frame,
        from_=0,
        to=100,
        number_of_steps=100,
        command=update_opacity,
        fg_color=("gray75", "gray25"),
        progress_color="#3a7ebf",
        button_color="#3a7ebf",
        button_hover_color="#2b5d8b",
    )
    opacity_slider.pack(side="left", fill="x", expand=True)
    opacity_slider.set(modules.globals.face_opacity)

    main_frame.grid_columnconfigure((0, 2), weight=1)
    main_frame.grid_rowconfigure((0, 1, 2), weight=1)

    root.grid_columnconfigure(0, weight=1)
    root.grid_rowconfigure(0, weight=1)

    return root


def analyze_target(start: Callable[[], None], root: ctk.CTk):
    if POPUP is not None and POPUP.winfo_exists():
        update_status("Please complete pop-up or close it.")
        return

    if modules.globals.map_faces:
        modules.globals.souce_target_map = []

        if is_image(modules.globals.target_path):
            update_status("Getting unique faces")
            get_unique_faces_from_target_image()
        elif is_video(modules.globals.target_path):
            update_status("Getting unique faces")
            get_unique_faces_from_target_video()
        else:
            update_status("Invalid target file. Please select an image or video.")
            return

        if len(modules.globals.souce_target_map) > 0:
            create_source_target_popup(start, root, modules.globals.souce_target_map)
        else:
            update_status("No faces found in target")
    else:
        select_output_path(start)


def create_source_target_popup(
    start: Callable[[], None], root: ctk.CTk, map: list
) -> None:
    global POPUP, popup_status_label

    POPUP = ctk.CTkToplevel(root)
    POPUP.title("Source x Target Mapper")
    POPUP.geometry(f"{POPUP_WIDTH}x{POPUP_HEIGHT}")
    POPUP.focus()
    POPUP.resizable(True, True)

    def on_submit_click(start):
        if has_valid_map():
            POPUP.destroy()
            select_output_path(start)
        else:
            update_pop_status("At least 1 source with target is required!")

    scrollable_frame = ctk.CTkScrollableFrame(
        POPUP, width=POPUP_SCROLL_WIDTH, height=POPUP_SCROLL_HEIGHT
    )
    scrollable_frame.pack(fill="both", expand=True, padx=10, pady=10)

    def on_button_click(map, button_num):
        update_popup_source(scrollable_frame, map, button_num)

    # Create a frame to hold the table
    table_frame = ctk.CTkFrame(scrollable_frame)
    table_frame.pack(expand=True)

    for item in map:
        id = item["id"]

        row_frame = ctk.CTkFrame(table_frame)
        row_frame.pack(fill="x", pady=5)

        source_button = SourceMapperButton(
            row_frame,
            map,
            id,
            text="Select source image",
            command=lambda id=id: on_button_click(map, id),
            width=DEFAULT_BUTTON_WIDTH,
            height=DEFAULT_BUTTON_HEIGHT,
            fg_color=("gray75", "gray25"),
            hover_color=("gray85", "gray35"),
            corner_radius=10,
        )
        source_button.pack(side="left", padx=(0, 10))

        source_image = ctk.CTkLabel(
            row_frame,
            text=f"S-{id}",
            width=MAPPER_PREVIEW_MAX_WIDTH,
            height=MAPPER_PREVIEW_MAX_HEIGHT,
            font=("Arial", 14, "bold"),
        )
        source_image.pack(side="left", padx=(0, 10))

        x_label = ctk.CTkLabel(
            row_frame,
            text=f"X",
            width=30,
            font=("Arial", 14, "bold"),
        )
        x_label.pack(side="left", padx=(0, 10))

        image = Image.fromarray(cv2.cvtColor(item["target"]["cv2"], cv2.COLOR_BGR2RGB))
        image = image.resize(
            (MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
        )
        tk_image = ctk.CTkImage(image, size=image.size)

        target_image = ctk.CTkLabel(
            row_frame,
            text=f"T-{id}",
            width=MAPPER_PREVIEW_MAX_WIDTH,
            height=MAPPER_PREVIEW_MAX_HEIGHT,
            font=("Arial", 14, "bold"),
        )
        target_image.pack(side="left")
        target_image.configure(image=tk_image)

    popup_status_label = ctk.CTkLabel(
        POPUP, text=None, justify="center", font=("Arial", 14, "bold")
    )
    popup_status_label.pack(pady=10)

    submit_button = ctk.CTkButton(
        POPUP,
        text="Submit",
        command=lambda: on_submit_click(start),
        fg_color=("DodgerBlue", "DodgerBlue"),
        hover_color=("RoyalBlue", "RoyalBlue"),
        corner_radius=10,
        font=("Arial", 16, "bold"),
        width=200,
        height=50,
    )
    submit_button.pack(pady=10)

    POPUP.update()
    POPUP.minsize(POPUP.winfo_width(), POPUP.winfo_height())


def update_popup_source(
    scrollable_frame: ctk.CTkScrollableFrame,
    map: list,
    button_num: int,
    source_path: str = None,
) -> list:
    global source_label_dict, RECENT_DIRECTORY_SOURCE

    if source_path is None:
        source_path = ctk.filedialog.askopenfilename(
            title="select an source image",
            initialdir=RECENT_DIRECTORY_SOURCE,
            filetypes=[img_ft],
        )

    if "source" in map[button_num]:
        map[button_num].pop("source")
        if button_num in source_label_dict:
            source_label_dict[button_num].destroy()
            del source_label_dict[button_num]

    if source_path == "":
        return map
    else:
        RECENT_DIRECTORY_SOURCE = os.path.dirname(source_path)
        cv2_img = cv2.imread(source_path)
        face = get_one_face(cv2_img)

        if face:
            x_min, y_min, x_max, y_max = face["bbox"]

            map[button_num]["source"] = {
                "cv2": cv2_img[int(y_min) : int(y_max), int(x_min) : int(x_max)],
                "face": face,
            }

            image = Image.fromarray(
                cv2.cvtColor(map[button_num]["source"]["cv2"], cv2.COLOR_BGR2RGB)
            )
            image = image.resize(
                (MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
            )
            tk_image = ctk.CTkImage(image, size=image.size)

            source_image = ctk.CTkLabel(
                scrollable_frame.winfo_children()[button_num],
                text=f"S-{button_num}",
                width=MAPPER_PREVIEW_MAX_WIDTH,
                height=MAPPER_PREVIEW_MAX_HEIGHT,
                font=("Arial", 14, "bold"),
            )
            source_image.pack(
                side="left",
                padx=(0, 10),
                after=scrollable_frame.winfo_children()[button_num].winfo_children()[0],
            )
            source_image.configure(image=tk_image)
            source_label_dict[button_num] = source_image
        else:
            update_pop_status("Face could not be detected in last upload!")
        return map


def create_preview(parent: ctk.CTkToplevel) -> ctk.CTkToplevel:
    global preview_label, preview_slider

    preview = ctk.CTkToplevel(parent)
    preview.withdraw()
    preview.title("Preview")
    preview.configure()
    preview.protocol("WM_DELETE_WINDOW", lambda: toggle_preview())
    preview.resizable(width=True, height=True)

    preview_label = ctk.CTkLabel(preview, text=None, font=("Arial", 14, "bold"))
    preview_label.pack(fill="both", expand=True)

    preview_slider = ctk.CTkSlider(
        preview,
        from_=0,
        to=0,
        command=lambda frame_value: update_preview(int(frame_value)),
        fg_color=("gray75", "gray25"),
        progress_color=("DodgerBlue", "DodgerBlue"),
        button_color=("DodgerBlue", "DodgerBlue"),
        button_hover_color=("RoyalBlue", "RoyalBlue"),
    )
    preview_slider.pack(fill="x", padx=20, pady=10)

    last_update_time = 0
    debounce_delay = 0.1  # Adjust this delay as needed (in seconds)

    def on_key_press(event):
        nonlocal last_update_time

        current_time = time.time()
        if current_time - last_update_time > debounce_delay:
            current_frame = int(preview_slider.get())
            if event.keysym == "Left":
                new_frame = max(0, current_frame - 1)
            elif event.keysym == "Right":
                new_frame = min(int(preview_slider.cget("to")), current_frame + 1)
            else:
                return  # Ignore other key presses

            preview_slider.set(new_frame)
            update_preview(new_frame)
            last_update_time = current_time

    preview.bind("<Left>", on_key_press)
    preview.bind("<Right>", on_key_press)

    return preview


def update_status(text: str) -> None:
    status_label.configure(text=text)
    ROOT.update()


def update_pop_status(text: str) -> None:
    popup_status_label.configure(text=text)


def update_pop_live_status(text: str) -> None:
    popup_status_label_live.configure(text=text)


def update_tumbler(var: str, value: bool) -> None:
    modules.globals.fp_ui[var] = value


def select_source_path() -> None:
    global RECENT_DIRECTORY_SOURCE, img_ft, vid_ft

    PREVIEW.withdraw()
    source_path = ctk.filedialog.askopenfilename(
        title="select an source image",
        initialdir=RECENT_DIRECTORY_SOURCE,
        filetypes=[img_ft],
    )
    if is_image(source_path):
        modules.globals.source_path = source_path
        RECENT_DIRECTORY_SOURCE = os.path.dirname(modules.globals.source_path)
        image = render_image_preview(modules.globals.source_path, (200, 200))
        source_label.configure(image=image)
        source_label.configure(text="")
    else:
        modules.globals.source_path = None
        source_label.configure(image=None)
        source_label.configure(text="Drag & Drop Source Image Here")


def swap_faces_paths() -> None:
    global RECENT_DIRECTORY_SOURCE, RECENT_DIRECTORY_TARGET

    source_path = modules.globals.source_path
    target_path = modules.globals.target_path

    if not is_image(source_path) or not is_image(target_path):
        return

    modules.globals.source_path = target_path
    modules.globals.target_path = source_path

    RECENT_DIRECTORY_SOURCE = os.path.dirname(modules.globals.source_path)
    RECENT_DIRECTORY_TARGET = os.path.dirname(modules.globals.target_path)

    PREVIEW.withdraw()

    source_image = render_image_preview(modules.globals.source_path, (200, 200))
    source_label.configure(image=source_image)
    source_label.configure(text="")

    target_image = render_image_preview(modules.globals.target_path, (200, 200))
    target_label.configure(image=target_image)
    target_label.configure(text="")


def select_target_path() -> None:
    global RECENT_DIRECTORY_TARGET, img_ft, vid_ft

    PREVIEW.withdraw()
    target_path = ctk.filedialog.askopenfilename(
        title="select an target image or video",
        initialdir=RECENT_DIRECTORY_TARGET,
        filetypes=[img_ft, vid_ft],
    )
    if is_image(target_path):
        modules.globals.target_path = target_path
        RECENT_DIRECTORY_TARGET = os.path.dirname(modules.globals.target_path)
        image = render_image_preview(modules.globals.target_path, (200, 200))
        target_label.configure(image=image)
        target_label.configure(text="")
    elif is_video(target_path):
        modules.globals.target_path = target_path
        RECENT_DIRECTORY_TARGET = os.path.dirname(modules.globals.target_path)
        video_frame = render_video_preview(target_path, (200, 200))
        target_label.configure(image=video_frame)
        target_label.configure(text="")
    else:
        modules.globals.target_path = None
        target_label.configure(image=None)
        target_label.configure(text="Drag & Drop Target Image/Video Here")


def select_output_path(start: Callable[[], None]) -> None:
    global RECENT_DIRECTORY_OUTPUT, img_ft, vid_ft

    if is_image(modules.globals.target_path):
        output_path = ctk.filedialog.asksaveasfilename(
            title="save image output file",
            filetypes=[img_ft],
            defaultextension=".png",
            initialfile="output.png",
            initialdir=RECENT_DIRECTORY_OUTPUT,
        )
    elif is_video(modules.globals.target_path):
        output_path = ctk.filedialog.asksaveasfilename(
            title="save video output file",
            filetypes=[vid_ft],
            defaultextension=".mp4",
            initialfile="output.mp4",
            initialdir=RECENT_DIRECTORY_OUTPUT,
        )
    else:
        output_path = None
    if output_path:
        modules.globals.output_path = output_path
        RECENT_DIRECTORY_OUTPUT = os.path.dirname(modules.globals.output_path)
        start()


def check_and_ignore_nsfw(target, destroy: Callable = None) -> bool:
    """Check if the target is NSFW.
    TODO: Consider to make blur the target.
    """
    from numpy import ndarray
    from modules.predicter import predict_image, predict_video, predict_frame

    if type(target) is str:  # image/video file path
        check_nsfw = predict_image if has_image_extension(target) else predict_video
    elif type(target) is ndarray:  # frame object
        check_nsfw = predict_frame
    if check_nsfw and check_nsfw(target):
        if destroy:
            destroy(
                to_quit=False
            )  # Do not need to destroy the window frame if the target is NSFW
        update_status("Processing ignored!")
        return True
    else:
        return False


def fit_image_to_size(image, width: int, height: int):
    if width is None and height is None:
        return image
    h, w, _ = image.shape
    ratio_h = 0.0
    ratio_w = 0.0
    if width > height:
        ratio_h = height / h
    else:
        ratio_w = width / w
    ratio = max(ratio_w, ratio_h)
    new_size = (int(ratio * w), int(ratio * h))
    return cv2.resize(image, dsize=new_size)


def render_image_preview(image_path: str, size: Tuple[int, int]) -> ctk.CTkImage:
    image = Image.open(image_path)
    if size:
        image = ImageOps.fit(image, size, Image.LANCZOS)
    return ctk.CTkImage(image, size=image.size)


def render_video_preview(
    video_path: str, size: Tuple[int, int], frame_number: int = 0
) -> ctk.CTkImage:
    capture = cv2.VideoCapture(video_path)
    if frame_number:
        capture.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
    has_frame, frame = capture.read()
    if has_frame:
        image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        if size:
            image = ImageOps.fit(image, size, Image.LANCZOS)
        return ctk.CTkImage(image, size=image.size)
    capture.release()
    cv2.destroyAllWindows()


def toggle_preview() -> None:
    if PREVIEW.state() == "normal":
        PREVIEW.withdraw()
    elif modules.globals.source_path and modules.globals.target_path:
        init_preview()
        update_preview()


def init_preview() -> None:
    if is_image(modules.globals.target_path):
        preview_slider.pack_forget()
    if is_video(modules.globals.target_path):
        video_frame_total = get_video_frame_total(modules.globals.target_path)
        preview_slider.configure(to=video_frame_total)
        preview_slider.pack(fill="x")
        preview_slider.set(0)
    # Disable slider if it's an image
    if is_image(modules.globals.target_path):
        preview_slider.configure(state="disabled")
    else:
        preview_slider.configure(state="normal")


def update_preview(frame_number: int = 0) -> None:
    if modules.globals.source_path and modules.globals.target_path:
        update_status("Processing...")

        # Debug: Print the target path and frame number
        print(
            f"Target path: {modules.globals.target_path}, Frame number: {frame_number}"
        )

        temp_frame = None
        if is_video(modules.globals.target_path):
            temp_frame = get_video_frame(modules.globals.target_path, frame_number)
        elif is_image(modules.globals.target_path):
            temp_frame = cv2.imread(modules.globals.target_path)

        # Debug: Check if temp_frame is None
        if temp_frame is None:
            print("Error: temp_frame is None")
            update_status("Error: Could not read frame from video or image.")
            return

        if modules.globals.nsfw_filter and check_and_ignore_nsfw(temp_frame):
            return

        for frame_processor in get_frame_processors_modules(
            modules.globals.frame_processors
        ):
            # Debug: Print the type of frame_processor
            print(f"Processing frame with: {type(frame_processor).__name__}")

            temp_frame = frame_processor.process_frame(
                get_one_face(cv2.imread(modules.globals.source_path)), temp_frame
            )

            # Debug: Check if temp_frame is None after processing
            if temp_frame is None:
                print("Error: temp_frame is None after processing")
                update_status("Error: Frame processing failed.")
                return

        image = Image.fromarray(cv2.cvtColor(temp_frame, cv2.COLOR_BGR2RGB))
        image = ImageOps.contain(
            image, (PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT), Image.LANCZOS
        )
        image = ctk.CTkImage(image, size=image.size)
        preview_label.configure(image=image)
        update_status("Processing succeed!")
        PREVIEW.deiconify()


def webcam_preview(root: ctk.CTk):
    if not modules.globals.map_faces:
        if modules.globals.source_path is None:
            # No image selected
            return
        create_webcam_preview()
    else:
        modules.globals.souce_target_map = []
        create_source_target_popup_for_webcam(root, modules.globals.souce_target_map)


# Add this function to update the opacity value
def update_opacity(value):
    modules.globals.face_opacity = int(value)


# Modify the create_webcam_preview function to include the slider
def create_webcam_preview():
    global preview_label, PREVIEW

    camera = cv2.VideoCapture(0)
    camera.set(cv2.CAP_PROP_FRAME_WIDTH, PREVIEW_DEFAULT_WIDTH)
    camera.set(cv2.CAP_PROP_FRAME_HEIGHT, PREVIEW_DEFAULT_HEIGHT)
    camera.set(cv2.CAP_PROP_FPS, 60)

    PREVIEW.deiconify()

    # Remove any existing widgets in PREVIEW window
    for widget in PREVIEW.winfo_children():
        widget.destroy()

    # Create a main frame to hold all widgets
    main_frame = ctk.CTkFrame(PREVIEW)
    main_frame.pack(fill="both", expand=True)

    # Create a frame for the preview label
    preview_frame = ctk.CTkFrame(main_frame)
    preview_frame.pack(fill="both", expand=True, padx=10, pady=10)

    preview_label = ctk.CTkLabel(preview_frame, text="")
    preview_label.pack(fill="both", expand=True)

    frame_processors = get_frame_processors_modules(modules.globals.frame_processors)

    source_image = None

    def update_frame_size(event):
        nonlocal temp_frame
        if modules.globals.live_resizable:
            temp_frame = fit_image_to_size(temp_frame, event.width, event.height)

    preview_frame.bind("<Configure>", update_frame_size)

    while camera:
        ret, frame = camera.read()
        if not ret:
            break

        temp_frame = frame.copy()

        if modules.globals.live_mirror:
            temp_frame = cv2.flip(temp_frame, 1)

        if not modules.globals.map_faces:
            if source_image is None and modules.globals.source_path:
                source_image = get_one_face(cv2.imread(modules.globals.source_path))

            original_frame = temp_frame.copy()
            for frame_processor in frame_processors:
                temp_frame = frame_processor.process_frame(source_image, temp_frame)

            # Apply opacity
            opacity = modules.globals.face_opacity / 100
            temp_frame = cv2.addWeighted(
                temp_frame, opacity, original_frame, 1 - opacity, 0
            )
        else:
            modules.globals.target_path = None

            for frame_processor in frame_processors:
                temp_frame = frame_processor.process_frame_v2(temp_frame)

        image = cv2.cvtColor(temp_frame, cv2.COLOR_BGR2RGB)
        image = Image.fromarray(image)
        image = ImageOps.contain(
            image,
            (preview_frame.winfo_width(), preview_frame.winfo_height()),
            Image.LANCZOS,
        )
        image = ctk.CTkImage(image, size=image.size)
        preview_label.configure(image=image)
        ROOT.update()

        if PREVIEW.state() == "withdrawn":
            break

    camera.release()
    PREVIEW.withdraw()


def create_source_target_popup_for_webcam(root: ctk.CTk, map: list) -> None:
    global POPUP_LIVE, popup_status_label_live

    POPUP_LIVE = ctk.CTkToplevel(root)
    POPUP_LIVE.title("Source x Target Mapper")
    POPUP_LIVE.geometry(f"{POPUP_LIVE_WIDTH}x{POPUP_LIVE_HEIGHT}")
    POPUP_LIVE.focus()

    def on_submit_click():
        if has_valid_map():
            POPUP_LIVE.destroy()
            simplify_maps()
            create_webcam_preview()
        else:
            update_pop_live_status("At least 1 source with target is required!")

    def on_add_click():
        add_blank_map()
        refresh_data(map)
        update_pop_live_status("Please provide mapping!")

    popup_status_label_live = ctk.CTkLabel(POPUP_LIVE, text=None, justify="center")
    popup_status_label_live.grid(row=1, column=0, pady=15)

    add_button = ctk.CTkButton(
        POPUP_LIVE,
        text="Add",
        command=lambda: on_add_click(),
        fg_color=("gray75", "gray25"),
        hover_color=("gray85", "gray35"),
        corner_radius=10,
    )
    add_button.place(relx=0.2, rely=0.92, relwidth=0.2, relheight=0.05)

    close_button = ctk.CTkButton(
        POPUP_LIVE,
        text="Submit",
        command=lambda: on_submit_click(),
        fg_color=("DodgerBlue", "DodgerBlue"),
        hover_color=("RoyalBlue", "RoyalBlue"),
        corner_radius=10,
    )
    close_button.place(relx=0.6, rely=0.92, relwidth=0.2, relheight=0.05)

    refresh_data(map)  # Initial data refresh


def refresh_data(map: list):
    global POPUP_LIVE

    # Destroy existing widgets in the scrollable frame
    for widget in POPUP_LIVE.winfo_children():
        if isinstance(widget, ctk.CTkScrollableFrame):
            widget.destroy()

    scrollable_frame = ctk.CTkScrollableFrame(
        POPUP_LIVE, width=POPUP_LIVE_SCROLL_WIDTH, height=POPUP_LIVE_SCROLL_HEIGHT
    )
    scrollable_frame.grid(row=0, column=0, padx=0, pady=0, sticky="nsew")

    def on_sbutton_click(map, button_num):
        map = update_webcam_source(scrollable_frame, map, button_num)

    def on_tbutton_click(map, button_num):
        map = update_webcam_target(scrollable_frame, map, button_num)

    for item in map:
        id = item["id"]

        row_frame = ctk.CTkFrame(scrollable_frame)
        row_frame.pack(fill="x", pady=5)

        source_button = SourceMapperButton(  # Use SourceMapperButton here
            row_frame,
            map,
            id,
            text="Select source image",
            command=lambda id=id: on_sbutton_click(map, id),
            width=DEFAULT_BUTTON_WIDTH,
            height=DEFAULT_BUTTON_HEIGHT,
            fg_color=("gray75", "gray25"),
            hover_color=("gray85", "gray35"),
            corner_radius=10,
        )
        source_button.pack(side="left", padx=(0, 10))

        source_image_label = ctk.CTkLabel(  # Create a label for source image
            row_frame,
            text=f"S-{id}",
            width=MAPPER_PREVIEW_MAX_WIDTH,
            height=MAPPER_PREVIEW_MAX_HEIGHT,
        )
        source_image_label.pack(side="left", padx=(0, 10))

        if "source" in item:
            image = Image.fromarray(
                cv2.cvtColor(item["source"]["cv2"], cv2.COLOR_BGR2RGB)
            )
            image = image.resize(
                (MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
            )
            tk_image = ctk.CTkImage(image, size=image.size)
            source_image_label.configure(image=tk_image)

        x_label = ctk.CTkLabel(
            row_frame,
            text=f"X",
            width=30,
        )
        x_label.pack(side="left", padx=(0, 10))

        target_button = DragDropButton(  # Use DragDropButton for target
            row_frame,
            text="Select target image",
            command=lambda id=id: on_tbutton_click(map, id),
            width=DEFAULT_BUTTON_WIDTH,
            height=DEFAULT_BUTTON_HEIGHT,
            fg_color=("gray75", "gray25"),
            hover_color=("gray85", "gray35"),
            corner_radius=10,
        )

        target_button.handle_drop = lambda file_path, id=id: update_webcam_target(
            scrollable_frame, map, id, file_path
        )  # Add handle_drop for drag and drop
        target_button.pack(side="left", padx=(0, 10))

        target_image_label = ctk.CTkLabel(  # Create a label for target image
            row_frame,
            text=f"T-{id}",
            width=MAPPER_PREVIEW_MAX_WIDTH,
            height=MAPPER_PREVIEW_MAX_HEIGHT,
        )
        target_image_label.pack(side="left")

        if "target" in item:
            image = Image.fromarray(
                cv2.cvtColor(item["target"]["cv2"], cv2.COLOR_BGR2RGB)
            )
            image = image.resize(
                (MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
            )
            tk_image = ctk.CTkImage(image, size=image.size)
            target_image_label.configure(image=tk_image)


def update_webcam_source(
    scrollable_frame: ctk.CTkScrollableFrame, map: list, button_num: int
) -> list:
    global source_label_dict_live

    source_path = ctk.filedialog.askopenfilename(
        title="select an source image",
        initialdir=RECENT_DIRECTORY_SOURCE,
        filetypes=[img_ft],
    )

    if "source" in map[button_num]:
        map[button_num].pop("source")
        if button_num in source_label_dict_live:
            source_label_dict_live[button_num].destroy()
            del source_label_dict_live[button_num]

    if source_path == "":
        return map
    else:
        cv2_img = cv2.imread(source_path)
        face = get_one_face(cv2_img)

        if face:
            x_min, y_min, x_max, y_max = face["bbox"]

            map[button_num]["source"] = {
                "cv2": cv2_img[int(y_min) : int(y_max), int(x_min) : int(x_max)],
                "face": face,
            }

            image = Image.fromarray(
                cv2.cvtColor(map[button_num]["source"]["cv2"], cv2.COLOR_BGR2RGB)
            )
            image = image.resize(
                (MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
            )
            tk_image = ctk.CTkImage(image, size=image.size)

            # Find the source image label in the row frame
            row_frame = scrollable_frame.winfo_children()[button_num]
            source_image_label = row_frame.winfo_children()[1]

            source_image_label.configure(image=tk_image)
            source_label_dict_live[button_num] = source_image_label
        else:
            update_pop_live_status("Face could not be detected in last upload!")
        return map


def update_webcam_target(
    scrollable_frame: ctk.CTkScrollableFrame,
    map: list,
    button_num: int,
    target_path: str = None,
) -> list:
    global target_label_dict_live

    if target_path is None:
        target_path = ctk.filedialog.askopenfilename(
            title="select an target image",
            initialdir=RECENT_DIRECTORY_SOURCE,
            filetypes=[img_ft],
        )

    if "target" in map[button_num]:
        map[button_num].pop("target")
        if button_num in target_label_dict_live:
            target_label_dict_live[button_num].destroy()
            del target_label_dict_live[button_num]

    if target_path == "":
        return map
    else:
        cv2_img = cv2.imread(target_path)
        face = get_one_face(cv2_img)

        if face:
            x_min, y_min, x_max, y_max = face["bbox"]

            map[button_num]["target"] = {
                "cv2": cv2_img[int(y_min) : int(y_max), int(x_min) : int(x_max)],
                "face": face,
            }

            image = Image.fromarray(
                cv2.cvtColor(map[button_num]["target"]["cv2"], cv2.COLOR_BGR2RGB)
            )
            image = image.resize(
                (MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
            )
            tk_image = ctk.CTkImage(image, size=image.size)

            # Find the target image label in the row frame
            row_frame = scrollable_frame.winfo_children()[button_num]
            target_image_label = row_frame.winfo_children()[4]

            target_image_label.configure(image=tk_image)
            target_label_dict_live[button_num] = target_image_label
        else:
            update_pop_live_status("Face could not be detected in last upload!")
        return map