# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (C) 2025-2026 Benjamin Abendroth <braph93@gmx.de>

'''This module contains code for completing arguments in Zsh.'''

from . import shell
from .pattern import bash_glob_to_zsh_glob
from .str_utils import join_with_wrap, indent
from .zsh_utils import (
    escape_colon, escape_square_brackets, make_file_extension_pattern
)
from .type_utils import is_dict_type


CHOICES_INLINE_THRESHOLD = 80
# The `choices` command can in Zsh be expressed inline in an optspec, like:
#   (foo bar baz)
# or:
#   (foo:"Foo value" bar:"Bar value" baz:"Baz value)
#
# This variable defines how big this string can get before a function
# is used instead.


class ZshCompletionBase:
    '''Base class for Zsh completions.'''

    def get_action_string(self):
        '''Return an action string that can be used in an option spec.'''
        raise NotImplementedError

    def get_function(self):
        '''Return a function that can be used in e.g. `_sequence`.'''
        raise NotImplementedError


class ZshComplFunc(ZshCompletionBase):
    '''Complete using a function.'''

    def __init__(self, ctxt, args, needs_braces=False):
        self.ctxt = ctxt
        self.args = args
        self.needs_braces = needs_braces

    def get_action_string(self):
        if len(self.args) == 1:
            cmd = self.args[0]
        else:
            cmd = shell.join_quoted(self.args)

        if self.needs_braces:
            return shell.quote(f'{{{cmd}}}')

        return shell.quote(cmd)

    def get_function(self):
        if len(self.args) == 1:
            return self.args[0]

        code = shell.join_quoted(self.args)
        funcname = self.ctxt.helpers.add_dynamic_func(self.ctxt, code)
        return funcname


class ZshCompleteChoices(ZshCompletionBase):
    '''Complete from a set of words.'''

    def __init__(self, ctxt, trace, choices):
        self.ctxt = ctxt
        self.trace = trace
        self.choices = choices

    def _list_action_string(self):
        items = [str(item) for item in self.choices]
        quoted = [shell.quote(item) for item in items]
        action = shell.quote('(%s)' % ' '.join(quoted))
        if len(action) <= CHOICES_INLINE_THRESHOLD:
            return action

        return self._list_function()

    def _list_function(self):
        metavar = shell.quote(self.ctxt.option.metavar or '')
        quoted = [shell.quote(escape_colon(c)) for c in self.choices]
        line_length = self.ctxt.config.line_length - 2

        code = 'local items=(\n'
        code += indent(join_with_wrap(' ', '\n', line_length, quoted), 2)
        code += '\n)\n\n'
        code += f'_describe -- {metavar} items'

        funcname = self.ctxt.helpers.add_dynamic_func(self.ctxt, code)
        return funcname

    def _dict_action_string(self):
        def str0(s):
            return str(s) if s is not None else ''

        items = [str0(item) for item in self.choices.keys()]
        descriptions = [str0(value) for value in self.choices.values()]
        colon = any(':' in s for s in items + descriptions)
        quoted = []

        for item, desc in zip(items, descriptions):
            val = shell.quote(item)
            if desc:
                val += ':%s' % shell.quote(desc)
            quoted.append(val)

        action = shell.quote('((%s))' % ' '.join(quoted))
        if not colon and len(action) <= CHOICES_INLINE_THRESHOLD:
            return action

        return self._dict_function()

    def _dict_function(self):
        metavar = shell.quote(self.ctxt.option.metavar or '')

        code = 'local items=(\n'
        for item, desc in self.choices.items():
            item = shell.quote(escape_colon(str(item)))
            if desc:
                desc = shell.quote(str(desc))
                code += f'  {item}:{desc}\n'
            else:
                code += f'  {item}\n'
        code += ')\n\n'
        code += f'_describe -- {metavar} items'

        funcname = self.ctxt.helpers.add_dynamic_func(self.ctxt, code)
        return funcname

    def get_action_string(self):
        if is_dict_type(self.choices):
            return self._dict_action_string()

        return self._list_action_string()

    def get_function(self):
        if is_dict_type(self.choices):
            return self._dict_function()

        return self._list_function()


