#!/usr/bin/python3

from __future__ import annotations
import typing

import bmo._meta
from clapp import (
    AppError,
    BaseApp,
)
from clapp.sh import Action
from clapp.cli import (
    Pattern,
    KeyValue,
    ListPos,
    LiteralPos,
    P_HELP,
)
from saturnin.context import MetafileConfigRepo, get_config_repo
import neaty.log as LOG


IdxT = typing.Union[int, str]
AttrsT = typing.Dict[str, typing.Any]


def cut_line(line, delim: str, fspec: str) -> str:
    """
    Cut *line* like cut(1) with *delim* and return fields *fspec*
    """

    class Rng:

        @classmethod
        def from_spec(cls, spec: str) -> Rng:
            def idx(S: str) -> IdxT:
                if S:
                    return int(S)
                return '*'
            if '-' not in spec:
                # just a number N means the same as N-N
                return cls.from_spec(f'{spec}-{spec}')
            raw_bgn, raw_end = spec.split('-', maxsplit=1)
            return cls(
                bgn=idx(raw_bgn),
                end=idx(raw_end),
            )

        def __init__(self, bgn: IdxT, end: IdxT):
            self.bgn = bgn
            self.end = end

        def select_from(self, fields: list[str]) -> list[str]:
            """
            Select from *fields* based on own range
            """
            if self.bgn == '*' and self.end == '*':
                return fields[:]
            if self.bgn == '*':
                assert isinstance(self.end, int)    # for mypy
                return fields[:self.end]
            if self.end == '*':
                assert isinstance(self.bgn, int)    # for mypy
                return fields[self.bgn - 1:]
            assert isinstance(self.bgn, int)        # for mypy
            assert isinstance(self.end, int)        # for mypy
            return fields[self.bgn - 1:self.end]

    def parse_fspec(fspec: str) -> typing.Iterator[Rng]:
        """
        Return set of normalized ranges
        """
        for range_spec in fspec.split(','):
            if not range_spec:
                continue
            yield Rng.from_spec(range_spec)

    fields = line.split(delim)
    selected = []
    for rng in parse_fspec(fspec):
        selected += rng.select_from(fields)
    return delim.join(selected)


def maybe_split(params: dict, dmenu_out: str) -> str:
    """
    Split selected output if required
    """
    if not params['split']:
        return dmenu_out
    return cut_line(
        line=dmenu_out,
        delim=params['split_delim'],
        fspec=params['split_field'],
    )


def run_dmenu(params: dict) -> str:
    """
    Compose dmenu command
    """
    def maybe_argpair(aname, key):
        """Return list of two argument pairs if the value is configured"""
        if not params.get(key):
            return []
        return [aname, str(params[key])]
    cmd = ['dmenu', '-b', '-i']
    cmd.extend(maybe_argpair('-l', 'lines'))
    cmd.extend(maybe_argpair('-nb', 'nbg'))
    cmd.extend(maybe_argpair('-nf', 'nfg'))
    cmd.extend(maybe_argpair('-sb', 'sbg'))
    cmd.extend(maybe_argpair('-sf', 'sfg'))
    cmd.extend(maybe_argpair('-fn', 'font'))
    cmd.extend(params['extra_args'])
    LOG.warn(f'run_dmenu():cmd={cmd!r}')
    result = Action.from_cmd(cmd=cmd, name='menu').run()
    if not result.is_success():
        raise AppError("menu command returned non-zero status")
    return result.out


def run_a(params: dict, app: App) -> int:
    dmenu_out = run_dmenu(params)
    print(
        maybe_split(
            params=params,
            dmenu_out=dmenu_out.rstrip('\n'),
        )
    )
    return 0


class App(BaseApp):

    version: str = bmo._meta.VERSION

    name: str = 'bmo dmenu'

    DEFAULT_OPTIONS: AttrsT = {
        'lines': 10,            # number of lines to display
        'split': False,         # splitting function
        'split_delim': ' ',     # splitting delimiter
        'split_field': '2-',    # splitting fields
        'font': 'font:--*-terminus-medium-r-*-*-16-*-*-*-*-*-*-*',
        'clapp.action': run_a,
    }

    default_action_cb = run_a

    @property
    def arg_scheme(self) -> Pattern:
        options = {
            'lines': KeyValue('-l|--lines', factory=int),
            'split_delim': KeyValue('--split', trigger={'split': True}),
            'split_field': KeyValue('--split-field', trigger={'split': True}),
        }
        S = Pattern()
        S.add_pattern(P_HELP)
        S.add_pattern(Pattern(
            options=options,
            trigger={'clapp.action': run_a},
            positionals=[
                ('_', LiteralPos('--')),
                ('extra_args', ListPos()),
            ],
        ))
        S.add_pattern(Pattern(
            options=options,
            trigger={'clapp.action': run_a},
            positionals=[
                ('extra_args', ListPos()),
            ],
        ))
        return S

    def _load_config(self) -> AttrsT:
        """
        Return dictionary with loaded configuration
        """
        def get01(data: dict[str, list[str]],
                  key: str,
                  ) -> typing.Optional[str]:
            found = data.get(key, [])
            if not found:
                return None
            return found[0].strip()
        config_repo = get_config_repo('style.ini')
        data = config_repo.raw_data(prefix='style')
        return {
            'nbg': get01(data, 'color.nbg'),
            'nfg': get01(data, 'color.nfg'),
            'sbg': get01(data, 'color.sbg'),
            'sfg': get01(data, 'color.sfg'),
            'font': get01(data, 'font'),
        }

    def init_params(self, from_args: AttrsT) -> AttrsT:
        out = self.DEFAULT_OPTIONS.copy()
        out.update(self._load_config())
        out.update(from_args)
        return out

    @property
    def usage_patterns(self) -> list[str]:
        return ['[options] [-- DMENU_ARGS]']

    @property
    def help_lines(self) -> list[str]:
        return self.base_usage + [
            "",
            "options:",
            "  -l|--lines NUM       limit displayed items to NUM lines",
            "  --split CHAR         split on CHAR character before passing",
            "  --split-field RANGE  select RANGE (see cut(1)) before passing",
            "",
            "DMENU_ARGS will be passed to dmenu (beware; quoting may",
            "not work properly).",
        ]


if __name__ == '__main__':
    App.main()
