#!/bin/fish

function __fish_query
  # ===========================================================================
  #
  # This function implements the parsing of options and positionals in the Fish shell.
  #
  # Usage: __fish_query <OPTIONS> <COMMAND> [ARGS...]
  #
  # The first argument is a comma-separated list of options that the parser should know about.
  # Short options (-o), long options (--option), and old-style options (-option) are supported.
  #
  # If an option takes an argument, it is suffixed by '='.
  # If an option takes an optional argument, it is suffixed by '=?'.
  #
  # For example:
  #   __fish_query '-f,--flag,-old-style,--with-arg=,--with-optional=?' [...]
  #
  #   Here, -f, --flag and -old-style don't take options, --with-arg requires an
  #   argument and --with-optional takes an optional argument.
  #
  # COMMANDS
  #   positional_contains <NUM> <WORDS...>
  #     Checks if the positional argument number NUM is one of WORDS.
  #     NUM counts from one.
  #
  #   has_option [WITH_INCOMPLETE] <OPTIONS...>
  #     Checks if an option given in OPTIONS is passed on commandline.
  #     If an option requires an argument, this command returns true only if the
  #     option includes an argument. If 'WITH_INCOMPLETE' is specified, it also
  #     returns true for options missing their arguments.
  #
  #   option_is <OPTIONS...> -- <VALUES...>
  #     Checks if any option in OPTIONS has a value of VALUES.
  #
  #   num_of_positionals [<OPERATOR> <NUMBER>]
  #     Checks the number of positional arguments.
  #     If no arguments are provided, print the total count of positional arguments.
  #     If two arguments are provided, the first argument should be one of
  #     the comparison operators: '-lt', '-le', '-eq', '-ne', '-gt', '-ge'.
  #     Returns 0 if the count of positional arguments matches the
  #     specified NUMBER according to the comparison operator, otherwise returns 1.
  #
  # ===========================================================================
  
  set -l positionals
  set -l having_options
  set -l option_values
  
#ifdef DEBUG
  switch (count $argv)
    case 0
      echo "%FUNCNAME%: missing OPTIONS argument" >&2
      return 1
    case 1
      echo "%FUNCNAME%: missing COMMAND" >&2
      return 1
  end
#endif
  
  set -l options $argv[1]
  set -e argv[1]
  
  set -l cmd $argv[1]
  set -e argv[1]
  
  set -l my_cache_key "$(commandline -b) $options"
  
  if test "$__QUERY_CACHE_KEY" = "$my_cache_key"
    set positionals    $__QUERY_CACHE_POSITIONALS
    set having_options $__QUERY_CACHE_HAVING_OPTIONS
    set option_values  $__QUERY_CACHE_OPTION_VALUES
  else
    # =========================================================================
    # Parsing of OPTIONS argument
    # =========================================================================
  
#ifdef short_options
    set -l short_opts_with_arg
    set -l short_opts_without_arg
    set -l short_opts_with_optional_arg
#endif
#ifdef long_options
    set -l long_opts_with_arg
    set -l long_opts_without_arg
    set -l long_opts_with_optional_arg
#endif
#ifdef old_options
    set -l old_opts_with_arg
    set -l old_opts_without_arg
    set -l old_opts_with_optional_arg
#endif
  
    set -l option
  
    if test -n "$options"
      for option in (string split -- ',' $options)
        # Using one big switch case is the fastest way
        switch $option
#ifdef long_options
          case '--?*=';   set -a long_opts_with_arg           (string replace -- '='  '' $option)
          case '--?*=\?'; set -a long_opts_with_optional_arg  (string replace -- '=?' '' $option)
          case '--?*';    set -a long_opts_without_arg        $option
#endif
#ifdef short_options
          case '-?=';     set -a short_opts_with_arg          (string replace -- '='  '' $option)
          case '-?=\?';   set -a short_opts_with_optional_arg (string replace -- '=?' '' $option)
          case '-?';      set -a short_opts_without_arg       $option
#endif
#ifdef old_options
          case '-??*=';   set -a old_opts_with_arg            (string replace -- '='  '' $option)
          case '-??*=\?'; set -a old_opts_with_optional_arg   (string replace -- '=?' '' $option)
          case '-??*';    set -a old_opts_without_arg         $option
#endif
#ifdef DEBUG
          case '*'
            echo "%FUNCNAME%: argv[1]: '$option' is not a short, long or old-style option" >&2
            return 1
#endif
        end
      end
    end
  
    # =========================================================================
    # Parsing of options and positionals
    # =========================================================================
  
    set -l cmdline (commandline -poc)
    set -l cmdline_count (count $cmdline)
  
    set -l argi 2 # cmdline[1] is command name
    while test $argi -le $cmdline_count
      set -l arg "$cmdline[$argi]"
      set -l have_trailing_arg (test $argi -lt $cmdline_count && echo true || echo false)
  
      switch $arg
        case '-'
          set -a positionals -
        case '--'
          for argi in (seq (math $argi + 1) $cmdline_count)
            set -a positionals $cmdline[$argi]
          end
          break
        case '--*=*'
          set -l split (string split -m 1 -- '=' $arg)
          set -a having_options $split[1]
          set -a option_values "$split[2]"
        case '--*'