class ZshCompleteCommand(ZshCompletionBase):
    '''Complete a command from $PATH.'''

    def __init__(self, ctxt, opts):
        self.ctxt = ctxt
        self.code = None
        path = None
        append = None
        prepend = None

        if opts:
            path = opts.get('path', None)
            append = opts.get('path_append', None)
            prepend = opts.get('path_prepend', None)

        if path:
            self.code = 'local -x PATH=%s' % shell.quote(path)
        elif append and prepend:
            append = shell.quote(append)
            prepend = shell.quote(prepend)
            self.code = 'local -x PATH=%s:"$PATH":%s' % (prepend, append)
        elif append:
            self.code = 'local -x PATH="$PATH":%s' % shell.quote(append)
        elif prepend:
            self.code = 'local -x PATH=%s:"$PATH"' % shell.quote(prepend)

    def get_action_string(self):
        if not self.code:
            return '_command_names'

        code = f'{self.code}\n_command_names'
        funcname = self.ctxt.helpers.add_dynamic_func(self.ctxt, code)
        return funcname

    def get_function(self):
        if not self.code:
            return '_command_names'

        code = f'{self.code}\n_command_names'
        funcname = self.ctxt.helpers.add_dynamic_func(self.ctxt, code)
        return funcname


class ZshCompleteRange(ZshCompletionBase):
    '''Complete a range of integers.'''

    def __init__(self, ctxt, start, stop, step):
        self.ctxt = ctxt
        self.start = start
        self.stop = stop
        self.step = step

    def get_action_string(self):
        if self.step == 1:
            return f"'({{{self.start}..{self.stop}}})'"

        return f"'({{{self.start}..{self.stop}..{self.step}}})'"

    def get_function(self):
        if self.step == 1:
            code = f"command seq {self.start} {self.stop}"
        else:
            code = f"command seq {self.start} {self.step} {self.stop}"

        code = f'compadd -- $({code})'

        funcname = self.ctxt.helpers.add_dynamic_func(self.ctxt, code)
        return funcname


class ZshKeyValueList(ZshComplFunc):
    '''Used for completing a list of key-value pairs.'''

    # pylint: disable=too-many-arguments
    # pylint: disable=too-many-positional-arguments

    def __init__(self, ctxt, trace, completer, pair_separator, value_separator, values):
        trace.append('key_value_list')
        spec = []

        for key, desc, complete in values:
            key = escape_colon(key)

            if desc:
                desc = shell.quote('[%s]' % escape_square_brackets(desc))
            else:
                desc = ''

            if not complete:
                spec.append(f'{key}{desc}')
            elif complete[0] == 'none':
                spec.append(f'{key}{desc}:::')
            else:
                compl_obj = completer.complete_from_def(ctxt, trace, complete)
                action = compl_obj.get_action_string()
                spec.append(f'{key}{desc}:::{action}')

        code = '_values -s %s -S %s %s \\\n%s' % (
            shell.quote(pair_separator),
            shell.quote(value_separator),
            shell.quote(ctxt.option.metavar or ''),
            indent(' \\\n'.join(spec), 2)
        )

        func = ctxt.helpers.add_dynamic_func(ctxt, code)

        super().__init__(ctxt, [func], needs_braces=True)


class ZshCompleteCombine(ZshCompletionBase):
    '''Combine multiple completers into one.'''

    def __init__(self, ctxt, trace, completer, commands):
        trace.append('combine')

        completions = []
        for command_args in commands:
            compl_obj = completer.complete_from_def(ctxt, trace, command_args)
            action_string = compl_obj.get_action_string()
            completions.append(f'::{action_string}')

        code = '_alternative \\\n'
        code += indent(' \\\n'.join(completions), 2)

        self.func = ctxt.helpers.add_dynamic_func(ctxt, code)

    def get_function(self):
        return self.func

    def get_action_string(self):
        return shell.quote('{%s}' % self.func)


