#!/bin/bash
#---------------------------------------------------------------------
##
## @Synopsis Set of functions used by gaze for time calculations
## @Copyright (C) 2008-2014 The Source Mage Team <http://www.sourcemage.org>
##
## This file holds various statistical functions and an interface to
## the activity log for getting the input data.
#---------------------------------------------------------------------

#---------------------------------------------------------------------
##
## Computes all the casting times of the passed spell by inspecting
## the activity log.
##
## @param spell
## @param version (optional)
##
## @Stdout casting time(s)
#---------------------------------------------------------------------
function compute_cast_times() {
  awk -v spell=$1 -v version=$2 '
    # log timestamps are in the "%Y%m%d:%H%M\(%z\)" format (20080625:0853(+0000))
    # we need them in "%Y %m %d %H %M %S" and %s (epoch time). We ignore the
    # timezone, since the time is stored in UTC and %z is always +0000
    function since_epoch(time,      date) {
      # The date:
      date = substr(time,1,4) " " substr(time,5,2) " " substr(time,7,2)
      # The time (use 00 for seconds):
      date = date " " substr(time,10,2) " " substr(time,12,2) " 00"
      return mktime(date)
    }

    /^.*\tcast\t.*\t.*\t.*\t.*$/ {
      # check the spell and version manually - literally
      if ($3 != spell) next
      if (version != "" && $4 != version) next

      # check all valid start/succes pairs
      if ($5 == "start") {
        start_time = $1
        start_version = $4
      }
      if ($5 == "success" && start_time != 0 && start_version == $4) {
        succes_time = $1
        print since_epoch(succes_time)-since_epoch(start_time)
        start_time = 0
      }
  }' $ACTIVITY_LOG
}

#---------------------------------------------------------------------
##
## Display the time in seconds a spell took to compile and install.
## @param spell
## @Stdout cast time of spell in seconds
#---------------------------------------------------------------------
function compute_cast_time() {
  # FIXME: make the default configurable
  local spell=$1
  local type=${2:---last}

  compute_cast_times $spell |
  case $type in
    --last)
      tail -n 1 ;;
    --median)
      compute_median ;;
    --mean)
      compute_mean ;;
    --weigh-last)
      compute_weighted_mean last-cast $(private_installed_version $spell) ;;
  esac
}

#---------------------------------------------------------------------
##
## Display the time in seconds a spell took to compile and install.
## All known algorithms are used (see compute_cast_time). Mean estimates
## also show the estimation error.
##
## @param spell
## @param verbosity 0-machine readable/quiet
## @Stdout pretty-printed cast times of spell
## @return 1 if there are no valid times available
## @return 0 otherwise
#---------------------------------------------------------------------
function compute_all_cast_times() {
  local spell=$1 verbosity=$2
  local times time

  times=$(compute_cast_times $spell)
  [[ -z $times ]] && return 1

  if [[ $verbosity == false ]]; then
    echo -n $spell:
    echo "$times" | tail -n 1
    echo -n $spell:
    echo "$times" | compute_median
    echo -n $spell:
    echo "$times" | compute_mean
    echo -n $spell:
    echo "$times" | compute_weighted_mean last-cast $(private_installed_version $spell)
    echo -n $spell:n $(wc -l <<< "$times")
    echo
  else
    message -n "Last cast time of ${SPELL_COLOR}$spell$DEFAULT_COLOR: "
    time=$(echo "$times" | tail -n 1)
    pretty_print_time $time

    message -n "Median cast time of ${SPELL_COLOR}$spell$DEFAULT_COLOR: "
    time=$(echo "$times" | compute_median)
    pretty_print_time $time

    message -n "Mean cast time of ${SPELL_COLOR}$spell$DEFAULT_COLOR: "
    time=$(echo "$times" | compute_mean)
    pretty_print_time $time

    message -n "Weighted mean cast time of ${SPELL_COLOR}$spell$DEFAULT_COLOR: "
    time=$(echo "$times" | compute_weighted_mean last-cast $(private_installed_version $spell))
    pretty_print_time $time

    message "Number of samples: $(wc -l <<< "$times")"
    echo
  fi

  return 0
}
#---------------------------------------------------------------------
##
## Computes the mean of the passed arguments and its standard error
##
## @Stdin  numbers separated by newlines
## @Stdout mean and its error
#---------------------------------------------------------------------
function compute_mean() {
  awk '
    {
      sum += $0
      numbers[NR] = $0
    }
    END {
      if (NR == 0) exit
      mean = sum / NR
      if (NR == 1) {
        error = 0
      } else {
        variance = 0
        for (i in numbers)
          variance += (numbers[i] - mean) ^ 2
        error = int(sqrt(variance / (NR-1) / NR) * 1.96 + .5);
      }
      print int(mean + .5), error
    }'
}

