#!/bin/bash
########################################################################
# yruba -- whY RUles for BAsh?
#   ... because it's cool!
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation
# Foundation, Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA.
#
# (C) 2005, 2006 Harald Kirsch
########################################################################
set -e				# stop on any error

yruba_me="${0##*/}"
yruba_version=2.9

########################################################################
# Functions that are helpful in built tasks
########################################################################

########################################################################
# old t1 [...] -d [d1 [...]]
#
# returns true if any of the file targets t1 [...] is older than (test
# -ot) any of the dependency files listed after -d. 
#
# Option -d must be present. If no dependency is given, false is
# returned, meaning the target(s) are not old, i.e. up-to-date. If any
# of the dependencies does not exist as a file (test -f) yruba exits
# with an error message. If any of the targets does not exist as a
# file, true is returned (this is a feature of 'test -ot').
#
old() {
  local x
  local tlist=""
  while [ "$#" -gt 0 ]; do
    x="$1"; shift
    if [ "$x" = "-d" ]; then break; fi
    tlist="$tlist '${x//\'/\'\\\'\'}'"
  done
  if [ "$x" != "-d" ]; then
    die ${FUNCNAME}: called for "'"$(eval echo "$tlist")"'" \
          but required option -d is missing
  fi

  ## Due to the double loop, we would need two "$@", one for the
  ## target list and one for the dependency list. I try to simulate
  ## this here, but it is a bit of a hack, because it relies on the
  ## fact that once the loop is started, "$@" can safely be changed
  ## without influencing the loop.
  local target
  local d
  local dlist=$(lcreate "$@")
  local first=true
  eval set -- "$tlist"
  for target in "$@"; do
    $first && eval set -- "$dlist"
    first=false
    for d in "$@"; do 
      test -e "$d" || die ${FUNCNAME}: dependency "'$d'" does not exist
      if [ "$target" -ot "$d" ]; then
        dlog "  '$target'" older than "'$d'"
        return 0
      fi
    done
  done
  return 1
}
haveClass() {
  local class="$1"
  local c="x$$"
  local f=/tmp/$c.java
  echo "class $c { $class y; }" >$f
  if ${JAVAC:-javac} -d /tmp $f 1>/dev/null 2>/dev/null; then
    local r=0
  else 
    local r=1;
  fi
  rm -f $f /tmp/$c.class
  return $r
}
#map filenames to other directory and change extension
mapFilenames() {
  local headlen="${#1}"
  local newhead="$2"
  local newext="$3"
  shift 3
  local result=""
  local fname=""
  if [ . = "$newext" ]; then 
    doext=false
    newext=''
  else
    doext=true
    test -z "$newext" || newext=".$newext"
  fi
  for fname; do
    eval fname='${fname:'"$headlen"'}'
    $doext && fname="${fname%.*}"
    fname="${newhead}${fname}${newext}"
    #use of lappend would be cleaner, but its deadly slow
    fname=${fname//\'/\'\\\'\'}
    result="$result '$fname'"
  done
  echo "$result"
}
die() {
  echo 1>&2 ${yruba_me}: "$@"
  exit 1
}
dlog() {
  $yruba_debug || return 0
  local n=""
  if [ "$1" = "-n" ]; then n=-n; shift; fi
  echo $n "${yruba_indent}$*"
}
mapTarget() {
  local pattern="$1"
  local ttag="$2"
  yruba_mapped=$(yruba_put "$yruba_mapped" "$ttag" "$pattern")
  yruba_tagmap="$pattern) yruba_ttag=\"$ttag\" ;; ${yruba_tagmap}"
}
########################################################################
#
# A set of functions to simulate an associative array. The associative
# array is stored as a string in any variable. Both, key and value are
# encoded not to contain single vertical bars. Instead vertical bars
# are encoded as "|." A key/value pair is encoded like "||key|=value"
# and in addition, the whole string is terminated by "||".
#
yruba_encode() {
  local p=${1//|/|.}
  echo "$p"
}
yruba_decode() {
  local p=${1//|./|}
  echo "$p"
}
#####
# usage: yruba_remove map key
#   removes from the given map the entry for key and returns the result
yruba_remove() {
  local table=$1
  local key=$(yruba_encode "$2")
  local head=${table%||$key|=*}
  if [ "$head" = "$table" ]; then echo "$table"; return; fi
  local tail="${table#*||$key|=*||}"
  echo "${head}||${tail}"
}
#####
# usage: yruba_get mapname key [default]
yruba_get() {
  # usage: yruba_get mapname
  local table=${1}
  local key=$(yruba_encode "$2")
  local dflt=$3

  local tail="${table#*||$key|=}"
  if [ "$table" = "$tail" ]; then echo "$dflt"; return; fi
  local value=$(yruba_decode "${tail%%||*}")
  echo "$value"
}
#####
# usage: yruba_put map key value
#   enters the key/value pair into the map represented by "$1" and
#   returns the resulting map representation.
yruba_put() {
  local map=$1
  if [ -z "$map" ]; then 
    map='||'; 
  else 
    map=$(yruba_remove "$map" "$2")
  fi
  local key=$(yruba_encode "$2")
  local value=$(yruba_encode "$3")
  echo "||$key|=${value}$map"
}
#####
# usage: mapname
#   Returns a list in the sense of lcreate, lget, etc. that contains
#   all the keys in the given map.
#   Example: keys=$(yruba_keys "$map")
yruba_keys() {
  test -z "$table" && table="||"
  local table=$1
  local result=""
  local l
  while [ "$table" != "||" ]; do
    local key=$(yruba_decode "${table%%|=*}")
    result=$(lappend "$result" "${key:2}")
    table="||${table#||*||}"
  done
  echo "$result"
}
########################################################################
# Simple list functions.
# A list is maintained as a single string that contains proper quoting
# such that for a list l the command
#   eval set "$l"
# results in the positional parameters to be set to the list elements.

# Creates a list from all its arguments and returns it.
#   list=$(lcreate elem1 [...])
lcreate() {
  local result=""
  local x tmp
  local sep=""
  for x in "$@"; do
    tmp=${x//\'/\'\\\'\'}
    s="${s}${sep}'$tmp'"
    sep=" "
  done
  echo "$s"
}

# Sticks an element in front of a list and returns the list.
#   list=$(lpush elem "$list")
lpush() {
  local top=${1//\'/\'\\\'\'}
  (test -z "$2" && echo "'$top'") || echo "'$top' $2"
}

# Appends an element to the end of a list and returns the list.
#  list=$(lappend "$list" elem)
lappend() {
  local ele=${2//\'/\'\\\'\'}
  (test -z "$1" && echo "'$ele'") || echo "$1 '$ele'"
}
# Returns the first element of the list
#  top=$(lhead "$list")
lhead() {
  eval set -- "$1"
  if [ "$#" = 0 ]; then
    echo 1>&2 lhead: list empty
    return 1
  fi
  echo "$1"
}

# Returns the list after removing the first element.
#  list=$(ltail "$list")
ltail() {
  eval set -- "$1"
  if [ "$#" = 0 ]; then
    echo 1>&2 ltail: list empty
    return 1
  fi
  shift
  lcreate "$@"
}

# Returns the i'th element of a list, where i counts from zero. If
# the index is to large, the empty string is returned.
#  value=$(lget "$list" 3)
lget() {
  local i=$(($2+1))
  eval set -- "$1"
  if [ "$#" = 0 ]; then
    echo 1>&2 lget: list empty
    return 1
  fi
  if [ $i -le 0 -o $i -gt "$#" ]; then
    echo 1>&2 lget: index "$((i-1))" out of range 0.."$(($#-1))"
    return 1
  fi
  echo "${!i}"
}
# Returns the number of elements in the list.
llength() {
  eval set -- "$1"
  echo "$#"
}
########################################################################
# store one-line documentation for targets
ydoc() {
  local target=$1; shift
  local text=$*
  YRUBA_DOCS=$(yruba_put "$YRUBA_DOCS" "$target" "$text")
}
########################################################################
yruba_savecmd() {
  local yruba_here=$(pwd)
  "$@"
  local retval=$?
  cd "$yruba_here"
  return "$retval"
}
########################################################################

yruba_makeTarget() {
  local yruba_t="$1"

  ## map the target through pattern matching to a target tag that is
  ## then used to find dependencies, test and command for this target
  local yruba_ttag
  eval "case \"$1\" in $yruba_tagmap *) yruba_ttag=\"$1\" ;; esac"

  # did we work on this target before already, then just return
  if [ $(yruba_get "$yruba_done" "$yruba_t" 0) -eq 1 ]; then
    dlog have seen "'$yruba_t'" already
    return 0
  fi

  dlog making target "'$yruba_t'"

  # is t being made currently? That would be a bug
  test $(yruba_get "$yruba_active" "$yruba_t" 0) -eq 1 \
    && die "target '$yruba_t' has dependency loop: ${yruba_stack}"
  yruba_active=$(yruba_put "$yruba_active" "$yruba_t" 1)
  yruba_stack=$(lpush "$yruba_t" "$yruba_stack")

  if $yruba_sdeps; then echo "${yruba_indent}$yruba_t"; fi

  # make all dependencies for this target
  local yruba_cmd="dep_$yruba_ttag"
  local yruba_deps=""
  eval set --       # "$@" will be the dependency list
  if type -p "$yruba_cmd" >/dev/null; then
    yruba_deps=$("$yruba_cmd" "$yruba_t")
    eval set -- "$yruba_deps"
    yruba_make "$yruba_t" "$@"
  fi

  if ! $yruba_sdeps; then 

    # prepare to run the test for this target, if there is one,
    # otherwise default to true
    yruba_cmd="test_$yruba_ttag"
    if type -p "$yruba_cmd" >/dev/null; then
      dlog running "'$yruba_cmd' on '$yruba_t'" $yruba_deps
    else 
      yruba_cmd=true
    fi

    # run the test and eventually the command

    if yruba_savecmd "$yruba_cmd" "$yruba_t" "$@"; then
      yruba_cmd="cmd_$yruba_ttag"
      if type -p "$yruba_cmd" >/dev/null; then
        $yruba_debug || $yruba_quiet || echo "+ $yruba_t"
        dlog executing: $(lcreate "$yruba_cmd" "$yruba_t" "$@")
        yruba_savecmd "$yruba_cmd" "$yruba_t" "$@"
      elif ! test -e "$yruba_t"; then
        die no command to make "'$yruba_t'" and file does not exist
      fi
    else
      $yruba_debug || $yruba_quiet || echo "- $yruba_t"
      dlog no need to run: "'cmd_$yruba_ttag' '$yruba_t'"
    fi
  fi

  # clean up the stack, this target is finished completely
  yruba_stack=$(ltail "$yruba_stack")
  yruba_active=$(yruba_remove "$yruba_active" "$yruba_t")
  yruba_done=$(yruba_put "$yruba_done" "$yruba_t" 1)

  #echo '------------------------------------'
  #set | egrep '^[a-z_]+='

}
########################################################################
yruba_make() {
  local -r yruba_target="$1"
  shift

  if [ "$#" -eq 0 ]; then return; fi

  dlog "considering dependencies of '$yruba_target':" $(lcreate "$@")
  yruba_indent="  $yruba_indent"

  local yruba_t
  for yruba_t in "$@"; do
    yruba_makeTarget "$yruba_t"
  done
  yruba_indent="${yruba_indent:2}"
}
########################################################################
yruba_usage() {
  cat <<EOF
usage: ${yruba_me} [-find] [-d] [-q] [-x] [-h] [-f yrules] target [...]
      version $yruba_version --- (C) 2005, 2006-2009 Harald Kirsch
       
  Make the given target(s) according to the rules in yrules.
  -f: specifies rules file, default is yrules
  -d: show detailed course of reasoning
  -i: list information for targets as defined with ydoc
  -g: only show full dependency list for targets given
  -q: quiet, show only error messages
  -x: set option -x of the shell
  -find: find yrules file by walking up the directory hierarchy
EOF
  exit 1
}
########################################################################

trap 'die exit due to error' ERR
yruba_rulefile=yrules
yruba_indent=""
yruba_quiet=false
yruba_debug=false
yruba_find=false
yruba_info=false
yruba_sdeps=false
YRUBA_DOCS="||"                 # initialize as map

while [ "$#" -gt 0 ]; do
  case "$1" in
  -f) test "$#" -gt 1 || die missing parameter for option -f
    ## make sure rulefile has a slash to avoid search along PATH
    if [ "${2#*/}" = "$2" ]; then yruba_rulefile="./$2";
    else yruba_rulefile="$2"; fi
    shift
    ;;
  -find) yruba_find=true
    ;;
  -d) yruba_debug=true
    ;;
  -i) yruba_info=true
    ;;
  -g) yruba_sdeps=true
    ;;
  -q) yruba_quiet=true
    ;;
  -x) set -x
    ;;
  -h|--help|--h) yruba_usage
    ;;
  -*) echo -e $yruba_me: unknown option \"$1\"'\n' ; yruba_usage
    ;;
  *) yruba_targets=$(lappend "$yruba_targets" "$1")
    ;;
  esac
  shift
