/**
 * @file
 * @brief Source file for Clip class
 * @author Jonathan Thomas <jonathan@openshot.org>
 *
 * @ref License
 */

/* LICENSE
 *
 * Copyright (c) 2008-2019 OpenShot Studios, LLC
 * <http://www.openshotstudios.com/>. This file is part of
 * OpenShot Library (libopenshot), an open-source project dedicated to
 * delivering high quality video editing and animation solutions to the
 * world. For more information visit <http://www.openshot.org/>.
 *
 * OpenShot Library (libopenshot) is free software: you can redistribute it
 * and/or modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * OpenShot Library (libopenshot) 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
 */

#include "Clip.h"
#include "Exceptions.h"
#include "FFmpegReader.h"
#include "FrameMapper.h"
#ifdef USE_IMAGEMAGICK
	#include "ImageReader.h"
	#include "TextReader.h"
#endif
#include "QtImageReader.h"
#include "ChunkReader.h"
#include "DummyReader.h"
#include "Timeline.h"

using namespace openshot;

// Init default settings for a clip
void Clip::init_settings()
{
	// Init clip settings
	Position(0.0);
	Layer(0);
	Start(0.0);
	End(0.0);
	gravity = GRAVITY_CENTER;
	scale = SCALE_FIT;
	anchor = ANCHOR_CANVAS;
	display = FRAME_DISPLAY_NONE;
	mixing = VOLUME_MIX_NONE;
	waveform = false;
	previous_properties = "";
	parentObjectId = "";

	// Init scale curves
	scale_x = Keyframe(1.0);
	scale_y = Keyframe(1.0);

	// Init location curves
	location_x = Keyframe(0.0);
	location_y = Keyframe(0.0);

	// Init alpha
	alpha = Keyframe(1.0);

	// Init time & volume
	time = Keyframe(1.0);
	volume = Keyframe(1.0);

	// Init audio waveform color
	wave_color = Color((unsigned char)0, (unsigned char)123, (unsigned char)255, (unsigned char)255);

	// Init shear and perspective curves
	shear_x = Keyframe(0.0);
	shear_y = Keyframe(0.0);
	origin_x = Keyframe(0.5);
	origin_y = Keyframe(0.5);
	perspective_c1_x = Keyframe(-1.0);
	perspective_c1_y = Keyframe(-1.0);
	perspective_c2_x = Keyframe(-1.0);
	perspective_c2_y = Keyframe(-1.0);
	perspective_c3_x = Keyframe(-1.0);
	perspective_c3_y = Keyframe(-1.0);
	perspective_c4_x = Keyframe(-1.0);
	perspective_c4_y = Keyframe(-1.0);

	// Init audio channel filter and mappings
	channel_filter = Keyframe(-1.0);
	channel_mapping = Keyframe(-1.0);

	// Init audio and video overrides
	has_audio = Keyframe(-1.0);
	has_video = Keyframe(-1.0);

	// Initialize the attached object and attached clip as null pointers
	parentTrackedObject = nullptr;
	parentClipObject = NULL;

	// Init reader info struct
	init_reader_settings();
}

// Init reader info details
void Clip::init_reader_settings() {
	if (reader) {
		// Init rotation (if any)
		init_reader_rotation();

		// Initialize info struct
		info = reader->info;
	}
}

// Init reader's rotation (if any)
void Clip::init_reader_rotation() {
	// Dont init rotation if clip has keyframes
	if (rotation.GetCount() > 0)
		return;

	// Init rotation
	if (reader && reader->info.metadata.count("rotate") > 0) {
		// Use reader metadata rotation (if any)
		// This is typical with cell phone videos filmed in different orientations
		try {
			float rotate_metadata = strtof(reader->info.metadata["rotate"].c_str(), 0);
			rotation = Keyframe(rotate_metadata);
		} catch (const std::exception& e) {}
	}
	else
		// Default no rotation
		rotation = Keyframe(0.0);
}

// Default Constructor for a clip
Clip::Clip() : resampler(NULL), reader(NULL), allocated_reader(NULL), is_open(false)
{
	// Init all default settings
	init_settings();
}

// Constructor with reader
Clip::Clip(ReaderBase* new_reader) : resampler(NULL), reader(new_reader), allocated_reader(NULL), is_open(false)
{
	// Init all default settings
	init_settings();

	// Open and Close the reader (to set the duration of the clip)
	Open();
	Close();

	// Update duration and set parent
	if (reader) {
		End(reader->info.duration);
		reader->ParentClip(this);
		// Init reader info struct
		init_reader_settings();
	}
}

// Constructor with filepath
Clip::Clip(std::string path) : resampler(NULL), reader(NULL), allocated_reader(NULL), is_open(false)
{
	// Init all default settings
	init_settings();

	// Get file extension (and convert to lower case)
	std::string ext = get_file_extension(path);
	std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);

	// Determine if common video formats
	if (ext=="avi" || ext=="mov" || ext=="mkv" ||  ext=="mpg" || ext=="mpeg" || ext=="mp3" || ext=="mp4" || ext=="mts" ||
		ext=="ogg" || ext=="wav" || ext=="wmv" || ext=="webm" || ext=="vob")
	{
		try
		{
			// Open common video format
			reader = new openshot::FFmpegReader(path);

		} catch(...) { }
	}
	if (ext=="osp")
	{
		try
		{
			// Open common video format
			reader = new openshot::Timeline(path, true);

		} catch(...) { }
	}


	// If no video found, try each reader
	if (!reader)
	{
		try
		{
			// Try an image reader
			reader = new openshot::QtImageReader(path);

		} catch(...) {
			try
			{
				// Try a video reader
				reader = new openshot::FFmpegReader(path);

			} catch(...) { }
		}
	}

	// Update duration and set parent
	if (reader) {
		End(reader->info.duration);
		reader->ParentClip(this);
		allocated_reader = reader;
		// Init reader info struct
		init_reader_settings();
	}
}

// Destructor
Clip::~Clip()
{
	// Delete the reader if clip created it
	if (allocated_reader) {
		delete allocated_reader;
		allocated_reader = NULL;
	}

	// Close the resampler
	if (resampler) {
		delete resampler;
		resampler = NULL;
	}
}

// Attach clip to bounding box
void Clip::AttachToObject(std::string object_id)
{
	// Search for the tracked object on the timeline
	Timeline* parentTimeline = (Timeline *) ParentTimeline();

	if (parentTimeline) {
		// Create a smart pointer to the tracked object from the timeline
		std::shared_ptr<openshot::TrackedObjectBase> trackedObject = parentTimeline->GetTrackedObject(object_id);
		Clip* clipObject = parentTimeline->GetClip(object_id);

		// Check for valid tracked object
		if (trackedObject){
			SetAttachedObject(trackedObject);
		}
		else if (clipObject) {
			SetAttachedClip(clipObject);
		}
	}
	return;
}

// Set the pointer to the trackedObject this clip is attached to
void Clip::SetAttachedObject(std::shared_ptr<openshot::TrackedObjectBase> trackedObject){
	parentTrackedObject = trackedObject;
	return;
}

// Set the pointer to the clip this clip is attached to
void Clip::SetAttachedClip(Clip* clipObject){
	parentClipObject = clipObject;
	return;
}

/// Set the current reader
void Clip::Reader(ReaderBase* new_reader)
{
	// set reader pointer
	reader = new_reader;

	// set parent
	reader->ParentClip(this);

	// Init reader info struct
	init_reader_settings();
}

/// Get the current reader
ReaderBase* Clip::Reader()
{
	if (reader)
		return reader;
	else
		// Throw error if reader not initialized
		throw ReaderClosed("No Reader has been initialized for this Clip.  Call Reader(*reader) before calling this method.");
}

