
# for tab completion

typedef Completer = (Module, [String], Int) -> Candidates?

let _compMap : [String : Completer]

function _defaultHook(ctx : Module, words : [String], cursor : Int) : Candidates? {
    if (command -v $words[0] &>> /dev/null) {
        var cmd = $words[0]
        if !$cmd.contains($'\0') {
            $cmd = $cmd.basename();
            if ($OSTYPE == 'cygwin' || $OSTYPE == 'msys') && $cmd.endsWith(".exe") {
                $cmd = $cmd.slice(0, -4)
            }
        }
        $words[0] = $cmd
        var comp = $_compMap.get($cmd) ?? $_compMap.get("") ?? function(m, w, c) => $none
        return $comp($ctx, $words, $cursor)
    }
    return $none
}

# if $cmd is empty string, treat as fallback handler
function compAdd(cmd : String, comp : Completer) : Completer? {
    $COMP_HOOK = $_defaultHook
    return $_compMap.put($cmd, $comp)
}


# for completion helper

typedef _CompDef(o : String?, t : Int?, a : String?) {
    let option = $o ?? ""   # option (length 0: empty, 1: short, 2<=: long, other)
    let type = $t ?? -1     # arg type (-1: empty, 0: list, 1: cmdline, 2: action, 3: comp)
    let arg = $a ?? ""      # argument
}

let _compdefMap : [
    String :        # command name
    [ _CompDef ]
]


#
# $prefix starts with '-'
#
function _comp_opt(deflist : [_CompDef], prefix : String) : Candidates {
    var ret : Candidates
    for def in $deflist {
        var opt = case $def.option.size() {
            0 => continue
            1 => '-' + $def.option
            else => '--' + $def.option
        }
        if $opt.startsWith($prefix) {
            var long = $opt.startsWith('--') && $opt.endsWith('=')
            $ret.add($opt, $space: $long ? 0 : -1)
        } elif $opt.startsWith("--") && $def.type != -1 {
            # opt -> --opt
            # prefix -> --opt=hoge
            if $prefix.startsWith($opt) && ($prefix.slice($opt.size()).startsWith('=') || $opt.endsWith('=')) {
                $ret.add($opt)
            }
        }
    }
    return $ret
}

#
# complete arguments of specified option
# $opt is empty string or starts with '-'
#
function _comp_arg(ctx : Module, deflist: [_CompDef], opt : String, prefix : String) : Candidates? {
    for def in $deflist {
        $opt == case $def.option.size() {
            0 => ""
            1 => '-' + $def.option
            else => '--' + $def.option
        } || continue

        var arg = $def.arg
        return case $def.type {
            0 => {
                var r : Candidates
                for a in $arg.split(" ") {
                    if $a.startsWith($prefix) {
                        $r.add($a)
                    }
                }
                $r
            }
            1 => {
                var ret : Candidates
                for line in <(call $BIN_NAME -c "$arg") {
                    if $line.startsWith($prefix) {
                        $ret.add($line)
                    }
                }
                $ret
            }
            2 => {
                var actions : [String]
                for a in $arg.split("|") {
                    $actions.add("-A")
                    $actions.add($a)
                }
                if $prefix.startsWith('~') {
                    $actions.add('-A').add('tilde')
                }
                complete -q -m $ctx $actions $prefix;
                $COMPREPLY
            }
            else => break
        }
    }
    return $none
}

let _bash_completion_dirs = [
    '/etc/bash_completion.d/',
    '/usr/share/bash-completion/completions/',
    '/usr/local/etc/bash_completion.d/',
    '/usr/local/share/bash-completion/completions/',

    '/opt/homebrew/etc/bash_completion.d/',
    '/opt/homebrew/share/bash-completion/completions/',
]

function findBashCompScript(name : String) : String? {
    $name.contains($'\0') && return $none
    for dir in $_bash_completion_dirs {
        var path = $dir + $name
        test -f $path && return $path
    }
    return $none
}

let _bash_completion_paths = [
    "/usr/share/bash-completion/bash_completion",
    "/usr/local/share/bash-completion/bash_completion",
    "/opt/homebrew/share/bash-completion/bash_completion",
]

