#!/usr/bin/env bash

# detect_action $extension action extension_args "${extension_args[@]}"
detect_action()
{
  extensions_path=$1
  shift
  extension="${1:-core}"
  shift

  local token _word _action_path="$extensions_path/$extension/actions"

  local _params=("$@")

  #loop when params or .actions exists
  while (( ${#_params[@]} > 0 )) || file_exists "${_action_path}/.actions"
  do
    array_shift _params token

    # Detect function
    if  variable_is_nonempty token &&
      match "$token" "*\(\)"
    then
      _params=( "${token}" "${_params[@]}" )
      break

    # Detect a namespace
    elif variable_is_nonempty token &&
      path_exists "${_action_path}/${token}"
    then
      _action_path+="/${token}"

    # Detect action
    elif variable_is_nonempty token &&
      file_is_executable "${_action_path}/${token}"
    then
      _params=( "${token}" "${_params[@]}" )
      break

    # Detect mapping in .actions
    elif file_exists "${_action_path}/.actions" &&
         file_contains "${_action_path}/.actions" "^${token}="
    then
      _array=( $(hash_file "${_action_path}/.actions" ${token}) )
      _params=( "${_array[@]}" "${_params[@]}" )
      [[ "${_array[@]}" == "$token" ]] && break #break if detected 2nd time the same

    # Detect default mapping in .actions
    elif file_exists "${_action_path}/.actions" &&
         file_contains "${_action_path}/.actions" "^default="
    then
      _array=( $(hash_file "${_action_path}/.actions" default) )
      _params=( "${_array[@]}" "${token}" "${_params[@]}" )
      [[ "${_array[@]}" == "$token" ]] && break #break if detected 2nd time the same

    # Detect help mapping in .actions
    elif file_exists "${_action_path}/.actions" \
    && file_contains "${_action_path}/.actions" "^help=" \
    && (( ${#_params[@]} == 0 ))
    then
      _array=( $(hash_file "${_action_path}/.actions" help) )
      _params=( "${_array[@]}" "${token}" "${_params[@]}" )
      [[ "${_array[@]}" == "$token" ]] && break #break if detected 2nd time the same

    # finish searching path
    else
      _params=( "${token}" "${_params[@]}" )
      break
    fi
  done

  if array_is_empty _params
  then
    token=""
  else
    array_shift _params token
  fi

  # Detect function
  if variable_is_nonempty token &&
    match "${token}" "*\(\)"
  then
    action_path="${_action_path}"
    action="$token"
    extension_args=( "${_params[@]}" )

  # Detect a file
  elif variable_is_nonempty token &&
    file_is_executable "${_action_path}/${token}"
  then
    action_path="${_action_path}"
    action="$token"
    extension_args=( "${_params[@]}" )

  # Detect default if nothing else matches
  elif file_exists "${_action_path}/default"
  then
    action_path="${_action_path}"
    action="default"
    extension_args=( "$token" "${_params[@]}" )

  # Detect help if no default
  elif file_exists "${_action_path}/help" \
  && (( ${#_params[@]} == 0 )) \
  && [[ "$token" == "" ]]
  then
    action_path="${_action_path}"
    action="help"
    [[ "$token" != "" ]] && extension_args=("$token") || extension_args=()
    extension_args+=( "${_params[@]}" )

  else
    # Not found, error.
    return 1
  fi

  # Found, OK
  return 0
}

# find modules on path
detect_modules_paths()
{
  local root="$1" path="" IFS='/'
  shift
  while (( $# > 0 ))
  do
    if is_module "${root}${path}"
    then
      printf "${path##\/}=$*\n"
    else
      break
    fi
    path+="/$1"
    shift
  done
}

# search for extension that modules dependency providing matching [namespace...] action
detect_modules_action()
{
  trace_filter modules_action

  extensions_path=$1 extension="${2:-core}"
  shift 2 # TODO: proper error handling here...

  local token _word _modules_path="$extensions_path/$extension/modules/shell"

  local _params=("$@") _module_path _module_params _module_name

  # iterate modules from longest path
  for _module_definition in $(detect_modules_paths ${_modules_path} "$@" | sort -r)
  do
    _module_path="${_module_definition//=*}"
    _module_params="${_module_definition//*=}"
    if [[ -s "${_modules_path}/${_module_path}/modules" ]]
    then
      for _module_name in $( cat "${_modules_path}/${_module_path}/modules" )
      do
        if in_search_paths detect_module "${_module_name}"
        then
          if [[ -s "${module_path}/actions" ]]
          then
            local _params="" _action="${_module_params}" _read_action
            while [[ -n "${_action}" ]]
            do
              if file_contains "${module_path}/actions" "${_action}="
              then
                _read_action=$( hash_file "${module_path}/actions" "${_action//\//\/}" )
                if [[ -n "$_read_action" ]]
                then
                  action="$_read_action"
                  action_path="$extensions_path/$extension/actions/${_module_path// /\/}"
                  extension_args=( $_params )
                  return 0
                fi
              fi
              if [[ "$_action" =~ "/" ]]
              then
                _params="${_action##*/} $_params"
                _action="${_action%/*}"
              else
                break
              fi
            done
          fi
        fi
      done
    fi
  done
  return 1
}

detect_action_type()
{
  local _path="${action_path:-}"

  case "${action}" in
    (*\(\))
      action_type="function"
      return 0
      ;;
    *)
      _path_type="$(file "${_path}/${action}")"
      ;;
  esac

  case "${_path_type}" in

    *sh[[:space:]]script*|*POSIX[[:space:]]shell*|*Bourne-Again*)
      action_type="shell"
      ;;

    *ASCII*)
      # Launch with helper dsl, if possible.
      extension="${_path//.}"

      case "${extension}" in
        shell|zsh|bash|sh)
          action_type="shell"
          #rb) action_type="ruby"   ;;
          ;;
        *)
          read -r shebang < "${_path}"

          case "${shebang}" in
            *ruby|*rbx|*jruby|*macruby)
              binary="${shebang##*(#|!)}"
              binary="${binary##* }"
              action_type="ruby"
              ;;
            *)
              if [[ -x "${_path}" ]] ; then
                action_type="binary"
              else
                action_type="not executable"
              fi
              ;;
          esac
          ;;
      esac
      result=$?
      ;;

    cannot[[:space:]]open)
      action_type="dne"
      ;;

    *)
      if file_is_executable "${_path}"
      then
        action_type="binary"
      else
        action_type="noexec"
      fi
      ;;
  esac

  return $?
}

extension_modules_load()
{
  local _token _namespace=()

  module_name="${extension}"
  module_path="${extension_modules_path}/shell"
  module "$module_name" skip_path_detection

  while (( $# > 0 ))
  do
    _namespace+=( "$1" )
    shift

    module_name="$(path_join "${extension}" ${_namespace[*]})"
    module_path="$(path_join "${extension_modules_path}" "shell" ${_namespace[*]})"
    module "$module_name" skip_path_detection || true
  done
}

extension_run()
{
  trace_filter extension_run
  local _type="${action_type:-}" _path="${action_path:-}" _namespaces

  builtin cd "${initial_pwd}"

  # Now based on the determined _type we launch the extension.
  case "${_type}" in
    function)
      _namespaces=${_path##${extension_actions_path}}
      extension_modules_load ${_namespaces//\// }

      if command_exists "${action##_*}_cli"
      then
        "${action##_*}_cli"
      fi

      action_call ${action%%(*} "${extension_args[@]}"
      ;;

    shell)
      if [[ "${action##*/}" == help ]]
      then # Load the help module also.
        modules ext/help
      fi
      _namespaces=${_path##${extension_actions_path}}
      extension_modules_load ${_namespaces//\// }
      action_source "${_path}/${action}" "${extension_args[@]}"
      ;;

    ruby)
      requires=()
      for script in dsl initialize
      do
        requires+=( "-r${modules_path}/ruby/core/${script}.rb" )
      done

      "${binary:-ruby}" -I"${modules_path}/ruby" -I"${extension_modules_path}/ruby" \
        ${requires[@]} "${_path}/${action}"
      ;;
    # python|lua|javascript)
      #   ADD "${modules_path}/${_type}/" to the lib path so the script can require "bdsm"
      #  "${_path}"
      #  ;;
    dne|noexec)
      fail "Processing ${action} failed, file type is unknown, file does not exist or file is not executable."
      ;;
    binary|*)
      "${_path}/${action}" ${extension_action} ${extension_args[*]}
      ;;
  esac
}

#
# ## extension\_action()
#
# Load the environment for a given extension action and then call it.
# This is the main function for BDSM.
#
# ### Input Parameters
#
# One or more extension names.
#
# ### Stream Outputs
#
# None.
#
# ### Environmental effects
#
# The current extension's initialize file will be resourced into the current
# environment.
#
# ### Return Codes
#
# 0 for success
# 1 for failure
#
# ### Failure Scenarios
#
# Fails if no extension was given.
# Fails if no action was given.
#
# ### Usage Examples
#
#     $ cat $HOME/test
#     #!/usr/bin/env bash
#     source "/usr/local/bdsm/extensions/builtin/core/modules/shell/core/initialize" # Load BDSM framework core.
#     modules extensions
#     extension_action ext list
#
#     $  $HOME/test
#     bash fossil git libevent nginx p7zip postgresql rails redis srv tig tmux zeromq zlib
#
#
extension_action()
{
  if (( extension_action_calls > 0 ))
  then
    fail "can not call 2nd time\n  extension_action ${extension_args[@]}"
  fi

  extension_action_calls+=1

  unset extension action

  #TODO: Should be moved to core/cli?
  case "${extension_args[0]}" in
    *=*) # exact version specifier
      package_name="${extension_args[0]}"
      export package_version="${package_name##*=}"
      export package_name="${package_name%%=*}"
      export extension="${package_name}"
      extension_args[0]="${extension}"
      ;;
    *:*)
      package_name="${extension_args[0]}"
      export package_version="${package_name##*:}"
      export package_name="${package_name%%:*}"
      export extension="${package_name}"
      extension_args[0]="${extension}"
      ;;
  esac

  # Search in extensions actions
  if in_search_paths detect_action "${extension_args[@]}"
  then
    log_search action "$action" "${action_path##${bdsm_path}\/extensions\/} params:${extension_args[@]}"

  # Search in loaded modules actions
  elif in_search_paths detect_modules_action "${extension_args[@]}"
  then
    log_search action "$action" "${action_path##${bdsm_path}\/extensions\/} params:${extension_args[@]}"

  else
    error "Unknown action path: "${extension_args[@]}"\n\nUsage: \n  bdsm extension [namespace] [action]\n"
  fi

  extension_path="$extensions_path/$extension"
  extension_config_path="$extension_path/config"
  extension_templates_path="$extension_path/templates"
  extension_modules_path="$extension_path/modules"
  extension_actions_path="$extension_path/actions"
  extension_log_path="$log_path/$extension"

  paths=$(env | awk -F= -v ORS=' ' '/_path/{print $1}')
  flags=$(env | awk -F= -v ORS=' ' '/_flag/{print $1}')

  enter "${extension_path}"
  bdsm_exports
}

extension_path()
{
  local path extension=$1
  for path in ${extensions_search_paths[@]}
  do
    if [[ -d "${path}/${extension}/actions" || -d "${path}/${extension}/modules" ]]
    then
      extension_path="${path}/${extension}"
      return 0
    fi
  done
  return 1
}

#
# ## extension\_reload()
#
# Reloads (re-sources) the current extension's DSL and initialization files.
#
# ### Input Parameters
#
# None.
#
# ### Stream Outputs
#
# None.
#
# ### Environmental effects
#
# The current extension's DSL and initialization files are re-sourced into the
# calling environment.
#
# ### Return Codes
#
# 0 for success
#
# ### Failure Scenarios
#
# No current failure scenarios.
#
# ### Usage Examples
#
#     user$ extension_reload
#
extension_reload()
{
  local _path="${extension_modules_path}/shell"

  source_files "${_path}/dsl" "${_path}/initialize"
}

#
# ## extension\_reinitialize()
#
# Reinitializes the current extension.
#
# ### Input Parameters
#
# None.
#
# ### Stream Outputs
#
# None.
#
# ### Environmental effects
#
# The current extension's initialize file will be resourced into the current
# environment.
#
# ### Return Codes
#
# 0 for success
#
# ### Failure Scenarios
#
# No failure scenarios currently.
#
# ### Usage Examples
#
#     user$ extension_reinitialize
#
extension_reinitialize()
{
  local _path="${extension_modules_path}/shell"

  source_files "${_path}/initialize"
}

#
# ## extension\_version()
#
# Outputs the named extension's version
#
# ### Input Parameters
#
# First parameter must be the name of an extension.
#
# ### Stream Outputs
#
# The extension-version string for the named extension.
#
# ### Environmental effects
#
# None.
#
# ### Return Codes
#
# 0 for success
#
# ### Failure Scenarios
#
# Fails if the extension name is not given.
#
# ### Usage Examples
#
#     user$ extension_reinitialize
#
extension_version()
{
  local _extension="${1:-}"

  if variable_is_nonempty _extension
  then
    shift
    true ${extension_path:="${extensions_path}/${_extension}"}

    if file_exists "${extension_path}/VERSION"
    then
      read -r extension_version < "${extension_path}/VERSION"
    else
      extension_version="head"
    fi

    log "${extension}-${extension_version}"
  else
    fail "Can not retrieve extension version as no extension was given."
  fi
}

#
# ## extension\_licence()
#
# Emits the extension's license file, if it exists.
#
# ### Input Parameters
#
# First parameter may optionally be an extension name.
#
# ### Stream Outputs
#
# If the extension has a LICENSE file then it wil be printed to STDOUT.
#
# ### Environmental effects
#
# None.
#
# ### Return Codes
#
# 0 for success.
#
# ### Failure Scenarios
#
# No failure scenarios currently exist.
#
# ### Usage Examples
#
#     $ cat $HOME/test
#     !/usr/bin/env bash
#     source "/usr/local/bdsm/extensions/builtin/core/modules/shell/core/initialize" # Load BDSM framework core.
#     modules extensions
#     extension_license postgresql
#
#     $ $HOME/test
#     Copyright (c) 2009-2011 Wayne E. Seguin
#
#     Licensed under the Apache License, Version 2.0 (the \"License\");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an \"AS IS\" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
extension_license()
{
  local _extension="${1:-${extension}}"

  if variable_is_nonempty _extension
  then
    cat -v "${extensions_path}/${_extension}/LICENSE"
  else
    fail "Can not display extension license as an extension was not given."
  fi
}

#
# ## extension\_actions()
#
# Lists actions exposed by the named extension.
#
# ### Input Parameters
#
# First parameter is the name of the extension to list actions for.
#
# ### Stream Outputs
#
# The names of the extensions in the given directory are printed to the
# calling environment's STDOUT.
#
# ### Environmental effects
#
# None.
#
# ### Return Codes
#
# 0 for success.
#
# ### Failure Scenarios
#
# Fails if no extension name is given.
#
# ### Usage Examples
#
#      $ cat $HOME/test
#      #!/usr/bin/env bash
#      source "/usr/local/bdsm/extensions/builtin/core/modules/shell/core/initialize" # Load BDSM framework core.
#      modules extensions
#      extension_actions "postgresql"
#
#      $ $HOME/test
#      backup configure help initialize install restart start status stop uninstall upgrade
#
#
#
extension_actions()
{
  local _extension="${1:-}" _files _file _actions=()

  if variable_is_nonempty _extension
  then
    if path_exists "${extension_actions_path}"
    then
      _files=($(
      find "${extension_actions_path}" -mindepth 1 -type f -perm -o+rx
      ))

      for _file in "${_files[@]}"
      do
        _actions+=("${_file#${extension_actions_path}}")
      done
      _actions="${_actions[@]}" # Convert from array to string.
      printf "${_actions}"
    else
      return 0
    fi
  else
    fail "Cannot list actions for extension as no extension was given."
  fi
}

#
# ## extension\_install()
#
# Installs the given external extension.
#
# ### Input Parameters
#
# First parameter is the name of the extension to install
# Second parameter is the src_path of the extension to install
#
# ### Stream Outputs
#
# None.
#
# ### Environmental effects
#
# The named extension will be installed to the filesystem in the BDSM
# external extension directory.
#
# ### Return Codes
#
# 0 for success
#
# ### Failure Scenarios
#
# Fails if no extension are passed in to install.
#
# ### Usage Examples
#
#     user$ extension_install backup mpapis_bdsm_backup
#
extension_install()
{
  local _extension="${1:-}" _src
  _src="${2:-${_extension}}"

  if variable_is_nonempty _extension
  then
    if extension_is_valid "${extensions_src_path}/${_src}"
    then
      log "Installing ${_extension}"
      if path_exists "${bdsm_path}/extensions/external/${_extension}"
      then
        remove_paths "${bdsm_path}/extensions/external/${_extension}"
      fi

      copy_directory "${extensions_src_path}/${_src}" \
        to "${bdsm_path}/extensions/external/${_extension}"

      write "${extension_uri}" to "${bdsm_path}/extensions/external/${_extension}/.uri"

      #
      # TODO: Process extension dependencies...
      #
    else
      error "${extensions_src_path}/${_src} is not a valid extension"\
        "as it is missing VERSION and/or actions/help files."
    fi
  else
    fail "Cannot install an extension as no extension name was given."
  fi
}

extension_is_valid()
{
  local _path="$1"

  if [[ -n "${_path}" ]]
  then
    # TODO: Add all extension requirement checks here.
    [[ -d "${_path}/actions" ]] || [[ -d "${_path}/modules" ]]
  else
    fail "Cannot determine if an extension is valid as the path to the extension was not given."
  fi
}

extension_package()
{
  local _name="${1:-}" _path _version file

  if [[ -n "${_name}" ]]
  then
    _path="${extensions_development_path:-"$extensions_src_path"}"

    ensure_paths_exist "${_path}"
    enter "${_path}"

    if extension_is_valid "${_path}/${_name}"
    then
      _version="$(cat "${_name}/VERSION")"
      log "Packaging extension ${_name}"
      log "${_name}-${_version}:"

      for archiver in "gzip" "bzip2 -z" "xz -z"
      do
        if command_exists ${archiver// *}
        then
          if tar cf "${_name}-${_version}.tar" "${_name}/"
          then
            # TODO: parallelize this.
            ${archiver} -f -9 "${_name}-${_version}.tar"
          else
            error "There was an error ($?) while trying to create a tar of the '${_name}' directory while packaging."
          fi
        fi

        ensure_paths_exist "${_path}/pkg"

        for file in "${_name}-${_version}".tar.*
        do
          log "  - ${file}"
          write "$( file_md5 "${file}" )" to "${file}.md5"
          log "  - ${file}.md5"
          move_files --force "${file}" "${file}.md5" \
            to "${_path}/pkg" from "${_path}"
        done
      done
      log "Packaging complete (packages are located in '${_path}/pkg' )"
    else
      error "Cannot package extension '${_name}' as the extension is not valid."
    fi
  else
    fail "Cannot package an extension as no extension name was given."
  fi
}

extension_publish()
{
  local _name="${1:-}" _path _version

  NIY "Extension publishing to extensions.beginrescueend.com has not yet been implemented."

  if [[ -n "${_name}" ]]
  then
    _path="${extensions_development_path:-"$extensions_src_path"}"

    enter "${_path}"

    if extension_is_valid "${_path}/${_name}"
    then
      _version="$(cat "${_name}/VERSION")"
      log "Packaging extension ${_name}"
      log "${_name}-${_version}:"

      for formats in ".tar.gz" ".tar.bz2" ".tar.xz"
      do
        if path_exists "${_path}/pkg/"
        then
          log "publishing ${_name}-${_version}"

          for file in "${_pth}/${_name}-${_version}".tar.{gz,xz,bz2}
          do
            log "  - ${file}"
            log "  - ${file}.md5"
            NIY "publishing"
          done
        else
          error "The local package path '${_path}' does not exist, did you run"\
            "'extension package ${_name}' before 'extension publish ${_name}'?"
        fi
      done
      log "Packaging complete (packages are located in '${_path}/pkg' )"
    else
      error "Cannot package extension '${_name}' as the extension is not valid."
    fi
  else
    fail "Cannot publish an extension as no extension name was given."
  fi
}

extension_module_add()
{
  local _extension="${1}" _module="${2}" _path
  _path="${3:-"${extensions_development_path}/${_extension}"}"

  if ! file_contains "${_path}/modules/shell/modules" "${_module}"
  then
    write "\n${_module}" append to "${_path}/modules/shell/modules"
  fi
}