// Open the internal reader
void Clip::Open()
{
	if (reader)
	{
		// Open the reader
		reader->Open();
		is_open = true;

		// Copy Reader info to Clip
		info = reader->info;

		// Set some clip properties from the file reader
		if (end == 0.0)
			End(reader->info.duration);
	}
	else
		// Throw error if reader not initialized
		throw ReaderClosed("No Reader has been initialized for this Clip.  Call Reader(*reader) before calling this method.");
}

// Close the internal reader
void Clip::Close()
{
	is_open = false;
	if (reader) {
		ZmqLogger::Instance()->AppendDebugMethod("Clip::Close");

		// Close the reader
		reader->Close();
	}
	else
		// Throw error if reader not initialized
		throw ReaderClosed("No Reader has been initialized for this Clip.  Call Reader(*reader) before calling this method.");
}

// Get end position of clip (trim end of video), which can be affected by the time curve.
float Clip::End() const
{
	// if a time curve is present, use its length
	if (time.GetCount() > 1)
	{
		// Determine the FPS fo this clip
		float fps = 24.0;
		if (reader)
			// file reader
			fps = reader->info.fps.ToFloat();
		else
			// Throw error if reader not initialized
			throw ReaderClosed("No Reader has been initialized for this Clip.  Call Reader(*reader) before calling this method.");

		return float(time.GetLength()) / fps;
	}
	else
		// just use the duration (as detected by the reader)
		return end;
}

// Create an openshot::Frame object for a specific frame number of this reader.
std::shared_ptr<Frame> Clip::GetFrame(int64_t frame_number)
{
	// Check for open reader (or throw exception)
	if (!is_open)
		throw ReaderClosed("The Clip is closed.  Call Open() before calling this method.");

	if (reader)
	{
		// Adjust out of bounds frame number
		frame_number = adjust_frame_number_minimum(frame_number);

		// Get the original frame and pass it to GetFrame overload
		std::shared_ptr<Frame> original_frame = GetOrCreateFrame(frame_number);
		return GetFrame(original_frame, frame_number, NULL);
	}
	else
		// Throw error if reader not initialized
		throw ReaderClosed("No Reader has been initialized for this Clip.  Call Reader(*reader) before calling this method.");
}

// Create an openshot::Frame object for a specific frame number of this reader.
std::shared_ptr<Frame> Clip::GetFrame(std::shared_ptr<openshot::Frame> background_frame, int64_t frame_number)
{
    // Check for open reader (or throw exception)
    if (!is_open)
        throw ReaderClosed("The Clip is closed.  Call Open() before calling this method.");

    if (reader)
    {
        // Adjust out of bounds frame number
        frame_number = adjust_frame_number_minimum(frame_number);

        // Get the original frame and pass it to GetFrame overload
        std::shared_ptr<Frame> original_frame = GetOrCreateFrame(frame_number);
        return GetFrame(original_frame, frame_number, NULL);
    }
    else
        // Throw error if reader not initialized
        throw ReaderClosed("No Reader has been initialized for this Clip.  Call Reader(*reader) before calling this method.");
}

// Use an existing openshot::Frame object and draw this Clip's frame onto it
std::shared_ptr<Frame> Clip::GetFrame(std::shared_ptr<openshot::Frame> background_frame, int64_t frame_number, openshot::TimelineInfoStruct* options)
{
	// Check for open reader (or throw exception)
	if (!is_open)
		throw ReaderClosed("The Clip is closed.  Call Open() before calling this method.");

	if (reader)
	{
		// Adjust out of bounds frame number
		frame_number = adjust_frame_number_minimum(frame_number);

		// Is a time map detected
		int64_t new_frame_number = frame_number;
		int64_t time_mapped_number = adjust_frame_number_minimum(time.GetLong(frame_number));
		if (time.GetLength() > 1)
			new_frame_number = time_mapped_number;

		// Now that we have re-mapped what frame number is needed, go and get the frame pointer
		std::shared_ptr<Frame> original_frame = GetOrCreateFrame(new_frame_number);

		// Get time mapped frame number (used to increase speed, change direction, etc...)
		// TODO: Handle variable # of samples, since this resamples audio for different speeds (only when time curve is set)
		get_time_mapped_frame(original_frame, new_frame_number);

		// Apply local effects to the frame (if any)
		apply_effects(original_frame);

        // Apply global timeline effects (i.e. transitions & masks... if any)
        if (timeline != NULL && options != NULL) {
            if (options->is_top_clip) {
                // Apply global timeline effects (only to top clip... if overlapping, pass in timeline frame number)
                Timeline* timeline_instance = (Timeline*) timeline;
                original_frame = timeline_instance->apply_effects(original_frame, background_frame->number, Layer());
            }
        }

		// Apply keyframe / transforms
		apply_keyframes(original_frame, background_frame->GetImage());

		// Return processed 'frame'
		return original_frame;
	}
	else
		// Throw error if reader not initialized
		throw ReaderClosed("No Reader has been initialized for this Clip.  Call Reader(*reader) before calling this method.");
}

// Look up an effect by ID
openshot::EffectBase* Clip::GetEffect(const std::string& id)
{
	// Find the matching effect (if any)
	for (const auto& effect : effects) {
		if (effect->Id() == id) {
			return effect;
		}
	}
	return nullptr;
}

// Get file extension
std::string Clip::get_file_extension(std::string path)
{
	// return last part of path
	return path.substr(path.find_last_of(".") + 1);
}

// Reverse an audio buffer
void Clip::reverse_buffer(juce::AudioSampleBuffer* buffer)
{
	int number_of_samples = buffer->getNumSamples();
	int channels = buffer->getNumChannels();

	// Reverse array (create new buffer to hold the reversed version)
	juce::AudioSampleBuffer *reversed = new juce::AudioSampleBuffer(channels, number_of_samples);
	reversed->clear();

	for (int channel = 0; channel < channels; channel++)
	{
		int n=0;
		for (int s = number_of_samples - 1; s >= 0; s--, n++)
			reversed->getWritePointer(channel)[n] = buffer->getWritePointer(channel)[s];
	}

	// Copy the samples back to the original array
	buffer->clear();
	// Loop through channels, and get audio samples
	for (int channel = 0; channel < channels; channel++)
		// Get the audio samples for this channel
		buffer->addFrom(channel, 0, reversed->getReadPointer(channel), number_of_samples, 1.0f);

	delete reversed;
	reversed = NULL;
}