#---------------------------------------------------------------------
##
## Computes the weighted mean of the passed and some more arguments.
## WARNING: only usable for cast times!
##
## @param type - weigh either just the times of the latest version
##
## @Stdin  numbers separated by newlines
## @Stdout mean value
#---------------------------------------------------------------------
function compute_weighted_mean() {
  local type=$1
  local version=$2

  if [[ $type == last-cast ]]; then
    local weight=10 #make this relative someday?
    local i command
    for ((i=1; i < $weight; i++)); do
      command="p; $command"
    done
    {
      # weigh the latest version by adding it another weight-1 times
      # to the piped list of casting times of all versions
      cat -
      compute_cast_times $spell $version | sed -n "$command"
    } | compute_mean
  fi
}

#---------------------------------------------------------------------
##
## Computes the median of the passed arguments
##
## @Stdin  numbers separated by newlines
## @Stdout median value
#---------------------------------------------------------------------
function compute_median() {
  sort -n | awk '
    { n[NR] = $0 }
    END {
      if (NR > 1) {
        m = n[1 + int(NR / 2)]
        print (NR % 2) ? m : (m + n[NR / 2]) / 2
      } else {
        print n[0]
      }
    }
  '
}

#---------------------------------------------------------------------
##
## Computes the combined error of independent variables
##
## @param error for each variable
## @param ...
## @Stdout total error
#---------------------------------------------------------------------
function compute_total_error() {
  tr ' ' '\n' <<< "$@" |
  awk '
    { total += $0 ^ 2 }

    END {
      print int(sqrt(total))
    }'
}

#---------------------------------------------------------------------
##
## Converts a time in seconds and the estimation error (if available)
## into a human readable format
##
## @param time in seconds
## @param estimation error in seconds (optional)
## @Stdout time (DD:HH:MM or HH:MM or MM or "less than a minute")
#---------------------------------------------------------------------
function pretty_print_time() {
  if [[ -z $2 ]]; then
    pretty_print_time_sub "$@"
  else
    local time=$(pretty_print_time_sub $1)
    echo -n "$time "
    pretty_print_time_error $2
  fi
}

#---------------------------------------------------------------------
##
## Converts a time in seconds into a human readable format.
##
## @param time in seconds
## @Stdout time (DD:HH:MM or HH:MM or MM or "less than a minute")
#---------------------------------------------------------------------
function pretty_print_time_sub() {
  # date can only print day of year starting with 1, so we can't use it,
  # yet we have to display the number of days, since the total can be big
  awk -v sum=$1 'END {
    days = int(sum/3600/24)
    sum -= days * 3600 * 24
    hours = int(sum/3600)
    sum -= hours * 3600
    minutes = int(sum/60)
    # sum is now < 3600

    if (sum < 60 && days == 0 && hours == 0) {
      print "less than a minute."
      exit
    }
    if (days > 0) {
      print days "d " hours "h " minutes "m"
    } else if (hours > 0) {
      print hours "h " minutes "m"
    } else {
      print minutes "m"
    }
  }' /dev/null
}

#---------------------------------------------------------------------
##
## Converts a time estimation error into a human readable format.
## If the error is very small or 0, it isn't shown.
##
## @param estimation error in seconds
## @Stdout time +/- error (see pretty_print_time)
#---------------------------------------------------------------------
function pretty_print_time_error() {
  local error=$1

  if (( $error < 60 )); then
    error=0
  else
    error=$(pretty_print_time_sub $error)
  fi

  if [[ $error == 0 ]]; then
    echo
  else
    echo "(+/- $error)"
  fi
}

#---------------------------------------------------------------------
## @License
##
## This software 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 software 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 software; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
##
#---------------------------------------------------------------------
