#! /usr/bin/env bash
#
#   Copyright (c) 2018 Nat! - Mulle kybernetiK
#   All rights reserved.
#
#   Redistribution and use in source and binary forms, with or without
#   modification, are permitted provided that the following conditions are met:
#
#   Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
#   Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
#   Neither the name of Mulle kybernetiK nor the names of its contributors
#   may be used to endorse or promote products derived from this software
#   without specific prior written permission.
#
#   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#   ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
#   LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
#   CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
#   SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
#   INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
#   CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
#   ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
#   POSSIBILITY OF SUCH DAMAGE.
#

[ "${TRACE}" = "YES" ] && set -x && : "$0" "$@"


usage()
{
   cat <<EOF >&2
Usage:
   c-sourcetree-update <action> <filename> <category>

   Produces the _<project>-include.h files and related headers.

EOF
   exit 1
}


uname_to_cpp()
{
   log_entry "uname_to_cmake" "$@"

   local uname="$1"  # the part after only-os or no

   case "${uname}" in
      'darwin')
         echo "__APPLE__"
      ;;

      'freebsd')
         echo "BSD4_3"  # guess
      ;;

      'mingw'*)
         echo "_WIN32"
      ;;

      *)
         echo "__${uname}__"
      ;;
   esac
}


osexclude_to_cpp_if()
{
   log_entry "osexclude_to_cpp_if" "$@"

   local marks="$1"

   local cppvar

   local excludes
   local onlys

   set -o noglob ; IFS=","

   for osexclude in ${marks}
   do
      IFS="${DEFAULT_IFS}"; set +o noglob

      case "${osexclude}" in
         only-os-*)
            cppvar="`uname_to_cpp "${osexclude:8}"`"
            onlys="`concat "${onlys}" "defined( ${cppvar})" " || " `"
         ;;

         no-os-*)
            cppvar="`uname_to_cpp "${osexclude:6}"`"
            excludes="`concat "${excludes}" "! defined( ${cppvar})" " && "`"
         ;;
      esac
   done
   IFS="${DEFAULT_IFS}"; set +o noglob

   if [ ! -z "${onlys}" ]
   then
      echo "# if ${onlys}"
      return
   fi

   if [ ! -z "${excludes}" ]
   then
      echo "# if ${excludes}"
   fi
}


_emit_c_include()
{
   log_entry "_emit_c_include" "$@"

   local address="$1"
   local header="$2"
   local marks="$3"

   local indent
   local ifstatement
   local include
   local mark

   #
   # so objc can reuse this, make a provision for import with --objc flag
   # but allow #include if the mark no-objc is set
   #
   include="include"
   if [ "${OPTION_IMPORT}" = "YES" ]
   then
      include="import"

      case ",${marks}," in
         *,no-import,*)
            include="include"
         ;;
      esac
   fi

   if [ ! -z "${marks}" ]
   then
      ifstatement="`osexclude_to_cpp_if "${marks}" `"
   fi

   if [ ! -z "${ifstatement}" ]
   then
      echo "${ifstatement}"
      indent=" "
   fi

   # TODO: remove support __has_include for C ? Not all C compilers have
   #       __has_include
   case ",${marks}," in
      *,no-require,*)
         echo "# if __has_include(<${header}>)"
         echo "#  ${indent}${include} <${header}>   // ${address}"
         echo "# endif"
      ;;

      *)
         echo "# ${indent}${include} <${header}>   // ${address}"
      ;;
   esac


   if [ ! -z "${ifstatement}" ]
   then
      echo "# endif"
   fi

   echo
}


emit_c_include_library()
{
   log_entry "emit_c_include_library" "$@"

   local address="$1"
   local marks="$2"
   local header="$3"

   #
   # the default for libraries is old style <name.h> not <name/name.h>
   #
   if [ -z "${header}" ]
   then
      local name

      name="`fast_basename "${address}"`"
      header="${name}.h"
   fi

   _emit_c_include "${address}" "${header}" "${marks}"

}


