#!/usr/bin/env bash
#
# TROUBLESHOOTING
#   - If you see 'undefined reference to `AES_encrypt'',
#       make sure you have openssl installed.
#       If libcrypto.a is in a non-standard directory, try ./build -L[your-lib-dir]
#   - If you see 'undefined reference to `clock_gettime'',
#       try ./build -lrt instead.
#   - If you see 'x86.S:202: Error: junk at end of line, first unrecognized character is `,'',
#       try commenting the line in lib/bcrypt/x86.S.
#   - Take a look at the "Optional features" section.  Some features have dependencies,
#       either make sure you have them or disable those features.
#       eg. mpw_color=0 ./build
#
# BUGS
#   masterpassword@lyndir.com
#
# AUTHOR
#   Maarten Billemont
#
cd "${BASH_SOURCE%/*}"
shopt -s extglob
set -e


### CONFIGURATION

# Targets to build.
if [[ $targets ]]; then
    read -ra targets <<< "$targets"
else
    # Default targets.
    # Modify here or override using targets='mpw mpw-bench' ./build
    targets=(
        mpw                         # C CLI version of Master Password, requires libsodium or openssl-dev.
        #mpw-bench                  # C CLI Master Password benchmark utility.
        #mpw-tests                  # C Master Password algorithm test suite, requires libxml2.
    )
fi

# Optional features.
mpw_color=${mpw_color:-1}   # Colorized Identicon, requires libncurses-dev.
mpw_json=${mpw_json:-1}     # Support for JSON-based user configuration format.
mpw_sodium=${mpw_sodium:-1} # Use libsodium if available instead of cperciva's libscrypt.

# Default build flags.
export CFLAGS="-O3 $CFLAGS"
export LDFLAGS="$LDFLAGS"

# Distribution specific configuration.
# Homebrew - openssl for scrypt
if hash brew 2>/dev/null; then
    opensslPath=$(brew --prefix openssl)
    CFLAGS+="  -I$opensslPath/include"
    LDFLAGS+=" -L$opensslPath/lib"
fi

### DEPENDENCIES