// Adjust the audio and image of a time mapped frame
void Clip::get_time_mapped_frame(std::shared_ptr<Frame> frame, int64_t frame_number)
{
	// Check for valid reader
	if (!reader)
		// Throw error if reader not initialized
		throw ReaderClosed("No Reader has been initialized for this Clip.  Call Reader(*reader) before calling this method.");

	// Check for a valid time map curve
	if (time.GetLength() > 1)
	{
		const GenericScopedLock<juce::CriticalSection> lock(getFrameCriticalSection);

		// create buffer and resampler
		juce::AudioSampleBuffer *samples = NULL;
		if (!resampler)
			resampler = new AudioResampler();

		// Get new frame number
		int new_frame_number = frame->number;

		// Get delta (difference in previous Y value)
		int delta = int(round(time.GetDelta(frame_number)));

		// Init audio vars
		int channels = reader->info.channels;
		int number_of_samples = GetOrCreateFrame(new_frame_number)->GetAudioSamplesCount();

		// Only resample audio if needed
		if (reader->info.has_audio) {
			// Determine if we are speeding up or slowing down
			if (time.GetRepeatFraction(frame_number).den > 1) {
				// SLOWING DOWN AUDIO
				// Resample data, and return new buffer pointer
				juce::AudioSampleBuffer *resampled_buffer = NULL;

				// SLOW DOWN audio (split audio)
				samples = new juce::AudioSampleBuffer(channels, number_of_samples);
				samples->clear();

				// Loop through channels, and get audio samples
				for (int channel = 0; channel < channels; channel++)
					// Get the audio samples for this channel
					samples->addFrom(channel, 0, GetOrCreateFrame(new_frame_number)->GetAudioSamples(channel),
									 number_of_samples, 1.0f);

				// Reverse the samples (if needed)
				if (!time.IsIncreasing(frame_number))
					reverse_buffer(samples);

				// Resample audio to be X times slower (where X is the denominator of the repeat fraction)
				resampler->SetBuffer(samples, 1.0 / time.GetRepeatFraction(frame_number).den);

				// Resample the data (since it's the 1st slice)
				resampled_buffer = resampler->GetResampledBuffer();

				// Just take the samples we need for the requested frame
				int start = (number_of_samples * (time.GetRepeatFraction(frame_number).num - 1));
				if (start > 0)
					start -= 1;
				for (int channel = 0; channel < channels; channel++)
					// Add new (slower) samples, to the frame object
					frame->AddAudio(true, channel, 0, resampled_buffer->getReadPointer(channel, start),
										number_of_samples, 1.0f);

				// Clean up
				resampled_buffer = NULL;

			}
			else if (abs(delta) > 1 && abs(delta) < 100) {
				int start = 0;
				if (delta > 0) {
					// SPEED UP (multiple frames of audio), as long as it's not more than X frames
					int total_delta_samples = 0;
					for (int delta_frame = new_frame_number - (delta - 1);
						 delta_frame <= new_frame_number; delta_frame++)
						total_delta_samples += Frame::GetSamplesPerFrame(delta_frame, reader->info.fps,
																		 reader->info.sample_rate,
																		 reader->info.channels);

					// Allocate a new sample buffer for these delta frames
					samples = new juce::AudioSampleBuffer(channels, total_delta_samples);
					samples->clear();

					// Loop through each frame in this delta
					for (int delta_frame = new_frame_number - (delta - 1);
						 delta_frame <= new_frame_number; delta_frame++) {
						// buffer to hold detal samples
						int number_of_delta_samples = GetOrCreateFrame(delta_frame)->GetAudioSamplesCount();
						juce::AudioSampleBuffer *delta_samples = new juce::AudioSampleBuffer(channels,
																					   number_of_delta_samples);
						delta_samples->clear();

						for (int channel = 0; channel < channels; channel++)
							delta_samples->addFrom(channel, 0, GetOrCreateFrame(delta_frame)->GetAudioSamples(channel),
												   number_of_delta_samples, 1.0f);

						// Reverse the samples (if needed)
						if (!time.IsIncreasing(frame_number))
							reverse_buffer(delta_samples);

						// Copy the samples to
						for (int channel = 0; channel < channels; channel++)
							// Get the audio samples for this channel
							samples->addFrom(channel, start, delta_samples->getReadPointer(channel),
											 number_of_delta_samples, 1.0f);

						// Clean up
						delete delta_samples;
						delta_samples = NULL;

						// Increment start position
						start += number_of_delta_samples;
					}
				}
				else {
					// SPEED UP (multiple frames of audio), as long as it's not more than X frames
					int total_delta_samples = 0;
					for (int delta_frame = new_frame_number - (delta + 1);
						 delta_frame >= new_frame_number; delta_frame--)
						total_delta_samples += Frame::GetSamplesPerFrame(delta_frame, reader->info.fps,
																		 reader->info.sample_rate,
																		 reader->info.channels);

					// Allocate a new sample buffer for these delta frames
					samples = new juce::AudioSampleBuffer(channels, total_delta_samples);
					samples->clear();

					// Loop through each frame in this delta
					for (int delta_frame = new_frame_number - (delta + 1);
						 delta_frame >= new_frame_number; delta_frame--) {
						// buffer to hold delta samples
						int number_of_delta_samples = GetOrCreateFrame(delta_frame)->GetAudioSamplesCount();
						juce::AudioSampleBuffer *delta_samples = new juce::AudioSampleBuffer(channels,
																					   number_of_delta_samples);
						delta_samples->clear();

						for (int channel = 0; channel < channels; channel++)
							delta_samples->addFrom(channel, 0, GetOrCreateFrame(delta_frame)->GetAudioSamples(channel),
												   number_of_delta_samples, 1.0f);

						// Reverse the samples (if needed)
						if (!time.IsIncreasing(frame_number))
							reverse_buffer(delta_samples);

						// Copy the samples to
						for (int channel = 0; channel < channels; channel++)
							// Get the audio samples for this channel
							samples->addFrom(channel, start, delta_samples->getReadPointer(channel),
											 number_of_delta_samples, 1.0f);

						// Clean up
						delete delta_samples;
						delta_samples = NULL;

						// Increment start position
						start += number_of_delta_samples;
					}
				}

				// Resample audio to be X times faster (where X is the delta of the repeat fraction)
				resampler->SetBuffer(samples, float(start) / float(number_of_samples));

				// Resample data, and return new buffer pointer
				juce::AudioSampleBuffer *buffer = resampler->GetResampledBuffer();

				// Add the newly resized audio samples to the current frame
				for (int channel = 0; channel < channels; channel++)
					// Add new (slower) samples, to the frame object
					frame->AddAudio(true, channel, 0, buffer->getReadPointer(channel), number_of_samples, 1.0f);

				// Clean up
				buffer = NULL;
			}
			else {
				// Use the samples on this frame (but maybe reverse them if needed)
				samples = new juce::AudioSampleBuffer(channels, number_of_samples);
				samples->clear();

				// Loop through channels, and get audio samples
				for (int channel = 0; channel < channels; channel++)
					// Get the audio samples for this channel
					samples->addFrom(channel, 0, frame->GetAudioSamples(channel), number_of_samples, 1.0f);

				// reverse the samples
				if (!time.IsIncreasing(frame_number))
					reverse_buffer(samples);

				// Add reversed samples to the frame object
				for (int channel = 0; channel < channels; channel++)
					frame->AddAudio(true, channel, 0, samples->getReadPointer(channel), number_of_samples, 1.0f);


			}

			delete samples;
			samples = NULL;
		}
	}
}

// Adjust frame number minimum value
int64_t Clip::adjust_frame_number_minimum(int64_t frame_number)
{
	// Never return a frame number 0 or below
	if (frame_number < 1)
		return 1;
	else
		return frame_number;

}

// Get or generate a blank frame
std::shared_ptr<Frame> Clip::GetOrCreateFrame(int64_t number)
{
	try {
		// Debug output
		ZmqLogger::Instance()->AppendDebugMethod("Clip::GetOrCreateFrame (from reader)", "number", number);

		// Attempt to get a frame (but this could fail if a reader has just been closed)
		auto reader_frame = reader->GetFrame(number);

		// Return real frame
		if (reader_frame) {
			// Create a new copy of reader frame
			// This allows a clip to modify the pixels and audio of this frame without
			// changing the underlying reader's frame data
			auto reader_copy = std::make_shared<Frame>(*reader_frame.get());
			reader_copy->SampleRate(reader_frame->SampleRate());
			reader_copy->ChannelsLayout(reader_frame->ChannelsLayout());
			return reader_copy;
		}

	} catch (const ReaderClosed & e) {
		// ...
	} catch (const OutOfBoundsFrame & e) {
		// ...
	}

	// Estimate # of samples needed for this frame
	int estimated_samples_in_frame = Frame::GetSamplesPerFrame(number, reader->info.fps, reader->info.sample_rate, reader->info.channels);

	// Debug output
	ZmqLogger::Instance()->AppendDebugMethod("Clip::GetOrCreateFrame (create blank)", "number", number, "estimated_samples_in_frame", estimated_samples_in_frame);

	// Create blank frame
	auto new_frame = std::make_shared<Frame>(
		number, reader->info.width, reader->info.height,
		"#000000", estimated_samples_in_frame, reader->info.channels);
	new_frame->SampleRate(reader->info.sample_rate);
	new_frame->ChannelsLayout(reader->info.channel_layout);
	new_frame->AddAudioSilence(estimated_samples_in_frame);
	return new_frame;
}

