#compdef rmlint rmlint.sh -P rmlint.*.sh

# Copyright (c) 2021 Github zsh-users - http://github.com/zsh-users
#
# Permission is hereby granted, without written agreement and without
# licence or royalty fees, to use, copy, modify, and distribute this
# software and to distribute modified versions of this software for any
# purpose, provided that the above copyright notice and the following
# two paragraphs appear in all copies of this software.
#
# In no event shall the Zsh Development Group be liable to any party for
# direct, indirect, special, incidental, or consequential damages arising out
# of the use of this software and its documentation, even if the Zsh
# Development Group have been advised of the possibility of such damage.
#
# The Zsh Development Group specifically disclaim any warranties, including,
# but not limited to, the implied warranties of merchantability and fitness
# for a particular purpose.  The software provided hereunder is on an "as is"
# basis, and the Zsh Development Group have no obligation to provide
# maintenance, support, updates, enhancements, or modifications.
#
# Description
# -----------
#
# Zsh completion for rmlint 2.10.1 (https://github.com/sahib/rmlint)
#
# Authors
# -------
#
# * oxiedi (https://github.com/oxiedi)

(( $+functions[_rmlint_types] )) ||
_rmlint_types() {
  compset -P '*[+-]'
  # FIXME: all values before `-` are swallowed by `*`, which breaks deduplication of the swallowed values
  # TODO: respect `prefix-needed`
  _values -s ',' 'list [defaults]' \
    'all[enables all lint types]' \
    'defaults[enables all lint types, but nonstripped]' \
    'minimal[defaults minus emptyfiles and emptydirs]' \
    'minimaldirs[defaults minus emptyfiles, emptydirs and duplicates, but with duplicatedirs]' \
    'none[disable all lint types]' \
    '(badids bi)'{badids,bi}'[find files with bad UID, GID or both]' \
    '(badlinks bl)'{badlinks,bl}'[find bad symlinks pointing nowhere valid]' \
    '(emptydirs ed)'{emptydirs,ed}'[find empty directories]' \
    '(emptyfiles ef)'{emptyfiles,ef}'[find empty files]' \
    '(nonstripped ns)'{nonstripped,ns}'[find nonstripped binaries]' \
    '(duplicates df)'{duplicates,df}'[find duplicate files]' \
    '(duplicatedirs dd)'{duplicatedirs,dd}'[find duplicate directories (This is the same -D!)]'
}

(( $+functions[__rmlint_setup_formatter_descriptions] )) ||
__rmlint_setup_formatter_descriptions() {
  typeset -gA formatter_descriptions=(
    csv         'output all found lint as comma-separated-value list'
    sh          'output all found lint as shell script'
    json        'print a JSON-formatted dump of all found reports'
    py          'outputs a python script and a JSON document, just like the json formatter'
    uniques     'outputs all unique paths found during the run, one path per line'
    stamp       'outputs a timestamp of the time rmlint was run'
    progressbar 'shows a progressbar'
    pretty      'shows all found items in realtime nicely colored'
    summary     'shows counts of files and their respective size after the run. Also list all written output files'
    fdupes      'prints an output similar to the popular duplicate finder fdupes(1)'
  )
}

(( $+functions[_rmlint_output] )) ||
_rmlint_output() {
  local -A formatter_descriptions
  __rmlint_setup_formatter_descriptions
  if compset -P "(${(kj:|:)formatter_descriptions}):"; then
    _alternative \
      'files:file:_files' \
      'outputs:output:(stdout stderr)'
  else
    local -a outputs
    local f d
    for f d in ${(kv)formatter_descriptions}; do
      outputs+=( "$f:$d" )
    done
    _describe -t outputs 'output' outputs -S ':' -q
  fi
}

