#!/bin/sh
#------------------------------------------------------------------------------
# =========                 |
# \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
#  \\    /   O peration     | Website:  https://openfoam.org
#   \\  /    A nd           | Copyright (C) 2024 OpenFOAM Foundation
#    \\/     M anipulation  |
#------------------------------------------------------------------------------
# License
#     This file is part of OpenFOAM.
#
#     OpenFOAM is free software: you can redistribute it and/or modify it
#     under the terms of the GNU General Public License as published by
#     the Free Software Foundation, either version 3 of the License, or
#     (at your option) any later version.
#
#     OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
#     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#     FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
#     for more details.
#
#     You should have received a copy of the GNU General Public License
#     along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
#
# Script
#     foamFind
#
# Description
#     Finds one or more files in OpenFOAM and optionally processes the contents
#     by printing the file or lines matching a search string
#
#------------------------------------------------------------------------------
usage () {
    cat <<USAGE

Usage: ${0##*/} [OPTIONS] <filename>
options:
  -a | -applications     search for the file from the \$FOAM_APP directory
  -d | -dir <dir>        specify search directory
  -e | -edit             open the file in a file editor, see below for info
  -f | -files            find wmake 'files' file associated with searched file
  -h | -help             help
  -i | -isearch <string> searches files for a <string>, case insensitive
  -m | -modules          search for the file from the \$FOAM_MODULES directory
  -n | -numbers          print line numbers with file output
  -o | -options          find wmake 'options' file associated with searched file
  -p | -print            print the file(s)
  -s | -search <string>  searches files for a <string>, case sensitive
  -t | -tutorials        search for the file from the \$FOAM_TUTORIALS directory

Finds one or more files in OpenFOAM and optionally processes the contents by:
+ printing the file(s) ('-print' option);
+ printing lines within the file matching a search string ('-search' option);
+ opening a single (only) file in a text editor ('-edit' option).

The '-edit' option required the user to specify their choice of text editor by
setting the EDITOR environment variable. For example, to set the 'gedit' editor,
they could add to their \$HOME/.bashrc file, the line:

export EDITOR=gedit

With source code files, can locate the 'files' and 'options' files associated
with their compilation using 'wmake'.

By default, files are searched from the src (\$FOAM_SRC) directory.
Alternatively the '-dir' option allows the user to specify the search directory
The '-applications', '-modules' and '-tutorials' options specifically set the
search path to the \$FOAM_APP, \$FOAM_MODULES and \$FOAM_TUTORIALS directories,
respectively.

Examples:
    foamFind -print wallHeatFlux.C | less
    + click space bar to scroll down
    + enter line number (after ":") to jump to line
    + enter "/text" to search for "text" (or any other string)

    foamFind -applications -isearch "momentumtransport" -options fluid.C
    foamFind -numbers -search laminar BirdCarreau.C

USAGE
}

error() {
    exec 1>&2
    while [ "$#" -ge 1 ]; do echo "$1"; shift; done
    usage
    exit 1
}

findWmakeFiles () {
    _file="$1" ; shift
    _wmakeFiles="$*"

    # Check if file is source code
    ! file -b "$_file" | grep -Eq "C.*source" && \
        "'-$_filename' invalid, file is not source code" >&2 && \
        return 1

    _files=""
    while true
    do
        _oldFile="$_file"
        _file="${_file%/*}"

        # cannot contract the path further
        [ "$_oldFile" = "$_file" ] && break

        # search for 'files' and/or 'options'
        [ -d "$_file" ] && \
            for _w in $_wmakeFiles
            do
                _files="$(echo "$_files" \
                       "$(find "$_file" -name "$_w" -type f)" | xargs)"
            done

        [ "$_files" ] && break
    done

    ! [ "$_files" ] && \
        error "no '$_filename' file found for '-files' or '-options'" >&2 && \
        return 1

    echo "$_files"
    return 0
}

nParams () {
    [ "$1" ] || return 1

    echo "$1" | xargs | awk -F' ' '{print NF}'
    return 0
}

selectFile () {
    _filename="$1"; shift
    _files="$*"
    _nFiles="$(nParams "$_files")"

    # Return file if only one
    [ "$_nFiles" -eq 1 ] && echo "$_files" && exit 0

    printf "Multiple files" >&2
    [ "$_filename" ] && printf " with name '%s'" "$_filename" >&2
    printf " found:\n" >&2

    _i=1
    for _f in $_files
    do
        printf "%i) %s\n" "$_i" "$_f" >&2
        _i=$((_i + 1))
    done
    printf "Type ENTER to select all, or " >&2
    printf "enter file number (1-%s) for specific file: " \
           "$_nFiles" >&2

    read -r _nFile

    # Enter nothing
    [ -z "$_nFile" ] && echo "$_files" && return 0

    # Check incorrect number
    ! [ "$_nFile" -eq "$_nFile" ] 2>/dev/null && \
        echo "\"$_nFile\" is not a number" >&2 && \
        return 1
    { [ "$_nFile" -lt 1 ] || [ "$_nFile" -gt "$_nFiles" ] ; } && \
        echo "\"$_nFile\" is not a number between 1 and $_nFiles" >&2 && \
        return 1

    # Enter number corresponding to file
    # shellcheck disable=SC2086
    echo $_files | awk -v n="$_nFile" '{print $n}'
    return 0
}

setFile () {
    _files="$1"
    _n="$2"
    echo "$_files" | xargs -n 1 | awk -v n="${_n}" 'NR==n'
}

isSrcDir () {
    echo "$1" | grep -wqE "^($FOAM_SRC|$FOAM_APP)"
}

listFiles () {
    _dir="$1"
    _filename="$2"
    _search="$3"
    _ins=""

    [ "$4" = "on" ] && _ins="i"

    # Specify code files if _dir is in $FOAM_SRC or $FOAM_APP
    _name="*"
    isSrcDir "$dir" && _name="*.[CHL]"
    [ "$_filename" ] && _name="$_filename"

    [ "$_search" ] && \
        find "$_dir" -name "$_name" -type f -print0 | \
            xargs -0 grep -l$_ins "$search" && \
        return 0

    # Remove "\n"
    find "$dir" -name "$filename" -type f -print0 | xargs -0
}

grepFile () {
    _string="$1"
    _file="$2"
    _ins=""
    [ "$3" = "on" ] && _ins="i"
    _arg="$4"

    # use $_arg for different cases: test, and with and without line numbers
    case "$_arg" in
        test)
            grep -q"$_ins" "$_string" "$_file"
            ;;
        numbers)
            grep -hn"$_ins" "$_string" "$_file" | \
                awk '{
                         split($0,f,":");
                         sub(/^([^:]+:)/,"",$0);
                         printf "%6i  %s\n", f[1], $0
                     }'
            ;;
        *)
            grep -h"$_ins" "$_string" "$_file"
            ;;
    esac
}