// Generate JSON string of this object
std::string Clip::Json() const {

	// Return formatted string
	return JsonValue().toStyledString();
}

// Get all properties for a specific frame
std::string Clip::PropertiesJSON(int64_t requested_frame) const {

	// Generate JSON properties list
	Json::Value root;
	root["id"] = add_property_json("ID", 0.0, "string", Id(), NULL, -1, -1, true, requested_frame);
	root["position"] = add_property_json("Position", Position(), "float", "", NULL, 0, 30 * 60 * 60 * 48, false, requested_frame);
	root["layer"] = add_property_json("Track", Layer(), "int", "", NULL, 0, 20, false, requested_frame);
	root["start"] = add_property_json("Start", Start(), "float", "", NULL, 0, 30 * 60 * 60 * 48, false, requested_frame);
	root["end"] = add_property_json("End", End(), "float", "", NULL, 0, 30 * 60 * 60 * 48, false, requested_frame);
	root["duration"] = add_property_json("Duration", Duration(), "float", "", NULL, 0, 30 * 60 * 60 * 48, true, requested_frame);
	root["gravity"] = add_property_json("Gravity", gravity, "int", "", NULL, 0, 8, false, requested_frame);
	root["scale"] = add_property_json("Scale", scale, "int", "", NULL, 0, 3, false, requested_frame);
	root["display"] = add_property_json("Frame Number", display, "int", "", NULL, 0, 3, false, requested_frame);
	root["mixing"] = add_property_json("Volume Mixing", mixing, "int", "", NULL, 0, 2, false, requested_frame);
	root["waveform"] = add_property_json("Waveform", waveform, "int", "", NULL, 0, 1, false, requested_frame);
	if (!parentObjectId.empty()) {
		root["parentObjectId"] = add_property_json("Parent", 0.0, "string", parentObjectId, NULL, -1, -1, false, requested_frame);
	} else {
		root["parentObjectId"] = add_property_json("Parent", 0.0, "string", "", NULL, -1, -1, false, requested_frame);
	}
	// Add gravity choices (dropdown style)
	root["gravity"]["choices"].append(add_property_choice_json("Top Left", GRAVITY_TOP_LEFT, gravity));
	root["gravity"]["choices"].append(add_property_choice_json("Top Center", GRAVITY_TOP, gravity));
	root["gravity"]["choices"].append(add_property_choice_json("Top Right", GRAVITY_TOP_RIGHT, gravity));
	root["gravity"]["choices"].append(add_property_choice_json("Left", GRAVITY_LEFT, gravity));
	root["gravity"]["choices"].append(add_property_choice_json("Center", GRAVITY_CENTER, gravity));
	root["gravity"]["choices"].append(add_property_choice_json("Right", GRAVITY_RIGHT, gravity));
	root["gravity"]["choices"].append(add_property_choice_json("Bottom Left", GRAVITY_BOTTOM_LEFT, gravity));
	root["gravity"]["choices"].append(add_property_choice_json("Bottom Center", GRAVITY_BOTTOM, gravity));
	root["gravity"]["choices"].append(add_property_choice_json("Bottom Right", GRAVITY_BOTTOM_RIGHT, gravity));

	// Add scale choices (dropdown style)
	root["scale"]["choices"].append(add_property_choice_json("Crop", SCALE_CROP, scale));
	root["scale"]["choices"].append(add_property_choice_json("Best Fit", SCALE_FIT, scale));
	root["scale"]["choices"].append(add_property_choice_json("Stretch", SCALE_STRETCH, scale));
	root["scale"]["choices"].append(add_property_choice_json("None", SCALE_NONE, scale));

	// Add frame number display choices (dropdown style)
	root["display"]["choices"].append(add_property_choice_json("None", FRAME_DISPLAY_NONE, display));
	root["display"]["choices"].append(add_property_choice_json("Clip", FRAME_DISPLAY_CLIP, display));
	root["display"]["choices"].append(add_property_choice_json("Timeline", FRAME_DISPLAY_TIMELINE, display));
	root["display"]["choices"].append(add_property_choice_json("Both", FRAME_DISPLAY_BOTH, display));

	// Add volume mixing choices (dropdown style)
	root["mixing"]["choices"].append(add_property_choice_json("None", VOLUME_MIX_NONE, mixing));
	root["mixing"]["choices"].append(add_property_choice_json("Average", VOLUME_MIX_AVERAGE, mixing));
	root["mixing"]["choices"].append(add_property_choice_json("Reduce", VOLUME_MIX_REDUCE, mixing));

	// Add waveform choices (dropdown style)
	root["waveform"]["choices"].append(add_property_choice_json("Yes", true, waveform));
	root["waveform"]["choices"].append(add_property_choice_json("No", false, waveform));
	
	// Add the parentTrackedObject's properties
	if (parentTrackedObject)
	{	
		// Convert Clip's frame position to Timeline's frame position
		long clip_start_position = round(Position() * info.fps.ToDouble()) + 1;
		long clip_start_frame = (Start() * info.fps.ToDouble()) + 1;
		double timeline_frame_number = requested_frame + clip_start_position - clip_start_frame;

		// Get attached object's parent clip properties
		std::map< std::string, float > trackedObjectParentClipProperties = parentTrackedObject->GetParentClipProperties(timeline_frame_number);
		double parentObject_frame_number = trackedObjectParentClipProperties["frame_number"];
		// Get attached object properties
		std::map< std::string, float > trackedObjectProperties = parentTrackedObject->GetBoxValues(parentObject_frame_number);
		
		// Correct the parent Tracked Object properties by the clip's reference system
		float parentObject_location_x = trackedObjectProperties["cx"] - 0.5 + trackedObjectParentClipProperties["cx"];
		float parentObject_location_y = trackedObjectProperties["cy"] - 0.5 + trackedObjectParentClipProperties["cy"];
		float parentObject_scale_x = trackedObjectProperties["w"]*trackedObjectProperties["sx"];
		float parentObject_scale_y = trackedObjectProperties["h"]*trackedObjectProperties["sy"];
		float parentObject_rotation = trackedObjectProperties["r"] + trackedObjectParentClipProperties["r"];

		// Add the parent Tracked Object properties to JSON
		root["location_x"] = add_property_json("Location X", parentObject_location_x, "float", "", &location_x, -1.0, 1.0, false, requested_frame);
		root["location_y"] = add_property_json("Location Y", parentObject_location_y, "float", "", &location_y, -1.0, 1.0, false, requested_frame);
		root["scale_x"] = add_property_json("Scale X", parentObject_scale_x, "float", "", &scale_x, 0.0, 1.0, false, requested_frame);
		root["scale_y"] = add_property_json("Scale Y", parentObject_scale_y, "float", "", &scale_y, 0.0, 1.0, false, requested_frame);
		root["rotation"] = add_property_json("Rotation", parentObject_rotation, "float", "", &rotation, -360, 360, false, requested_frame);
		root["shear_x"] = add_property_json("Shear X", shear_x.GetValue(requested_frame), "float", "", &shear_x, -1.0, 1.0, false, requested_frame);
		root["shear_y"] = add_property_json("Shear Y", shear_y.GetValue(requested_frame), "float", "", &shear_y, -1.0, 1.0, false, requested_frame);
	}
	// Add the parentClipObject's properties
	else if (parentClipObject)
	{
		// Convert Clip's frame position to Timeline's frame position
		long clip_start_position = round(Position() * info.fps.ToDouble()) + 1;
		long clip_start_frame = (Start() * info.fps.ToDouble()) + 1;
		double timeline_frame_number = requested_frame + clip_start_position - clip_start_frame;

		// Correct the parent Clip Object properties by the clip's reference system
		float parentObject_location_x = parentClipObject->location_x.GetValue(timeline_frame_number); 
		float parentObject_location_y = parentClipObject->location_y.GetValue(timeline_frame_number);
		float parentObject_scale_x = parentClipObject->scale_x.GetValue(timeline_frame_number);
		float parentObject_scale_y = parentClipObject->scale_y.GetValue(timeline_frame_number);
		float parentObject_shear_x = parentClipObject->shear_x.GetValue(timeline_frame_number);
		float parentObject_shear_y = parentClipObject->shear_y.GetValue(timeline_frame_number);
		float parentObject_rotation = parentClipObject->rotation.GetValue(timeline_frame_number);

		// Add the parent Clip Object properties to JSON
		root["location_x"] = add_property_json("Location X", parentObject_location_x, "float", "", &location_x, -1.0, 1.0, false, requested_frame);
		root["location_y"] = add_property_json("Location Y", parentObject_location_y, "float", "", &location_y, -1.0, 1.0, false, requested_frame);
		root["scale_x"] = add_property_json("Scale X", parentObject_scale_x, "float", "", &scale_x, 0.0, 1.0, false, requested_frame);
		root["scale_y"] = add_property_json("Scale Y", parentObject_scale_y, "float", "", &scale_y, 0.0, 1.0, false, requested_frame);
		root["rotation"] = add_property_json("Rotation", parentObject_rotation, "float", "", &rotation, -360, 360, false, requested_frame);
		root["shear_x"] = add_property_json("Shear X", parentObject_shear_x, "float", "", &shear_x, -1.0, 1.0, false, requested_frame);
		root["shear_y"] = add_property_json("Shear Y", parentObject_shear_y, "float", "", &shear_y, -1.0, 1.0, false, requested_frame);
	}
	else
	{
		// Add this own clip's properties to JSON
		root["location_x"] = add_property_json("Location X", location_x.GetValue(requested_frame), "float", "", &location_x, -1.0, 1.0, false, requested_frame);
		root["location_y"] = add_property_json("Location Y", location_y.GetValue(requested_frame), "float", "", &location_y, -1.0, 1.0, false, requested_frame);
		root["scale_x"] = add_property_json("Scale X", scale_x.GetValue(requested_frame), "float", "", &scale_x, 0.0, 1.0, false, requested_frame);
		root["scale_y"] = add_property_json("Scale Y", scale_y.GetValue(requested_frame), "float", "", &scale_y, 0.0, 1.0, false, requested_frame);
		root["rotation"] = add_property_json("Rotation", rotation.GetValue(requested_frame), "float", "", &rotation, -360, 360, false, requested_frame);
		root["shear_x"] = add_property_json("Shear X", shear_x.GetValue(requested_frame), "float", "", &shear_x, -1.0, 1.0, false, requested_frame);
		root["shear_y"] = add_property_json("Shear Y", shear_y.GetValue(requested_frame), "float", "", &shear_y, -1.0, 1.0, false, requested_frame);
	}

	// Keyframes
	root["alpha"] = add_property_json("Alpha", alpha.GetValue(requested_frame), "float", "", &alpha, 0.0, 1.0, false, requested_frame);
	root["origin_x"] = add_property_json("Origin X", origin_x.GetValue(requested_frame), "float", "", &origin_x, 0.0, 1.0, false, requested_frame);
	root["origin_y"] = add_property_json("Origin Y", origin_y.GetValue(requested_frame), "float", "", &origin_y, 0.0, 1.0, false, requested_frame);
	root["volume"] = add_property_json("Volume", volume.GetValue(requested_frame), "float", "", &volume, 0.0, 1.0, false, requested_frame);
	root["time"] = add_property_json("Time", time.GetValue(requested_frame), "float", "", &time, 0.0, 30 * 60 * 60 * 48, false, requested_frame);
	root["channel_filter"] = add_property_json("Channel Filter", channel_filter.GetValue(requested_frame), "int", "", &channel_filter, -1, 10, false, requested_frame);
	root["channel_mapping"] = add_property_json("Channel Mapping", channel_mapping.GetValue(requested_frame), "int", "", &channel_mapping, -1, 10, false, requested_frame);
	root["has_audio"] = add_property_json("Enable Audio", has_audio.GetValue(requested_frame), "int", "", &has_audio, -1, 1.0, false, requested_frame);
	root["has_video"] = add_property_json("Enable Video", has_video.GetValue(requested_frame), "int", "", &has_video, -1, 1.0, false, requested_frame);

	// Add enable audio/video choices (dropdown style)
	root["has_audio"]["choices"].append(add_property_choice_json("Auto", -1, has_audio.GetValue(requested_frame)));
	root["has_audio"]["choices"].append(add_property_choice_json("Off", 0, has_audio.GetValue(requested_frame)));
	root["has_audio"]["choices"].append(add_property_choice_json("On", 1, has_audio.GetValue(requested_frame)));
	root["has_video"]["choices"].append(add_property_choice_json("Auto", -1, has_video.GetValue(requested_frame)));
	root["has_video"]["choices"].append(add_property_choice_json("Off", 0, has_video.GetValue(requested_frame)));
	root["has_video"]["choices"].append(add_property_choice_json("On", 1, has_video.GetValue(requested_frame)));

	root["wave_color"] = add_property_json("Wave Color", 0.0, "color", "", &wave_color.red, 0, 255, false, requested_frame);
	root["wave_color"]["red"] = add_property_json("Red", wave_color.red.GetValue(requested_frame), "float", "", &wave_color.red, 0, 255, false, requested_frame);
	root["wave_color"]["blue"] = add_property_json("Blue", wave_color.blue.GetValue(requested_frame), "float", "", &wave_color.blue, 0, 255, false, requested_frame);
	root["wave_color"]["green"] = add_property_json("Green", wave_color.green.GetValue(requested_frame), "float", "", &wave_color.green, 0, 255, false, requested_frame);


	// Return formatted string
	return root.toStyledString();
}

