#!/bin/bash -p
###############################################################################
# BRLTTY - A background process providing access to the console screen (when in
#          text mode) for a blind person using a refreshable braille display.
#
# Copyright (C) 1995-2024 by The BRLTTY Developers.
#
# BRLTTY comes with ABSOLUTELY NO WARRANTY.
#
# This is free software, placed under the terms of the
# GNU Lesser General Public License, as published by the Free Software
# Foundation; either version 2.1 of the License, or (at your option) any
# later version. Please see the file LICENSE-LGPL for details.
#
# Web Page: http://brltty.app/
#
# This software is maintained by Dave Mielke <dave@mielke.cc>.
###############################################################################

set -e
umask 022
shopt -s nullglob

readonly autoconfOldVersion=2.13
readonly autoconfNewVersion=2.59
readonly automakeVersion=1.9.6
readonly crxVersion=2.04

readonly defaultArchivesSubdirectory="Archives"
readonly defaultBuildSubdirectory="Build"
readonly defaultInstallSubdirectory="Tools"
readonly defaultTargetSystem="i586-pc-msdosdjgpp"

function setVariable {
   local variable="${1}"
   local value="${2}"

   eval "${variable}"'="${value}"'
}

function setArrayElement {
   local array="${1}"
   local index="${2}"
   local value="${3}"

   setVariable "${array}[${index}]" "${value}"
}

function defineEnumeration {
   local array="${1}"
   shift 1

   declare -g -i -A "${array}"
   local value=0

   while [ "${#}" -gt 0 ]
   do
      setArrayElement "${array}" "${1}" $((value++))
      shift 1
   done

   readonly "${array}"
}

readonly programName="${0##*/}"
readonly programDirectory="$(realpath "$(dirname "${0}")")"

function programMessage {
   local message="${1}"

   echo >&2 "${programName}: ${message}"
}

defineEnumeration logLevels error warning task step detail
declare -i logLevel="${logLevels[task]}"

function logMore {
   ((logLevel += 1)) || :
}

function logLess {
   ((logLevel -= 1)) || :
}

function logMessage {
   local level="${1}"
   local message="${2}"

   local value="${logLevels[$level]}"

   [ -n "${value}" ] || {
      logWarning "undefined log level: ${level}"
      value=0
   }

   [ "${value}" -gt "${logLevel}" ] || programMessage "${message}"
}

function syntaxError {
   local message="${1}"

   logError "${message}"
   exit 2
}

function semanticError {
   local message="${1}"

   logError "${message}"
   exit 3
}

function externalError {
   local message="${1}"

   logError "${message}"
   exit 4
}

function showUsage {
   cat <<-END-OF-USAGE
	usage: ${programName} [-option ...]
	-h            display a usage summary (this text), and then exit
	-q            decrease output verbosity (may be specified more than once)
	-v            increase output verbosity (may be specified more than once)
	-d directory  specify the ROOT directory (default is the current directory)
	-a directory  specify the archives directory (default is ROOT/${defaultArchivesSubdirectory})
	-b directory  specify the build directory (default is ROOT/${defaultBuildSubdirectory})
	-i directory  specify the install directory (default is ROOT/${defaultInstallSubdirectory})
	-t system     specify the target system (default is ${defaultTargetSystem})
	-g version    the gcc version to build (defaults if only one is archived)
	END-OF-USAGE

   exit 0
}

function handleDirectoryOption {
   local variable="${1}"
   local default="${2}"
   local name="${3}"

   [ -n "${!variable}" ] || setVariable "${variable}" "${default}"
   setVariable "${variable}" "$(realpath "${!variable}")"
   readonly "${variable}"

   [ -z "${name}" ] || logMessage detail "${name} directory: ${!variable}"
}

function logArrayElements {
   local array="${1}"
   local name="${2}"

   message="${name} properties:"

   eval local 'indeces="${!'"${array}"'[*]}"'
   local index

   for index in ${indeces}
   do
      local variable="${array}[${index}]"
      message+=" ${index}=${!variable}"
   done

   logMessage detail "${message}"
}

function findHostCommand {
   local variable="${1}"
   shift 1

   local command
   for command
   do
      local path="$(type -p "${command}")"

      [ -z "${path}" ] || {
         setVariable "${variable}" "${path}"
         export "${variable}"
         logMessage detail "host command location: ${variable} -> ${!variable}"
         return 0
      }
   done

   semanticError "host command not found: ${variable} (${*})"
}

function pathChange {
   local newPath="${1}"

   export PATH="${newPath}"
   logMessage detail "host command search path: ${PATH}"
}

function pathPrepend {
   local directory="${1}"

   pathChange "${directory}:${PATH}"
}

function makeDirectory {
   local path="${1}"

   mkdir -p "${path}"
}