dirError () {
    cat<<EOF
Search directory specified with more than one option from '-applications',
'-dir', '-modules' and '-tutorials'. Please specify only one of these options.
EOF
}

editConfigError () {
    cat<<EOF
The '-edit' option requires the user to specify their chosen text editor using
the EDITOR environment variable. For example, to set the 'gedit' editor, they
could add to their \$HOME/.bashrc file, the line:

export EDITOR=gedit

EOF
}

searchError () {
    cat<<EOF
The '-search' and '-isearch' options are both specified.
Please specify only one of these options, either '-isearch' for a
case-insensitive search or '-search' for case-sensitive.
EOF
}

optionsError () {
    cat<<EOF
Two or more of the '-search/-isearch', '-print' and '-edit' options are
specified. Please specify only one of these options, either '-search/-isearch'
to print lines of files(s) matching an expression, or '-print' or '-edit' to
view the entire file(s).
EOF
}

dir=""
print=""
edit=""
wmakeFiles=""
search=""
insensitive=""
numbers=""
filename=""

while [ "$#" -gt 0 ]
do
   case "$1" in
   -a | -applications)
       [ "$dir" ] && error "$(dirError)"
       dir="$FOAM_APP"
       shift
       ;;
   -d | -dir)
       [ "$dir" ] && error "$(dirError)"
       [ "$#" -ge 2 ] || error "'$1' option requires an argument"
       [ -d "$2" ] || error "'$2' argument to '-dir' option is not a directory"
       # shellcheck disable=SC2164
       dir="$(cd "$2"; pwd)"
       # Check $dir is in OpenFOAM
       ! echo "$dir" | grep -wq "^$WM_PROJECT_DIR" && \
           error "Specified directory '$dir' is not in OpenFOAM installation"
       shift 2
       ;;
   -e | -edit)
       { [ "$insensitive" ] || [ "$print" ]; } && error "$(optionsError)"
       [ "$EDITOR" ] || error "$(editConfigError)"
       edit="$(which "$EDITOR")"
       shift
       ;;
   -f | -files)
       wmakeFiles="$wmakeFiles files"
       shift
       ;;
   -h | -help)
       usage
       exit
       ;;
   -i | -isearch)
       [ "$#" -ge 2 ] || error "'$1' option requires an argument"
       [ "$insensitive" ] && error "$(searchError)"
       { [ "$print" ] || [ "$edit" ]; } && error "$(optionsError)"
       search="$2"
       insensitive=on
       shift 2
       ;;
   -m | -modules)
       [ "$dir" ] && error "$(dirError)"
       dir="$FOAM_MODULES"
       shift
       ;;
   -n | -numbers)
       numbers="numbers" # flag for line numbers in cat or grep
       shift
       ;;
   -o | -options)
       wmakeFiles="$wmakeFiles options"
       shift
       ;;
   -p | -print)
       { [ "$insensitive" ] || [ "$edit" ]; } && error "$(optionsError)"
       print="yes"
       shift
       ;;
   -s | -search)
       [ "$#" -ge 2 ] || error "'$1' option requires an argument"
       [ "$insensitive" ] && error "$(searchError)"
       { [ "$print" ] || [ "$edit" ]; } && error "$(optionsError)"
       search="$2"
       insensitive=off
       shift 2
       ;;
   -t | -tutorials)
       [ "$dir" ] && error "$(dirError)"
       dir="$FOAM_TUTORIALS"
       shift
       ;;
   -*)
       error "Invalid option '$1'"
       ;;
   *)
       # If filename is already set, then exit
       [ "$filename" ] && error "Arguments specified ($#) =/= 1"
       filename="$1"
       shift
       ;;
    esac
