#!/usr/bin/env python
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>

import os
import sys
from collections.abc import Iterable, Mapping, Sequence
from contextlib import suppress
from functools import partial
from types import MappingProxyType
from typing import Any

from kitty.cli import parse_args
from kitty.cli_stub import PanelCLIOptions
from kitty.constants import is_macos, kitten_exe
from kitty.fast_data_types import (
    GLFW_EDGE_BOTTOM,
    GLFW_EDGE_CENTER,
    GLFW_EDGE_CENTER_SIZED,
    GLFW_EDGE_LEFT,
    GLFW_EDGE_NONE,
    GLFW_EDGE_RIGHT,
    GLFW_EDGE_TOP,
    GLFW_FOCUS_EXCLUSIVE,
    GLFW_FOCUS_NOT_ALLOWED,
    GLFW_FOCUS_ON_DEMAND,
    GLFW_LAYER_SHELL_BACKGROUND,
    GLFW_LAYER_SHELL_OVERLAY,
    GLFW_LAYER_SHELL_PANEL,
    GLFW_LAYER_SHELL_TOP,
    layer_shell_config_for_os_window,
    set_layer_shell_config,
    toggle_os_window_visibility,
)
from kitty.simple_cli_definitions import panel_options_spec
from kitty.types import LayerShellConfig, run_once
from kitty.typing_compat import BossType
from kitty.utils import log_error

args = PanelCLIOptions()
help_text = 'Use a command line program to draw a GPU accelerated panel on your desktop'
usage = '[cmdline-to-run ...]'


def panel_kitten_options_spec() -> str:
    if not hasattr(panel_kitten_options_spec, 'ans'):
           setattr(panel_kitten_options_spec, 'ans', panel_options_spec())
    ans: str = getattr(panel_kitten_options_spec, 'ans')
    return ans


def parse_panel_args(args: list[str], track_seen_options: dict[str, Any] | None = None) -> tuple[PanelCLIOptions, list[str]]:
    return parse_args(
        args, panel_kitten_options_spec, usage, help_text, 'kitty +kitten panel',
        result_class=PanelCLIOptions, track_seen_options=track_seen_options)


def dual_distance(spec: str, min_cell_value_if_no_pixels: int = 0) -> tuple[int, int]:
    with suppress(Exception):
        return int(spec), 0
    if spec.endswith('px'):
        return min_cell_value_if_no_pixels, int(spec[:-2])
    if spec.endswith('c'):
        return int(spec[:-1]), 0
    return min_cell_value_if_no_pixels, 0


def layer_shell_config(opts: PanelCLIOptions) -> LayerShellConfig:
    ltype = {
        'background': GLFW_LAYER_SHELL_BACKGROUND,
        'bottom': GLFW_LAYER_SHELL_PANEL,
        'top': GLFW_LAYER_SHELL_TOP,
        'overlay': GLFW_LAYER_SHELL_OVERLAY
    }.get(opts.layer, GLFW_LAYER_SHELL_PANEL)
    ltype = GLFW_LAYER_SHELL_BACKGROUND if opts.edge == 'background' else ltype
    edge = {
        'top': GLFW_EDGE_TOP, 'bottom': GLFW_EDGE_BOTTOM, 'left': GLFW_EDGE_LEFT, 'right': GLFW_EDGE_RIGHT,
        'center': GLFW_EDGE_CENTER, 'none': GLFW_EDGE_NONE, 'center-sized': GLFW_EDGE_CENTER_SIZED,
    }.get(opts.edge, GLFW_EDGE_TOP)
    focus_policy = {
        'not-allowed': GLFW_FOCUS_NOT_ALLOWED, 'exclusive': GLFW_FOCUS_EXCLUSIVE, 'on-demand': GLFW_FOCUS_ON_DEMAND
    }.get(opts.focus_policy, GLFW_FOCUS_NOT_ALLOWED)
    if opts.hide_on_focus_loss:
        focus_policy = GLFW_FOCUS_ON_DEMAND
    x, y = dual_distance(opts.columns, min_cell_value_if_no_pixels=1), dual_distance(opts.lines, min_cell_value_if_no_pixels=1)
    return LayerShellConfig(type=ltype,
                            edge=edge,
                            x_size_in_cells=x[0], x_size_in_pixels=x[1],
                            y_size_in_cells=y[0], y_size_in_pixels=y[1],
                            requested_top_margin=max(0, opts.margin_top),
                            requested_left_margin=max(0, opts.margin_left),
                            requested_bottom_margin=max(0, opts.margin_bottom),
                            requested_right_margin=max(0, opts.margin_right),
                            focus_policy=focus_policy,
                            requested_exclusive_zone=opts.exclusive_zone,
                            override_exclusive_zone=opts.override_exclusive_zone,
                            hide_on_focus_loss=opts.hide_on_focus_loss,
                            output_name=opts.output_name or '')


@run_once
def cli_option_to_lsc_configs_map() -> MappingProxyType[str, tuple[str, ...]]:
    return MappingProxyType({
        'lines': ('y_size_in_cells', 'y_size_in_pixels'),
        'columns': ('x_size_in_cells', 'x_size_in_pixels'),
        'margin_top': ('requested_top_margin',),
        'margin_left': ('requested_left_margin',),
        'margin_bottom': ('requested_bottom_margin',),
        'margin_right': ('requested_right_margin',),
        'edge': ('edge',),
        'layer': ('type',),
        'output_name': ('output_name',),
        'focus_policy': ('focus_policy',),
        'exclusive_zone': ('requested_exclusive_zone',),
        'override_exclusive_zone': ('override_exclusive_zone',),
        'hide_on_focus_loss': ('hide_on_focus_loss',)
    })


