/*
 * Copyright (C) 2017 Paul Davis <paul@linuxaudiosystems.com>
 *
 * 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, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#define Timecode_IS_AROUND_ZERO(sm) (!(sm).frames && !(sm).seconds && !(sm).minutes && !(sm).hours)
#define Timecode_IS_ZERO(sm) (!(sm).frames && !(sm).seconds && !(sm).minutes && !(sm).hours && !(sm.subframes))

#include <math.h>
#include <stdio.h>
#include <stdlib.h>

#include "temporal/time.h"

namespace Timecode {

double Time::default_rate = 30.0;


/** Increment @a timecode by exactly one sample (keep subframes value).
 * Realtime safe.
 * @return true if seconds wrap.
 */
Wrap
increment (Time& timecode, uint32_t subframes_per_frame)
{
	Wrap wrap = NONE;

	if (timecode.negative) {
		if (Timecode_IS_AROUND_ZERO (timecode) && timecode.subframes) {
			// We have a zero transition involving only subframes
			timecode.subframes = subframes_per_frame - timecode.subframes;
			timecode.negative = false;
			return SECONDS;
		}

		timecode.negative = false;
		wrap = decrement (timecode, subframes_per_frame);
		if (!Timecode_IS_ZERO (timecode)) {
			timecode.negative = true;
		}
		return wrap;
	}

	switch ((int)ceil (timecode.rate)) {
	case 24:
		if (timecode.frames == 23) {
			timecode.frames = 0;
			wrap = SECONDS;
		}
		break;
	case 25:
		if (timecode.frames == 24) {
			timecode.frames = 0;
			wrap = SECONDS;
		}
		break;
	case 30:
		if (timecode.drop) {
			if (timecode.frames == 29) {
				if (((timecode.minutes + 1) % 10) && (timecode.seconds == 59)) {
					timecode.frames = 2;
				}
				else {
					timecode.frames = 0;
				}
				wrap = SECONDS;
			}
		} else {

			if (timecode.frames == 29) {
				timecode.frames = 0;
				wrap = SECONDS;
			}
		}
		break;
	case 60:
		if (timecode.frames == 59) {
			timecode.frames = 0;
			wrap = SECONDS;
		}
		break;
	}

	if (wrap == SECONDS) {
		if (timecode.seconds == 59) {
			timecode.seconds = 0;
			wrap = MINUTES;
			if (timecode.minutes == 59) {
				timecode.minutes = 0;
				wrap = HOURS;
				timecode.hours++;
			} else {
				timecode.minutes++;
			}
		} else {
			timecode.seconds++;
		}
	} else {
		timecode.frames++;
	}

	return wrap;
}


/** Decrement @a timecode by exactly one sample (keep subframes value)
 * Realtime safe.
 * @return true if seconds wrap. */
Wrap
decrement (Time& timecode, uint32_t subframes_per_frame)
{
	Wrap wrap = NONE;

	if (timecode.negative || Timecode_IS_ZERO (timecode)) {
		timecode.negative = false;
		wrap = increment (timecode, subframes_per_frame);
		timecode.negative = true;
		return wrap;
	} else if (Timecode_IS_AROUND_ZERO (timecode) && timecode.subframes) {
		// We have a zero transition involving only subframes
		timecode.subframes = subframes_per_frame - timecode.subframes;
		timecode.negative = true;
		return SECONDS;
	}

	switch ((int)ceil (timecode.rate)) {
	case 24:
		if (timecode.frames == 0) {
			timecode.frames = 23;
			wrap = SECONDS;
		}
		break;
	case 25:
		if (timecode.frames == 0) {
			timecode.frames = 24;
			wrap = SECONDS;
		}
		break;
	case 30:
		if (timecode.drop) {
			if ((timecode.minutes % 10) && (timecode.seconds == 0)) {
				if (timecode.frames <= 2) {
					timecode.frames = 29;
					wrap = SECONDS;
				}
			} else if (timecode.frames == 0) {
				timecode.frames = 29;
				wrap = SECONDS;
			}

		} else {
			if (timecode.frames == 0) {
				timecode.frames = 29;
				wrap = SECONDS;
			}
		}
		break;
	case 60:
		if (timecode.frames == 0) {
			timecode.frames = 59;
			wrap = SECONDS;
		}
		break;
	}

	if (wrap == SECONDS) {
		if (timecode.seconds == 0) {
			timecode.seconds = 59;
			wrap = MINUTES;
			if (timecode.minutes == 0) {
				timecode.minutes = 59;
				wrap = HOURS;
				timecode.hours--;
			}
			else {
				timecode.minutes--;
			}
		} else {
			timecode.seconds--;
		}
	} else {
		timecode.frames--;
	}

	if (Timecode_IS_ZERO (timecode)) {
		timecode.negative = false;
	}

	return wrap;
}


