#!/usr/bin/perl -w

#
# TCTP Training Generator for Suunto Ambit/Ambit2/Ambit3
#

# Copyright (c) 2014, Petr Holub (<hopet@ics.muni.cz>)
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer. 
# 2. 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.
# 
# 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 OWNER 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.


# This is a generator that allows you to create applications for Suunto
# Ambit watches, somewhat similar to http://mytrainingplan.eu/ or 
# http://www.seckle.ch/tools/suunto-ambit-apps/time-interval-app/
# thus overcoming the limitations of Ambit/Ambit2/Ambit3 watches regarding
# more complex interval training sessions.
#
# Compared to the other apps, it is designed to work with great Carmichael's
# Time-Crunched Cyclist training methodology, i.e., it is more flexible
# to allow for overlapping zones. It also allows very compact notation
# of complex intervals, such as 
# 20mEM+3x(3x(2mPFPI+2mRBI)+8mRBS)+30mEM+4x3mO1U2+15mEM
# which equals to a training with
# - 30 minutes of Endurance Miles
# - 3x sets of 3x 2 minutes Peak and Fade Power Intervals with 2 minutes
#   Rest Between Intervals and 8 minutes Rest Between Sets
# - 30 minutes of Endurance Miles
# - 4x 3 minutes Over-Under intervals, where Over interval lasts for 1 minute
#   and Under interval for 2 minutes, repeated to fill in the whole OU interval
#   and the Over portion is the last part of the interval
# - 15 minutes of Endurance Miles
#
# Be aware that the both Ambit and Ambit2 are memory-constrained
# (Ambit in particular) and thus very complex training apps may not fit
# into any of these models (I have no experience with Ambit3 though).
# However, common Carmichael's Time-Crunched Cyclist trainings should
# be fine, on the edge of the memory limit of Ambit2.
#
# Btw, we should push Suunto to put the complex intervals into their Ambit
# product line directly, comparable to Polar or Garmin....



# Set up your limits 

$highest_CTS_FT_HR = 182;
$highest_CTS_FT_POWER = 283;

# You can choose to use either heart rate limits (HR) or power limits (POWER)

$use_limits = shift @ARGV || die "Limits parameter (HR/POWER) is mandatory.\n";
die "Unknown limits: $use_limits, expected HR or POWER.\n" if ($use_limits ne "HR" && $use_limits ne "POWER");

$maxlimit{"HR"}{"REG"} = sprintf("%d", .70 * $highest_CTS_FT_HR);
$minlimit{"HR"}{"EM"} = sprintf("%d", .50 * $highest_CTS_FT_HR);
$maxlimit{"HR"}{"EM"} = sprintf("%d", .91 * $highest_CTS_FT_HR);
$maxlimit{"HR"}{"RBI"} = sprintf("%d", .91 * $highest_CTS_FT_HR);
$minlimit{"HR"}{"TEMPO"} = sprintf("%d", .88 * $highest_CTS_FT_HR);
$maxlimit{"HR"}{"TEMPO"} = sprintf("%d", .90 * $highest_CTS_FT_HR);
$minlimit{"HR"}{"SS"} = sprintf("%d", .92 * $highest_CTS_FT_HR);
$maxlimit{"HR"}{"SS"} = sprintf("%d", .94 * $highest_CTS_FT_HR);
$minlimit{"HR"}{"CR"} = sprintf("%d", .95 * $highest_CTS_FT_HR);
$maxlimit{"HR"}{"CR"} = sprintf("%d", .97 * $highest_CTS_FT_HR);
$minlimit{"HR"}{"PI"} = sprintf("%d", 1.0 * $highest_CTS_FT_HR);

$maxlimit{"POWER"}{"REG"} = sprintf("%d", .50 * $highest_CTS_FT_POWER);
$minlimit{"POWER"}{"EM"} = sprintf("%d", .45 * $highest_CTS_FT_POWER);
$maxlimit{"POWER"}{"EM"} = sprintf("%d", .73 * $highest_CTS_FT_POWER);
$maxlimit{"POWER"}{"RBI"} = sprintf("%d", .73 * $highest_CTS_FT_POWER);
$minlimit{"POWER"}{"TEMPO"} = sprintf("%d", .80 * $highest_CTS_FT_POWER);
$maxlimit{"POWER"}{"TEMPO"} = sprintf("%d", .85 * $highest_CTS_FT_POWER);
$minlimit{"POWER"}{"SS"} = sprintf("%d", .86 * $highest_CTS_FT_POWER);
$maxlimit{"POWER"}{"SS"} = sprintf("%d", .90 * $highest_CTS_FT_POWER);
$minlimit{"POWER"}{"CR"} = sprintf("%d", .95 * $highest_CTS_FT_POWER);
$maxlimit{"POWER"}{"CR"} = sprintf("%d", 1.0 * $highest_CTS_FT_POWER);
$minlimit{"POWER"}{"PI"} = sprintf("%d", 1.01 * $highest_CTS_FT_POWER);

# These are not supported yet
$minlimit{"CAD"}{"FP"} = 108;
$maxlimit{"CAD"}{"FP"} = 120;


$timepos = 1;

print "RESULT = 0;\nPWR = SUUNTO_BIKE_POWER_AVG[30];\nprefix = \"\";\npostfix = \"free\";\n\n";

while(<>) {
	chomp;
	&parser($_,0);
}