// Generate Json::Value for this object
Json::Value Clip::JsonValue() const {

	// Create root json object
	Json::Value root = ClipBase::JsonValue(); // get parent properties
	root["parentObjectId"] = parentObjectId;
	root["gravity"] = gravity;
	root["scale"] = scale;
	root["anchor"] = anchor;
	root["display"] = display;
	root["mixing"] = mixing;
	root["waveform"] = waveform;
	root["scale_x"] = scale_x.JsonValue();
	root["scale_y"] = scale_y.JsonValue();
	root["location_x"] = location_x.JsonValue();
	root["location_y"] = location_y.JsonValue();
	root["alpha"] = alpha.JsonValue();
	root["rotation"] = rotation.JsonValue();
	root["time"] = time.JsonValue();
	root["volume"] = volume.JsonValue();
	root["wave_color"] = wave_color.JsonValue();
	root["shear_x"] = shear_x.JsonValue();
	root["shear_y"] = shear_y.JsonValue();
	root["origin_x"] = origin_x.JsonValue();
	root["origin_y"] = origin_y.JsonValue();
	root["channel_filter"] = channel_filter.JsonValue();
	root["channel_mapping"] = channel_mapping.JsonValue();
	root["has_audio"] = has_audio.JsonValue();
	root["has_video"] = has_video.JsonValue();
	root["perspective_c1_x"] = perspective_c1_x.JsonValue();
	root["perspective_c1_y"] = perspective_c1_y.JsonValue();
	root["perspective_c2_x"] = perspective_c2_x.JsonValue();
	root["perspective_c2_y"] = perspective_c2_y.JsonValue();
	root["perspective_c3_x"] = perspective_c3_x.JsonValue();
	root["perspective_c3_y"] = perspective_c3_y.JsonValue();
	root["perspective_c4_x"] = perspective_c4_x.JsonValue();
	root["perspective_c4_y"] = perspective_c4_y.JsonValue();

	// Add array of effects
	root["effects"] = Json::Value(Json::arrayValue);

	// loop through effects
	for (auto existing_effect : effects)
	{
		root["effects"].append(existing_effect->JsonValue());
	}

	if (reader)
		root["reader"] = reader->JsonValue();
	else
		root["reader"] = Json::Value(Json::objectValue);

	// return JsonValue
	return root;
}