/** Go to lowest absolute subframe value in this sample (set to 0 :-)) */
void
frames_floot (Time& timecode)
{
	timecode.subframes = 0;
	if (Timecode_IS_ZERO (timecode)) {
		timecode.negative = false;
	}
}


/** Increment @a timecode by one subframe */
Wrap
increment_subframes (Time& timecode, uint32_t subframes_per_frame)
{
	Wrap wrap = NONE;

	if (timecode.negative) {
		timecode.negative = false;
		wrap = decrement_subframes (timecode, subframes_per_frame);
		if (!Timecode_IS_ZERO (timecode)) {
			timecode.negative = true;
		}
		return wrap;
	}

	timecode.subframes++;
	if (timecode.subframes >= subframes_per_frame) {
		timecode.subframes = 0;
		increment (timecode, subframes_per_frame);
		return FRAMES;
	}
	return NONE;
}


/** Decrement @a timecode by one subframe */
Wrap
decrement_subframes (Time& timecode, uint32_t subframes_per_frame)
{
	Wrap wrap = NONE;

	if (timecode.negative) {
		timecode.negative = false;
		wrap = increment_subframes (timecode, subframes_per_frame);
		timecode.negative = true;
		return wrap;
	}

	if (timecode.subframes <= 0) {
		timecode.subframes = 0;
		if (Timecode_IS_ZERO (timecode)) {
			timecode.negative = true;
			timecode.subframes = 1;
			return FRAMES;
		} else {
			decrement (timecode, subframes_per_frame);
			timecode.subframes = 79;
			return FRAMES;
		}
	} else {
		timecode.subframes--;
		if (Timecode_IS_ZERO (timecode)) {
			timecode.negative = false;
		}
		return NONE;
	}
}


/** Go to next whole second (frames == 0 or frames == 2) */
Wrap
increment_seconds (Time& timecode, uint32_t subframes_per_frame)
{
	Wrap wrap = NONE;

	// Clear subframes
	frames_floot (timecode);

	if (timecode.negative) {
		// Wrap second if on second boundary
		wrap = increment (timecode, subframes_per_frame);
		// Go to lowest absolute frame value
		seconds_floor (timecode);
		if (Timecode_IS_ZERO (timecode)) {
			timecode.negative = false;
		}
	} else {
		// Go to highest possible frame in this second
		switch ((int)ceil (timecode.rate)) {
		case 24:
			timecode.frames = 23;
			break;
		case 25:
			timecode.frames = 24;
			break;
		case 30:
			timecode.frames = 29;
			break;
		case 60:
			timecode.frames = 59;
			break;
		}

		// Increment by one frame
		wrap = increment (timecode, subframes_per_frame);
	}

	return wrap;
}


/** Go to lowest (absolute) frame value in this second
 * Doesn't care about positive/negative */