digest() {
    openssl sha -sha256 -binary < "$1" | od -t x1 -An -v | tr -d '[:space:]'
}
fetch() {
    if hash wget 2>/dev/null; then
        wget -O "${1##*/}" "$1"
    elif hash curl 2>/dev/null; then
        curl "$1" > "${1##*/}"
    fi
}
unpack() {
    printf 'Verifying package: %s, against digest: %s...' "$1" "$2"
    [[ $(digest "$1") = $2 ]] || {
        printf ' mismatch!\n'
        echo 2>&1 "Downloaded package doesn't match digest."
        exit 1
    }
    printf ' OK!\n'

    if [[ $1 = *.tar.gz || $1 = *.tgz ]]; then
        tar -xvzf "$1"

    elif [[ $1 = *.tar.bz2 || $1 = *.tbz2 ]]; then
        tar -xvjf "$1"

    elif [[ $1 = *.tar ]]; then
        tar -xvf "$1"

    else
        echo 2>&1 "Don't know how to unpack: $1"
    fi

    files=( * )
    if [[ -d $files ]] && (( ${#files[@]} == 1 )); then
        mv "$files"/* .
        rmdir "$files"
    fi
}
fetchSource() (
    local name=${PWD##*/}
    source .source

    if [[ -e .unpacked ]]; then
        true

    elif [[ $pkg && -e "${pkg##*/}" ]]; then
        [[ -e src ]] || {
            echo
            echo "Unpacking: $name, using package..."
            ( mkdir src && cd src && unpack "../${pkg##*/}" "$pkg_sha256" )
            touch .unpacked
        }

    elif [[ $git ]] && hash git 2>/dev/null; then
        [[ -e .git ]] || {
            echo
            echo "Fetching: $name, using git..."
            git clone "$git" src
            touch .unpacked
        }

    elif [[ $svn ]] && hash git 2>/dev/null && [[ -x "$(git --exec-path)/git-svn" ]]; then
        [[ -e .git ]] || {
            echo
            echo "Fetching: $name, using git-svn..."
            git svn clone --prefix=origin/ --stdlayout "$svn" src
            touch .unpacked
        }

    elif [[ $svn ]] && hash svn 2>/dev/null; then
        [[ -e .svn ]] || {
            echo
            echo "Fetching: $name, using svn..."
            svn checkout "$svn/trunk" src
            touch .unpacked
        }

    elif [[ $pkg ]]; then
        [[ -e src ]] || {
            echo
            echo "Fetching: $name, using package..."
            fetch "$pkg"
            ( mkdir src && cd src && unpack "../${pkg##*/}" "$pkg_sha256" )
            touch .unpacked
        }

    else

        echo >&2 "error: Missing git-svn or svn."
        echo >&2 "error: Please install either or manually check out the sources"
        echo >&2 "error: from: $home"
        echo >&2 "error: into: $PWD/src"
        exit 1
    fi

    if [[ ! -e .patched ]] && (( ${#patches[@]} )); then
        pushd src
        for patch in "${patches[@]}"; do
            echo
            echo "Patching: $name, for $patch..."
            patch -p0 < "../$patch.patch"
        done
        popd
        touch .patched
    fi
)
depend() {
    local name=$1

    echo
    echo "Checking dependency: $name..."
    [[ -e "lib/include/$name" ]] && return

    pushd "lib/$name"
    fetchSource
    pushd "src"

    echo
    echo "Configuring dependency: $name..."
    if [[ -e configure.ac ]]; then
        if [[ ! -e configure ]]; then
            # create configure using autotools.
            if ! hash aclocal || ! hash automake; then
                echo >&2 "Need autotools to build $name.  Please install automake and autoconf."
                exit 1
            fi

            aclocal
            autoheader
            autoconf
            mkdir -p config.aux
            automake --add-missing
        fi
    fi

    if [[ -e configure ]]; then
        ./configure
    fi

    echo
    echo "Building dependency: $name..."
    if [[ -e Makefile ]]; then
        if ! hash make; then
            echo >&2 "Need make to build $name.  Please install GNU make."
            exit 1
        fi

        make
        install -d "../../include/$name/"
        find . -name '*.h' -exec install -m 444 {} "../../include/$name/" \;
    else
        echo >&2 "error: Don't know how to build: $name"
        exit 1
    fi
    popd
    popd
}
depend_scrypt() {
    if (( mpw_sodium )) && haslib sodium; then
        if [[ $CFLAGS != *HAS_SODIUM=1* ]]; then
            CFLAGS+="  -DHAS_SODIUM=1"
            LDFLAGS+=" -lsodium"
        fi
        return
    fi

    depend scrypt
    if [[ $CFLAGS != *HAS_CPERCIVA=1* ]]; then
        local objects=(
            "lib/scrypt/src/libcperciva/"*/*.o
            "lib/scrypt/src/lib/crypto/"*.o
        )
        CFLAGS+="  -DHAS_CPERCIVA=1"
        LDFLAGS+=" -Llib/scrypt/src ${objects[*]}"
    fi
}


### MPW
mpw() {
    depend_scrypt

    echo
    echo "Building target: $target..."
    local CFLAGS=(
        $CFLAGS

        # library paths
        -I"lib/include"
        # mpw paths
        -I"core" -I"cli"
    )
    local LDFLAGS=(
        $LDFLAGS

        # link libraries
        -l"crypto"
    )
    # optional features
    (( mpw_color )) && CFLAGS+=( -DMPW_COLOR ) LDFLAGS+=( -l"curses" )
    (( mpw_json  )) && CFLAGS+=( -DMPW_JSON  ) LDFLAGS+=( -l"json-c" )

    cc "${CFLAGS[@]}" "$@"                  -c core/base64.c            -o core/base64.o
    cc "${CFLAGS[@]}" "$@"                  -c core/mpw-algorithm.c     -o core/mpw-algorithm.o
    cc "${CFLAGS[@]}" "$@"                  -c core/mpw-types.c         -o core/mpw-types.o
    cc "${CFLAGS[@]}" "$@"                  -c core/mpw-util.c          -o core/mpw-util.o
    cc "${CFLAGS[@]}" "$@"                  -c core/mpw-marshall-util.c -o core/mpw-marshall-util.o
    cc "${CFLAGS[@]}" "$@"                  -c core/mpw-marshall.c      -o core/mpw-marshall.o
    cc "${CFLAGS[@]}" "$@" "core/base64.o" "core/mpw-algorithm.o" "core/mpw-types.o" "core/mpw-util.o" "core/mpw-marshall-util.o" "core/mpw-marshall.o" \
       "${LDFLAGS[@]}"     "cli/mpw-cli.c" -o "mpw"
    echo "done!  Now run ./install or use ./mpw"
}


### MPW-BENCH
mpw-bench() {
    depend_scrypt
    depend bcrypt

    echo
    echo "Building target: $target..."
    local CFLAGS=(
        $CFLAGS

        # library paths
        -I"lib/include"
        # mpw paths
        -I"core" -I"cli"
    )
    local LDFLAGS=(
        $LDFLAGS

        # bcrypt
        "lib/bcrypt/src/crypt_blowfish.o"
        "lib/bcrypt/src/crypt_gensalt.o"
        "lib/bcrypt/src/wrapper.o"
        "lib/bcrypt/src/x86.o"
        # library paths
        -L"lib/bcrypt/src"
        # link libraries
        -l"crypto"
    )

    cc "${CFLAGS[@]}" "$@"                  -c core/base64.c            -o core/base64.o
    cc "${CFLAGS[@]}" "$@"                  -c core/mpw-algorithm.c -o core/mpw-algorithm.o
    cc "${CFLAGS[@]}" "$@"                  -c core/mpw-types.c     -o core/mpw-types.o
    cc "${CFLAGS[@]}" "$@"                  -c core/mpw-util.c      -o core/mpw-util.o
    cc "${CFLAGS[@]}" "$@" "core/base64.o" "core/mpw-algorithm.o" "core/mpw-types.o" "core/mpw-util.o" \
       "${LDFLAGS[@]}"     "cli/mpw-bench.c" -o "mpw-bench"
    echo "done!  Now use ./mpw-bench"
}


### MPW-TESTS
mpw-tests() {
    depend_scrypt

    echo
    echo "Building target: $target..."
    local CFLAGS=(
        $CFLAGS

        # library paths
        -I"lib/include"
        -I"/usr/include/libxml2"
        -I"/usr/local/include/libxml2"
        # mpw paths
        -I"core" -I"cli"
    )
    local LDFLAGS=(
        $LDFLAGS

        # link libraries
        -l"crypto" -l"xml2"
    )

    cc "${CFLAGS[@]}" "$@"                  -c core/base64.c            -o core/base64.o
    cc "${CFLAGS[@]}" "$@"                  -c core/mpw-algorithm.c -o core/mpw-algorithm.o
    cc "${CFLAGS[@]}" "$@"                  -c core/mpw-types.c     -o core/mpw-types.o
    cc "${CFLAGS[@]}" "$@"                  -c core/mpw-util.c      -o core/mpw-util.o
    cc "${CFLAGS[@]}" "$@"                  -c cli/mpw-tests-util.c -o cli/mpw-tests-util.o
    cc "${CFLAGS[@]}" "$@" "core/base64.o" "core/mpw-algorithm.o" "core/mpw-types.o" "core/mpw-util.o" \
       "${LDFLAGS[@]}"     "cli/mpw-tests-util.o" "cli/mpw-tests.c" -o "mpw-tests"
    echo "done!  Now use ./mpw-tests"
}


### TARGETS

haslib() {
    cc -l"$1" -x c -o /dev/null - <<< 'int main() { return 0; }'
}
cc() {
    if hash llvm-gcc 2>/dev/null; then
        llvm-gcc "$@"
    elif hash gcc 2>/dev/null; then
        gcc -std=gnu99 "$@"
    elif hash clang 2>/dev/null; then
        clang "$@"
    else
        echo >&2 "Need a compiler.  Please install GCC or LLVM."
        exit 1
    fi
}

echo "Will build targets: ${targets[*]}..."
for target in "${targets[@]}"; do
    "$target" "$@"
done