#!/usr/bin/env ksh
#
# Usage: bin/style [all | directory_or_filename...]
#
# Run the source through `clang-format`. If invoked with `all` then all the
# src/cmd/ksh93 source files will be linted. If invoked with one or more path
# names they will be restyled. If the pathname is a directory all *.c and *.h
# files inside it will be restyled. Otherwise any uncommitted source files are
# restyled. If there is no uncommitted change then the files in the most
# recent commit are restyled.
#

# shellcheck disable=SC2207
# Note: Disable SC2207 warning for the entire file since setting IFS to just
# newline makes it safe to handle file names with spaces.
IFS=$'\n'

# This is the minimum clang-format version we require. It is possible that
# older versions will produce results consistent with the latest stable version
# (7.0.0 as I write this). But we know that version 3.8.0 that is included in
# some distros (e.g., OpenSuse 42) does not produce compatible results.
#
# Requiring a compatible minimum version avoids pointless thrashing of the
# code style due to some people running this script on systems with a version
# of clang-format that is too old. Note that version 6.0.1 produces the same
# result as 7.0.0, the current stable release, so make that the minimum.
typeset -r MIN_CFORMAT_VER=6.0.1
typeset -r MAX_CFORMAT_VER=7.0.0

typeset all=no
typeset git_clang_format=no
typeset -a c_files=()
typeset -a files=()

function semver {
    echo $1 | awk '{ split($1, v, "."); printf "%d%03d%03d\n", v[1], v[2], v[3] }'
}

# Deal with any CLI flags.
while [[ "${#}" -ne 0 ]]
do
    case "${1}" in
        --all | all )
            all=yes
            ;;
        * )
            break
            ;;
    esac
    shift
done

if [[ ${all} == yes && "${#}" -ne 0 ]]
then
    echo "Unexpected arguments: '${1}'" >&2
    exit 1
fi


if command -v clang-format > /dev/null
then
    cformat_ver=$(clang-format --version | awk '{ print $3 }')
    if [[ $(semver $cformat_ver) -lt $(semver $MIN_CFORMAT_VER) ]]
    then
        echo
        echo "ERROR: clang-format version $cformat_ver less than min required $MIN_CFORMAT_VER"
        echo
        exit 1
    fi
    if [[ $(semver $cformat_ver) -gt $(semver $MAX_CFORMAT_VER) ]]
    then
        echo
        echo "ERROR: clang-format version $cformat_ver greater than max allowed $MAX_CFORMAT_VER"
        echo
        exit 1
    fi
else
    echo
    echo 'ERROR: Cannot find clang-format command'
    echo
    exit 1
fi

if [[ ${all} == yes ]]
then
    if [[ "$(git status --porcelain --short --untracked-files=all)" != "" ]]
    then
        echo >&2
        echo 'You have uncommitted changes. Cowardly refusing to restyle the entire code base.' >&2
        echo 'We do not want to include style cleanups unrelated to your work in progress.' >&2
        echo 'It should be sufficient to simply run `bin/style` to fix issues with your wip.' >&2
        echo >&2
        exit
    fi

    files=( $(find src -name "*.h" -o -name "*.c") )
elif [[ "${#}" -ne 0 ]]
then
    for next_file in "$@"
    do
        if [[ -f ${next_file} ]]
        then
            files+=( "${next_file}" )
        elif [[ -d ${next_file} ]]
        then
            files+=( $(find "${next_file}" -name "*.h" -o -name "*.c") )
        fi
    done
else
    # We haven't been asked to reformat all the source or specific files. If there are
    # uncommitted changes reformat those using `git clang-format`. Else reformat the
    # files in the most recent commit. Select cached files, modified but not cached,
    # and untracked files.
    files=( $(git diff-index --cached HEAD --name-only) )
    files+=( $(git ls-files --exclude-standard --others --modified) )
    if [[ "${#files[@]}" -ne 0 ]]
    then
        # Pending changes so restyle just the regions modified.
        git_clang_format=yes
    else
        # No pending changes so restyle the files in the most recent commit.
        files=( $(git diff-tree --no-commit-id --name-only -r HEAD) )
    fi
fi

# Filter out non C source files.
for file in "${files[@]}"
do
    case "${file}" in
        *.c | *.h )
            if [[ -f "${file}" ]]
            then
                c_files+=( "${file}" )
            fi
            ;;
    esac
done

# Run the C reformatter if we have any C files
if [[ "${#c_files[@]}" -eq 0 ]]
then
    if [ "$STAGE_STYLE_FIXUPS" = 1 ]
    then
        # We're presumably being run from a git pre-commit hook so it's okay
        # if no C modules are in the commit.
        exit 0
    fi
    echo
    echo 'WARNING: No C files to restyle'
    echo
    exit 1
fi

if [[ ${git_clang_format} == yes ]]
then
    if command -v git-clang-format > /dev/null
    then
        echo
        echo ========================================
        echo Running git-clang-format
        echo ========================================
        git add "${c_files[@]}"
        git-clang-format
    else
        echo
        echo 'ERROR: Cannot find git-clang-format command'
        echo
        exit 1
    fi
else
    echo
    echo ========================================
    echo Running clang-format
    echo ========================================
    for file in "${c_files[@]}"
    do
        cp "${file}" "${file}.new"  # preserves mode bits
        clang-format "${file}" >"${file}.new"
        if cmp -s "${file}" "${file}.new"
        then
            rm "${file}.new"
        else
            echo "${file} was NOT correctly formatted"
            mv "${file}.new" "${file}"
        fi
    done
fi

# This feature is meant to be used from a git pre-commit hook.
if [ "$STAGE_STYLE_FIXUPS" = 1 ]
then
    git add "${c_files[@]}"
fi