var BASH_COMPLETION_PATH = "/usr/share/bash-completion/bash_completion"
var BASH_COMPLETION_COMPAT = "/etc/bash_completion.d/000_bash_completion_compat.bash"

# Conceptually based on git-completion.tcsh 
# (https://github.com/git/git/blob/master/contrib/completion/git-completion.tcsh)
function _comp_wrap(file : String, target : String, words : [String], cursor : Int): Candidates? {
    test -f $file || return $none
    if !(test -f $BASH_COMPLETION_PATH) {
        $BASH_COMPLETION_PATH = 
            for path in $_bash_completion_paths {
                test -f $path && break $path
            } ?? return $none
    }
    if !(test -f $BASH_COMPLETION_COMPAT) {
        $BASH_COMPLETION_COMPAT = $findBashCompScript("000_bash_completion_compat.bash") ?? ""
    }

    let script = '
    complete() { true; }   # ignore `complete` function
    compopt() { true; }    # FIXME: emulate `-o option`
    BASH_COMPLETION_COMPAT_DIR=/dev/null
    BASH_COMPLETION_USER_FILE=/dev/null

    bash_completion_path="$1"
    comp_script="$2"
    comp_func="$3"
    bash_completion_compat="$4"
    COMP_CWORD="$5"
    shift 5
    COMP_WORDS=("$@")
    if [ ${#COMP_WORDS[@]} -eq $COMP_CWORD ]; then
        COMP_WORDS+=("")
    fi
    COMP_LINE="${COMP_WORDS[@]}"
    COMP_POINT=${#COMP_LINE}

    # load script and call completion function
    source "$bash_completion_path"
    if [ "x${bash_completion_compat}" != x ]; then
        source "$bash_completion_compat"   # for bash-completion v2.12
        if ! declare -f _split_longopt &>> /dev/null && declare -f _comp__split_longopt &>> /dev/null; then
            _comp_deprecate_func 2.12 _split_longopt _comp__split_longopt  # workaround for old completion script
        fi
    fi
    source "$comp_script"
    "$comp_func" "$(basename ${COMP_WORDS[0]})"

    for r in "${COMPREPLY[@]}"; do
        printf "%s\n" "$r"
    done
    '

    var ret : Candidates
    bash -c $script bash \
      $BASH_COMPLETION_PATH $file $target $BASH_COMPLETION_COMPAT $cursor $words | for line in $STDIN {
        if test -d $line && $line[-1] != '/' && $line.contains('/') { # for directory
            $line += '/'
        }
        if (test -e $line) {  # FIXME: more precise quote requirement checking
            $line = $line.quote()
        }
        var long = $line.startsWith('--') && $line.endsWith('=') # suppress automatic space insertion for `--long=`
        $ret.add($line, $space: $long ? 0 : -1)
    }
    return $ret.size() == 0 ? $none : $ret
}

# extract completion target (func, cmd). ex `complete -o default -F func cmd`
# if found, return (func, cmd)
function extractCompTarget(path : String) : (String, String)? {
    var func : String?
    var cmd : String?

    for line in $STDIN {
        var rr = $/ *complete( +.+)? +-F +([^ ]+) +([^ ]+)/.match($line)
        if let r = $rr {
            $func = $r.group(2) ?? continue
            $cmd = $r.group(3) ?? continue
            break
        }
    } with < $path
    return $func && $cmd ? ($func!, $cmd!) : $none
}

# for lazy loading bash completion script
function _comp_autoload(cmd : String) : String? {
    var path = $findBashCompScript($cmd) ?? return $none
    var target = $extractCompTarget($path)
    if let tt = $target {
        call compdef --cmd $cmd --comp-path $cmd --comp-target ${tt._0} && return $cmd
    }
    return $none
}

function _from_compdef(ctx : Module, words : [String], cursor : Int) : Candidates? {
    var cmd = $words[0]
    var deflist = $_compdefMap.get($cmd) ?? {
        $cmd = $_comp_autoload($cmd) ?? return $none
        $_compdefMap[$cmd]
    }
    if $deflist[0].type == 3 {
        return $_comp_wrap($deflist[0].option, $deflist[0].arg, $words, $cursor)
    }

    if $words.size() == $cursor {    # complete arguments or options
        # 'CMD '
        # 'CMD arg '
        # 'CMD -s '
        var prev = $words.size() == 1 ? "" : $words[-1]
        return $_comp_arg($ctx, $deflist, $prev.startsWith("-") ? $prev : "", "")
    } else {
        # 'CMD d'
        # 'CMD -'
        # 'CMD --d'
        # 'CMD' --dump=s
        # 'CMD -s d'
        # 'CMD arg1 d'
        assert $words.size() > 1
        var prev = $words[-1]
        if $prev.startsWith('-') { # 'CMD -', 'CMD --d'
            var ret = $_comp_opt($deflist, $prev)
            if $ret.size() == 1 {
                var opt = $ret[0]
                if $opt.startsWith("--") && $prev.startsWith($opt) {
                    # for `--opt=' style option
                    if $opt.endsWith('=') {  # defined as `--long opt=' form
                        $prev = $prev.slice($opt.size())
                        return $_comp_arg($ctx, $deflist, $opt, $prev)
                    }
                    if $prev.slice($opt.size()).startsWith('=') {
                        $prev = $prev.slice($opt.size() + 1)
                        return $_comp_arg($ctx, $deflist, $opt, $prev)
                    }
                }
            }
            return $ret
        }
        var opt = $words.size() == 2 ? "" : $words[-2]
        return $_comp_arg($ctx, $deflist, $opt.startsWith("-") ? $opt : "", $prev)
    }
}

[<CLI()>]
typedef _CompDefParam() {
    [<Option(required: $true, placeholder: "COMMAND", help: "set target command")>]
    var cmd = ''

    [<Option(placeholder: "SHORT_OPTION", xor: 0, help: "set short option (single character argument)")>]
    var short : String?

    [<Option(placeholder: "LONG_OPTION", xor: 0, help: "set long option (two or more characters argument)")>]
    var long: String?

    [<Option(placeholder: "LIST", xor: 1, help: "generate candidates from LIST split by ' '")>]
    var arg_list: String?

    [<Option(placeholder: "CMDLINE", xor: 1, help: "generate candidates from CMDLINE evaluation")>]
    var arg_cmd: String?

    [<Option(placeholder: "ACTION", xor: 1, help: "generate candidates from ACTION")>]
    var arg_action: String?

    [<Option(placeholder: "SCRIPT", xor: 0, help: "set bash completion script")>]
    var comp_path: String?

    [<Option(placeholder: "TARGET", xor: 1, help: "set bash completion function")>]
    var comp_target: String?
}

function _error($p : _CompDefParam, $msg : String) : Int {
    echo 1>&2 ${$p.usage($msg)}
    return 1
}

compdef(param : _CompDefParam) {
    $compAdd("", $_from_compdef)

    var opt = ""
    if let short = $param.short {
        $opt = $short
        $opt.size() == 1 || return $_error($param, "--short needs single character argument")
    }
    if let long = $param.long {
        $opt = $long
        $opt.size() > 1 || return $_error($param, "--long needs two or more characters argument")
    }
    var argType : Int?
    var arg : String?
    if var path = $param.comp_path {
        if !$path.contains('/') {  # if not full-path, resolve full-path
            $path = $findBashCompScript($path) ?? return 1  # do nothing
        }
        if let target = $param.comp_target {
            $argType = 3
            $opt = $path
            $arg = $target
        } else {
            return $_error($param, "need \`--comp-target' option")
        }
    } elif let arg_list = $param.arg_list {
        $argType = 0
        $arg = $arg_list
    } elif let arg_cmd = $param.arg_cmd {
        $argType = 1
        $arg = $arg_cmd
    } elif let arg_action = $param.arg_action {
        $argType = 2
        $arg = $arg_action
    }

    var deflist = $_compdefMap.get($param.cmd) ?? {
        var tmp : [_CompDef]
        $_compdefMap[$param.cmd] = $tmp
        $tmp
    }
    $deflist.add(new _CompDef($opt, $argType, $arg))
}

function compDelegate(ctx : Module, words : [String], cursor : Int) : Candidates? {
    return $_defaultHook($ctx, $words, $cursor)
}