void
seconds_floor (Time& timecode)
{
	// Clear subframes
	frames_floot (timecode);

	// Go to lowest possible frame in this second
	switch ((int)ceil (timecode.rate)) {
	case 24:
	case 25:
	case 30:
	case 60:
		if (!(timecode.drop)) {
			timecode.frames = 0;
		} else {
			if ((timecode.minutes % 10) && (timecode.seconds == 0)) {
				timecode.frames = 2;
			} else {
				timecode.frames = 0;
			}
		}
		break;
	}

	if (Timecode_IS_ZERO (timecode)) {
		timecode.negative = false;
	}
}


/** Go to next whole minute (seconds == 0, frames == 0 or frames == 2) */
Wrap
increment_minutes (Time& timecode, uint32_t subframes_per_frame)
{
	Wrap wrap = NONE;

	// Clear subframes
	frames_floot (timecode);

	if (timecode.negative) {
		// Wrap if on minute boundary
		wrap = increment_seconds (timecode, subframes_per_frame);
		// Go to lowest possible value in this minute
		minutes_floor (timecode);
	} else {
		// Go to highest possible second
		timecode.seconds = 59;
		// Wrap minute by incrementing second
		wrap = increment_seconds (timecode, subframes_per_frame);
	}

	return wrap;
}


/** Go to lowest absolute value in this minute */
void
minutes_floor (Time& timecode)
{
	// Go to lowest possible second
	timecode.seconds = 0;
	// Go to lowest possible frame
	seconds_floor (timecode);

	if (Timecode_IS_ZERO (timecode)) {
		timecode.negative = false;
	}
}


/** Go to next whole hour (minute = 0, second = 0, frame = 0) */
Wrap
increment_hours (Time& timecode, uint32_t subframes_per_frame)
{
	Wrap wrap = NONE;

	// Clear subframes
	frames_floot (timecode);

	if (timecode.negative) {
		// Wrap if on hour boundary
		wrap = increment_minutes (timecode, subframes_per_frame);
		// Go to lowest possible value in this hour
		hours_floor(timecode);
	} else {
		timecode.minutes = 59;
		wrap = increment_minutes (timecode, subframes_per_frame);
	}

	return wrap;
}


/** Go to lowest absolute value in this hour */
void
hours_floor(Time& timecode)
{
	timecode.minutes   = 0;
	timecode.seconds   = 0;
	timecode.frames    = 0;
	timecode.subframes = 0;

	if (Timecode_IS_ZERO (timecode)) {
		timecode.negative = false;
	}
}

double
timecode_to_frames_per_second(TimecodeFormat t)
{
	switch (t) {
	case timecode_23976:
		return (24000.0/1001.0); //23.976;

		break;
	case timecode_24:
		return 24;

		break;
	case timecode_24976:
		return (25000.0/1001.0); //24.976;

		break;
	case timecode_25:
		return 25;

		break;
	case timecode_2997:
		return (30000.0/1001.0); //29.97;

		break;
	case timecode_2997drop:
		return (30000.0/1001.0); //29.97;

		break;
	case timecode_2997000:
		return 29.97;

		break;
	case timecode_2997000drop:
		return 29.97;

		break;
	case timecode_30:
		return 30;

		break;
	case timecode_30drop:
		return 30;

		break;
	case timecode_5994:
		return (60000.0/1001.0); //59.94;

		break;
	case timecode_60:
		return 60;

		break;
	default:
		//std::cerr << "Editor received unexpected timecode type" << std::endl;
		break;
	}
	return 30.0;
}

bool
timecode_has_drop_frames(TimecodeFormat t)
{
	switch (t) {
	case timecode_23976:
		return false;

		break;
	case timecode_24:
		return false;

		break;
	case timecode_24976:
		return false;

		break;
	case timecode_25:
		return false;

		break;
	case timecode_2997:
		return false;

		break;
	case timecode_2997drop:
		return true;

		break;
	case timecode_2997000:
		return false;

		break;
	case timecode_2997000drop:
		return true;

		break;
	case timecode_30:
		return false;

		break;
	case timecode_30drop:
		return true;

		break;
	case timecode_5994:
		return false;

		break;
	case timecode_60:
		return false;

		break;
	default:
		//error << "Editor received unexpected timecode type" << endmsg;
		break;
	}

	return false;
}