# processes current level of recursion and returns end of the scope (position in a line)
sub parser($$) {
	my $input = shift;
	my $inputlen = length($input);
	my $curpos = shift;
	my $token = "";
	my $reps = 1;


	while (1) {
		my $curchar = substr($input,$curpos,1);
		if ($curchar =~ /\s/) {
			# kill whitespaces
			$curpos++;
			next;
		}
		elsif ($curchar eq "+" || $curchar eq ")" || $curpos >= $inputlen ) {
			my $duration;
			my $exercise;
			if ($token eq "") {
				if ($curchar eq ")" || $curpos >= $inputlen) {
					return $curpos;
				}
				$curpos++;
				$reps = 1;
				next;
			}
			if ($token =~ /^(\d+)m(\S+)$/) {
				$duration = $1 * 60;
				$exercise = $2;
			}
			else {
				die "Unknown token format: $token - should be of 20mEM type!\n";
			}
			$display_exercise = $exercise;
			if ($exercise eq "PFPI" || $exercise eq "SEPI") {
				$exercise = "PI";
			}
			elsif ($exercise eq "RBS") {
				$exercise = "RBI";
			}
			for (my $i = 0; $i < $reps; $i++) {
				my $starttime = $timepos;
				$timepos += $duration;
				my $stoptime = $starttime+$duration-1;

				if ($exercise =~ /^O(\d+)U(\d+)$/) {
					# Over-under intervals need a special care:
					# - number after O/U specifies length of Over/Under interval in minutes
					# - the OU intervals should fill in the prescribed length of the overall interval
					# - Over interval corresponds to CR, Under interval to SS
					# - the overall interval should end with Over (not handled programmatically)

					my $original_exercise = $exercise;
					my $over_duration = $1 * 60;
					my $under_duration = $2 * 60;

					$exercise = "SS";
					$display_exercise = "U";
					my $sub_starttime = $starttime;
					my $sub_stoptime = $sub_starttime + $under_duration - 1;

					while ($sub_stoptime <= $stoptime) {
						&print_output($sub_starttime,$sub_stoptime,$exercise,$display_exercise);
						if ($display_exercise eq "U") {
							$exercise = "CR";
							$display_exercise = "O";
							$sub_starttime = $sub_stoptime + 1;
							$sub_stoptime = $sub_starttime + $over_duration - 1;
						}
						else {
							$exercise = "SS";
							$display_exercise = "U";
							$sub_starttime = $sub_stoptime + 1;
							$sub_stoptime = $sub_starttime + $under_duration - 1;
						}
					}
					$exercise = $original_exercise;
				}
				else {
					&print_output($starttime,$stoptime,$exercise,$display_exercise);
				}

			}
			$token = "";
			$reps = 1;
			if ($curchar eq ")" || $curpos >= $inputlen) {
				return $curpos;
			}
		}
		elsif ($curchar eq "(") {
			my $newpos;
			for (my $i = 0; $i < $reps; $i++) {
				$newpos = &parser($input,$curpos+1);
			}
			$curpos = $newpos;
		}
		elsif ($curchar eq "x") {
			die "Unexpected format: $token should have been an integer!\n" if ($token !~ /\d+/);
			$reps = $token;
			$token = "";
		}
		else {
			$token = $token . $curchar;
		}
		$curpos++;
	}
}

sub print_output($$$$) {
	my $starttime = shift;
	my $stoptime = shift;
	my $exercise = shift;
	my $display_exercise = shift;
	my $measure;

	my $alarm = "Suunto.alarmBeep(); ";  # set to "" if not desired
	#$alarm = "";

	# prefix + postfix length must be 6 or less characters
	my $result = "RESULT = $stoptime - SUUNTO_DURATION;";
	my $prefix = 'prefix = "";';
	my $slower = 'prefix = "sl";';
	my $faster = 'prefix = "fa";';
	my $free = 'prefix = "fr";';
	my $postfix = 'postfix = "' . substr($display_exercise, 0, 4) . '";';

	if ($use_limits eq "HR") {
		$measure = "SUUNTO_HR";
	}
	elsif ($use_limits eq "POWER") {
		$measure = "PWR"; # this is 5s tail average to avoid harsh oscillations of power
	}
	else {
		die "Undefined mode: $use_limits!\n";
	}

	if (! defined $minlimit{$use_limits}{$exercise} && ! defined $maxlimit{$use_limits}{$exercise} ) {
		print << "EOF" ;
if (SUUNTO_DURATION > $starttime && SUUNTO_DURATION <= $stoptime ) { $result $free $postfix }

EOF
		;
	}
	elsif (! defined $minlimit{$use_limits}{$exercise}) {
		print << "EOF" ;
if (SUUNTO_DURATION > $starttime && SUUNTO_DURATION <= $stoptime) { $result $prefix $postfix if ( $measure > $maxlimit{$use_limits}{$exercise} ) { $slower $alarm } }

EOF
		;
	}
	elsif (! defined $maxlimit{$use_limits}{$exercise}) {
		print << "EOF" ;
if (SUUNTO_DURATION > $starttime && SUUNTO_DURATION <= $stoptime ) { $result $prefix $postfix if ( $measure < $minlimit{$use_limits}{$exercise} ) { $faster if (Suunto.mod(SUUNTO_DURATION, 2) == 0) { $$alarm } } }

EOF
		;
	}
	else {
		print << "EOF" ;
if (SUUNTO_DURATION > $starttime && SUUNTO_DURATION <= $stoptime ) { $result $prefix $postfix if ( $measure < $minlimit{$use_limits}{$exercise} ) { $faster if (Suunto.mod(SUUNTO_DURATION, 2) == 0) { $$alarm } } if ( $measure > $maxlimit{$use_limits}{$exercise} ) { $slower $alarm } }

EOF
		;
	}

}