emit_c_include_dependency()
{
   log_entry "emit_c_include_dependency" "$@"

   local address="$1"
   local marks="$2"
   local header="$3"

   #
   # the default for dependencies is <name/name.h>
   #
   if [ -z "${header}" ]
   then
      local name

      name="`fast_basename "${address}"`"
      header="${name}/${name}.h"
   fi

   _emit_c_include "${address}" "${header}" "${marks}"
}


emit_c_header()
{
   log_entry "emit_c_header" "$@"

   local marks="$1"

   local filter_marks

   filter_marks="`comma_concat "header" "${marks}" `"

   # since the order of the headers is important and needs to be in treeorder
   # we use mulle-sourcetree directly
   local headers

   headers="`exekutor mulle-sourcetree list \
                                       --format "%a;%m;%i={include,,-------}\\n" \
                                       --marks "${filter_marks}" \
                                       --output-raw \
                                       --no-output-header`" || return 1

   local dependency

   set -o noglob ; IFS="
"
   for header in ${headers}
   do
      IFS="${DEFAULT_IFS}"; set +o noglob

      local address
      local marks
      local include

      IFS=";" read address marks include <<< "${header}"

      if [ -z "${address}" ]
      then
         continue
      fi

      log_verbose "Emit cmake statements for ${C_MAGENTA}${C_BOLD}${address}"

      case ",${marks}," in
         *,no-dependency,*)
            emit_c_include_library "${address}" \
                                   "${marks}" \
                                   "${include}" \
                                   "$@"
         ;;

         *)
            emit_c_include_dependency "${address}" \
                                      "${marks}" \
                                      "${include}" \
                                      "$@"
         ;;
      esac
   done

   IFS="${DEFAULT_IFS}"; set +o noglob
}


