// Copyright (c) 2010, Thomas Goyne <plorkyeran@aegisub.org>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

#pragma once

#include <cstdint>
#include <vector>

#include <libaegisub/exception.h>
#include <libaegisub/fs_fwd.h>

namespace agi {
	/// Framerate handling.
	namespace vfr {

enum Time {
	/// Use the actual frame times
	/// With 1 FPS video, frame 0 is [0, 999] ms
	EXACT,
	/// Calculate based on the rules for start times of lines
	/// Lines are first visible on the first frame with start time less than
	/// or equal to the start time; thus with 1.0 FPS video, frame 0 is
	/// [-999, 0] ms
	START,
	/// Calculate based on the rules for end times of lines
	/// Lines are last visible on the last frame with start time less than the
	/// end time; thus with 1.0 FPS video, frame 0 is [1, 1000] ms
	END
};

DEFINE_EXCEPTION(Error, Exception);
/// FPS specified is not a valid frame rate
DEFINE_EXCEPTION(InvalidFramerate, Error);
/// Unknown timecode file format
DEFINE_EXCEPTION(UnknownFormat, Error);
/// Invalid line encountered in a timecode file
DEFINE_EXCEPTION(MalformedLine, Error);

/// @class Framerate
/// @brief Class for managing everything related to converting frames to times
///        or vice versa
class Framerate {
	/// Denominator of the FPS
	///
	/// For v1 VFR, the assumed FPS is used, for v2 the average FPS
	int64_t denominator = 0;
	/// Numerator of the FPS
	///
	/// For v1 VFR, the assumed FPS is used, for v2 the average FPS
	int64_t numerator = 0;

	/// Unrounded frame-seconds of the final frame in timecodes. For CFR and v2,
	/// this is simply frame count * denominator, but for v1 it's the
	/// "unrounded" frame count, since override ranges generally don't exactly
	/// cover timebase-unit ranges of time. This is needed to match mkvmerge's
	/// rounding past the end of the final override range.
	int64_t last = 0;

	/// Start time in milliseconds of each frame
	std::vector<int> timecodes;

	/// Does this frame rate need drop frames and have them enabled?
	bool drop = false;

	/// Set FPS properties from the timecodes vector
	void SetFromTimecodes();
public:
	Framerate(Framerate const&) = default;
	Framerate& operator=(Framerate const&) = default;

#ifndef _MSC_VER
	Framerate(Framerate&&) = default;
	Framerate& operator=(Framerate&&) = default;
#endif

	/// @brief VFR from timecodes file
	/// @param filename File with v1 or v2 timecodes
	///
	/// Note that loading a v1 timecode file with Assume X and no overrides is
	/// not the same thing as CFR X. When timecodes are loaded from a file,
	/// mkvmerge-style rounding is applied, while setting a constant frame rate
	/// uses truncation.
	Framerate(fs::path const& filename);

	/// @brief CFR constructor
	/// @param fps Frames per second or 0 for unloaded
	Framerate(double fps = 0.);

	/// @brief CFR constructor with rational timebase
	/// @param numerator Timebase numerator
	/// @param denominator Timebase denominator
	/// @param drop Enable drop frames if the FPS requires it
	Framerate(int64_t numerator, int64_t denominator, bool drop=true);

	/// @brief VFR from frame times
	/// @param timecodes Vector of frame start times in milliseconds
	Framerate(std::vector<int> timecodes);
	Framerate(std::initializer_list<int> timecodes);

	/// @brief Get the frame visible at a given time
	/// @param ms Time in milliseconds
	/// @param type Time mode
	///
	/// When type is EXACT, the frame returned is the frame visible at the given
	/// time; when it is START or END it is the frame on which a line with that
	/// start/end time would first/last be visible
	int FrameAtTime(int ms, Time type = EXACT) const;

	/// @brief Get the time at a given frame
	/// @param frame Frame number
	/// @param type Time mode
	///
	/// When type is EXACT, the frame's exact start time is returned; START and
	/// END give a time somewhere within the range that will result in the line
	/// starting/ending on that frame
	///
	/// With v2 timecodes, frames outside the defined range are not an error
	/// and are guaranteed to be monotonically increasing/decreasing values
	/// which when passed to FrameAtTime will return the original frame; they
	/// are not guaranteed to be sensible or useful for any other purpose
	///
	/// v1 timecodes and CFR do not have a defined range, and will give sensible
	/// results for all frame numbers
	int TimeAtFrame(int frame, Time type = EXACT) const;

	/// @brief Get the components of the SMPTE timecode for the given time
	/// @param[out] h Hours component
	/// @param[out] m Minutes component
	/// @param[out] s Seconds component
	/// @param[out] f Frames component
	///
	/// For NTSC (30000/1001), this generates proper SMPTE timecodes with drop
	/// frames handled. For multiples of NTSC, this multiplies the number of
	/// dropped frames. For other non-integral frame rates, it drops frames in
	/// an undefined manner which results in no more than half a second error
	/// from wall clock time.
	///
	/// For integral frame rates, no frame dropping occurs.
	void SmpteAtTime(int ms, int *h, int *m, int *s, int *f) const;

	/// @brief Get the components of the SMPTE timecode for the given frame
	/// @param[out] h Hours component
	/// @param[out] m Minutes component
	/// @param[out] s Seconds component
	/// @param[out] f Frames component
	///
	/// For NTSC (30000/1001), this generates proper SMPTE timecodes with drop
	/// frames handled. For multiples of NTSC, this multiplies the number of
	/// dropped frames. For other non-integral frame rates, it drops frames in
	/// an undefined manner which results in no more than half a second error
	/// from wall clock time.
	///
	/// For integral frame rates, no frame dropping occurs.
	void SmpteAtFrame(int frame, int *h, int *m, int *s, int *f) const;

	/// @brief Get the frame indicated by the SMPTE timecode components
	/// @param h Hours component
	/// @param m Minutes component
	/// @param s Seconds component
	/// @param f Frames component
	/// @return Frame number
	/// @see SmpteAtFrame
	int FrameAtSmpte(int h, int m, int s, int f) const;

	/// @brief Get the time indicated by the SMPTE timecode components
	/// @param h Hours component
	/// @param m Minutes component
	/// @param s Seconds component
	/// @param f Frames component
	/// @return Time in milliseconds
	/// @see SmpteAtTime
	int TimeAtSmpte(int h, int m, int s, int f) const;

	/// @brief Save the current time codes to a file as v2 timecodes
	/// @param file File name
	/// @param length Minimum number of frames to output
	///
	/// The length parameter is only particularly useful for v1 timecodes (and
	/// CFR, but saving CFR timecodes is a bit silly). Extra timecodes generated
	/// to hit length with v2 timecodes will monotonically increase but may not
	/// be otherwise sensible.
	void Save(fs::path const& file, int length = -1) const;

	/// Is this frame rate possibly variable?
	bool IsVFR() const {return timecodes.size() > 1; }

	/// Does this represent a valid frame rate?
	bool IsLoaded() const { return numerator > 0; }

	/// Get average FPS of this frame rate
	double FPS() const { return double(numerator) / denominator; }

	/// Does this frame rate need drop frames for SMPTE timeish frame numbers?
	bool NeedsDropFrames() const { return drop; }
};

	} // namespace vfr
} // namespace agi