(( $+functions[_rmlint_config] )) ||
_rmlint_config() {
  local -A formatter_descriptions
  __rmlint_setup_formatter_descriptions
  unset 'formatter_descriptions['{py,pretty,summary}']'
  local -a match mbegin mend
  if compset -P "(#b)(${(kj:|:)formatter_descriptions}):"; then
    case $match[1] in
    (csv)
      _values 'option' \
        'no_header[do not write a first line describing the column headers]' \
        'unique[include unique files in the output]'
      ;;
    (sh)
      local context state state_descr line
      _values 'option' \
        'cmd[specify a user defined command to run on duplicates]: :_cmdstring' \
        'handler[define a comma separated list of handlers to try on duplicate files in that given order until one handler succeeds]:handler:->handler' \
        'link[shortcut for -c sh:handler=clone,reflink,hardlink,symlink]' \
        'hardlink[shortcut for -c sh:handler=hardlink,symlink]' \
        'symlink[shortcut for -c sh:handler=symlink]'
      case $state in
      (handler)
        _values -s ',' $state_descr \
          'clone[try to clone both files with the FIDEDUPERANGE ioctl(3p) (or BTRFS_IOC_FILE_EXTENT_SAME on older kernels)]' \
          'reflink[try to reflink the duplicate file to the original]' \
          'hardlink[replace the duplicate file with a hardlink to the original file]' \
          'symlink[tries to replace the duplicate file with a symbolic link to the original]' \
          'remove[remove the file using rm -rf]' \
          'usercmd[use the provided user defined command (-c sh:cmd=something)]'
        ;;
      esac
      ;;
    (json)
      _values 'option' \
        'unique[include unique files in the output]' \
        'no_header[print the header with metadata]:boolean [true]:(false true)' \
        'no_footer[print the footer with statistics]:boolean [true]:(false true)' \
        'oneline[print one json document per line]:boolean [false]:(true false)'
      ;;
    (uniques)
      _values 'option' \
        'print0[do not put newlines between paths but zero bytes]'
      ;;
    (stamp)
      _values 'option' \
        'iso8601[write an ISO8601 formatted timestamps or seconds since epoch]:boolean:(true false)'
      ;;
    (progressbar)
      _values 'option' \
        'update_interval[number of milliseconds to wait between updates (default: 50)]: :_guard "[0-9]#" "update interval (milliseconds) [50]"' \
        'ascii[do not attempt to use unicode characters, which might not be supported by some terminals]' \
        'fancy[use a more fancy style for the progressbar]'
      ;;
    (fdupes)
      _values 'option' \
        'omitfirst[omits the first line of each set of duplicates]' \
        'sameline[does not print newlines between files, only a space]'
      ;;
    esac
  else
    local -a formatters
    local f d
    for f d in ${(kv)formatter_descriptions}; do
      formatters+=( "$f:$d" )
    done
    _describe -t formatters 'formatter' formatters -S ':'
  fi
}

(( $+functions[_rmlint_algorithm] )) ||
_rmlint_algorithm() {
  local -a tmp=( sha{1,256,512} sha3-{256,384,512} blake{2s,2b,2sp,2bp} highway{64,128,256} )
  _alternative \
    '512bit-algorithms:512-bit:(blake2b blake2bp sha3-512 sha512)' \
    '384bit-algorithms:384-bit:(sha3-384)' \
    '256bit-algorithms:256-bit:(blake2s blake2sp sha3-256 sha256 highway256 metro256 metrocrc256)' \
    '160bit-algorithms:160-bit:(sha1)' \
    '128bit-algorithms:128-bit:(md5 murmur metro metrocrc)' \
    '64bit-algorithms:64-bit:(highway64 xxhash)' \
    "cryptographic-algorithms:cryptographic:($tmp)" \
    'non-cryptographic-algorithms:non-cryptographic:(metro metro256 xxhash murmur)' \
    'not-useful-algorithms:not-useful:(cumulative paranoid ext)'
}

(( $+functions[_rmlint_sort] )) ||
_rmlint_sort() {
  local -A letter_descriptions=(
    s 'sort by size of group'
    a 'sort alphabetically by the basename of the original'
    m 'sort by mtime of the original'
    p 'sort by path-index of the original'
    o 'sort by natural found order (might be different on each run)'
    n 'sort by number of files in the group'
  )
  local -a letters
  local l d
  for l d in ${(kv)letter_descriptions}; do
    letters+=( "${l}[$d]" "${(U)l}[$d (in reverse order)]" )
  done
  _values -s '' 'order' $letters
}

(( $+functions[__rmlint_describe_multipliers] )) ||
__rmlint_describe_multipliers() {
  local -a multipliers=(
    'C:1^1'
    'W:2^1'
    'B:512^1'
    'K:1000^1'
    'KB:1024^1'
    'M:1000^2'
    'MB:1024^2'
    'G:1000^3'
    'GB:1024^3'
    'T:1000^4'
    'TB:1024^4'
    'P:1000^5'
    'PB:1024^5'
    'E:1000^6'
    'EB:1024^6'
  )
  _describe -t multiplier 'multiplier' multipliers "$@"
}

(( $+functions[__rmlint_multipliers] )) ||
__rmlint_multipliers() {
  compset -P '[0-9]##' || return
  __rmlint_describe_multipliers "$@"
}

(( $+functions[_rmlint_size] )) ||
_rmlint_size() {
  ! __rmlint_multipliers && [[ -z $PREFIX ]] && _message -e "${1:-size}"
}

