# This file is part of beets.
# Copyright 2015, winters jean-marie.
# Copyright 2020, Justin Mayer <https://justinmayer.com>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

"""This plugin generates tab completions for Beets commands for the Fish shell
<https://fishshell.com/>, including completions for Beets commands, plugin
commands, and option flags. Also generated are completions for all the album
and track fields, suggesting for example `genre:` or `album:` when querying the
Beets database. Completions for the *values* of those fields are not generated
by default but can be added via the `-e` / `--extravalues` flag. For example:
`beet fish -e genre -e albumartist`
"""


from beets.plugins import BeetsPlugin
from beets import library, ui
from beets.ui import commands
from operator import attrgetter
import os
BL_NEED2 = """complete -c beet -n '__fish_beet_needs_command' {} {}\n"""
BL_USE3 = """complete -c beet -n '__fish_beet_using_command {}' {} {}\n"""
BL_SUBS = """complete -c beet -n '__fish_at_level {} ""' {}  {}\n"""
BL_EXTRA3 = """complete -c beet -n '__fish_beet_use_extra {}' {} {}\n"""

HEAD = '''
function __fish_beet_needs_command
    set cmd (commandline -opc)
    if test (count $cmd) -eq 1
        return 0
    end
    return 1
end

function __fish_beet_using_command
    set cmd (commandline -opc)
    set needle (count $cmd)
    if test $needle -gt 1
        if begin test $argv[1] = $cmd[2];
            and not contains -- $cmd[$needle] $FIELDS; end
                return 0
        end
    end
    return 1
end

function __fish_beet_use_extra
    set cmd (commandline -opc)
    set needle (count $cmd)
    if test $argv[2]  = $cmd[$needle]
        return 0
    end
    return 1
end
'''


class FishPlugin(BeetsPlugin):

    def commands(self):
        cmd = ui.Subcommand('fish', help='generate Fish shell tab completions')
        cmd.func = self.run
        cmd.parser.add_option('-f', '--noFields', action='store_true',
                              default=False,
                              help='omit album/track field completions')
        cmd.parser.add_option(
            '-e',
            '--extravalues',
            action='append',
            type='choice',
            choices=library.Item.all_keys() +
            library.Album.all_keys(),
            help='include specified field *values* in completions')
        return [cmd]

    def run(self, lib, opts, args):
        # Gather the commands from Beets core and its plugins.
        # Collect the album and track fields.
        # If specified, also collect the values for these fields.
        # Make a giant string of all the above, formatted in a way that
        # allows Fish to do tab completion for the `beet` command.
        home_dir = os.path.expanduser("~")
        completion_dir = os.path.join(home_dir, '.config/fish/completions')
        try:
            os.makedirs(completion_dir)
        except OSError:
            if not os.path.isdir(completion_dir):
                raise
        completion_file_path = os.path.join(completion_dir, 'beet.fish')
        nobasicfields = opts.noFields  # Do not complete for album/track fields
        extravalues = opts.extravalues  # e.g., Also complete artists names
        beetcmds = sorted(
            (commands.default_commands +
             commands.plugins.commands()),
            key=attrgetter('name'))
        fields = sorted(set(
            library.Album.all_keys() + library.Item.all_keys()))
        # Collect commands, their aliases, and their help text
        cmd_names_help = []
        for cmd in beetcmds:
            names = list(cmd.aliases)
            names.append(cmd.name)
            for name in names:
                cmd_names_help.append((name, cmd.help))
        # Concatenate the string
        totstring = HEAD + "\n"
        totstring += get_cmds_list([name[0] for name in cmd_names_help])
        totstring += '' if nobasicfields else get_standard_fields(fields)
        totstring += get_extravalues(lib, extravalues) if extravalues else ''
        totstring += "\n" + "# ====== {} =====".format(
            "setup basic beet completion") + "\n" * 2
        totstring += get_basic_beet_options()
        totstring += "\n" + "# ====== {} =====".format(
            "setup field completion for subcommands") + "\n"
        totstring += get_subcommands(
            cmd_names_help, nobasicfields, extravalues)
        # Set up completion for all the command options
        totstring += get_all_commands(beetcmds)

        with open(completion_file_path, 'w') as fish_file:
            fish_file.write(totstring)


def _escape(name):
    # Escape ? in fish
    if name == "?":
        name = "\\" + name
    return name


def get_cmds_list(cmds_names):
    # Make a list of all Beets core & plugin commands
    substr = ''
    substr += (
        "set CMDS " + " ".join(cmds_names) + ("\n" * 2)
    )
    return substr