done

## if so requested walk up the directory hierarchy until we find the
## specified rules file
if $yruba_find; then 
  while ! [ -f "$yruba_rulefile" -o "$PWD" = / ]; do cd ..; done
  pwd
fi

. "$yruba_rulefile"

## check for each ydoc entry if a cmd_* exists and for -i print the
## resulting documentation strings
eval set -- $(yruba_keys "$YRUBA_DOCS")
for x in "$@"; do 
  if [ "$(type -t "cmd_${x}")" = "" ]; then
     echo 1>&2 "$yruba_me: '$x' documented but 'cmd_$x' does not exist"
     continue
  fi
  if ! $yruba_info; then continue; fi

  ## if the target is mapped from a pattern, we rather print the
  ## pattern than the target in the info string
  y=$(yruba_get "$yruba_mapped" "$x")
  if ! [ -z "$y" ]; then t="$y"; else t="$x"; fi
  ## fetch the doc into a variable first because the function call
  ## in quotes as param of echo is nasty syntax
  y=$(yruba_get "$YRUBA_DOCS" "$x")
  echo "$t" -- "$y"
done
unset t x

if $yruba_info; then exit 0; fi

if [ -z "$yruba_targets" ]; then
  if [ -z "$defaultTarget" ]; then die no target given; fi
  yruba_targets=$(lcreate "$defaultTarget")
fi  

eval set -- "$yruba_targets"
yruba_make '#top' "$@"

