#!/bin/bash
# This script sets your computer to wake at a given time (h)h:mm nearest in the
# future or recurrently for some general cron-formatted times. The computer is
# actually woken up 5 minutes earlier than specified, and a given script is set
# to run at the exact time given via crontab. This script must be run as root.
# Copyright (C) 2012 David Glass <dsglass@gmail.com>
# Copyright is GPLv3 or later, see /usr/share/common-licenses/GPL-3

# For only print next wakeup time
if [[ $1 == "-p" ]]; then
    only_print=true
    shift
fi

if [[ ! $only_print ]]; then
    if [[ `whoami` != "root" ]]; then
        echo "setalarm must be run as root"
        exit 1
    fi

    # Clear the system wakeup alarm.
    echo 0 > /sys/class/rtc/rtc0/wakealarm;

    # For delete alarms
    if [[ $1 == "-d" ]]; then
        (crontab -l | sed /^.*setalarm.*$/d) | crontab -
        exit 0
    fi
fi

# Check if correct number of arguments given
if [[ $1 == "-c" && $# -lt 6 && ! $only_print ||
      $1 == "-u" && ! $2 =~ [0-9]+ ||
      $1 != "-c" && $1 != "-u" && ! $1 =~ [0-9]{1,2}\:[0-9]{2} ]]; then
        echo "Usage: setalarm [-p] [hh:mm] [-u utc] [-c m h dom mon dow] [-o offset] [command]"
        echo "Asterisks may need to be surrounded by quotes"
        exit 1
fi
if [[ $1 != "-c" && $2 == "-o" || $1 == "-c" && $7 == "-o" || $1 == "-u"  && $3 == "-o" ]]; then
    if [[ $1 != "-c" && ! $3 =~ [0-9]+ || $1 == "-c" && ! $8 =~ [0-9]+ ]]; then
        if [[ $1 == "-u" && ! $4 =~ [0-9]+ ]]; then
            echo "Invalid offset value"
            exit 1
        fi
    fi
    if [[ $1 == "-c" ]]; then
        offset=$8
    elif [[ $1 == "-u" ]]; then
        offset=$4
    else
        offset=$3
    fi
    offset_set=true
else
    offset=5 #minutes (default)
fi

# Get wakeup script
if [[ ! $only_print ]]; then
    script_start=$(if [[ $1 == "-u" ]]; then echo 3; elif [[ $1 != "-c" ]]; then echo 2; else echo 7; fi)
    if [[ $offset_set ]]; then script_start=$(( script_start + 2 )); fi
    wakeup_script=${*:$script_start}
fi

# Check if an element exists in an array
function check_element
{
    for i in ${*:2}; do
        if [[ $i == $1 ]]; then echo 1; return; fi
    done
    echo 0
    return
}

# Read in cron input
function read_cron_input
{
    if [[ $4 == "Months" ]]; then
        input=$(echo "$1" | sed -e 's/Sat/6/ig' \
            -e 's/Jan/1/ig' -e 's/Feb/2/ig' -e 's/Mar/3/ig' -e 's/Apr/4/ig' \
            -e 's/May/5/ig' -e 's/Jun/6/ig' -e 's/Jul/7/ig' -e 's/Aug/8/ig' \
            -e 's/Sep/9/ig' -e 's/Oct/10/ig' -e 's/Nov/11/ig' -e 's/Dec/12/ig')
    elif [[ $4 == "Days" ]]; then
        input=$(echo "$1" | sed -e 's/Sun/0/ig' -e 's/Mon/1/ig' \
            -e 's/Tue/2/ig' -e 's/Wed/3/ig' -e 's/Thu/4/ig' -e 's/Fri/5/ig' )
    else
        input=$1
    fi
    # Check for bad input: exit only exits function, need to check for "" output
    if [[ $input =~ [^\-\,0-9\*] ]];  then exit 1; fi
    if [[ $input == "*" ]]; then
        seq $2 $3
    elif [[ $input =~ .*-.* ]]; then
        seq ${input/-/ }
    else
        echo $input | sed 's/,/\n/g' | sort -g
    fi
}




# for utc times
if [[ $1 == "-u" ]]; then
    wake_time=$2
    if [[ $only_print ]]; then
        echo $(( $wake_time - $offset * 60 ))
        exit
    fi
    crontab_time=`date -d @$wake_time "+%M %H %d %m %w"`
    newline="$crontab_time $wakeup_script >/dev/null 2>&1 #entered by setalarm"
    if [[ $wakeup_script != "" ]]; then
        (crontab -l | sed /^.*setalarm.*$/d; echo $newline) | crontab -
    fi
    