std::string
timecode_format_name (TimecodeFormat const t)
{
	switch (t) {
	case timecode_23976:
		return "23.98";

		break;
	case timecode_24:
		return "24";

		break;
	case timecode_24976:
		return "24.98";

		break;
	case timecode_25:
		return "25";

		break;
	case timecode_2997000:
	case timecode_2997:
		return "29.97";

		break;
	case timecode_2997000drop:
	case timecode_2997drop:
		return "29.97 drop";

		break;
	case timecode_30:
		return "30";

		break;
	case timecode_30drop:
		return "30 drop";

		break;
	case timecode_5994:
		return "59.94";

		break;
	case timecode_60:
		return "60";

		break;
	default:
		break;
	}

	return "??";
}

std::string timecode_format_time (Timecode::Time TC)
{
	char buf[32];
	if (TC.negative) {
		snprintf (buf, sizeof (buf), "-%02" PRIu32 ":%02" PRIu32 ":%02" PRIu32 "%c%02" PRIu32,
		          TC.hours, TC.minutes, TC.seconds, TC.drop ? ';' : ':', TC.frames);
	} else {
		snprintf (buf, sizeof (buf), " %02" PRIu32 ":%02" PRIu32 ":%02" PRIu32 "%c%02" PRIu32,
		          TC.hours, TC.minutes, TC.seconds, TC.drop ? ';' : ':', TC.frames);
	}
	return std::string(buf);
}

std::string timecode_format_sampletime (
	int64_t sample,
	double sample_sample_rate,
	double timecode_frames_per_second, bool timecode_drop_frames)
{
	Time t;
	sample_to_timecode(
		sample, t, false, false,
		timecode_frames_per_second, timecode_drop_frames,
		sample_sample_rate,
		80, false, 0);
	return timecode_format_time(t);
}

bool parse_timecode_format(std::string tc, Timecode::Time &TC) {
	char negative[2];
	char ignored[2];
	TC.subframes = 0;
	if (sscanf (tc.c_str(), "%[- ]%" PRId32 ":%" PRId32 ":%" PRId32 "%[:;]%" PRId32,
	            negative, &TC.hours, &TC.minutes, &TC.seconds, ignored, &TC.frames) != 6) {
		TC.hours = TC.minutes = TC.seconds = TC.frames = 0;
		TC.negative = false;
		return false;
	}
	if (negative[0]=='-') {
		TC.negative = true;
	} else {
		TC.negative = false;
	}
	return true;
}