#ifdef long_options
          if contains -- $arg $long_opts_with_arg
            if $have_trailing_arg
              set -a having_options $arg
              set -a option_values $cmdline[(math $argi + 1)]
              set argi (math $argi + 1)
            end
          else
            set -a having_options $arg
            set -a option_values ""
          end
#endif
        case '-*'
          set -l end_of_parsing false

#ifdef old_options
          if string match -q -- "*=*" $arg
            set -l split (string split -m 1 -- '=' $arg)
            if contains -- $split[1] $old_opts_with_arg $old_opts_with_optional_arg
              set -a having_options $split[1]
              set -a option_values "$split[2]"
              set end_of_parsing true
            end
          else if contains -- $arg $old_opts_with_arg
            set end_of_parsing true
            if $have_trailing_arg
              set -a having_options $arg
              set -a option_values $cmdline[(math $argi + 1)]
              set argi (math $argi + 1)
            end
          else if contains -- $arg $old_opts_without_arg $old_opts_with_optional_arg
            set -a having_options $arg
            set -a option_values ""
            set end_of_parsing true
          end
#endif

#ifdef short_options
          set -l arg_length (string length -- $arg)
          set -l i 2
          while not $end_of_parsing; and test $i -le $arg_length
            set -l option "-$(string sub -s $i -l 1 -- $arg)"
            set -l trailing_chars "$(string sub -s (math $i + 1) -- $arg)"

            if contains -- $option $short_opts_without_arg
              set -a having_options $option
              set -a option_values ""
            else if contains -- $option $short_opts_with_arg
              set end_of_parsing true

              if test -n "$trailing_chars"
                set -a having_options $option
                set -a option_values $trailing_chars
              else if $have_trailing_arg
                set -a having_options $option
                set -a option_values $cmdline[(math $argi + 1)]
                set argi (math $argi + 1)
              end
            else if contains -- $option $short_opts_with_optional_arg
              set end_of_parsing true
              set -a having_options $option
              set -a option_values "$trailing_chars" # may be empty
            end

            set i (math $i + 1)
          end
#endif
        case '*'
          set -a positionals $arg
      end
  
      set argi (math $argi + 1)
    end
  
    set -g __QUERY_CACHE_POSITIONALS    $positionals
    set -g __QUERY_CACHE_HAVING_OPTIONS $having_options
    set -g __QUERY_CACHE_OPTION_VALUES  $option_values
    set -g __QUERY_CACHE_KEY            $my_cache_key
  end
  
  # ===========================================================================
  # Commands
  # ===========================================================================
  
  switch $cmd
#ifdef positional_contains
    case 'positional_contains'
      if test (count $argv) -eq 0
        echo "%FUNCNAME%: positional_contains: argv[3]: missing number" >&2
        return 1
      end
  
      set -l positional_num $argv[1]
      set -e argv[1]
      contains -- $positionals[$positional_num] $argv && return 0 || return 1
#endif
#ifdef has_option
    case 'has_option'
#ifdef with_incomplete
      set -l with_incomplete false

      if test $argv[1] = "WITH_INCOMPLETE"
        set with_incomplete true
        set -e argv[1]
      end

#endif
      for option in $having_options
        contains -- $option $argv && return 0
      end

#ifdef with_incomplete
      if $with_incomplete
        set -l tokens (commandline -po)
        set -e tokens[1]
        for option in $argv
          switch $option
            case '-?'
              set option (string sub -l 1 -s 2 -- $option)
              string match -q -r -- "^-[A-z0-9]*$option\$"   $tokens[-2] && return 0
              string match -q -r -- "^-[A-z0-9]*$option.*\$" $tokens[-1] && return 0
            case '*'
              string match -q -r -- "^$option\$"       $tokens[-2] && return 0
              string match -q -r -- "^$option(=.*)?\$" $tokens[-1] && return 0
              contains -- $tokens[-1] $argv && return 0
          end
        end
      end

#endif
      return 1
#endif
#ifdef num_of_positionals
    case 'num_of_positionals'
      switch (count $argv)
        case 0
          count $positionals
        case 1
          echo "%FUNCNAME%: num_of_positionals: $argv[1]: missing operand" >&2
          return 1
        case 2
          if contains -- $argv[1] -lt -le -eq -ne -gt -ge;
            test (count $positionals) $argv[1] $argv[2] && return 0 || return 1
          else
            echo "%FUNCNAME%: num_of_positionals: $argv[1]: unknown operator" >&2
            return 1
          end
        case '*'
          echo "%FUNCNAME%: num_of_positionals: too many arguments" >&2
          return 1
      end
#endif
#ifdef option_is
    case 'option_is'
      set -l options
      set -l values
  
      for arg in $argv
        set -e argv[1]
  
        if string match -q -- -- $arg
          set values $argv
          break
        else
          set -a options $arg
        end
      end
  
      if test (count $values) -eq 0
        echo "%FUNCNAME%: missing values" >&2
        return 1
      end
  
      set -l i (count $having_options)
      while test $i -ge 1
        if contains -- $having_options[$i] $options
          if contains -- $option_values[$i] $values
            return 0
          end
        end
  
        set i (math $i - 1)
      end
  
      return 1