done

# Default search directory if not specified
[ "$dir" ] || dir="$FOAM_SRC"

# Disallow '-files' and '-options' if search directory does not include source code
[ "$wmakeFiles" ] && ! isSrcDir "$dir" && \
    error "With search directory '$dir' (not source code)," \
          "cannot use '-files' or '-options'"

# Check number of arguments
[ "$#" -eq 0 ] || error "Arguments specified =/= 1 (optional)"

# If filename is not set, check search string is specified
! [ "$filename" ] && ! [ "$search" ] && \
    error "<filename> can only be omitted if a search string is specified" \
          "with the '-search' option"

# If '-files' or '-options' is selected, ensure <filename> is set
! [ "$filename" ] && [ "$wmakeFiles" ] && \
    error "With '-files' or '-options', <filename> must be specified"

# Find files
files="$(listFiles "$dir" "$filename" "$search" "$insensitive")"

# Ensure there is at least one file
nParams "$files" > /dev/null || error "No file '$filename' found in '$dir'"

# Select files when more than one is found
files="$(selectFile "$filename" "$files")" || error

# With '-files' and/or '-options', reuse file variable as the file
{ [ "$wmakeFiles" ] || [ "$edit" ] ; } && \
    [ "$(nParams "$files")" -ne 1 ] && \
    error "With '-files', '-options' or '-edit', a single file must be selected"

[ "$wmakeFiles" ] &&
    ! files="$(findWmakeFiles "$files" "$wmakeFiles")" && error

# Output
for f in $files
do
    printf "\nFile: %s\n" "$f"

    # Printing entire files
    [ "$print" ] && \
        printf ">>>\n" && \
        { [ "$numbers" ] && cat -n "$f" || cat "$f" ; } && \
        printf "<<<\n"

    # Opening files in a editor
    [ "$edit" ] && "$edit" "$f"

    # Printing lines matching strings
    [ "$search" ] || continue
    ! grepFile "$search" "$f" "$insensitive" test && \
        echo "Search: no match" && continue
    printf ">>>\n"
    grepFile "$search" "$f" "$insensitive" "$numbers"
    printf "<<<\n"
done