(( $+functions[_rmlint_size_range] )) ||
_rmlint_size_range() {
  if compset -P '[0-9]##'; then
    if compset -P '(C|W|B|K|KB|M|MB|G|GB|T|TB|P|PB|E|EB)-'; then
      _rmlint_size 'max'
    else
      __rmlint_describe_multipliers -S '-' -q
    fi
  elif [[ -z $PREFIX ]]; then
    _message -e 'min'
  fi
}

(( $+functions[_rmlint_iso8601_or_unix_timestamp] )) ||
_rmlint_iso8601_or_unix_timestamp() {
  _alternative \
    'dates:iso8601_timestamp: _dates -f "%FT%T"' \
    'seconds:unix_timestamp:_guard "[0-9]#" "seconds since epoch"'
}

(( $+functions[_rmlint_rank] )) ||
_rmlint_rank() {
  # TODO: {r,R,x,X}<regex>
  _values -s '' 'criteria [pOma]' \
    'm[keep lowest mtime (oldest)]'             'M[keep highest mtime (newest)]' \
    'a[keep first alphabetically]'              'A[keep last alphabetically]' \
    'p[keep first named path]'                  'P[keep last named path]' \
    'd[keep path with lowest depth]'            'D[keep path with highest depth]' \
    'l[keep path with shortest basename]'       'L[keep path with longest basename]' \
    'r[keep paths matching regex]'              'R[keep path not matching regex]' \
    'x[keep basenames matching regex]'          'X[keep basenames not matching regex]' \
    'h[keep file with lowest hardlink count]'   'H[keep file with highest hardlink count]' \
    'o[keep file with lowest number of hardlinks outside of the paths traversed by rmlint]' \
    'O[keep file with highest number of hardlinks outside of the paths traversed by rmlint]'
}

(( $+functions[_rmlint_percent] )) ||
_rmlint_percent() {
  if compset -P '(100|[1-9][0-9]|[1-9])'; then
    compadd "$@" -- '%'
  elif [[ -z $PREFIX ]]; then
    _message -e 'percent%%'
  fi
}

(( $+functions[_rmlint_clamp] )) ||
_rmlint_clamp() {
  _alternative \
    "factor: :_guard '((|0)(|.[0-9]#)|1(|.0#))' 'fac.tor [$1]'" \
    'percent:percent%%:_rmlint_percent' \
    'multiplier:offset: _rmlint_size "offset"'
}