c_include()
{
   log_entry "c_include" "$@"

   local projectname="$1"
   local projectidentifier="$2"

   local text_h

   if [ "${MULLE_SDE_INCLUDE_FILENAME}" = "NONE" ]
   then
      return
   fi
   if [ -z "${MULLE_SDE_INCLUDE_FILENAME}" ]
   then
      MULLE_SDE_INCLUDE_FILENAME="${PROJECT_SOURCE_DIR}/_${projectname}-include.h"
   fi

   local marks

   case "${PROJECT_DIALECT}" in
      'objc')
         marks="no-import,public"
      ;;

      *)
         marks="public"
      ;;
   esac

   text_h="`emit_c_header "${marks}"`" || exit 1

   if [ -z "${text_h}" ]
   then
      text_h="/* no headers */"
   fi

   text_h="/*
   This file will be regenerated by \`mulle-sde update|monitor\`.
   Edits will be lost.
*/

#ifndef _${projectidentifier}_include_h__
#define _${projectidentifier}_include_h__

${text_h}

#endif"

   log_verbose "Writing ${C_RESET_BOLD}${MULLE_SDE_INCLUDE_FILENAME}"
   redirect_exekutor "${MULLE_SDE_INCLUDE_FILENAME}" echo "${text_h}"
}


objc_import()
{
   log_entry "objc_import" "$@"

   local projectname="$1"
   local projectidentifier="$2"

   local text_h

   if [ "${MULLE_SDE_IMPORT_FILENAME}" = "NONE" ]
   then
      return
   fi
   if [ -z "${MULLE_SDE_IMPORT_FILENAME}" ]
   then
      MULLE_SDE_IMPORT_FILENAME="${PROJECT_SOURCE_DIR}/_${projectname}-import.h"
   fi

   text_h="`emit_c_header "import,public"`" || exit 1

   if [ -z "${text_h}" ]
   then
      text_h="/* no headers */"
   fi


   text_h="/*
   This file will be regenerated by \`mulle-sde update|monitor\`.
   Edits will be lost.
*/

#ifndef _${projectidentifier}_import_h__
#define _${projectidentifier}_import_h__

${text_h}

#endif"

   log_verbose "Writing ${C_RESET_BOLD}${MULLE_SDE_IMPORT_FILENAME}"
   redirect_exekutor "${MULLE_SDE_IMPORT_FILENAME}" echo "${text_h}"
}


c_include_private()
{
   log_entry "c_include_private" "$@"

   local projectname="$1"
   local projectidentifier="$2"

   local text_h

   if [ "${MULLE_SDE_INCLUDE_PRIVATE_FILENAME}" = "NONE" ]
   then
      return
   fi
   if [ -z "${MULLE_SDE_INCLUDE_PRIVATE_FILENAME}" ]
   then
      MULLE_SDE_INCLUDE_PRIVATE_FILENAME="${PROJECT_SOURCE_DIR}/_${projectname}-include-private.h"
   fi

   local marks

   case "${PROJECT_DIALECT}" in
      'objc')
         marks="no-import,no-public"
      ;;

      *)
         marks="no-public"
      ;;
   esac

   text_h="`emit_c_header "${marks}"`" || exit 1

   if [ -z "${text_h}" ]
   then
      text_h="/* no headers */"
   fi

   text_h="/*
   This file will be regenerated by \`mulle-sde update|monitor\`.
   Edits will be lost.
*/

#ifndef _${projectidentifier}_include_private_h__
#define _${projectidentifier}_include_private_h__

${text_h}

#endif"

   log_verbose "Writing ${C_RESET_BOLD}${MULLE_SDE_INCLUDE_PRIVATE_FILENAME}"
   redirect_exekutor "${MULLE_SDE_INCLUDE_PRIVATE_FILENAME}" echo "${text_h}"
}


objc_import_private()
{
   log_entry "objc_import_private" "$@"

   local projectname="$1"
   local projectidentifier="$2"

   local text_h

   if [ "${MULLE_SDE_IMPORT_PRIVATE_FILENAME}" = "NONE" ]
   then
      return
   fi
   if [ -z "${MULLE_SDE_IMPORT_PRIVATE_FILENAME}" ]
   then
      MULLE_SDE_IMPORT_PRIVATE_FILENAME="${PROJECT_SOURCE_DIR}/_${projectname}-import-private.h"
   fi

   text_h="`emit_c_header "import,no-public"`" || exit 1

   if [ -z "${text_h}" ]
   then
      text_h="/* no headers */"
   fi


   text_h="/*
   This file will be regenerated by \`mulle-sde update|monitor\`.
   Edits will be lost.
*/

#ifndef _${projectidentifier}_import_private_h__
#define _${projectidentifier}_import_private_h__

${text_h}

#endif"

   log_verbose "Writing ${C_RESET_BOLD}${MULLE_SDE_IMPORT_PRIVATE_FILENAME}"
   redirect_exekutor "${MULLE_SDE_IMPORT_PRIVATE_FILENAME}" echo "${text_h}"
}


main()
{
   log_entry "main" "$@"

   # technical flags
   local MULLE_TRACE
   local MULLE_FLAG_DONT_DEFER="NO"
   local MULLE_FLAG_EXEKUTOR_DRY_RUN="NO"
   local MULLE_FLAG_FOLLOW_SYMLINKS="YES"
   local MULLE_FLAG_LOG_CACHE="NO"
   local MULLE_FLAG_LOG_DEBUG="NO"
   local MULLE_FLAG_LOG_EXEKUTOR="NO"
   local MULLE_FLAG_LOG_FLUFF="NO"
   local MULLE_FLAG_LOG_MERGE="NO"
   local MULLE_FLAG_LOG_SCRIPTS="NO"
   local MULLE_FLAG_LOG_SETTINGS="NO"
   local MULLE_FLAG_LOG_VERBOSE="NO"
   local MULLE_TRACE_PATHS_FLIP_X="NO"
   local MULLE_TRACE_POSTPONE="NO"
   local MULLE_TRACE_RESOLVER_FLIP_X="NO"
   local MULLE_TRACE_SETTINGS_FLIP_X="NO"

   local OPTION_IMPORT="DEFAULT"

   while [ $# -ne 0 ]
   do
      if options_technical_flags "$1"
      then
         shift
         continue
      fi

      case "$1" in
         -h*|--help|help)
            usage
         ;;

         --import)
            OPTION_IMPORT="YES"
         ;;

         --no-import)
            OPTION_IMPORT="NO"
         ;;

         -p|--project-name)
            [ "$#" -eq 1 ] && usage "Missing argument to \"$1\""
            shift

            PROJECT_NAME="$1"
         ;;

         -t|--project-type)
            [ "$#" -eq 1 ] && usage "Missing argument to \"$1\""
            shift

            PROJECT_TYPE="$1"
         ;;

         --project-source-dir)
            [ "$#" -eq 1 ] && usage "Missing argument to \"$1\""
            shift

            PROJECT_SOURCE_DIR="$1"
         ;;

         --project-dialect)
            [ "$#" -eq 1 ] && usage "Missing argument to \"$1\""
            shift

            PROJECT_DIALECT="$1"
         ;;

         -*)
            log_error "Unknown option \"$1\""
            usage
         ;;

         *)
            break
         ;;
      esac

      shift
   done

   options_setup_trace "${MULLE_TRACE}"

   if [ -z "${MULLE_CASE_SH}" ]
   then
      # shellcheck source=mulle-case.sh
      . "${MULLE_BASHFUNCTIONS_LIBEXEC_DIR}/mulle-case.sh"      || return 1
   fi

   if [ -z "${PROJECT_NAME}" ]
   then
      PROJECT_NAME="`fast_basename "${PWD}"`" # could be nicer
   fi

   local PROJECT_IDENTIFIER
   local PROJECT_DOWNCASE_IDENTIFIER

   PROJECT_IDENTIFIER="`tweaked_de_camel_case "${PROJECT_NAME}"`"
   PROJECT_IDENTIFIER="`printf "%s" "${PROJECT_IDENTIFIER}" | tr -c 'a-zA-Z0-9' '_'`"
   PROJECT_DOWNCASE_IDENTIFIER="`tr 'A-Z' 'a-z' <<< "${PROJECT_IDENTIFIER}"`"

   [ -z "${PROJECT_SOURCE_DIR}" ] && internal_fail "PROJECT_SOURCE_DIR not set"
   [ -z "${PROJECT_DIALECT}" ] && log_warning "PROJECT_DIALECT not set"

   c_include "${PROJECT_NAME}" "${PROJECT_DOWNCASE_IDENTIFIER}"
   c_include_private "${PROJECT_NAME}" "${PROJECT_DOWNCASE_IDENTIFIER}"

   if [ "${PROJECT_DIALECT}" = "objc" ]
   then
      if [ "${OPTION_IMPORT}" = "DEFAULT" ]
      then
         OPTION_IMPORT="YES"
      fi

      objc_import "${PROJECT_NAME}" "${PROJECT_DOWNCASE_IDENTIFIER}"
      objc_import_private "${PROJECT_NAME}" "${PROJECT_DOWNCASE_IDENTIFIER}"
   fi
}


_init()
{
   if [ -z "${MULLE_BASHFUNCTIONS_LIBEXEC_DIR}" ]
   then
      MULLE_BASHFUNCTIONS_LIBEXEC_DIR="`mulle-bashfunctions-env "libexec-dir" 2> /dev/null`"
      [ -z "${MULLE_BASHFUNCTIONS_LIBEXEC_DIR}" ] && \
         echo "mulle-bashfunctions-env not installed" >&2 && \
         exit 1
   fi

   . "${MULLE_BASHFUNCTIONS_LIBEXEC_DIR}/mulle-bashfunctions.sh" "minimal" || exit 1
}

_init "$@"
main "$@"