"""PySimpleGUI-based desktop GUI for DslrDirector.

This file provides a simpler, single-file GUI implementation using PySimpleGUI
instead of Tkinter. It mirrors the core functionality: Animations tab (home,
countdown, sharing), Communication tab for BLE controls (scan/connect/disconnect),
and a Logs tab with a refresh button.

It reuses the project's existing persistence helpers (load_animation, save_animation,
load_comm, save_comm) and imports the optional BleComm module if available.

Logo handling: use Pillow (if available) to apply the same un-matte/defringe and
30%-scale pipeline. On Windows the code will compose the processed image onto the
panel background color and save as data/ata_logo_out.png so that the displayed
image has no halo issues.

Run with: python src/gui_psg.py (from project root, venv recommended)
"""

import os
import sys
import json
import threading
import time

import PySimpleGUI as sg
# Some PySimpleGUI builds (private PyPI message) may present an unexpected API.
# Verify the module exposes the expected elements; otherwise fall back to the
# existing Tkinter GUI so the user still gets a working UI and gets guidance.
try:
    if not hasattr(sg, 'Text') or not hasattr(sg, 'Window'):
        raise ImportError('PySimpleGUI API appears incompatible')
except Exception as _psg_err:
    msg = (
        "PySimpleGUI import succeeded but the installed package does not expose the\n"
        "expected API (Text/Window). This can happen with a private or mismatched\n"
        "PySimpleGUI build.\n\n"
        "Recommended fix:\n"
        "  python -m pip uninstall PySimpleGUI\n"
        "  python -m pip cache purge\n"
        "  python -m pip install --upgrade --extra-index-url https://PySimpleGUI.net/install PySimpleGUI\n\n"
        "Falling back to the Tkinter GUI (src/gui.py) for now...\n"
    )
    try:
        # write to log if available
        with open(os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')), 'data', 'gui_start.log'), 'a', encoding='utf-8') as lf:
            lf.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} PSG: {msg}\n")
    except Exception:
        pass
    print(msg)
    try:
        # Ensure repo root is on sys.path so 'src' package imports work when running as a script
        repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
        if repo_root not in sys.path:
            sys.path.insert(0, repo_root)
        # Launch existing Tk GUI as a fallback
        from src.gui import DSLRGui
        app = DSLRGui()
        app.mainloop()
        # clean exit
        raise SystemExit(0)
    except SystemExit:
        raise
    except Exception as e:
        # if fallback also fails, log and re-raise
        try:
            with open(os.path.join(repo_root, 'data', 'gui_start.log'), 'a', encoding='utf-8') as lf:
                lf.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} PSG fallback error: {e}\n")
        except Exception:
            pass
        raise

# reuse helpers from existing module
try:
    import src.app as appmod
    import importlib
    BleComm = importlib.import_module('src.BleComm')
except Exception:
    appmod = None
    BleComm = None

REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
DATA_DIR = os.path.join(REPO_ROOT, 'data')
CONFIG_PATH = os.path.join(DATA_DIR, 'config.json')
COMM_PATH = os.path.join(DATA_DIR, 'comm.json')
GUI_LOG = os.path.join(REPO_ROOT, 'data', 'gui_start.log')


def _write_gui_log(msg: str):
    try:
        with open(GUI_LOG, 'a', encoding='utf-8') as f:
            f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {msg}\n")
    except Exception:
        pass


# reuse persistence helpers if available in appmod by importing functions from gui.py
try:
    from src.gui import load_animation, save_animation, load_comm, save_comm