def incrementally_update_layer_shell_config(existing: dict[str, Any], cli_options: Iterable[str]) -> LayerShellConfig:
    seen_options: dict[str, Any] = {}
    cli_options = [('' if x.startswith('--') else '--') + x for x in cli_options]
    try:
        try:
            opts, _ = parse_panel_args(cli_options, track_seen_options=seen_options)
        except SystemExit as e:
            raise ValueError(str(e))
        lsc = layer_shell_config(opts)
    except Exception as e:
        raise ValueError(f'Invalid panel options specified: {e}')
    lsc_cli_map = cli_option_to_lsc_configs_map()
    for option in seen_options:
        for config in lsc_cli_map.get(option, ()):
            existing[config] = getattr(lsc, config)
    if seen_options.get('edge') == 'background':
        existing['type'] = GLFW_LAYER_SHELL_BACKGROUND
    if existing['hide_on_focus_loss']:
        existing['focus_policy'] = GLFW_FOCUS_ON_DEMAND
    return LayerShellConfig(**existing)


mtime_map: dict[str, float] = {}


def have_config_files_been_updated(config_files: Iterable[str]) -> bool:
    ans = False
    for cf in config_files:
        try:
            mtime = os.path.getmtime(cf)
        except OSError:
            mtime = 0
        if mtime_map.get(cf, 0) != mtime:
            ans = True
        mtime_map[cf] = mtime
    return ans


def handle_single_instance_command(boss: BossType, sys_args: Sequence[str], environ: Mapping[str, str], notify_on_os_window_death: str | None = '') -> None:
    global args
    from kitty.cli import parse_override
    from kitty.tabs import SpecialWindow
    try:
        new_args, items = parse_panel_args(list(sys_args[1:]))
    except BaseException as e:
        log_error(f'Invalid arguments received over single instance socket: {sys_args} with error: {e}')
        return
    lsc = layer_shell_config(new_args)
    config_changed = have_config_files_been_updated(new_args.config) or args.config != new_args.config or args.override != new_args.override
    args = new_args
    if config_changed:
        boss.load_config_file(*args.config, overrides=tuple(map(parse_override, new_args.override)))
    if args.toggle_visibility and boss.os_window_map:
        for os_window_id in boss.os_window_map:
            existing = layer_shell_config_for_os_window(os_window_id)
            layer_shell_config_changed = not existing or any(f for f in lsc._fields if getattr(lsc, f) != existing.get(f))
            toggle_os_window_visibility(os_window_id, move_to_active_screen=args.move_to_active_monitor)
            if layer_shell_config_changed:
                set_layer_shell_config(os_window_id, lsc)
        return
    items = items or [kitten_exe(), 'run-shell']
    os_window_id = boss.add_os_panel(lsc, args.cls, args.name)
    if notify_on_os_window_death:
        boss.os_window_death_actions[os_window_id] = partial(boss.notify_on_os_window_death, notify_on_os_window_death)
    tm = boss.os_window_map[os_window_id]
    tm.new_tab(SpecialWindow(cmd=items, env=dict(environ)))


def main(sys_args: list[str]) -> None:
    # run_kitten runs using runpy.run_module which does not import into
    # sys.modules, which means the module will be re-imported later, causing
    # global variables to be duplicated, so do it now.
    from kittens.panel.main import actual_main
    actual_main(sys_args)
    return


def actual_main(sys_args: list[str]) -> None:
    global args
    args, items = parse_panel_args(sys_args[1:])
    have_config_files_been_updated(args.config)
    sys.argv = ['kitty']
    if args.debug_rendering:
        sys.argv.append('--debug-rendering')
    if args.debug_input:
        sys.argv.append('--debug-input')
    for config in args.config:
        sys.argv.extend(('--config', config))
    if not is_macos:
        sys.argv.extend(('--class', args.cls))
    if args.name:
        sys.argv.extend(('--name', args.name))
    if args.start_as_hidden:
        sys.argv.append('--start-as=hidden')
    if args.grab_keyboard:
        sys.argv.append('--grab-keyboard')
    for override in args.override:
        sys.argv.extend(('--override', override))
    sys.argv.append('--override=linux_display_server=auto')
    sys.argv.append('--override=macos_quit_when_last_window_closed=yes')
    sys.argv.append('--override=macos_hide_from_tasks=yes')
    sys.argv.append('--override=macos_window_resizable=no')
    if args.single_instance:
        sys.argv.append('--single-instance')
    if args.instance_group:
        sys.argv.append(f'--instance-group={args.instance_group}')
    if args.listen_on:
        sys.argv.append(f'--listen-on={args.listen_on}')

    sys.argv.extend(items)
    from kitty.main import main as real_main
    from kitty.main import run_app
    run_app.cached_values_name = 'panel'
    run_app.layer_shell_config = layer_shell_config(args)
    real_main(called_from_panel=True)


if __name__ == '__main__':
    main(sys.argv)
elif __name__ == '__doc__':
    cd: dict = sys.cli_docs  # type: ignore
    cd['usage'] = usage
    cd['options'] = panel_kitten_options_spec
    cd['help_text'] = help_text
    cd['short_desc'] = help_text