class ZshCompleter(shell.ShellCompleter):
    '''Code generator for completing arguments in Zsh.'''

    # pylint: disable=too-many-public-methods
    # pylint: disable=missing-function-docstring
    # pylint: disable=too-many-arguments
    # pylint: disable=too-many-positional-arguments

    def none(self, ctxt, _trace, *_):
        return ZshComplFunc(ctxt, [' '])

    def integer(self, ctxt, _trace, options=None):
        args = []
        suffixes = []
        help_ = ctxt.option.help or ctxt.option.metavar or ''

        if options:
            if 'min' in options:
                args += ['-l', str(options['min'])]
            if 'max' in options:
                args += ['-m', str(options['max'])]
            if 'suffixes' in options:
                for suffix, description in options['suffixes'].items():
                    suffixes.append(f'{suffix}:{description}')
            if 'help' in options:
                help_ = options['help']

        return ZshComplFunc(ctxt, ['_numbers', *args, help_, *suffixes])

    def float(self, ctxt, _trace, options=None):
        args = ['-f']
        suffixes = []
        help_ = ctxt.option.help or ctxt.option.metavar or ''

        if options:
            if 'min' in options:
                args += ['-l', str(options['min'])]
            if 'max' in options:
                args += ['-m', str(options['max'])]
            if 'suffixes' in options:
                for suffix, description in options['suffixes'].items():
                    suffixes.append(f'{suffix}:{description}')
            if 'help' in options:
                help_ = options['help']

        return ZshComplFunc(ctxt, ['_numbers', *args, help_, *suffixes])

    def choices(self, ctxt, trace, choices):
        return ZshCompleteChoices(ctxt, trace, choices)

    def command(self, ctxt, _trace, opts=None):
        return ZshCompleteCommand(ctxt, opts)

    def directory(self, ctxt, _trace, opts=None):
        directory = None if opts is None else opts.get('directory', None)

        if not directory:
            return ZshComplFunc(ctxt, ['_directories'])

        if directory.startswith('/'):
            return ZshComplFunc(ctxt, ['_directories', '-W', directory])

        func = ctxt.helpers.use_function('path_files_relative')
        return ZshComplFunc(ctxt, [func, directory, '-/'], needs_braces=True)

    def file(self, ctxt, _trace, opts=None):
        fuzzy = False
        directory = None
        extensions = None
        ignore_globs = None

        if opts:
            fuzzy = opts.get('fuzzy', False)
            directory = opts.get('directory', None)
            extensions = opts.get('extensions', None)
            ignore_globs = opts.get('ignore_globs', None)

        args = []

        if extensions:
            args.extend(['-g', make_file_extension_pattern(extensions, fuzzy)])

        if ignore_globs:
            patterns = map(bash_glob_to_zsh_glob, ignore_globs)
            args.extend(['-F', '(%s)' % ' '.join(patterns)])

        if not directory:
            return ZshComplFunc(ctxt, ['_files'] + args)

        if directory.startswith('/'):
            return ZshComplFunc(ctxt, ['_files', '-W', directory, *args])

        func = ctxt.helpers.use_function('path_files_relative')
        return ZshComplFunc(ctxt, [func, directory, *args], needs_braces=True)

    def mime_file(self, ctxt, _trace, pattern):
        func = ctxt.helpers.use_function('mime_file')
        return ZshComplFunc(ctxt, [func, pattern], needs_braces=True)

    def group(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_groups'])

    def hostname(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_hosts'])

    def pid(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_pids'])

    def process(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_process_names', '-a'])

    def range(self, ctxt, _trace, start, stop, step=1):
        return ZshCompleteRange(ctxt, start, stop, step)

    def user(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_users'])

    def variable(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_vars'])

    def environment(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_parameters', '-g', '*export*'])

    def exec(self, ctxt, _trace, command):
        funcname = ctxt.helpers.use_function('exec')
        return ZshComplFunc(ctxt, [funcname, command], needs_braces=True)

    def exec_fast(self, ctxt, _trace, command):
        funcname = ctxt.helpers.use_function('exec')
        return ZshComplFunc(ctxt, [funcname, command], needs_braces=True)

    def exec_internal(self, ctxt, _trace, command):
        return ZshComplFunc(ctxt, [command], needs_braces=True)

    def value_list(self, ctxt, _trace, opts):
        desc = ctxt.option.metavar or ''
        values = opts['values']
        separator = opts.get('separator', ',')
        duplicates = opts.get('duplicates', False)

        if is_dict_type(values):
            esc = escape_square_brackets
            values = ['%s[%s]' % (esc(item), esc(desc)) for item, desc in values.items()]
        else:
            values = [escape_square_brackets(i) for i in values]

        if not duplicates:
            return ZshComplFunc(ctxt, ['_values', '-s', separator, desc] + values)

        values_func = ZshComplFunc(ctxt, ['_values', desc] + values).get_function()

        if separator == ',':
            return ZshComplFunc(ctxt, ['_sequence', '-d', values_func])

        return ZshComplFunc(ctxt, ['_sequence', '-s', separator, '-d', values_func])

    def key_value_list(self, ctxt, trace, pair_separator, value_separator, values):
        return ZshKeyValueList(ctxt, trace, self, pair_separator, value_separator, values)

    def combine(self, ctxt, trace, commands):
        return ZshCompleteCombine(ctxt, trace, self, commands)

    def list(self, ctxt, trace, command, opts=None):
        separator = opts.get('separator', ',') if opts else ','
        duplicates = opts.get('duplicates', False) if opts else False
        trace.append('list')

        obj = self.complete_from_def(ctxt, trace, command)
        func = obj.get_function()

        args = []

        if separator != ',':
            args.extend(['-s', separator])

        if duplicates:
            args.append('-d')

        return ZshComplFunc(ctxt, ['_sequence', *args, func])

    def history(self, ctxt, _trace, pattern):
        func = ctxt.helpers.use_function('history')
        return ZshComplFunc(ctxt, [func, pattern], needs_braces=True)

    def commandline_string(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_cmdstring'])

    def command_arg(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_normal'])

    def date(self, ctxt, _trace, format_):
        return ZshComplFunc(ctxt, ['_dates', '-f', format_])

    def date_format(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_date_formats'])

    def uid(self, ctxt, _trace):
        func = ctxt.helpers.use_function('uid_list')
        return ZshComplFunc(ctxt, [func], needs_braces=True)

    def gid(self, ctxt, _trace):
        func = ctxt.helpers.use_function('gid_list')
        return ZshComplFunc(ctxt, [func], needs_braces=True)

    def filesystem_type(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_file_systems'])

    def signal(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_signals'])

    def prefix(self, ctxt, trace, prefix, command):
        prefix_func = ctxt.helpers.use_function('prefix')
        obj = self.complete_from_def(ctxt, trace, command)
        func = obj.get_function()
        return ZshComplFunc(ctxt, [prefix_func, prefix, func], needs_braces=True)

    def ip_address(self, ctxt, _trace, type_='all'):
        if type_ == 'ipv4':
            return ZshComplFunc(ctxt, ['_bind_addresses', '-4'])

        if type_ == 'ipv6':
            return ZshComplFunc(ctxt, ['_bind_addresses', '-6'])

        return ZshComplFunc(ctxt, ['_bind_addresses'])

    # =========================================================================
    # Bonus
    # =========================================================================

    def mountpoint(self, ctxt, _trace):
        func = ctxt.helpers.use_function('mountpoint')
        return ZshComplFunc(ctxt, [func], needs_braces=True)

    def net_interface(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_net_interfaces'])

    def timezone(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_time_zone'])

    def locale(self, ctxt, _trace):
        return ZshComplFunc(ctxt, ['_locales'])

    def charset(self, ctxt, _trace):
        func = ctxt.helpers.use_function('charset_list')
        return ZshComplFunc(ctxt, [func], needs_braces=True)

    def alsa_card(self, ctxt, _trace):
        func = ctxt.helpers.use_function('alsa_complete_cards')
        return ZshComplFunc(ctxt, [func], needs_braces=True)

    def alsa_device(self, ctxt, _trace):
        func = ctxt.helpers.use_function('alsa_complete_devices')
        return ZshComplFunc(ctxt, [func], needs_braces=True)