// Load JSON string into this object
void Clip::SetJson(const std::string value) {

	// Parse JSON string into JSON objects
	try
	{
		const Json::Value root = openshot::stringToJson(value);
		// Set all values that match
		SetJsonValue(root);
	}
	catch (const std::exception& e)
	{
		// Error parsing JSON (or missing keys)
		throw InvalidJSON("JSON is invalid (missing keys or invalid data types)");
	}
}

// Load Json::Value into this object
void Clip::SetJsonValue(const Json::Value root) {

	// Set parent data
	ClipBase::SetJsonValue(root);

	// Set data from Json (if key is found)
	if (!root["parentObjectId"].isNull()){
		parentObjectId = root["parentObjectId"].asString();
		if (parentObjectId.size() > 0 && parentObjectId != ""){
			AttachToObject(parentObjectId);
		} else{
			parentTrackedObject = nullptr;
			parentClipObject = NULL;
		}
	}
	if (!root["gravity"].isNull())
		gravity = (GravityType) root["gravity"].asInt();
	if (!root["scale"].isNull())
		scale = (ScaleType) root["scale"].asInt();
	if (!root["anchor"].isNull())
		anchor = (AnchorType) root["anchor"].asInt();
	if (!root["display"].isNull())
		display = (FrameDisplayType) root["display"].asInt();
	if (!root["mixing"].isNull())
		mixing = (VolumeMixType) root["mixing"].asInt();
	if (!root["waveform"].isNull())
		waveform = root["waveform"].asBool();
	if (!root["scale_x"].isNull())
		scale_x.SetJsonValue(root["scale_x"]);
	if (!root["scale_y"].isNull())
		scale_y.SetJsonValue(root["scale_y"]);
	if (!root["location_x"].isNull())
		location_x.SetJsonValue(root["location_x"]);
	if (!root["location_y"].isNull())
		location_y.SetJsonValue(root["location_y"]);
	if (!root["alpha"].isNull())
		alpha.SetJsonValue(root["alpha"]);
	if (!root["rotation"].isNull())
		rotation.SetJsonValue(root["rotation"]);
	if (!root["time"].isNull())
		time.SetJsonValue(root["time"]);
	if (!root["volume"].isNull())
		volume.SetJsonValue(root["volume"]);
	if (!root["wave_color"].isNull())
		wave_color.SetJsonValue(root["wave_color"]);
	if (!root["shear_x"].isNull())
		shear_x.SetJsonValue(root["shear_x"]);
	if (!root["shear_y"].isNull())
		shear_y.SetJsonValue(root["shear_y"]);
	if (!root["origin_x"].isNull())
		origin_x.SetJsonValue(root["origin_x"]);
	if (!root["origin_y"].isNull())
		origin_y.SetJsonValue(root["origin_y"]);
	if (!root["channel_filter"].isNull())
		channel_filter.SetJsonValue(root["channel_filter"]);
	if (!root["channel_mapping"].isNull())
		channel_mapping.SetJsonValue(root["channel_mapping"]);
	if (!root["has_audio"].isNull())
		has_audio.SetJsonValue(root["has_audio"]);
	if (!root["has_video"].isNull())
		has_video.SetJsonValue(root["has_video"]);
	if (!root["perspective_c1_x"].isNull())
		perspective_c1_x.SetJsonValue(root["perspective_c1_x"]);
	if (!root["perspective_c1_y"].isNull())
		perspective_c1_y.SetJsonValue(root["perspective_c1_y"]);
	if (!root["perspective_c2_x"].isNull())
		perspective_c2_x.SetJsonValue(root["perspective_c2_x"]);
	if (!root["perspective_c2_y"].isNull())
		perspective_c2_y.SetJsonValue(root["perspective_c2_y"]);
	if (!root["perspective_c3_x"].isNull())
		perspective_c3_x.SetJsonValue(root["perspective_c3_x"]);
	if (!root["perspective_c3_y"].isNull())
		perspective_c3_y.SetJsonValue(root["perspective_c3_y"]);
	if (!root["perspective_c4_x"].isNull())
		perspective_c4_x.SetJsonValue(root["perspective_c4_x"]);
	if (!root["perspective_c4_y"].isNull())
		perspective_c4_y.SetJsonValue(root["perspective_c4_y"]);
	if (!root["effects"].isNull()) {

		// Clear existing effects
		effects.clear();

		// loop through effects
		for (const auto existing_effect : root["effects"]) {
			// Create Effect
			EffectBase *e = NULL;
			if (!existing_effect["type"].isNull()) {

				// Create instance of effect
				if ( (e = EffectInfo().CreateEffect(existing_effect["type"].asString()))) {
					
					// Load Json into Effect
					e->SetJsonValue(existing_effect);

					// Add Effect to Timeline
					AddEffect(e);
				}
			}
		}
	}
	if (!root["reader"].isNull()) // does Json contain a reader?
	{
		if (!root["reader"]["type"].isNull()) // does the reader Json contain a 'type'?
		{
			// Close previous reader (if any)
			bool already_open = false;
			if (reader)
			{
				// Track if reader was open
				already_open = reader->IsOpen();

				// Close and delete existing reader (if any)
				reader->Close();
				delete reader;
				reader = NULL;
			}

			// Create new reader (and load properties)
			std::string type = root["reader"]["type"].asString();

			if (type == "FFmpegReader") {

				// Create new reader
				reader = new openshot::FFmpegReader(root["reader"]["path"].asString(), false);
				reader->SetJsonValue(root["reader"]);

			} else if (type == "QtImageReader") {

				// Create new reader
				reader = new openshot::QtImageReader(root["reader"]["path"].asString(), false);
				reader->SetJsonValue(root["reader"]);

#ifdef USE_IMAGEMAGICK
			} else if (type == "ImageReader") {

				// Create new reader
				reader = new ImageReader(root["reader"]["path"].asString(), false);
				reader->SetJsonValue(root["reader"]);

			} else if (type == "TextReader") {

				// Create new reader
				reader = new TextReader();
				reader->SetJsonValue(root["reader"]);
#endif

			} else if (type == "ChunkReader") {

				// Create new reader
				reader = new openshot::ChunkReader(root["reader"]["path"].asString(), (ChunkVersion) root["reader"]["chunk_version"].asInt());
				reader->SetJsonValue(root["reader"]);

			} else if (type == "DummyReader") {

				// Create new reader
				reader = new openshot::DummyReader();
				reader->SetJsonValue(root["reader"]);

			} else if (type == "Timeline") {

				// Create new reader (always load from file again)
				// This prevents FrameMappers from being loaded on accident
				reader = new openshot::Timeline(root["reader"]["path"].asString(), true);
			}

			// mark as managed reader and set parent
			if (reader) {
				reader->ParentClip(this);
				allocated_reader = reader;
			}

			// Re-Open reader (if needed)
			if (already_open)
				reader->Open();

		}
	}
}

// Sort effects by order
void Clip::sort_effects()
{
	// sort clips
	effects.sort(CompareClipEffects());
}

