#!/usr/bin/env python3

"""
Dependencies:
pip install pillow
"""

import os
import threading
from pathlib import Path
from tkinter import (
    Tk, Frame, Label, Button, Scale, IntVar, BooleanVar, StringVar,
    HORIZONTAL, filedialog, messagebox, ttk, Entry, Checkbutton
)
from PIL import Image, ImageOps

IMAGE_FILETYPES = [("Image files", "*.jpg *.JPG *.jpeg *.png *.bmp *.tiff *.webp *.gif *.tif *.tiff"), ("All files", "*.*")]


class WebPBatchConverter:
    def __init__(self, root: Tk):
        self.root = root
        self.root.title("WebP Batch Converter")
        self.root.geometry("800x800")
        self.root.resizable(False, False)

        # State
        self.files = []
        self.output_dir = StringVar(value="")
        self.quality = IntVar(value=85)
        self.delete_original = BooleanVar(value=False)
        self.export_same_dir = BooleanVar(value=True)
        self.export_custom_dir = BooleanVar(value=False)
        self.resize_enabled = BooleanVar(value=False)
        self.width_var = StringVar(value="1024")
        self.height_var = StringVar(value="768")

        # New scale-down state
        self.scale_down_enabled = BooleanVar(value=False)
        self.scale_factor = StringVar(value="0.5")

        # Widget references we need to change state for
        self.files_label = None
        self.quality_slider = None
        self.out_entry = None
        self.width_entry = None
        self.height_entry = None
        self.files_button = None
        self.browse_output_button = None
        self.start_button = None
        self.quit_button = None

        # Checkbutton references (so we can enable/disable them)
        self.export_same_chk = None
        self.delete_chk = None
        self.custom_chk = None

        # UI
        self._build_ui()

        # Progress state
        self.total_count = 0

    def _build_ui(self):
        padx = 12

        Label(self.root, text="Select image files to convert to WebP", font=("TkDefaultFont", 13)).pack(pady=(10, 6))

        # File selection frame
        frm_files = Frame(self.root)
        frm_files.pack(fill="x", padx=padx)

        self.files_button = Button(frm_files, text="Choose Images...", command=self.choose_images, width=16)
        self.files_button.grid(row=0, column=0, sticky="w")

        self.files_label = Label(frm_files, text="No files selected", anchor="w")
        self.files_label.grid(row=0, column=1, sticky="w", padx=(10, 0))

        # Options frame
        frm_opts = Frame(self.root)
        frm_opts.pack(fill="x", padx=padx, pady=(10, 0))

        Label(frm_opts, text="WebP quality:").grid(row=0, column=0, sticky="w")
        self.quality_slider = Scale(frm_opts, from_=10, to=100, orient=HORIZONTAL, variable=self.quality, length=300)
        self.quality_slider.grid(row=0, column=1, columnspan=3, sticky="w", padx=(8,0))

        # Export options
        Label(frm_opts, text="Export options:").grid(row=1, column=0, sticky="nw", pady=(8,0))
        cb_frame = Frame(frm_opts)
        cb_frame.grid(row=1, column=1, columnspan=3, sticky="w", padx=(8,0), pady=(8,0))

        # store checkbutton refs so we can enable/disable them when needed
        self.export_same_chk = Checkbutton(cb_frame,
                                           text="Save next to original (keep originals)",
                                           variable=self.export_same_dir,
                                           command=self._sync_export_options)
        self.export_same_chk.grid(row=0, column=0, sticky="w")

        self.delete_chk = Checkbutton(cb_frame,
                                      text="Delete original after conversion",
                                      variable=self.delete_original,
                                      command=self._sync_export_options)
        self.delete_chk.grid(row=1, column=0, sticky="w")

        self.custom_chk = Checkbutton(cb_frame,
                                      text="Export to custom folder",
                                      variable=self.export_custom_dir,
                                      command=self._sync_export_options)
        self.custom_chk.grid(row=2, column=0, sticky="w")

        # Output folder chooser
        out_frame = Frame(self.root)
        out_frame.pack(fill="x", padx=padx, pady=(6, 0))
        Label(out_frame, text="Custom folder:").grid(row=0, column=0, sticky="w")
        self.out_entry = Entry(out_frame, textvariable=self.output_dir, width=52, state="disabled")
        self.out_entry.grid(row=0, column=1, padx=(8, 4))
        self.browse_output_button = Button(out_frame, text="Browse...", command=self.choose_output_folder, state="disabled")
        self.browse_output_button.grid(row=0, column=2)

        # Resize options (exact dimensions)
        resize_frame = Frame(self.root)
        resize_frame.pack(fill="x", padx=padx, pady=(10, 0))
        Checkbutton(resize_frame, text="Resize images", variable=self.resize_enabled, command=self._toggle_resize_inputs).grid(row=0, column=0, sticky="w")

        Label(resize_frame, text="Width:").grid(row=1, column=0, sticky="e", pady=(6,0))
        self.width_entry = Entry(resize_frame, textvariable=self.width_var, width=10, state="disabled")
        self.width_entry.grid(row=1, column=1, sticky="w", pady=(6,0), padx=(6, 20))

        Label(resize_frame, text="Height:").grid(row=1, column=2, sticky="e", pady=(6,0))
        self.height_entry = Entry(resize_frame, textvariable=self.height_var, width=10, state="disabled")
        self.height_entry.grid(row=1, column=3, sticky="w", pady=(6,0), padx=(6, 20))

        # NEW: Scale down (preserve aspect ratio)
        scale_frame = Frame(self.root)
        scale_frame.pack(fill="x", padx=padx, pady=(6, 0))
        Checkbutton(scale_frame, text="Scale down (preserve aspect ratio)", variable=self.scale_down_enabled).grid(row=0, column=0, sticky="w")
        Label(scale_frame, text="Factor:").grid(row=0, column=1, padx=(10, 4))
        self.scale_combo = ttk.Combobox(scale_frame, textvariable=self.scale_factor, values=["0.5", "0.25"], state="readonly", width=6)
        self.scale_combo.grid(row=0, column=2, sticky="w")
        self.scale_combo.set("0.5")

        # Progress + Buttons
        bottom_frame = Frame(self.root)
        bottom_frame.pack(fill="x", padx=padx, pady=(12, 8))

        self.progress = ttk.Progressbar(bottom_frame, orient='horizontal', mode='determinate', length=520)
        self.progress.grid(row=0, column=0, columnspan=4, pady=(0,8))

        self.status_label = Label(bottom_frame, text="Ready")
        self.status_label.grid(row=1, column=0, sticky="w")

        self.start_button = Button(bottom_frame, text="Start Conversion", command=self.start_conversion, width=16)
        self.start_button.grid(row=1, column=2, sticky="e", padx=(8,0))
        self.quit_button = Button(bottom_frame, text="Quit", command=self.root.quit, width=10)
        self.quit_button.grid(row=1, column=3, sticky="e", padx=(8,0))

        # initialize states
        self._toggle_resize_inputs()
        self._sync_export_options()

    def choose_images(self):
        files = filedialog.askopenfilenames(title="Select images", filetypes=IMAGE_FILETYPES)
        if files:
            self.files = list(files)
            display = f"{len(self.files)} file(s) selected"
            if len(self.files) <= 6:
                display = ", ".join([Path(p).name for p in self.files])
            self.files_label.config(text=display)
            self.status_label.config(text="Files selected")
        else:
            self.files = []
            self.files_label.config(text="No files selected")
            self.status_label.config(text="Ready")

    def choose_output_folder(self):
        d = filedialog.askdirectory(title="Choose output folder")
        if d:
            self.output_dir.set(d)
            self.export_custom_dir.set(True)
            self.export_same_dir.set(False)
            self._sync_export_options()

    def _toggle_resize_inputs(self):
        state = "normal" if self.resize_enabled.get() else "disabled"
        self.width_entry.config(state=state)
        self.height_entry.config(state=state)
        return

    def _sync_export_options(self):
        """
        Rules enforced:
         - If custom folder selected -> uncheck & disable 'save next to original'.
         - If delete original selected -> uncheck & disable 'save next to original' (because they contradict).
         - If neither delete nor custom selected -> enable 'save next to original'.
        """

        # If custom folder selected, turn off save-next-to-original
        if self.export_custom_dir.get():
            self.export_same_dir.set(False)

        # If delete-original selected, turn off save-next-to-original
        if self.delete_original.get():
            self.export_same_dir.set(False)

        # If neither custom nor delete are selected and export_same_dir was False (user cleared both),
        # default to export_same_dir True to keep a sensible default
        if not self.export_custom_dir.get() and not self.delete_original.get() and not self.export_same_dir.get():
            self.export_same_dir.set(True)

        # Enable/disable the 'save next to original' checkbutton depending on conflicts
        if self.export_custom_dir.get() or self.delete_original.get():
            try:
                self.export_same_chk.configure(state="disabled")
            except Exception:
                pass
        else:
            try:
                self.export_same_chk.configure(state="normal")
            except Exception:
                pass

        # Enable/disable custom folder controls
        if self.export_custom_dir.get():
            self.out_entry.config(state="normal")
            self.browse_output_button.config(state="normal")
        else:
            self.out_entry.config(state="disabled")
            self.browse_output_button.config(state="disabled")

        # Update status text so user sees what's happening
        if self.delete_original.get() and self.export_custom_dir.get():
            self.status_label.config(text="Will save WebP to custom folder, then delete original files.")
        elif self.delete_original.get():
            self.status_label.config(text="Will save WebP (next to originals if selected) then delete original files.")
        elif self.export_custom_dir.get():
            self.status_label.config(text="Will save WebP to custom folder.")
        else:
            self.status_label.config(text="Ready")

    def _unique_target(self, p: Path) -> Path:
        if not p.exists():
            return p
        base = p.stem
        parent = p.parent
        i = 1
        while True:
            candidate = parent / f"{base}_converted{i}{p.suffix}"
            if not candidate.exists():
                return candidate
            i += 1

    def start_conversion(self):
        if not self.files:
            messagebox.showwarning("No files", "Please select one or more image files to convert.")
            return

        q = self.quality.get()
        if not (0 <= q <= 100):
            messagebox.showerror("Quality out of range", "Quality must be between 0 and 100.")
            return

        if self.resize_enabled.get():
            try:
                w = int(self.width_var.get())
                h = int(self.height_var.get())
                if w <= 0 or h <= 0:
                    raise ValueError
            except Exception:
                messagebox.showerror("Invalid size", "Please enter positive integers for width and height.")
                return

        if self.export_custom_dir.get():
            out = self.output_dir.get().strip()
            if not out:
                messagebox.showerror("No output folder", "Please choose a custom output folder or uncheck 'Export to custom folder'.")
                return
            if not os.path.isdir(out):
                try:
                    os.makedirs(out, exist_ok=True)
                except Exception as e:
                    messagebox.showerror("Invalid output folder", f"Cannot create or access the output folder:\n{e}")
                    return

        # disable main controls while running (leave progress & status active)
        self._set_ui_state(disabled=True)
        self.total_count = len(self.files)
        self.progress["maximum"] = self.total_count
        self.progress["value"] = 0
        self.status_label.config(text="Starting conversion...")

        thread = threading.Thread(target=self._worker_convert, daemon=True)
        thread.start()

    def _set_ui_state(self, disabled: bool):
        state = "disabled" if disabled else "normal"
        for child in self.root.winfo_children():
            for sub in child.winfo_children():
                try:
                    # keep progress & status usable
                    if sub not in (self.progress, self.status_label):
                        sub.configure(state=state)
                except Exception:
                    pass
        try:
            self.progress.configure(state="normal")
        except Exception:
            pass
        try:
            self.status_label.configure(state="normal")
        except Exception:
            pass

    def _worker_convert(self):
        success = 0
        errors = 0

        for idx, filepath in enumerate(self.files, start=1):
            try:
                src_path = Path(filepath)
                img = Image.open(src_path)

                # Apply EXIF orientation so the image pixels are oriented correctly before any processing.
                # This ensures the saved WebP will have the correct orientation regardless of EXIF.
                try:
                    img = ImageOps.exif_transpose(img)
                except Exception:
                    # If exif_transpose isn't available or fails, continue without raising;
                    # image will be processed as-is (may be mis-oriented).
                    pass

                # NEW: Scale down if enabled (preserves aspect ratio)
                if self.scale_down_enabled.get():
                    factor = float(self.scale_factor.get())
                    new_w = int(img.width * factor)
                    new_h = int(img.height * factor)
                    img = img.resize((new_w, new_h), Image.LANCZOS)

                # Resize if requested (exact dimensions) - only if scale-down is NOT active
                elif self.resize_enabled.get():
                    target_w = int(self.width_var.get())
                    target_h = int(self.height_var.get())
                    img = img.resize((target_w, target_h), Image.LANCZOS)

                # Prepare saving path
                if self.export_custom_dir.get():
                    out_dir = Path(self.output_dir.get())
                else:
                    out_dir = src_path.parent

                name_root = src_path.stem
                out_path = out_dir / f"{name_root}.webp"

                # Avoid clobbering
                if out_path.exists():
                    out_path = self._unique_target(out_path)

                # Preserve alpha if present
                try:
                    mode = img.mode
                    if "A" in mode or mode == "RGBA":
                        save_img = img.convert("RGBA")
                    else:
                        save_img = img.convert("RGB")
                except Exception:
                    save_img = img.convert("RGB")

                # Save as WebP with selected quality.
                # We intentionally do not copy EXIF orientation tag into the WebP: we've already applied it.
                save_img.save(out_path, format="WEBP", quality=int(self.quality.get()))

                # Delete original file if requested
                if self.delete_original.get():
                    try:
                        src_path.unlink()
                    except Exception as e:
                        print(f"Warning: could not delete original {src_path}: {e}")

                success += 1
            except Exception as e:
                errors += 1
                print(f"Failed processing {filepath}: {e}")

            # Update progress on main thread
            self.root.after(0, self._update_progress, idx, self.total_count, f"Processing {idx}/{self.total_count}")

        summary = f"Finished. {success} succeeded"
        if errors:
            summary += f", {errors} failed."
        else:
            summary += "."

        self.root.after(0, self._on_complete, summary)

    def _update_progress(self, done_count, total, status_text):
        self.progress["value"] = done_count
        self.status_label.config(text=status_text)
        self.progress.update_idletasks()

    def _on_complete(self, summary_text):
        self._set_ui_state(disabled=False)
        self.status_label.config(text=summary_text)
        messagebox.showinfo("Conversion complete", summary_text)

def main():
    try:
        from PIL import Image  # quick check
    except ImportError:
        messagebox.showerror("Missing Dependency", "Please install Pillow:\n\npip install pillow")
        return

    root = Tk()
    app = WebPBatchConverter(root)
    root.mainloop()


if __name__ == "__main__":
    main()