function emptyDirectory {
   local path="${1}"

   rm -f -r "${path}/"*
}

function initializeDirectory {
   local path="${1}"

   makeDirectory "${path}"
   emptyDirectory "${path}"
}

function verifyArchive {
   local array="${1}"
   local type="${2}"
   local prefix="${3}"
   local suffix="${4}"
   local version="${5}"

   declare -g -A "${array}"
   setArrayElement "${array}" type "${type}"
   setArrayElement "${array}" prefix "${prefix}"
   setArrayElement "${array}" suffix "${suffix}"

   local name="${prefix%-}"
   setArrayElement "${array}" name "${name}"

   local originalDirectory="${PWD}"
   cd "${archivesDirectory}"
   if [ -n "${version}" ]
   then
      local file="${prefix}${version}${suffix}"
      [ -f "${file}" ] || semanticError "${type} archive not found: ${file}"
   else
      local files=("${prefix}"*"${suffix}")
      local count="${#files[*]}"
      ((count > 0)) || semanticError "${type} archive not found: ${name}"
      ((count == 1)) || semanticError "${type} package with multiple archives: ${files[*]}"

      local file="${files[0]}"
      version="${file%${suffix}}"
      version="${version:${#prefix}}"
   fi
   cd "${originalDirectory}"

   [[ "${version}" =~ ^[0-9]{3}$ ]] && version="${version:0:1}.${version:1}"
   setArrayElement "${array}" version "${version}" 

   setArrayElement "${array}" file "${file}"
   setArrayElement "${array}" path "${archivesDirectory}/${file}"

   local source="${name}-${version}"
   setArrayElement "${array}" source "${source}"

   readonly "${array}"
   logArrayElements "${array}" "archive"
}

function gnuVerifyArchive {
   local array="${1}"
   local name="${2}"
   local version="${3}"

   verifyArchive "${array}" Gnu "${name}-" ".tar.gz" "${version}"
}

function djgppVerifyArchive {
   local array="${1}"
   local name="${2}"
   local type="${3}"
   local version="${4}"

   verifyArchive "${array}" DJGPP "${name}" "${type}.zip" "${version//./}"
}

function logPackageTask {
   local array="${1}"
   local task="${2}"

   local nameVariable="${array}[name]"
   local versionVariable="${array}[version]"
   logMessage task "${task}: ${!nameVariable}-${!versionVariable}"
}

function unpackArchive {
   local array="${1}"

   local typeVariable="${array}[type]"
   local pathVariable="${array}[path]"

   logPackageTask "${array}" "unpacking ${!typeVariable} archive"
   "unpackArchive_${!typeVariable}" "${!pathVariable}"
}

function unpackArchive_Gnu {
   local path="${1}"

   tar xfz "${path}"
}

function unpackArchive_DJGPP {
   local path="${1}"

   unzip -q -a "${path}"
}

function changeScriptVariable {
   local script="${1}"
   local variable="${2}"
   local value="${3}"

   sed -e "/^ *${variable} *=/s%=.*%='${value}'%" -i "${script}"
}

function logBuildDirectory {
   logNote "build directory: ${PWD}"
}

function runBuildCommand {
   local logFile="${1}"
   shift 1

   logNote "build command: ${*}"
   "${@}" >&"${logFile}" || externalError "build error: for details, see ${PWD}/${logFile}"
}

function configurePackage {
   local source="${1}"
   shift 1

   runBuildCommand configure.log "${source}/configure" "${@}"
}

function makePackage {
   runBuildCommand make.log make "${@}"
}

function installPackage {
   runBuildCommand install.log make install "${@}"
}

function buildHostPackage {
   local array="${1}"
   shift 1

   logPackageTask "${array}" "building host package"

   local sourceVariable="${array}[source]"
   local build="${!sourceVariable}-host"
   local prefix="$(realpath "${!sourceVariable}-install")"

   makeDirectory "${build}"
   cd "${build}"
   logBuildDirectory
   configurePackage "../${!sourceVariable}" --prefix="${prefix}"
   makePackage
   installPackage
   cd ..

   local bin="${prefix}/bin"
   pathPrepend "${bin}"

   while [ "${#}" -gt 0 ]
   do
      local command="${1}"
      local variable="${2}"
      shift 2

      changeScriptVariable "${gccUnpackScript}" "${variable}" "${bin}/${command}"
   done
}

function buildHostArchive {
   local array="${1}"
   shift 1

   unpackArchive "${array}"
   buildHostPackage "${array}" "${@}"
}

function buildHostAutoconf {
   local array="${1}"
   local autoconfVariable="${2}"
   local autoheaderVariable="${3}"

   buildHostArchive "${array}" autoconf "${autoconfVariable}" autoheader "${autoheaderVariable}"
}