# for recurrent alarms
elif [[ $1 == "-c" ]]; then
    # Grab current date
    now_dom=$(date +%-d); now_mon=$(date +%-m); now_dow=$(date +%w)
    now_hr=$(date +%-H); now_min=$(date +%-M)
    
    # Read cron input
    mins=($(read_cron_input "$2" 0 59))
    hrs=($(read_cron_input "$3" 0 23))
    doms=($(read_cron_input "$4" $now_dom 31))
    mons=($(read_cron_input "$5" $now_mon 12 "Months"))
    dows=($(read_cron_input "$6" 0 6 "Days"))
    # check if all values are valid
    if [[ $mins == "" || $hrs == "" || $doms == ""
                      || $mons == "" || $dows == "" ]]; then
        echo "bad cron time entries" >&2; 
        exit 1;
    fi
    last_hr=${hrs[$(( ${#hrs[@]} - 1 ))]}
    last_min=${mins[$(( ${#mins[@]} - 1 ))]}


    # Get the alarm month/day
    function chkw # check if date occurs on an acceptable day of week
    {
        check_element $(date --date @$wake_time +%w) ${dows[@]}
    }
    years=($(date +%Y) $(( $(date +%Y) + 1 )))
    for y in ${years[@]}; do
        for i in ${mons[@]}; do
            for j in ${doms[@]}; do
                if [[ $(date -d $i/$j/$y +%s) -ge $(date -d $now_mon/$now_dom/${years[1]} +%s) ]]; then break; fi
                wake_time=$(date --date "$i/$j/$y $last_hr:$last_min:00" +%s);
                if [[ `chkw` != 1 ]]; then continue; fi
                date="$i/$j/$y"
                if [[ $wake_time > `date +%s` ]]; then break 3; fi
            done
            if [[ $4 == "*" ]]; then doms=$(seq 1 31); fi
        done
        if [[ $5 == "*" ]]; then mons=$(seq 1 $now_mon); fi
    done 2>/dev/null

    if [[ $date == "" || $wake_time < `date +%s` || `chkw` != 1 ]]; then
        echo "The specified cron time does not occur within a year"
        exit 1
    fi

    # Get the hour:minute
    if [[ $date == $(date +%-m/%-d/%Y) ]]; then
        mins=($(read_cron_input "$2" $now_min 59))
        hrs=($(read_cron_input "$3" $now_hr 23))
    fi
    for k in ${hrs[@]}; do
        for l in ${mins[@]}; do
            wake_time=$(date --date "$date $k:$l:00" +%s);
            if [[ $wake_time > `date +%s` ]]; then break 2; fi
        done
        if [[ $2 == "*" ]]; then mins=$(seq 0 59); fi
    done 2>/dev/null
    
    if [[ $only_print ]]; then
        echo $(( $wake_time - $offset * 60 ))
        exit
    fi
    
    # add cron for setlarm that runs wtih the $wakeup_script and @reboot
    newline1="$2 $3 $4 $5 $6 $0 ${*/\*/\"*\"} >/dev/null 2>&1\n"
    newline2="@reboot $0 ${*/\*/\"*\"} >/dev/null 2>&1"
    if [[ $wakeup_script != "" ]]; then
        newline3="\n$2 $3 $4 $5 $6 $wakeup_script >/dev/null 2>&1 #entered by setalarm"
    fi
    (crontab -l | sed /^.*setalarm.*$/d; echo -e "$newline1$newline2$newline3") | crontab -

# for non-recurring alarms
else
    # determine a wakeup time. add a day if time has already passed for today
    wake_time=$(date --date "$(date +%D) $1:00" +%s)
    if [[ $wake_time < `date +%s` ]]; then
        wake_time=$(( $wake_time + 86400 ));
    fi
    if [[ $only_print ]]; then
        echo $(( $wake_time - $offset * 60 ))
        exit
    fi
    # format $wakeup_time in crontab format.
    crontab_time=`date -d @$wake_time "+%M %H %d %m %w"`
    if [[ $wakeup_script != "" ]]; then
        newline="$crontab_time $wakeup_script >/dev/null 2>&1 #entered by setalarm"
        (crontab -l | sed /^.*setalarm.*$/d; echo $newline) | crontab -
    fi
fi

# Account for hardware clock vs UTC mismatch
hwc=$(cat /sys/class/rtc/rtc0/time) # hwclock time
hwc=${hwc:0:5}                      # (ignoring seconds)
tim=$(date -u +%H:%M)               # utc time
if [[ $hwc == $tim ]]; then
    # hardware clock is in utc. Do not adjust for time zone
    tz_offset=0
else
    # hardware clock is in local time, not utc. Adjust for tzoffset
    tz_offset=$(date +%z)
    tz_hr=${tz_offset:0:3}
    tz_mn=${tz_offset:0:1}${tz_offset:3:2}
    tz_offset=$(( $tz_hr * 3600 + $tz_mn * 60 ))
fi

# Actually set alarm #
echo $(( $wake_time - $offset * 60 + $tz_offset)) > /sys/class/rtc/rtc0/wakealarm; 
echo -e "Alarm set to: $(date -d @$wake_time)"
# Remind user about offset
if [[ $offset_set ]]; then echo "Computer will wake $offset minutes earlier"; fi