except Exception:
    # fallback implementations (simple read/write)
    def load_animation():
        try:
            if not os.path.exists(CONFIG_PATH):
                return {}
            with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
                data = json.load(f) or {}
            return data.get('animation-events', {})
        except Exception:
            return {}

    def save_animation(ae: dict):
        try:
            base = {}
            if os.path.exists(CONFIG_PATH):
                try:
                    with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
                        base = json.load(f) or {}
                except Exception:
                    base = {}
            base['animation-events'] = ae
            with open(CONFIG_PATH, 'w', encoding='utf-8') as f:
                json.dump(base, f, indent=2)
            return True
        except Exception:
            return False

    def load_comm():
        try:
            if not os.path.exists(COMM_PATH):
                return {}
            with open(COMM_PATH, 'r', encoding='utf-8') as f:
                return json.load(f) or {}
        except Exception:
            return {}

    def save_comm(cfg: dict):
        try:
            with open(COMM_PATH, 'w', encoding='utf-8') as f:
                json.dump(cfg, f, indent=2)
            return True
        except Exception:
            return False


# Image processing pipeline (Pillow optional)
PIL_AVAILABLE = False
try:
    from PIL import Image
    PIL_AVAILABLE = True
except Exception:
    Image = None


def process_logo_for_psg(path: str, panel_hex='#252535'):
    """Open path, un-matte/defringe, resize to 30%, and return a filepath to a PNG.

    On Windows compose onto panel background and save to DATA_DIR/ata_logo_out.png
    and return that path. If Pillow not available, return original path.
    """
    if not path or not os.path.exists(path):
        return None
    if not PIL_AVAILABLE:
        return path
    try:
        img = Image.open(path).convert('RGBA')
        # crop transparent border
        try:
            alpha = img.getchannel('A')
            bbox = alpha.getbbox()
            if bbox:
                img = img.crop(bbox)
        except Exception:
            pass
        # (simple) un-matte: sample border matte and reverse for semi-alpha
        try:
            alpha = img.getchannel('A')
            w, h = img.size
            px = img.load()
            matte_samples = []
            for x in range(w):
                if alpha.getpixel((x, 0)) == 0:
                    matte_samples.append(px[x, 0][:3])
                if alpha.getpixel((x, h - 1)) == 0:
                    matte_samples.append(px[x, h - 1][:3])
            for y in range(h):
                if alpha.getpixel((0, y)) == 0:
                    matte_samples.append(px[0, y][:3])
                if alpha.getpixel((w - 1, y)) == 0:
                    matte_samples.append(px[w - 1, y][:3])
            if matte_samples:
                mr = int(sum(s[0] for s in matte_samples) / len(matte_samples))
                mg = int(sum(s[1] for s in matte_samples) / len(matte_samples))
                mb = int(sum(s[2] for s in matte_samples) / len(matte_samples))
                matte = (mr, mg, mb)
                new = Image.new('RGBA', img.size)
                for yy in range(h):
                    for xx in range(w):
                        r, g, b, a = img.getpixel((xx, yy))
                        if a == 0 or a == 255:
                            new.putpixel((xx, yy), (r, g, b, a))
                        else:
                            af = a / 255.0
                            rr = int(round((r - (1 - af) * matte[0]) / max(af, 1e-6)))
                            gg = int(round((g - (1 - af) * matte[1]) / max(af, 1e-6)))
                            bb = int(round((b - (1 - af) * matte[2]) / max(af, 1e-6)))
                            rr = max(0, min(255, rr))
                            gg = max(0, min(255, gg))
                            bb = max(0, min(255, bb))
                            new.putpixel((xx, yy), (rr, gg, bb, a))
                img = new
        except Exception:
            pass
        # conservative defringe
        try:
            alpha_cut = 32
            color_tol = 30
            w2, h2 = img.size
            px2 = img.load()
            matte_rgb = None
            if hasattr(img, 'info') and 'background' in img.info:
                matte_rgb = img.info['background']
            if matte_rgb is None and len(matte_samples) > 0:
                matte_rgb = matte
            if matte_rgb is None:
                matte_rgb = (30, 30, 43)
            removed = 0
            for yy in range(h2):
                for xx in range(w2):
                    r, g, b, a = px2[xx, yy]
                    if 0 < a <= alpha_cut:
                        dr = r - matte_rgb[0]
                        dg = g - matte_rgb[1]
                        db = b - matte_rgb[2]
                        dist = (dr * dr + dg * dg + db * db) ** 0.5
                        if dist <= color_tol:
                            px2[xx, yy] = (0, 0, 0, 0)
                            removed += 1
        except Exception:
            pass
        # resize to 30%
        sw, sh = img.size
        new_w = max(1, int(sw * 0.3))
        new_h = max(1, int(sh * 0.3))
        img_resized = img.resize((new_w, new_h), Image.LANCZOS)
        # On Windows, compose to opaque background and save
        out_path = os.path.join(DATA_DIR, 'ata_logo_out.png')
        try:
            if os.name == 'nt':
                def hex_to_rgb(h):
                    h = h.lstrip('#')
                    return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
                try:
                    panel_rgb = hex_to_rgb(panel_hex)
                except Exception:
                    panel_rgb = (37, 37, 53)
                bg = Image.new('RGBA', img_resized.size, panel_rgb + (255,))
                composed = Image.alpha_composite(bg, img_resized)
                composed.save(out_path, format='PNG')
                _write_gui_log(f'PSG Logo: wrote composed PNG to {out_path}')
                return out_path
            else:
                img_resized.save(out_path, format='PNG')
                _write_gui_log(f'PSG Logo: wrote PNG to {out_path}')
                return out_path
        except Exception:
            return None
    except Exception:
        return None