// Add an effect to the clip
void Clip::AddEffect(EffectBase* effect)
{
	// Set parent clip pointer
	effect->ParentClip(this);

	// Add effect to list
	effects.push_back(effect);

	// Sort effects
	sort_effects();

	// Get the parent timeline of this clip
	Timeline* parentTimeline = (Timeline *) ParentTimeline();

	if (parentTimeline)
		effect->ParentTimeline(parentTimeline);

    #ifdef USE_OPENCV
	// Add Tracked Object to Timeline
	if (effect->info.has_tracked_object){

		// Check if this clip has a parent timeline
		if (parentTimeline){

			effect->ParentTimeline(parentTimeline);

			// Iterate through effect's vector of Tracked Objects
			for (auto const& trackedObject : effect->trackedObjects){
				
				// Cast the Tracked Object as TrackedObjectBBox
				std::shared_ptr<TrackedObjectBBox> trackedObjectBBox = std::static_pointer_cast<TrackedObjectBBox>(trackedObject.second);

				// Set the Tracked Object's parent clip to this
				trackedObjectBBox->ParentClip(this);

				// Add the Tracked Object to the timeline
				parentTimeline->AddTrackedObject(trackedObjectBBox);
			}	
		}
	}
    #endif

	// Clear cache
	cache.Clear();
}

// Remove an effect from the clip
void Clip::RemoveEffect(EffectBase* effect)
{
	effects.remove(effect);
}

// Apply effects to the source frame (if any)
void Clip::apply_effects(std::shared_ptr<Frame> frame)
{
	// Find Effects at this position and layer
	for (auto effect : effects)
	{
		// Apply the effect to this frame
		frame = effect->GetFrame(frame, frame->number);

	} // end effect loop
}

// Compare 2 floating point numbers for equality
bool Clip::isEqual(double a, double b)
{
	return fabs(a - b) < 0.000001;
}

// Apply keyframes to the source frame (if any)
void Clip::apply_keyframes(std::shared_ptr<Frame> frame, std::shared_ptr<QImage> background_canvas) {
    // Skip out if video was disabled or only an audio frame (no visualisation in use)
    if (has_video.GetInt(frame->number) == 0 ||
        (!Waveform() && !Reader()->info.has_video))
        // Skip the rest of the image processing for performance reasons
        return;

    // Get image from clip
    std::shared_ptr<QImage> source_image = frame->GetImage();

    /* REPLACE IMAGE WITH WAVEFORM IMAGE (IF NEEDED) */
    if (Waveform())
    {
        // Debug output
        ZmqLogger::Instance()->AppendDebugMethod("Clip::get_transform (Generate Waveform Image)", "frame->number", frame->number, "Waveform()", Waveform());

        // Get the color of the waveform
        int red = wave_color.red.GetInt(frame->number);
        int green = wave_color.green.GetInt(frame->number);
        int blue = wave_color.blue.GetInt(frame->number);
        int alpha = wave_color.alpha.GetInt(frame->number);

        // Generate Waveform Dynamically (the size of the timeline)
        source_image = frame->GetWaveform(background_canvas->width(), background_canvas->height(), red, green, blue, alpha);
    }

    // Size of final image
    int width = background_canvas->width();
    int height = background_canvas->height();

    // Get transform from clip's keyframes
    QTransform transform = get_transform(frame, width, height);

    // Debug output
    ZmqLogger::Instance()->AppendDebugMethod("Clip::ApplyKeyframes (Transform: Composite Image Layer: Prepare)", "frame->number", frame->number);

    // Load timeline's new frame image into a QPainter
    QPainter painter(background_canvas.get());
    painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing, true);

    // Apply transform (translate, rotate, scale)
    painter.setTransform(transform);

    // Composite a new layer onto the image
    painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
    painter.drawImage(0, 0, *source_image);

    if (timeline) {
        Timeline *t = (Timeline *) timeline;

        // Draw frame #'s on top of image (if needed)
        if (display != FRAME_DISPLAY_NONE) {
            std::stringstream frame_number_str;
            switch (display) {
                case (FRAME_DISPLAY_NONE):
                    // This is only here to prevent unused-enum warnings
                    break;

                case (FRAME_DISPLAY_CLIP):
                    frame_number_str << frame->number;
                    break;

                case (FRAME_DISPLAY_TIMELINE):
                    frame_number_str << (position * t->info.fps.ToFloat()) + frame->number;
                    break;

                case (FRAME_DISPLAY_BOTH):
                    frame_number_str << (position * t->info.fps.ToFloat()) + frame->number << " (" << frame->number << ")";
                    break;
            }

            // Draw frame number on top of image
            painter.setPen(QColor("#ffffff"));
            painter.drawText(20, 20, QString(frame_number_str.str().c_str()));
        }
    }
    painter.end();

    // Add new QImage to frame
    frame->AddImage(background_canvas);
}