def get_standard_fields(fields):
    # Make a list of album/track fields and append with ':'
    fields = (field + ":" for field in fields)
    substr = ''
    substr += (
        "set FIELDS " + " ".join(fields) + ("\n" * 2)
    )
    return substr


def get_extravalues(lib, extravalues):
    # Make a list of all values from an album/track field.
    # 'beet ls albumartist: <TAB>' yields completions for ABBA, Beatles, etc.
    word = ''
    values_set = get_set_of_values_for_field(lib, extravalues)
    for fld in extravalues:
        extraname = fld.upper() + 'S'
        word += (
            "set  " + extraname + " " + " ".join(sorted(values_set[fld]))
            + ("\n" * 2)
        )
    return word


def get_set_of_values_for_field(lib, fields):
    # Get unique values from a specified album/track field
    fields_dict = {}
    for each in fields:
        fields_dict[each] = set()
    for item in lib.items():
        for field in fields:
            fields_dict[field].add(wrap(item[field]))
    return fields_dict


def get_basic_beet_options():
    word = (
        BL_NEED2.format("-l format-item",
                        "-f -d 'print with custom format'") +
        BL_NEED2.format("-l format-album",
                        "-f -d 'print with custom format'") +
        BL_NEED2.format("-s  l  -l library",
                        "-f -r -d 'library database file to use'") +
        BL_NEED2.format("-s  d  -l directory",
                        "-f -r -d 'destination music directory'") +
        BL_NEED2.format("-s  v  -l verbose",
                        "-f -d 'print debugging information'") +

        BL_NEED2.format("-s  c  -l config",
                        "-f -r -d 'path to configuration file'") +
        BL_NEED2.format("-s  h  -l help",
                        "-f -d 'print this help message and exit'"))
    return word


def get_subcommands(cmd_name_and_help, nobasicfields, extravalues):
    # Formatting for Fish to complete our fields/values
    word = ""
    for cmdname, cmdhelp in cmd_name_and_help:
        cmdname = _escape(cmdname)

        word += "\n" + "# ------ {} -------".format(
            "fieldsetups for  " + cmdname) + "\n"
        word += (
            BL_NEED2.format(
                ("-a " + cmdname),
                ("-f " + "-d " + wrap(clean_whitespace(cmdhelp)))))

        if nobasicfields is False:
            word += (
                BL_USE3.format(
                    cmdname,
                    ("-a " + wrap("$FIELDS")),
                    ("-f " + "-d " + wrap("fieldname"))))

        if extravalues:
            for f in extravalues:
                setvar = wrap("$" + f.upper() + "S")
                word += " ".join(BL_EXTRA3.format(
                    (cmdname + " " + f + ":"),
                    ('-f ' + '-A ' + '-a ' + setvar),
                    ('-d ' + wrap(f))).split()) + "\n"
    return word


def get_all_commands(beetcmds):
    # Formatting for Fish to complete command options
    word = ""
    for cmd in beetcmds:
        names = list(cmd.aliases)
        names.append(cmd.name)
        for name in names:
            name = _escape(name)

            word += "\n"
            word += ("\n" * 2) + "# ====== {} =====".format(
                "completions for  " + name) + "\n"

            for option in cmd.parser._get_all_options()[1:]:
                cmd_l = (" -l " + option._long_opts[0].replace('--', '')
                         )if option._long_opts else ''
                cmd_s = (" -s " + option._short_opts[0].replace('-', '')
                         ) if option._short_opts else ''
                cmd_need_arg = ' -r ' if option.nargs in [1] else ''
                cmd_helpstr = (" -d " + wrap(' '.join(option.help.split()))
                               ) if option.help else ''
                cmd_arglist = (' -a ' + wrap(" ".join(option.choices))
                               ) if option.choices else ''

                word += " ".join(BL_USE3.format(
                    name,
                    (cmd_need_arg + cmd_s + cmd_l + " -f " + cmd_arglist),
                    cmd_helpstr).split()) + "\n"

            word = (word + " ".join(BL_USE3.format(
                name,
                ("-s " + "h " + "-l " + "help" + " -f "),
                ('-d ' + wrap("print help") + "\n")
            ).split()))
    return word


def clean_whitespace(word):
    # Remove excess whitespace and tabs in a string
    return " ".join(word.split())


def wrap(word):
    # Need " or ' around strings but watch out if they're in the string
    sptoken = '\"'
    if ('"') in word and ("'") in word:
        word.replace('"', sptoken)
        return '"' + word + '"'

    tok = '"' if "'" in word else "'"
    return tok + word + tok