# Cosmetic theme/colors for a modern dark look
def _apply_psg_theme():
    # define core palette
    bg = '#12121a'
    panel = '#1e1e2b'
    fg = '#e6e6f0'
    accent = '#4fb0c6'

    # Add a named theme mapping to PySimpleGUI
    try:
        sg.LOOK_AND_FEEL_TABLE['DslrDark'] = {
            'BACKGROUND': bg,
            'TEXT': fg,
            'INPUT': '#262636',
            'TEXT_INPUT': fg,
            'SCROLL': '#2b2b36',
            'BUTTON': accent,
            'PROGRESS': ('#01826B', '#D0D0D0'),
            'BORDER': 1,
            'SLIDER_DEPTH': 0,
            'PROGRESS_DEPTH': 0
        }
        sg.theme('DslrDark')
        sg.set_options(element_padding=(6, 6), button_color=(fg, panel), font=('Segoe UI', 11))
    except Exception:
        try:
            sg.theme('Dark')
        except Exception:
            pass


_apply_psg_theme()


# Build the PySimpleGUI layout
def build_layout():
    anim = load_animation()
    comm = load_comm()
    # Anim tab layout
    anim_layout = [
        [sg.Text('Home')],
        [sg.Radio('Animation', 'home_mode', default=(anim.get('home-state', 'animation') == 'animation'), key='-HOME_ANIM-'),
         sg.Radio('Solid', 'home_mode', default=(anim.get('home-state') == 'solid'), key='-HOME_SOLID-')],
        [sg.Text('Anim ID'), sg.Input(anim.get('home-anim', '0'), key='-HOME_ID-', size=(8,1))],
        [sg.Text('Color'), sg.Input(anim.get('home-color', '#000000'), key='-HOME_COLOR-', size=(12,1)), sg.Button('Pick', key='-HOME_PICK-')],
        [sg.HorizontalSeparator()],
        [sg.Text('Countdown')],
        [sg.Radio('Animation', 'count_mode', default=(anim.get('countdown-state', 'animation') == 'animation'), key='-COUNT_ANIM-'),
         sg.Radio('Solid', 'count_mode', default=(anim.get('countdown-state') == 'solid'), key='-COUNT_SOLID-')],
        [sg.Text('Anim ID'), sg.Input(anim.get('countdown-anim', '0'), key='-COUNT_ID-', size=(8,1))],
        [sg.Text('Color'), sg.Input(anim.get('countdown-color', '#ffffff'), key='-COUNT_COLOR-', size=(12,1)), sg.Button('Pick', key='-COUNT_PICK-')],
        [sg.HorizontalSeparator()],
        [sg.Text('Sharing')],
        [sg.Radio('Animation', 'share_mode', default=(anim.get('sharing-state', 'animation') == 'animation'), key='-SHARE_ANIM-'),
         sg.Radio('Solid', 'share_mode', default=(anim.get('sharing-state') == 'solid'), key='-SHARE_SOLID-')],
        [sg.Text('Anim ID'), sg.Input(anim.get('sharing-anim', '0'), key='-SHARE_ID-', size=(8,1))],
        [sg.Text('Color'), sg.Input(anim.get('sharing-color', '#0000ff'), key='-SHARE_COLOR-', size=(12,1)), sg.Button('Pick', key='-SHARE_PICK-')],
        [sg.Button('Save Animations', key='-SAVE_ANIM-')]
    ]

    # Comm (BLE) layout
    comm_name = comm.get('ble', {}).get('device_name', '') if isinstance(comm, dict) else ''
    comm_addr = comm.get('ble', {}).get('address', '') if isinstance(comm, dict) else ''
    comm_filter = comm.get('ble', {}).get('filter_name', '') if isinstance(comm, dict) else ''
    comm_layout = [
        [sg.Text('Device Name'), sg.Input(comm_name, key='-COMM_NAME-', size=(30,1))],
        [sg.Text('Address/ID'), sg.Input(comm_addr, key='-COMM_ADDR-', size=(30,1))],
        [sg.Text('Filter'), sg.Input(comm_filter, key='-COMM_FILTER-', size=(30,1))],
        [sg.Button('Scan', key='-SCAN-', button_color=('white', '#2f313a'), size=(10,1)),
         sg.Button('Connect', key='-CONNECT-', button_color=('white', '#4fb0c6'), size=(10,1)),
         sg.Button('Disconnect', key='-DISCONNECT-', button_color=('white', '#a64b4b'), size=(10,1))],
        [sg.Listbox(values=[], size=(60,6), key='-SCAN_LIST-')]
    ]

    logs_layout = [
        [sg.Multiline('', size=(80, 20), key='-LOG_TEXT-')],
        [sg.Button('Refresh Logs', key='-REFRESH_LOGS-', button_color=('white', '#4fb0c6'))]
    ]

    tab_group = [[sg.Tab('Animations', anim_layout), sg.Tab('Communication', comm_layout), sg.Tab('Logs', logs_layout)]]

    # load logo (attempt processed path)
    logo_candidates = [
        os.path.join(REPO_ROOT, 'static', 'images', 'ata_logo.png'),
        os.path.join(os.getcwd(), 'static', 'images', 'ata_logo.png'),
        os.path.join(DATA_DIR, 'ata_logo.png')
    ]
    logo_path = None
    for p in logo_candidates:
        if p and os.path.exists(p):
            logo_path = p
            break
    if logo_path:
        processed = process_logo_for_psg(logo_path)
    else:
        processed = None

    # Header: left logo, centered title/subtitle
    if processed and os.path.exists(processed):
        logo_el = sg.Image(processed, pad=(6, 6))
    else:
        logo_el = sg.Text('', size=(12, 1))

    title_col = [[sg.Text('DSLR Director', font=('Segoe UI', 22, 'bold'), justification='center')],
                 [sg.Text('Control animations and BLE from your desktop', font=('Segoe UI', 10), text_color='#cbd6dc', justification='center')]]

    header_frame = [
        sg.Column([[logo_el]], vertical_alignment='center', element_justification='left', pad=(8, 8)),
        sg.VerticalSeparator(),
        sg.Column(title_col, vertical_alignment='center', element_justification='center', expand_x=True, pad=(8, 8))
    ]

    # Slightly larger tab group with padding
    layout = [
        header_frame,
        [sg.HorizontalSeparator()],
        [sg.TabGroup(tab_group, key='-TABS-', expand_x=True, expand_y=True, pad=(6, 8))],
        [sg.Button('Exit', button_color=('white', '#2f313a'))]
    ]

    return layout