#endif
#ifdef DEBUG
    case '*'
      echo "%FUNCNAME%: argv[2]: invalid command" >&2
      return 1
#endif
  end
end

function __fish_complete_filedir -d "Complete files or directories"
  # Function for completing files or directories
  #
  # Options:
  #   -d|--description=DESC   The description for completed entries
  #   -c|--comp=STR           Complete STR instead of current command line argument
  #   -D|--directories        Only complete directories
  #   -C|--cd=DIR             List contents in DIR
  #
  # This function is made out of /usr/share/fish/functions/__fish_complete_directories.fish

  argparse --max-args 0 'd/description=' 'c/comp=' 'D/directories' 'C/cd=' -- $argv || return 1

  set -l comp
  set -l desc

  if set -q _flag_description[1]
    set desc $_flag_description
  else if set -g _flag_directories
    set desc 'Directory'
  end

  if set -q _flag_comp[1]
    set comp $_flag_comp
  else
    set comp (commandline -ct | string replace -r -- '^-[^=]*=' '')
  end

  if set -q _flag_cd[1]
    pushd $_flag_cd || return 1
  end

  set -l files (complete -C"'' $comp")

  if set -q _flag_cd[1]
    popd
  end

  if set -q files[1]
    if set -q _flag_directories[1]
      set files (printf "%s\n" $files | string match -r '.*/$')
    end

    printf "%s\n" $files\t"$desc"
  end
end

function simple_values
  set -l SEPARATOR $argv[1]
  set -e argv[1]
  set -l VALUES $argv

  set -l cmdline (commandline -po)
  set -l cur $cmdline[(count $cmdline)]

  set -S cmdline cur >> /tmp/trace

  set -l VALUE
  set -l c (commandline -b)

  if test (string sub -s -1 -l 1 -- $c) = ' '
    for VALUE in $VALUES
      printf "%s\n" $VALUE
    end
    return
  end

  set -l HAVING_VALUES (string split -- $SEPARATOR $cur)
  set -l REMAINING_VALUES
  set -l HAVING_VALUE

  for VALUE in $VALUES
    set -l FOUND_VALUE false

    for HAVING_VALUE in $HAVING_VALUES
      echo "test '$VALUE' = '$HAVING_VALUE'" >> /tmp/trace
      if test $VALUE = $HAVING_VALUE
        set FOUND_VALUE true
        break
      end
    end

    if ! $FOUND_VALUE
      set -a REMAINING_VALUES $VALUE
    end
  end

  if string match -q -r -- $SEPARATOR'$' $cur # TODO
    for VALUE in $REMAINING_VALUES
      printf "%s%s\n" $cur $VALUE
    end
  else if test (count $HAVING_VALUES) -gt 0
    set CUR_LAST_VALUE $HAVING_VALUES[-1]

    for VALUE in $REMAINING_VALUES
      if string match -q -- $CUR_LAST_VALUE'*' $VALUE
        printf "%s\n" $VALUE
      end
    end
  end
end

function _has_hidden_option
  set -l option
  set -l tokens (commandline -po)
  set -e tokens[1]

  for option in $argv
    switch $option
      case '-?'
        set option (string sub -l 1 -s 2 -- $option)
        string match -q -r -- "^-[A-z0-9]*$option\$"   $tokens[-2] && return 0
        string match -q -r -- "^-[A-z0-9]*$option.*\$" $tokens[-1] && return 0
      case '*'
        string match -q -r -- "^$option\$"       $tokens[-2] && return 0
        string match -q -r -- "^$option(=.*)?\$" $tokens[-1] && return 0
    end
  end

        echo not enabled >> /tmp/trace
  return 1
end

# =============================================================================
# Test code
# =============================================================================

set -l options "--arg=,-a=,-oldarg=,-optional=?,--flag,-f,-flag"

complete -c foo -e
complete -c foo -l flag -o flag -s f -f
complete -c foo -n "__fish_query '$options' num_of_positionals -eq 0" -x -a 'first'
complete -c foo -n "__fish_query '$options' positional_contains 1 first && not __fish_query '$options' has_option -a --arg -oldarg" -f -s a -l arg -o oldarg -r -a '1 2 3'
complete -c foo -n "__fish_query '$options' positional_contains 1 first && not __fish_query '$options' has_option -optional" -f -o optional -a '1 2 3'

complete -c foo -l files      -x -a '(__fish_complete_filedir)'
complete -c foo -l dirs       -x -a '(__fish_complete_filedir -D)'
complete -c foo -l temp-files -x -a '(__fish_complete_filedir -C /tmp)'
complete -c foo -l temp-dirs  -x -a '(__fish_complete_filedir -D -C /tmp)'
complete -c foo -l desc-files -x -a '(__fish_complete_filedir -d Foo)'

complete -c foo -l simple-values -x -a '(simple_values , foo bar baz)'

complete -c foo -l hidden -o hidden -s h -x -a 'one two three' -n '_has_hidden_option -h --hidden -hidden'