void
timecode_to_sample(
	Timecode::Time const& timecode, int64_t& sample,
	bool use_offset, bool use_subframes,
	/* Note - framerate info is taken from Timecode::Time& */
	double sample_sample_rate /**< may include pull up/down */,
	uint32_t subframes_per_frame,
	/* optional offset  - can be improved: function pointer to lazily query this*/
	bool offset_is_negative, int64_t offset_samples
	)
{
	const double samples_per_timecode_frame = (double) sample_sample_rate / (double) timecode.rate;

	if (timecode.drop) {
		// The drop frame format was created to better approximate the 30000/1001 = 29.97002997002997....
		// framerate of NTSC color TV. The used frame rate of drop fra,e is 29.97, which drifts by about
		// 0.108 frame per hour, or about 1.3 frames per 12 hours. This is not perfect, but a lot better
		// than using 30 non drop, which will drift with about 1.8 frame per minute.
		// Using 29.97, drop frame real time can be accurate only every 10th minute (10 minutes of 29.97 fps
		// is exactly 17982 samples). One minute is 1798.2 samples, but we count 30 frames per second
		// (30 * 60 = 1800). This means that at the first minute boundary (at the end of 0:0:59:29) we
		// are 1.8 framess too late relative to real time. By dropping 2 frames (jumping to 0:1:0:2) we are
		// approx. 0.2 frames too early. This adds up with 0.2 too early for each minute until we are 1.8
		// samples too early at 0:9:0:2 (9 * 0.2 = 1.8). The 10th minute brings us 1.8 frames later again
		// (at end of 0:9:59:29), which sums up to 0 (we are back to zero at 0:10:0:0 :-).
		//
		// In table form:
		//
		// Timecode value    frames offset   subframes offset   seconds (rounded)  44100 sample (rounded)
		//  0:00:00:00        0.0             0                     0.000                0 (accurate)
		//  0:00:59:29        1.8           144                    60.027          2647177
		//  0:01:00:02       -0.2           -16                    60.060          2648648
		//  0:01:59:29        1.6           128                   120.020          5292883
		//  0:02:00:02       -0.4           -32                   120.053          5294354
		//  0:02:59:29        1.4           112                   180.013          7938588
		//  0:03:00:02       -0.6           -48                   180.047          7940060
		//  0:03:59:29        1.2            96                   240.007         10584294
		//  0:04:00:02       -0.8           -64                   240.040         10585766
		//  0:04:59:29        1.0            80                   300.000         13230000
		//  0:05:00:02       -1.0           -80                   300.033         13231471
		//  0:05:59:29        0.8            64                   359.993         15875706
		//  0:06:00:02       -1.2           -96                   360.027         15877177
		//  0:06:59:29        0.6            48                   419.987         18521411
		//  0:07:00:02       -1.4          -112                   420.020         18522883
		//  0:07:59:29        0.4            32                   478.980         21167117
		//  0:08:00:02       -1.6          -128                   480.013         21168589
		//  0:08:59:29        0.2            16                   539.973         23812823
		//  0:09:00:02       -1.8          -144                   540.007         23814294
		//  0:09:59:29        0.0+            0+                  599.967         26458529
		//  0:10:00:00        0.0             0                   600.000         26460000 (accurate)
		//
		//  Per Sigmond <per@sigmond.no>
		//
		//  This schma would compensate exactly for a frame-rate of 30 * 0.999. but the
		//  actual rate is 30000/1001 - which results in an offset of -3.6ms per hour or
		//  about -86ms over a 24-hour period. (SMPTE 12M-1999)
		//
		//  Robin Gareus <robin@gareus.org>

		const int64_t fps_i = ceil(timecode.rate);
		int64_t totalMinutes = 60 * timecode.hours + timecode.minutes;
		int64_t frameNumber  = fps_i * 3600 * timecode.hours
			+ fps_i * 60 * timecode.minutes
			+ fps_i * timecode.seconds + timecode.frames
			- 2 * (totalMinutes - totalMinutes / 10);
		sample = frameNumber * sample_sample_rate / (double) timecode.rate;
	} else {
		/*
		  Non drop is easy.. just note the use of
		  rint(timecode.rate) * samples_per_timecode_frame
		  (frames per Timecode second), which is larger than
		  sample_rate() in the non-integer Timecode rate case.
		*/

		sample = (int64_t) rint(
			(
				((timecode.hours * 60 * 60) + (timecode.minutes * 60) + timecode.seconds)
				*
				(rint(timecode.rate) * samples_per_timecode_frame)
				)
			+ (timecode.frames * samples_per_timecode_frame)
			);
	}

	if (use_subframes) {
		sample += (int64_t) rint(((double)timecode.subframes * samples_per_timecode_frame) / (double)subframes_per_frame);
	}

	if (use_offset) {
		if (offset_is_negative) {
			if (sample >= offset_samples) {
				sample -= offset_samples;
			} else {
				/* Prevent song-time from becoming negative */
				sample = 0;
			}
		} else {
			if (timecode.negative) {
				if (sample <= offset_samples) {
					sample = offset_samples - sample;
				} else {
					sample = 0;
				}
			} else {
				sample += offset_samples;
			}
		}
	}
}