(( $+functions[_rmlint_files_or_separator] )) ||
_rmlint_files_or_separator() {
  if (( $words[(i)-] < CURRENT )); then
    [[ -z $words[CURRENT] ]] && compadd "$@" -S '' -- -
    return
  fi
  local -a alts=( 'files:file:_files' )
  (( $line[(I)//] || $+opt_args[--equal] )) || alts+=( 'separator:separator:(//)' )
  _alternative $alts
}

_rmlint() {
  if [[ $service = *.sh ]]; then
    _arguments -s : \
      '(-)-h[show help message]' \
      '-d[do not ask before running]' \
      '-x[keep rmlint.sh; do not autodelete it]' \
      '-p[recheck that files are still identical before removing duplicates]' \
      '-r[allow deduplication of files on read-only btrfs snapshots (requires sudo)]' \
      '(-d -x)-n[do not perform any modifications, just print what would be done (implies -d and -x)]' \
      '-c[clean up empty directories while deleting duplicates]' \
      '-q[do not show progress]' \
      '-k[keep the timestamp of directories when removing duplicates]' \
      '-i[ask before deleting each file]'
    return
  fi

  local curcontext="$curcontext" state state_descr
  local -a line
  local -i ret=1
  typeset -A opt_args

  _arguments -s -w -C : \
    '(-T --types)'{-T,--types}'=[configure the types of lint rmlint will look for]: :_rmlint_types' \
    '*'{-o,--output}'=[configure the way rmlint outputs its results]:spec:_rmlint_output' \
    '*'{-O,--add-output}'=[configure the way rmlint outputs its results (preserve defaults)]:spec:_rmlint_output' \
    '*'{-c,--config}'=[configure a format]:spec:_rmlint_config' \
    '(-z --perms)'{-z,--perms}'=[only look into file if it is readable, writable or executable by the current user]: :_values -s "" perms r w x' \
    '(-a --algorithm)'{-a,--algorithm}'=[choose the algorithm to use for finding duplicate files]:algo:_rmlint_algorithm' \
    '*'{-p,--paranoid}'[increase the paranoia of rmlint'\''s duplicate algorithm]' \
    '*'{-P,--less-paranoid}'[decrease the paranoia of rmlint'\''s duplicate algorithm]' \
    '*'{-v,--loud}'[increase the verbosity]' \
    '*'{-V,--quiet}'[decrease the verbosity]' \
    '(-g --progress)'{-g,--progress}'[show a progressbar with sane defaults]' \
    '(-G --no-progress)'{-G,--no-progress}'[do not show a progressbar with sane defaults (default)]' \
    '(-D --merge-directories)'{-D,--merge-directories}'[makes rmlint use a special mode where all found duplicates are collected and checked if whole directory trees are duplicates]' \
    '(-j --honour-dir-layout)'{-j,--honour-dir-layout}'[only recognize directories as duplicates that have the same path layout]' \
    '(-y --sort-by)'{-y,--sort-by}'=[during output, sort the found duplicate groups by criteria described by order]:order:_rmlint_sort' \
    '(-w --with-color)'{-w,--with-color}'[use color escapes for pretty output (default)]' \
    '(-W --no-with-color)'{-W,--no-with-color}'[disable color escapes for pretty output]' \
    '(- *)'{-h,--help}'[show a shorter reference help text]' \
    '(- *)--help-all[show all help options]' \
    '(- *)'{-H,--show-man}'[show the full man page]' \
    '(- *)--version[print the version of rmlint]' \
    '(-s --size)'{-s,--size}'=[only consider files as duplicates in a certain size range]:range:_rmlint_size_range' \
    '(-d --max-depth)'{-d,--max-depth}'=[only recurse up to this depth]: :_guard "[0-9]#" "depth"' \
    '(-l --hardlinked)'{-l,--hardlinked}'[hardlinked files are treated as duplicates (default)]' \
    '--keep-hardlinked[rmlint will not delete any files that are hardlinked to an original in their respective group]' \
    '(-L --no-hardlinked)'{-L,--no-hardlinked}'[only one file (of a set of hardlinked files) is considered, all the others are ignored]' \
    '(-f --followlinks)'{-f,--followlinks}'[follow symbolic links]' \
    '(-F --no-followlinks)'{-F,--no-followlinks}'[ignore symbolic links completely]' \
    '(-@ --see-symlinks)'{-@,--see-symlinks}'[see symlinks and treats them like small files with the path to their target in them (default)]' \
    '(-x --no-crossdev)'{-x,--no-crossdev}'[stay always on the same device]' \
    '(-X --crossdev)'{-X,--crossdev}'[allow crossing mountpoints (default)]' \
    '(-r --hidden)'{-r,--hidden}'[traverse hidden directories]' \
    '(-R --no-hidden)'{-R,--no-hidden}'[don'\''t traverse hidden directories (default)]' \
    '--partial-hidden[traverse duplicate hidden directories]' \
    '(-b --match-basename)'{-b,--match-basename}'[only consider those files as dupes that have the same basename]' \
    '(-B --unmatched-basename)'{-B,--unmatched-basename}'[only consider those files as dupes that do not share the same basename]' \
    '(-e --match-with-extension)'{-e,--match-with-extension}'[only consider those files as dupes that have the same file extension]' \
    '(-E --no-match-with-extension)'{-E,--no-match-with-extension}'[don'\'t' consider those files as dupes that have the same file extension (default)]' \
    '(-i --match-without-extension)'{-i,--match-without-extension}'[only consider those files as dupes that have the same basename minus the file extension]' \
    '(-I --no-match-without-extension)'{-I,--no-match-without-extension}'[don'\'t' consider those files as dupes that have the same basename minus the file extension (default)]' \
    '(-n --newer-than-stamp)'{-n,--newer-than-stamp}'=[only consider files (and their size siblings for duplicates) newer than a certain modification time (mtime)]:timestamp_filename:_files' \
    '(-N --newer-than)'{-N,--newer-than}'=[don'\'t' consider files (and their size siblings for duplicates) newer than a certain modification time (mtime)]: :_rmlint_iso8601_or_unix_timestamp' \
    '(-k --keep-all-tagged)'{-k,--keep-all-tagged}'[don'\''t delete any duplicates that are in tagged paths]' \
    '(-K --keep-all-untagged)'{-K,--keep-all-untagged}'[don'\''t delete any duplicates that are in non-tagged paths]' \
    '(-m --must-match-tagged)'{-m,--must-match-tagged}'[only look for duplicates of which at least one is in one of the tagged paths]' \
    '(-M --must-match-untagged)'{-M,--must-match-untagged}'[only look for duplicates of which at least one is in one of the non-tagged paths]' \
    '(-S --rank-by)'{-S,--rank-by}'=[sort the files in a group of duplicates into originals and duplicates by one or more criteria]: :_rmlint_rank' \
    '--replay[read an existing json file and re-output it]' \
    '(-C --xattr)'{-C,--xattr}'[shortcut for --xattr-read, --xattr-write, --write-unfinished]' \
    '--xattr-read[read cached checksums from the extended file attributes]' \
    '--xattr-write[write cached checksums from the extended file attributes]' \
    '--xattr-clear[clear cached checksums from the extended file attributes]' \
    '(-U --write-unfinished)'{-U,--write-unfinished}'[include files in output that have not been hashed fully, i.e. files that do not appear to have a duplicate]' \
    '(-t --threads)'{-t,--threads}'=[the number of threads to use during file tree traversal and hashing (default: 16)]: :_guard "[0-9]#" "threads [16]"' \
    '(-u --limit-mem)'{-u,--limit-mem}'=[apply a maximum number of memory to use for hashing and --paranoid]:size: _rmlint_size' \
    '(-q --clamp-low)'{-q,--clamp-low}'=[only look at the content of files in the range of from low to (including) high (default: 0)]: : _rmlint_clamp 0' \
    '(-Q --clamp-top)'{-Q,--clamp-top}'=[only look at the content of files in the range of from low to (including) high (default: 1.0)]: : _rmlint_clamp 1.0' \
    '(-Z --mtime-window)'{-Z,--mtime-window}'=[only consider those files as duplicates that have the same content and the same modification time (mtime) within a certain window of T seconds (default: -1)]: :_guard "[0-9]#" "mtime window (seconds) [-1]"' \
    '--with-fiemap[enable reading the file extents on rotational disk in order to optimize disk access patterns (default)]' \
    '--without-fiemap[disable reading the file extents on rotational disk in order to optimize disk access patterns]' \
    '--gui[start the optional graphical frontend to rmlint called Shredder]:*: :->gui' \
    '--hash[make rmlint work as a multi-threaded file hash utility]:*: :->hash' \
    '--equal[check if the paths given on the commandline all have equal content]: :_rmlint_files_or_separator' \
    '(-0 --stdin0)'{-0,--stdin0}'[read null-separated file list from stdin]' \
    '--backup[do create backups of previous result files]' \
    '--no-backup[do not create backups of previous result files]' \
    '--dedupe[dedupe matching extents from source to dest (if filesystem supports)]:*:: := ->dedupe' \
    '--dedupe-xattr[check extended attributes to see if the file is already deduplicated]' \
    '--dedupe-readonly[(--dedupe option) even dedupe read-only snapshots (needs root)]' \
    '--is-reflink[test if two files are reflinks (share same data extents)]:*:: := ->reflink' \
    '*: :_rmlint_files_or_separator' && return

  case $state in
  (gui)
    _arguments -s -w : \
      '(- *)'{-h,--help}'[show help options]' \
      {-a,--add-location}'[add locations to locations view]' \
      {-s,--scan}'[add location to scan (as untagged path)]' \
      {-S,--scan-tagged}'[add location to scan (as tagged path)]' \
      {-l,--load-script}'[show `script` in editor view]' \
      '*'{-v,--verbose}'[be more verbose]' \
      '*'{-V,--less-verbose}'[be less verbose]' \
      {-c,--show-settings}'[show the settings view]' \
      '(- *)--version[show the version of Shredder]' && ret=0
    ;;
  (hash)
    _arguments -s -w : \
      '(- *)'{-h,--help}'[show help options]' \
      {-a,--algorithm}'[digest type \[bLAKE2B\]]:type:_rmlint_algorithm' \
      {-t,--num-threads}'[number of hashing threads \[8\]]: :_guard "[0-9]#" "threads [8]"' \
      {-b,--buffer-mbytes}'[megabytes read buffer \[256 MB\]]: :_guard "[0-9]#" "buffer (MB) [256]"' \
      {-i,--ignore-order}'[print hashes in order completed, not in order entered (reduces memory usage)]' \
      '*:file:_files' && ret=0
    ;;
  (dedupe)
    _arguments -s -w : \
      '-r[enable deduplication of read-only \[btrfs\] snapshots (requires root)]' \
      '(-V)*-v' \
      '(-v)*-V' \
      ':src:_files' \
      ':dst:_files' && ret=0
    ;;
  (reflink)
    _arguments -s -w : \
      '(-V)*-v' \
      '(-v)*-V' \
      ':file1:_files' \
      ':file2:_files' && ret=0
    ;;
  esac

  return ret
}

_rmlint "$@"