def run():
    layout = build_layout()
    window = sg.Window('DslrDirector - PySimpleGUI', layout, finalize=True)

    # Background: attempt to start BleComm if present
    try:
        if BleComm is not None and hasattr(BleComm, 'start'):
            BleComm.start()
    except Exception:
        pass

    while True:
        event, values = window.read(timeout=200)
        if event == sg.WIN_CLOSED or event == 'Exit':
            break
        if event == '-SAVE_ANIM-':
            ae = {}
            ae['home-state'] = 'animation' if values.get('-HOME_ANIM-') else 'solid'
            ae['home-color'] = values.get('-HOME_COLOR-', '#000000')
            try:
                ae['home-anim'] = int(values.get('-HOME_ID-', '0'))
            except Exception:
                ae['home-anim'] = 0
            ae['countdown-state'] = 'animation' if values.get('-COUNT_ANIM-') else 'solid'
            ae['countdown-color'] = values.get('-COUNT_COLOR-', '#ffffff')
            try:
                ae['countdown-anim'] = int(values.get('-COUNT_ID-', '0'))
            except Exception:
                ae['countdown-anim'] = 0
            ae['sharing-state'] = 'animation' if values.get('-SHARE_ANIM-') else 'solid'
            ae['sharing-color'] = values.get('-SHARE_COLOR-', '#0000ff')
            try:
                ae['sharing-anim'] = int(values.get('-SHARE_ID-', '0'))
            except Exception:
                ae['sharing-anim'] = 0
            ok = save_animation(ae)
            if ok:
                sg.popup('Saved', 'Animations saved to config.json')
            else:
                sg.popup('Error', 'Failed to save animations')

        if event == '-SCAN-':
            window['-SCAN_LIST-'].update([])
            def _scan():
                try:
                    res = BleComm.scan(values.get('-COMM_FILTER-', '') if BleComm else [])
                    items = []
                    for d in res:
                        name = d.get('name') or ''
                        addr = d.get('address') or d.get('id') or ''
                        items.append(f"{name} | {addr}")
                    window['-SCAN_LIST-'].update(items)
                except Exception as e:
                    window['-SCAN_LIST-'].update([f"Scan failed: {e}"])
            threading.Thread(target=_scan, daemon=True).start()

        if event == '-CONNECT-':
            addr = values.get('-COMM_ADDR-', '').strip()
            name = values.get('-COMM_NAME-', '').strip()
            base = load_comm()
            base['ble'] = base.get('ble', {})
            if addr:
                base['ble']['address'] = addr
            if name:
                base['ble']['device_name'] = name
            base['ble']['filter_name'] = values.get('-COMM_FILTER-', '')
            save_comm(base)
            def _do():
                try:
                    res = BleComm.connect(addr or name)
                    sg.popup('Connect', f'Connect result: {res}')
                except Exception as e:
                    sg.popup('Connect failed', str(e))
            threading.Thread(target=_do, daemon=True).start()

        if event == '-DISCONNECT-':
            def _do():
                try:
                    res = BleComm.disconnect()
                    sg.popup('Disconnect', f'Disconnect result: {res}')
                except Exception as e:
                    sg.popup('Disconnect failed', str(e))
            threading.Thread(target=_do, daemon=True).start()

        if event == '-REFRESH_LOGS-':
            content = ''
            if appmod is not None and hasattr(appmod, 'tail_log'):
                content = appmod.tail_log(200)
            else:
                lf = os.path.join(REPO_ROOT, 'dslrbooth_triggers.log')
                if os.path.exists(lf):
                    with open(lf, 'r', encoding='utf-8', errors='replace') as f:
                        content = ''.join(f.readlines()[-200:])
            window['-LOG_TEXT-'].update(content)

    window.close()


if __name__ == '__main__':
    try:
        _write_gui_log('PSG GUI: starting')
    except Exception:
        pass
    run()