void
sample_to_timecode (
	int64_t sample, Timecode::Time& timecode,
	bool use_offset, bool use_subframes,
	/* framerate info */
	double timecode_frames_per_second,
	bool   timecode_drop_frames,
	double sample_sample_rate/**< can include pull up/down */,
	uint32_t subframes_per_frame,
	/* optional offset  - can be improved: function pointer to lazily query this*/
	bool offset_is_negative, int64_t offset_samples
	)
{
	int64_t offset_sample;

	if (!use_offset) {
		timecode.negative = (sample < 0);
		offset_sample = ::llabs(sample);
	} else {
		if (offset_is_negative) {
			offset_sample = sample + offset_samples;
			timecode.negative = false;
		} else {
			if (sample < offset_samples) {
				offset_sample = (offset_samples - sample);
				timecode.negative = true;
			} else {
				offset_sample =  sample - offset_samples;
				timecode.negative = false;
			}
		}
	}

	if (timecode_drop_frames) {
		int64_t frameNumber = floor( (double)offset_sample * timecode_frames_per_second / sample_sample_rate);

		/* there are 17982 samples in 10 min @ 29.97df */
		const int64_t D = frameNumber / 17982;
		const int64_t M = frameNumber % 17982;

		timecode.subframes = rint(subframes_per_frame
		                          * ((double)offset_sample * timecode_frames_per_second / sample_sample_rate - (double)frameNumber));

		if (timecode.subframes == subframes_per_frame) {
			timecode.subframes = 0;
			frameNumber++;
		}

		frameNumber +=  18*D + 2*((M - 2) / 1798);

		timecode.frames  =    frameNumber % 30;
		timecode.seconds =   (frameNumber / 30) % 60;
		timecode.minutes =  ((frameNumber / 30) / 60) % 60;
		timecode.hours   = (((frameNumber / 30) / 60) / 60);

	} else {
		double timecode_frames_left_exact;
		double timecode_frames_fraction;
		int64_t timecode_frames_left;
		const double samples_per_timecode_frame = sample_sample_rate / timecode_frames_per_second;
		const int64_t frames_per_hour = (int64_t)(3600. * rint(timecode_frames_per_second) * samples_per_timecode_frame);

		timecode.hours = offset_sample / frames_per_hour;

		// Extract whole hours. Do this to prevent rounding errors with
		// high sample numbers in the calculations that follow.
		timecode_frames_left_exact = (double)(offset_sample % frames_per_hour) / samples_per_timecode_frame;
		timecode_frames_fraction = timecode_frames_left_exact - floor( timecode_frames_left_exact );

		timecode.subframes = (int32_t) rint(timecode_frames_fraction * subframes_per_frame);
		timecode_frames_left = (int64_t) floor (timecode_frames_left_exact);

		if (use_subframes && timecode.subframes == subframes_per_frame) {
			timecode_frames_left++;
			timecode.subframes = 0;
		}

		timecode.minutes = timecode_frames_left / ((int32_t) lrint (timecode_frames_per_second) * 60);
		timecode_frames_left = timecode_frames_left % ((int32_t) lrint (timecode_frames_per_second) * 60);
		timecode.seconds = timecode_frames_left / (int32_t) lrint(timecode_frames_per_second);
		timecode.frames = timecode_frames_left % (int32_t) lrint(timecode_frames_per_second);
	}

	if (!use_subframes) {
		timecode.subframes = 0;
	}
	/* set frame rate and drop sample */
	timecode.rate = timecode_frames_per_second;
	timecode.drop = timecode_drop_frames;
}

} // namespace Timecode

std::ostream&
operator<<(std::ostream& ostr, const Timecode::Time& t)
{
	return t.print (ostr);
}