function configureTargetPackage {
   local array="${1}"
   shift 1

   local sourceVariable="${array}[source]"
   configurePackage "../${!sourceVariable}" \
      "--prefix=${installDirectory}" \
      "--target=${targetSystem}" \
      "${@}"
}

function buildTargetPackage {
   local array="${1}"
   shift 1

   logPackageTask "${array}" "building target package"

   cd gnu
   local sourceVariable="${array}[source]"
   local build="${!sourceVariable}-target"
   makeDirectory "${build}"
   cd "${build}"
   logBuildDirectory
   configureTargetPackage "${array}" "${@}"
   makePackage
   installPackage
   cd ../..
}

rootDirectory=""
archivesDirectory=""
buildDirectory=""
installDirectory=""

targetSystem=""
gccVersion=""

while getopts ":hqvd:a:b:i:t:g:" option
do
   case "${option}"
   in
      h) showUsage;;

      q) logLess;;
      v) logMore;;

      d) rootDirectory="${OPTARG}";;
      a) archivesDirectory="${OPTARG}";;
      b) buildDirectory="${OPTARG}";;
      i) installDirectory="${OPTARG}";;

      t) targetSystem="${OPTARG}";;
      g) gccVersion="${OPTARG}";;

      :) syntaxError "missing ooperand: -${OPTARG}";;
     \?) syntaxError "unknown option: -${OPTARG}";;
      *) syntaxError "unimplemented option: -${option}";;
   esac
done

shift $((OPTIND - 1))
[ "${#}" -eq 0 ] || syntaxError "too many parameters"

handleDirectoryOption rootDirectory "${PWD}" "root"
handleDirectoryOption archivesDirectory "${rootDirectory}/${defaultArchivesSubdirectory}" "archives"
handleDirectoryOption buildDirectory "${rootDirectory}/${defaultBuildSubdirectory}" "build"
handleDirectoryOption installDirectory "${rootDirectory}/${defaultInstallSubdirectory}" "install"

[ -n "${targetSystem}" ] || targetSystem="${defaultTargetSystem}"
readonly targetSystem

pathChange "$(getconf PATH)"
unset MAKEFLAGS

logMessage task "finding host commands"
findHostCommand CC cc gcc
findHostCommand CXX c++ g++ cxx gxx
findHostCommand LIBTOOL libtool

logMessage task "verifying archives"
gnuVerifyArchive gnuAutoconfOld autoconf "${autoconfOldVersion}"
gnuVerifyArchive gnuAutoconfNew autoconf "${autoconfNewVersion}"
gnuVerifyArchive gnuAutomake automake "${automakeVersion}"
gnuVerifyArchive gnuBinutils binutils
gnuVerifyArchive gnuGcc gcc "${gccVersion}"
djgppVerifyArchive djgppGcc gcc s2 "${gnuGcc[version]}"
djgppVerifyArchive djgppCrx djcrx "" "${crxVersion}"

logMessage task "preparing build directory"
initializeDirectory "${buildDirectory}"
cd "${buildDirectory}"

unpackArchive djgppCrx
unpackArchive djgppGcc

gccUnpackScript="unpack-gcc.sh"
buildHostAutoconf gnuAutoconfOld AUTOCONF_OLD AUTOHEADER_OLD
buildHostAutoconf gnuAutoconfNew AUTOCONF AUTOHEADER
buildHostArchive gnuAutomake

logMessage task "patching gcc source"
logBuildDirectory
chmod u=rwx,go=r "${gccUnpackScript}"
runBuildCommand unpack-gcc.log "./${gccUnpackScript}" "$(realpath --relative-to=. "${gnuGcc["path"]}")"

cd gnu
unpackArchive gnuBinutils
cd ..

logMessage task "preparing install directory"
initializeDirectory "${installDirectory}"
pathPrepend "$${installDirectory}/bin"
readonly targetDirectory="${installDirectory}/${targetSystem}"
makeDirectory "${targetDirectory}"
makeDirectory "${targetDirectory}/bin"
cp -r lib "${targetDirectory}"
cp -r include "${targetDirectory}"

logMessage task "building stubify"
cd src/stub
logBuildDirectory
runBuildCommand compile.log "${CC}" -O2 stubify.c -o "${targetDirectory}/bin/stubify"
cd ../..

buildTargetPackage gnuBinutils
buildTargetPackage djgppGcc --with-headers="${targetDirectory}/include"

logMessage task "creating symbolic links"
cd "${targetDirectory}/lib"
ln -s libstdc++.a libstdcxx.a
ln -s libsupc++.a libsupcxx.a

logMessage task "cleaning up"
cd /
emptyDirectory "${buildDirectory}"

logMessage task "done"
exit 0