// Apply keyframes to the source frame (if any)
QTransform Clip::get_transform(std::shared_ptr<Frame> frame, int width, int height)
{
    // Get image from clip
    std::shared_ptr<QImage> source_image = frame->GetImage();

	/* ALPHA & OPACITY */
	if (alpha.GetValue(frame->number) != 1.0)
	{
		float alpha_value = alpha.GetValue(frame->number);

		// Get source image's pixels
		unsigned char *pixels = source_image->bits();

		// Loop through pixels
		for (int pixel = 0, byte_index=0; pixel < source_image->width() * source_image->height(); pixel++, byte_index+=4)
		{
			// Apply alpha to pixel values (since we use a premultiplied value, we must
			// multiply the alpha with all colors).
			pixels[byte_index + 0] *= alpha_value;
			pixels[byte_index + 1] *= alpha_value;
			pixels[byte_index + 2] *= alpha_value;
			pixels[byte_index + 3] *= alpha_value;
		}

		// Debug output
		ZmqLogger::Instance()->AppendDebugMethod("Clip::get_transform (Set Alpha & Opacity)", "alpha_value", alpha_value, "frame->number", frame->number);
	}

	/* RESIZE SOURCE IMAGE - based on scale type */
	QSize source_size = source_image->size();

	// Apply stretch scale to correctly fit the bounding-box
	if (parentTrackedObject){
		scale = SCALE_STRETCH;
	}

	switch (scale)
	{
		case (SCALE_FIT): {
			source_size.scale(width, height, Qt::KeepAspectRatio);

			// Debug output
			ZmqLogger::Instance()->AppendDebugMethod("Clip::get_transform (Scale: SCALE_FIT)", "frame->number", frame->number, "source_width", source_size.width(), "source_height", source_size.height());
			break;
		}
		case (SCALE_STRETCH): {
			source_size.scale(width, height, Qt::IgnoreAspectRatio);

			// Debug output
			ZmqLogger::Instance()->AppendDebugMethod("Clip::get_transform (Scale: SCALE_STRETCH)", "frame->number", frame->number, "source_width", source_size.width(), "source_height", source_size.height());
			break;
		}
		case (SCALE_CROP): {
			source_size.scale(width, height, Qt::KeepAspectRatioByExpanding);

			// Debug output
			ZmqLogger::Instance()->AppendDebugMethod("Clip::get_transform (Scale: SCALE_CROP)", "frame->number", frame->number, "source_width", source_size.width(), "source_height", source_size.height());
			break;
		}
		case (SCALE_NONE): {
		    // Image is already the original size (i.e. no scaling mode) relative
		    // to the preview window size (i.e. timeline / preview ratio). No further
		    // scaling is needed here.
			// Debug output
			ZmqLogger::Instance()->AppendDebugMethod("Clip::get_transform (Scale: SCALE_NONE)", "frame->number", frame->number, "source_width", source_size.width(), "source_height", source_size.height());
			break;
		}
	}

	// Initialize parent object's properties (Clip or Tracked Object)
	float parentObject_location_x = 0.0;
	float parentObject_location_y = 0.0;
	float parentObject_scale_x = 1.0;
	float parentObject_scale_y = 1.0;
	float parentObject_shear_x = 0.0;
	float parentObject_shear_y = 0.0;
	float parentObject_rotation = 0.0;

	// Get the parentClipObject properties
	if (parentClipObject){

		// Convert Clip's frame position to Timeline's frame position
		long clip_start_position = round(Position() * info.fps.ToDouble()) + 1;
		long clip_start_frame = (Start() * info.fps.ToDouble()) + 1;
		double timeline_frame_number = frame->number + clip_start_position - clip_start_frame;

		// Get parent object's properties (Clip)
		parentObject_location_x = parentClipObject->location_x.GetValue(timeline_frame_number); 
		parentObject_location_y = parentClipObject->location_y.GetValue(timeline_frame_number);
		parentObject_scale_x = parentClipObject->scale_x.GetValue(timeline_frame_number);
		parentObject_scale_y = parentClipObject->scale_y.GetValue(timeline_frame_number);
		parentObject_shear_x = parentClipObject->shear_x.GetValue(timeline_frame_number);
		parentObject_shear_y = parentClipObject->shear_y.GetValue(timeline_frame_number);
		parentObject_rotation = parentClipObject->rotation.GetValue(timeline_frame_number);
	}

	// Get the parentTrackedObject properties
	if (parentTrackedObject){

		// Convert Clip's frame position to Timeline's frame position
		long clip_start_position = round(Position() * info.fps.ToDouble()) + 1;
		long clip_start_frame = (Start() * info.fps.ToDouble()) + 1;
		double timeline_frame_number = frame->number + clip_start_position - clip_start_frame;

		// Get parentTrackedObject's parent clip's properties
		std::map<std::string, float> trackedObjectParentClipProperties = parentTrackedObject->GetParentClipProperties(timeline_frame_number);

		// Get the attached object's parent clip's properties
		if (!trackedObjectParentClipProperties.empty())
		{
			// Get parent object's properties (Tracked Object)
			float parentObject_frame_number = trackedObjectParentClipProperties["frame_number"];

			// Access the parentTrackedObject's properties
			std::map<std::string, float> trackedObjectProperties = parentTrackedObject->GetBoxValues(parentObject_frame_number);

			// Get the Tracked Object's properties and correct them by the clip's reference system
			parentObject_location_x = trackedObjectProperties["cx"] - 0.5 + trackedObjectParentClipProperties["location_x"];
			parentObject_location_y = trackedObjectProperties["cy"] - 0.5 + trackedObjectParentClipProperties["location_y"];
			parentObject_scale_x = trackedObjectProperties["w"]*trackedObjectProperties["sx"];
			parentObject_scale_y = trackedObjectProperties["h"]*trackedObjectProperties["sy"];
			parentObject_rotation = trackedObjectProperties["r"] + trackedObjectParentClipProperties["rotation"];
		} 
		else 
		{
			// Access the parentTrackedObject's properties
			std::map<std::string, float> trackedObjectProperties = parentTrackedObject->GetBoxValues(timeline_frame_number);

			// Get the Tracked Object's properties and correct them by the clip's reference system
			parentObject_location_x = trackedObjectProperties["cx"] - 0.5;
			parentObject_location_y = trackedObjectProperties["cy"] - 0.5;
			parentObject_scale_x = trackedObjectProperties["w"]*trackedObjectProperties["sx"];
			parentObject_scale_y = trackedObjectProperties["h"]*trackedObjectProperties["sy"];
			parentObject_rotation = trackedObjectProperties["r"];
		}
	}

	/* GRAVITY LOCATION - Initialize X & Y to the correct values (before applying location curves) */
	float x = 0.0; // left
	float y = 0.0; // top

	// Adjust size for scale x and scale y
	float sx = scale_x.GetValue(frame->number); // percentage X scale
	float sy = scale_y.GetValue(frame->number); // percentage Y scale

	// Change clip's scale to parentObject's scale
	if(parentObject_scale_x != 0.0 && parentObject_scale_y != 0.0){
		sx*= parentObject_scale_x;
		sy*= parentObject_scale_y;
	}

	float scaled_source_width = source_size.width() * sx;
	float scaled_source_height = source_size.height() * sy;
	
	switch (gravity)
	{
		case (GRAVITY_TOP_LEFT):
			// This is only here to prevent unused-enum warnings
			break;
		case (GRAVITY_TOP):
			x = (width - scaled_source_width) / 2.0; // center
			break;
		case (GRAVITY_TOP_RIGHT):
			x = width - scaled_source_width; // right
			break;
		case (GRAVITY_LEFT):
			y = (height - scaled_source_height) / 2.0; // center
			break;
		case (GRAVITY_CENTER):
			x = (width - scaled_source_width) / 2.0; // center
			y = (height - scaled_source_height) / 2.0; // center
			break;
		case (GRAVITY_RIGHT):
			x = width - scaled_source_width; // right
			y = (height - scaled_source_height) / 2.0; // center
			break;
		case (GRAVITY_BOTTOM_LEFT):
			y = (height - scaled_source_height); // bottom
			break;
		case (GRAVITY_BOTTOM):
			x = (width - scaled_source_width) / 2.0; // center
			y = (height - scaled_source_height); // bottom
			break;
		case (GRAVITY_BOTTOM_RIGHT):
			x = width - scaled_source_width; // right
			y = (height - scaled_source_height); // bottom
			break;
	}

	// Debug output
	ZmqLogger::Instance()->AppendDebugMethod("Clip::get_transform (Gravity)", "frame->number", frame->number, "source_clip->gravity", gravity, "scaled_source_width", scaled_source_width, "scaled_source_height", scaled_source_height);

	QTransform transform;
		
	/* LOCATION, ROTATION, AND SCALE */
	float r = rotation.GetValue(frame->number) + parentObject_rotation; // rotate in degrees
	x += (width * (location_x.GetValue(frame->number) + parentObject_location_x )); // move in percentage of final width
	y += (height * (location_y.GetValue(frame->number) + parentObject_location_y )); // move in percentage of final height
	float shear_x_value = shear_x.GetValue(frame->number) + parentObject_shear_x;
	float shear_y_value = shear_y.GetValue(frame->number) + parentObject_shear_y;
	float origin_x_value = origin_x.GetValue(frame->number);
	float origin_y_value = origin_y.GetValue(frame->number);

	// Transform source image (if needed)
	ZmqLogger::Instance()->AppendDebugMethod("Clip::get_transform (Build QTransform - if needed)", "frame->number", frame->number, "x", x, "y", y, "r", r, "sx", sx, "sy", sy);

	if (!isEqual(x, 0) || !isEqual(y, 0)) {
		// TRANSLATE/MOVE CLIP
		transform.translate(x, y);
	} 
	if (!isEqual(r, 0) || !isEqual(shear_x_value, 0) || !isEqual(shear_y_value, 0)) {
		// ROTATE CLIP (around origin_x, origin_y)
		float origin_x_offset = (scaled_source_width * origin_x_value);
		float origin_y_offset = (scaled_source_height * origin_y_value);
		transform.translate(origin_x_offset, origin_y_offset);
		transform.rotate(r);
		transform.shear(shear_x_value, shear_y_value);
		transform.translate(-origin_x_offset,-origin_y_offset);
	}
	// SCALE CLIP (if needed)
	float source_width_scale = (float(source_size.width()) / float(source_image->width())) * sx;
	float source_height_scale = (float(source_size.height()) / float(source_image->height())) * sy;
	if (!isEqual(source_width_scale, 1.0) || !isEqual(source_height_scale, 1.0)) {
		transform.scale(source_width_scale, source_height_scale);
	}

    return transform;
}
