#include "project.h"
#include "project/footage.h"

#include "panels/panels.h"
#include "panels/timeline.h"
#include "panels/viewer.h"
#include "playback/playback.h"
#include "project/effect.h"
#include "project/transition.h"
#include "panels/timeline.h"
#include "project/sequence.h"
#include "project/clip.h"
#include "io/previewgenerator.h"
#include "project/undo.h"
#include "mainwindow.h"
#include "io/config.h"
#include "playback/cacher.h"
#include "dialogs/replaceclipmediadialog.h"
#include "panels/effectcontrols.h"
#include "dialogs/newsequencedialog.h"
#include "dialogs/mediapropertiesdialog.h"
#include "dialogs/loaddialog.h"
#include "io/clipboard.h"
#include "project/media.h"
#include "ui/sourcetable.h"
#include "debug.h"

#include <QApplication>
#include <QFileDialog>
#include <QString>
#include <QVariant>
#include <QCharRef>
#include <QMessageBox>
#include <QDropEvent>
#include <QMimeData>
#include <QPushButton>
#include <QInputDialog>
#include <QSortFilterProxyModel>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include <QListView>
#include <QSizePolicy>
#include <QVBoxLayout>

extern "C" {
	#include <libavformat/avformat.h>
	#include <libavcodec/avcodec.h>
}

#define MAXIMUM_RECENT_PROJECTS 10

ProjectModel project_model;

QString autorecovery_filename;
QString project_url = "";
QStringList recent_projects;
QString recent_proj_file;

Project::Project(QWidget *parent) :
	QDockWidget(parent)
{
	setObjectName("Project");
	resize(504, 371);
	QSizePolicy sizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
	sizePolicy.setHorizontalStretch(0);
	sizePolicy.setVerticalStretch(0);
	sizePolicy.setHeightForWidth(this->sizePolicy().hasHeightForWidth());
	setSizePolicy(sizePolicy);

	QWidget* dockWidgetContents = new QWidget();
	QVBoxLayout* verticalLayout = new QVBoxLayout(dockWidgetContents);
	verticalLayout->setContentsMargins(0, 0, 0, 0);

	setWidget(dockWidgetContents);

	source_table = new SourceTable(dockWidgetContents);
    source_table->project_parent = this;
	source_table->setAcceptDrops(true);
	source_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
	source_table->setDragDropMode(QAbstractItemView::DragDrop);
	source_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
	verticalLayout->addWidget(source_table);
	//setWidget(source_table);

	//layout->addWidget(source_table);

    sorter = new QSortFilterProxyModel(this);
    sorter->setSourceModel(&project_model);
    source_table->setModel(sorter);

	//retranslateUi(Project);
	setWindowTitle(QApplication::translate("Project", "Project", nullptr));
}

Project::~Project() {
	delete sorter;
}

QString Project::get_next_sequence_name(QString start) {
	if (start.isEmpty()) start = "Sequence";

	int n = 1;
	bool found = true;
	QString name;
	while (found) {
		found = false;
		name = start + " ";
		if (n < 10) {
			name += "0";
		}
		name += QString::number(n);
        for (int i=0;i<project_model.childCount();i++) {
            if (QString::compare(project_model.child(i)->get_name(), name, Qt::CaseInsensitive) == 0) {
				found = true;
				n++;
				break;
			}
		}
	}
	return name;
}

Sequence* create_sequence_from_media(QVector<Media*>& media_list) {
	Sequence* s = new Sequence();

	s->name = panel_project->get_next_sequence_name();

	// shitty hardcoded default values
	s->width = 1920;
	s->height = 1080;
	s->frame_rate = 29.97;
	s->audio_frequency = 48000;
	s->audio_layout = 3;

	bool got_video_values = false;
	bool got_audio_values = false;
	for (int i=0;i<media_list.size();i++) {
        Media* media = media_list.at(i);
        switch (media->get_type()) {
		case MEDIA_TYPE_FOOTAGE:
		{
            Footage* m = media->to_footage();
			if (m->ready) {
				if (!got_video_values) {
					for (int j=0;j<m->video_tracks.size();j++) {
						FootageStream* ms = m->video_tracks.at(j);
						s->width = ms->video_width;
						s->height = ms->video_height;
						if (ms->video_frame_rate != 0) {
							s->frame_rate = ms->video_frame_rate;

							if (ms->video_interlacing != VIDEO_PROGRESSIVE) s->frame_rate *= 2;

							// only break with a decent frame rate, otherwise there may be a better candidate
							got_video_values = true;
							break;
						}
					}
				}
				if (!got_audio_values) {
					for (int j=0;j<m->audio_tracks.size();j++) {
						FootageStream* ms = m->audio_tracks.at(j);
						s->audio_frequency = ms->audio_frequency;
						got_audio_values = true;
						break;
					}
				}
			}
		}
			break;
		case MEDIA_TYPE_SEQUENCE:
		{
            Sequence* seq = media->to_sequence();
			s->width = seq->width;
			s->height = seq->height;
			s->frame_rate = seq->frame_rate;
			s->audio_frequency = seq->audio_frequency;
			s->audio_layout = seq->audio_layout;

			got_video_values = true;
			got_audio_values = true;
		}
			break;
		}
		if (got_video_values && got_audio_values) break;
	}

	return s;
}

void Project::duplicate_selected() {
    QModelIndexList items = source_table->selectionModel()->selectedRows();
    bool duped = false;
    ComboAction* ca = new ComboAction();
    for (int j=0;j<items.size();j++) {
        Media* i = item_to_media(items.at(j));
        if (i->get_type() == MEDIA_TYPE_SEQUENCE) {
            new_sequence(ca, i->to_sequence()->copy(), false, item_to_media(items.at(j).parent()));
            duped = true;
        }
    }
    if (duped) {
        undo_stack.push(ca);
    } else {
        delete ca;
    }
}

void Project::replace_selected_file() {
    QModelIndexList selected_items = source_table->selectionModel()->selectedRows();
	if (selected_items.size() == 1) {
        Media* item = item_to_media(selected_items.at(0));
        if (item->get_type() == MEDIA_TYPE_FOOTAGE) {
			replace_media(item, 0);
		}
	}
}

void Project::replace_media(Media* item, QString filename) {
	if (filename.isEmpty()) {
        filename = QFileDialog::getOpenFileName(this, "Replace '" + item->get_name() + "'", "", "All Files (*)");
	}
	if (!filename.isEmpty()) {
		ReplaceMediaCommand* rmc = new ReplaceMediaCommand(item, filename);
		undo_stack.push(rmc);
	}
}

void Project::replace_clip_media() {
	if (sequence == NULL) {
		QMessageBox::critical(this, "No active sequence", "No sequence is active, please open the sequence you want to replace clips from.", QMessageBox::Ok);
    } else {
        QModelIndexList selected_items = source_table->selectionModel()->selectedRows();
        if (selected_items.size() == 1) {
            Media* item = item_to_media(selected_items.at(0));
            if (item->get_type() == MEDIA_TYPE_SEQUENCE && sequence == item->to_sequence()) {
                QMessageBox::critical(this, "Active sequence selected", "You cannot insert a sequence into itself, so no clips of this media would be in this sequence.", QMessageBox::Ok);
            } else {
                ReplaceClipMediaDialog dialog(this, source_table, item);
                dialog.exec();
            }
        }
	}
}

void Project::open_properties() {
    QModelIndexList selected_items = source_table->selectionModel()->selectedRows();
    if (selected_items.size() == 1) {
        Media* item = item_to_media(selected_items.at(0));
        switch (item->get_type()) {
		case MEDIA_TYPE_FOOTAGE:
		{
            MediaPropertiesDialog mpd(this, item);
			mpd.exec();
		}
			break;
		case MEDIA_TYPE_SEQUENCE:
		{
			NewSequenceDialog nsd(this);
            Sequence* s = item->to_sequence();
			nsd.existing_sequence = s;
            nsd.existing_item = item;
			nsd.exec();
		}
			break;
		default:
		{
			// fall back to renaming
            QString new_name = QInputDialog::getText(this, "Rename '" + item->get_name() + "'", "Enter new name:", QLineEdit::Normal, item->get_name());
            if (!new_name.isEmpty()) {
                MediaRename* mr = new MediaRename(item, new_name);
				undo_stack.push(mr);
			}
		}
		}
	}
}

Media* Project::new_sequence(ComboAction *ca, Sequence *s, bool open, Media* parent) {
    if (parent == NULL) parent = project_model.get_root();
    Media* item = new Media(parent);
    item->set_sequence(s);

    if (ca != NULL) {
        ca->append(new NewSequenceCommand(item, parent));
        if (open) ca->append(new ChangeSequenceAction(s));
    } else {
        project_model.appendChild(NULL, item);
        if (open) set_sequence(s);
	}
    return item;
}

QString Project::get_file_name_from_path(const QString& path) {
	return path.mid(path.lastIndexOf('/')+1);
}

/*Media* Project::new_item() {
    Media* item = new Media(0);
    //item->setFlags(item->flags() | Qt::ItemIsEditable);
    return item;
}*/

bool Project::is_focused() {
    return source_table->hasFocus();
}

Media* Project::new_folder(QString name) {
    Media* item = new Media(0);
    item->set_folder();
    return item;
}

Media *Project::item_to_media(const QModelIndex &index) {
    return static_cast<Media*>(sorter->mapToSource(index).internalPointer());
//    return static_cast<Media*>(index.internalPointer());
}

void Project::get_all_media_from_table(QList<Media*> items, QList<Media*>& list, int search_type) {
    for (int i=0;i<items.size();i++) {
        Media* item = items.at(i);
        if (item->get_type() == MEDIA_TYPE_FOLDER) {
            QList<Media*> children;
            for (int j=0;j<item->childCount();j++) {
                children.append(item->child(j));
            }
			get_all_media_from_table(children, list, search_type);
        } else if (search_type == item->get_type() || search_type == -1) {
            list.append(item);
        }
    }
}

bool delete_clips_in_clipboard_with_media(ComboAction* ca, Media* m) {
    int delete_count = 0;
    if (clipboard_type == CLIPBOARD_TYPE_CLIP) {
        for (int i=0;i<clipboard.size();i++) {
            Clip* c = static_cast<Clip*>(clipboard.at(i));
            if (c->media == m) {
                ca->append(new RemoveClipsFromClipboard(i-delete_count));
                delete_count++;
            }
        }
    }
    return (delete_count > 0);
}

void Project::delete_selected_media() {
    ComboAction* ca = new ComboAction();
    QModelIndexList selected_items = source_table->selectionModel()->selectedRows();
    QList<Media*> items;
    for (int i=0;i<selected_items.size();i++) {
        items.append(item_to_media(selected_items.at(i)));
    }
    bool remove = true;
    bool redraw = false;

    // correctly sort (fixes qt bug - see AddMediaCommand::redo() for info)
    source_table->setSortingEnabled(false);
    source_table->setSortingEnabled(true);

    // check if media is in use
    QVector<Media*> parents;
    QList<Media*> sequence_items;
    QList<Media*> all_top_level_items;
    for (int i=0;i<project_model.childCount();i++) {
        all_top_level_items.append(project_model.child(i));
    }
	get_all_media_from_table(all_top_level_items, sequence_items, MEDIA_TYPE_SEQUENCE); // find all sequences in project
    if (sequence_items.size() > 0) {
        QList<Media*> media_items;
		get_all_media_from_table(items, media_items, MEDIA_TYPE_FOOTAGE);
        for (int i=0;i<media_items.size();i++) {
            Media* item = media_items.at(i);
            Footage* media = item->to_footage();
            bool confirm_delete = false;
            for (int j=0;j<sequence_items.size();j++) {
                Sequence* s = sequence_items.at(j)->to_sequence();
                for (int k=0;k<s->clips.size();k++) {
                    Clip* c = s->clips.at(k);
                    if (c != NULL && c->media == item) {
                        if (!confirm_delete) {
                            // we found a reference, so we know we'll need to ask if the user wants to delete it
                            QMessageBox confirm(this);
                            confirm.setWindowTitle("Delete media in use?");
                            confirm.setText("The media '" + media->name + "' is currently used in '" + s->name + "'. Deleting it will remove all instances in the sequence. Are you sure you want to do this?");
                            QAbstractButton* yes_button = confirm.addButton(QMessageBox::Yes);
                            QAbstractButton* skip_button = NULL;
                            if (items.size() > 1) skip_button = confirm.addButton("Skip", QMessageBox::NoRole);
                            QAbstractButton* abort_button = confirm.addButton(QMessageBox::Cancel);
                            confirm.exec();
                            if (confirm.clickedButton() == yes_button) {
                                // remove all clips referencing this media
                                confirm_delete = true;
                                redraw = true;
                            } else if (confirm.clickedButton() == skip_button) {
                                // remove media item and any folders containing it from the remove list
                                Media* parent = item;
                                while (parent != NULL) {
                                    parents.append(parent);

                                    // re-add item's siblings
                                    for (int m=0;m<parent->childCount();m++) {
                                        Media* child = parent->child(m);
                                        bool found = false;
                                        for (int n=0;n<items.size();n++) {
                                            if (items.at(n) == child) {
                                                found = true;
                                                break;
                                            }
                                        }
                                        if (!found) {
                                            items.append(child);
                                        }
                                    }

                                    parent = parent->parentItem();
                                }

                                j = sequence_items.size();
                                k = s->clips.size();
                            } else if (confirm.clickedButton() == abort_button) {
                                // break out of loop
                                i = media_items.size();
                                j = sequence_items.size();
                                k = s->clips.size();

                                remove = false;
                            }
                        }
                        if (confirm_delete) {
                            ca->append(new DeleteClipAction(s, k));
                        }
                    }
                }
			}
			if (confirm_delete) {
                delete_clips_in_clipboard_with_media(ca, item);
			}

        }
    }

    // remove
    if (remove) {
        // remove media and parents
        for (int m=0;m<parents.size();m++) {
            for (int l=0;l<items.size();l++) {
                if (items.at(l) == parents.at(m)) {
                    items.removeAt(l);
                    l--;
                }
            }
        }

		for (int i=0;i<items.size();i++) {
            ca->append(new DeleteMediaCommand(items.at(i)));

            if (items.at(i)->get_type() == MEDIA_TYPE_SEQUENCE) {
                redraw = true;

                Sequence* s = items.at(i)->to_sequence();

				if (s == sequence) {
                    ca->append(new ChangeSequenceAction(NULL));
                }

				if (s == panel_footage_viewer->seq) {
                    panel_footage_viewer->set_media(NULL);
				}
            } else if (items.at(i)->get_type() == MEDIA_TYPE_FOOTAGE) {
				if (panel_footage_viewer->seq != NULL) {
					for (int j=0;j<panel_footage_viewer->seq->clips.size();j++) {
						Clip* c = panel_footage_viewer->seq->clips.at(j);
						if (c != NULL) {
                            if (c->media == items.at(i)->to_object()) {
                                panel_footage_viewer->set_media(NULL);
							}
							break;
						}
					}
				}
			}
        }
        undo_stack.push(ca);

        // redraw clips
        if (redraw) {
			update_ui(true);
		}
    } else {
        delete ca;
    }
}

void Project::start_preview_generator(Media* item, bool replacing) {
    // set up throbber animation
    MediaThrobber* throbber = new MediaThrobber(item);
    throbber->moveToThread(QApplication::instance()->thread());
    item->throbber = throbber;
    QMetaObject::invokeMethod(throbber, "start", Qt::QueuedConnection);

    PreviewGenerator* pg = new PreviewGenerator(item, item->to_footage(), replacing);
    item->to_footage()->preview_gen = pg;
    connect(pg, SIGNAL(set_icon(int, bool)), throbber, SLOT(stop(int, bool)));
    pg->start(QThread::LowPriority);
}

void Project::process_file_list(QStringList& files, bool recursive, Media* replace, Media* parent) {
    bool imported = false;

    QVector<QString> image_sequence_urls;
    QVector<bool> image_sequence_importassequence;
    QStringList image_sequence_formats = config.img_seq_formats.split("|");

	if (!recursive) last_imported_media.clear();

	bool create_undo_action = (!recursive && replace == NULL);
    ComboAction* ca;
    if (create_undo_action) ca = new ComboAction();

	for (int i=0;i<files.size();i++) {
		if (QFileInfo(files.at(i)).isDir()) {
			QString folder_name = get_file_name_from_path(files.at(i));
            Media* folder = new_folder(folder_name);

			QDir directory(files.at(i));
			directory.setFilter(QDir::NoDotAndDotDot | QDir::AllEntries);

			QFileInfoList subdir_files = directory.entryInfoList();
			QStringList subdir_filenames;

			for (int j=0;j<subdir_files.size();j++) {
				subdir_filenames.append(subdir_files.at(j).filePath());
			}

            process_file_list(subdir_filenames, true, NULL, folder);

            if (create_undo_action) {
                ca->append(new AddMediaCommand(folder, parent));
            } else {
                project_model.appendChild(parent, folder);
			}

			imported = true;
		} else if (!files.at(i).isEmpty()) {
			QString file(files.at(i));
			bool skip = false;

			/* Heuristic to determine whether file is part of an image sequence */

			// check file extension (assume it's not a

			int lastcharindex = file.lastIndexOf(".");
			bool found = true;
			if (lastcharindex != -1 && lastcharindex > file.lastIndexOf('/')) {
				// image_sequence_formats
				found = false;
				QString ext = file.mid(lastcharindex+1);
				for (int j=0;j<image_sequence_formats.size();j++) {
					if (ext == image_sequence_formats.at(j)) {
						found = true;
						break;
					}
				}
			} else {
				lastcharindex = file.length();
			}

			if (lastcharindex == 0) lastcharindex++;

			if (found && file[lastcharindex-1].isDigit()) {
				bool is_img_sequence = false;

				// how many digits are in the filename?
				int digit_count = 0;
				int digit_test = lastcharindex-1;
				while (file[digit_test].isDigit()) {
					digit_count++;
					digit_test--;
				}

				// retrieve number from filename
				digit_test++;
				int file_number = file.mid(digit_test, digit_count).toInt();

				// Check if there are files with the same filename but just different numbers
				if (QFileInfo::exists(QString(file.left(digit_test) + QString("%1").arg(file_number-1, digit_count, 10, QChar('0')) + file.mid(lastcharindex)))
						|| QFileInfo::exists(QString(file.left(digit_test) + QString("%1").arg(file_number+1, digit_count, 10, QChar('0')) + file.mid(lastcharindex)))) {
					is_img_sequence = true;
				}

				if (is_img_sequence) {
					// get the URL that we would pass to FFmpeg to force it to read the image as a sequence
					QString new_filename = file.left(digit_test) + "%" + QString("%1").arg(digit_count, 2, 10, QChar('0')) + "d" + file.mid(lastcharindex);

					// add image sequence url to a vector in case the user imported several files that
					// we're interpreting as a possible sequence
					found = false;
					for (int i=0;i<image_sequence_urls.size();i++) {
						if (image_sequence_urls.at(i) == new_filename) {
							// either SKIP if we're importing as a sequence, or leave it if we aren't
							if (image_sequence_importassequence.at(i)) {
								skip = true;
							}
							found = true;
							break;
						}
					}
					if (!found) {
						image_sequence_urls.append(new_filename);
						if (QMessageBox::question(this, "Image sequence detected", "The file '" + file + "' appears to be part of an image sequence. Would you like to import it as such?", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) {
							file = new_filename;
							image_sequence_importassequence.append(true);
						} else {
							image_sequence_importassequence.append(false);
						}
					}
				}
			}

			if (!skip) {
                Media* item;
				Footage* m;

				if (replace != NULL) {
					item = replace;
                    m = replace->to_footage();
					m->reset();
				} else {
                    item = new Media(parent);
					m = new Footage();
				}

                m->using_inout = false;
                m->url = file;
				m->name = get_file_name_from_path(files.at(i));

                item->set_footage(m);

                // generate waveform/thumbnail in another thread
                start_preview_generator(item, replace != NULL);

                last_imported_media.append(item);

				if (replace == NULL) {
					if (create_undo_action) {
                        ca->append(new AddMediaCommand(item, parent));
					} else {
                        project_model.appendChild(parent, item);
					}
				}

				imported = true;
			}
		}
    }
	if (create_undo_action) {
		if (imported) {
            undo_stack.push(ca);
		} else {
            delete ca;
		}
	}
}

Media* Project::get_selected_folder() {
	// if one item is selected and it's a folder, return it
    QModelIndexList selected_items = source_table->selectionModel()->selectedRows();
    if (selected_items.size() == 1) {
        Media* m = item_to_media(selected_items.at(0));
        if (m->get_type() == MEDIA_TYPE_FOLDER) return m;
	}
    return NULL;
}

bool Project::reveal_media(void *media, QModelIndex parent) {
    for (int i=0;i<project_model.rowCount(parent);i++) {
        const QModelIndex& item = project_model.index(i, 0, parent);
        Media* m = project_model.getItem(item);

        if (m->get_type() == MEDIA_TYPE_FOLDER) {
			if (reveal_media(media, item)) return true;
        } else if (m->to_object() == media) {
			// expand all folders leading to this media
            QModelIndex hierarchy = item.parent();
            while (hierarchy.isValid()) {
                source_table->setExpanded(hierarchy, true);
                hierarchy = hierarchy.parent();
			}

            // select item
            source_table->selectionModel()->select(item, QItemSelectionModel::Select);

			return true;
		}
	}

	return false;
}

void Project::import_dialog() {
	QFileDialog fd(this, "Import media...", "", "All Files (*)");
	fd.setFileMode(QFileDialog::ExistingFiles);

	if (fd.exec()) {
		QStringList files = fd.selectedFiles();
        process_file_list(files, false, NULL, get_selected_folder());
	}
}

void Project::delete_clips_using_selected_media() {
	if (sequence == NULL) {
		QMessageBox::critical(this, "No active sequence", "No sequence is active, please open the sequence you want to delete clips from.", QMessageBox::Ok);
	} else {
        ComboAction* ca = new ComboAction();
		bool deleted = false;
        QModelIndexList items = source_table->selectionModel()->selectedRows();
        for (int i=0;i<sequence->clips.size();i++) {
            Clip* c = sequence->clips.at(i);
			if (c != NULL) {
				for (int j=0;j<items.size();j++) {
                    Media* m = item_to_media(items.at(j));
					if (c->media == m) {
                        ca->append(new DeleteClipAction(sequence, i));
						deleted = true;
					}
				}
			}
		}
		for (int j=0;j<items.size();j++) {
            Media* m = item_to_media(items.at(j));
			if (delete_clips_in_clipboard_with_media(ca, m)) deleted = true;
		}
		if (deleted) {
            undo_stack.push(ca);
			update_ui(true);
		} else {
            delete ca;
		}
	}
}

void Project::clear() {
	// clear effects cache
	panel_effect_controls->clear_effects(true);

    // delete sequences first because it's important to close all the clips before deleting the media
    QVector<Media*> sequences = list_all_project_sequences();
    for (int i=0;i<sequences.size();i++) {
        delete sequences.at(i)->to_sequence();
        sequences.at(i)->set_sequence(NULL);
    }

    // delete everything else
    project_model.clear();
}

void Project::new_project() {
	// clear existing project
    set_sequence(NULL);
    panel_footage_viewer->set_media(NULL);
    clear();
	mainWindow->setWindowModified(false);
}

void Project::load_project(bool autorecovery) {
    new_project();

    LoadDialog ld(this, autorecovery);
    ld.exec();
}

void Project::save_folder(QXmlStreamWriter& stream, int type, bool set_ids_only, const QModelIndex& parent) {
    bool root = (!parent.parent().isValid());
    for (int i=0;i<project_model.rowCount(parent);i++) {
        const QModelIndex& item = project_model.index(i, 0, parent);
        Media* m = project_model.getItem(item);

        if (type == m->get_type()) {
            if (m->get_type() == MEDIA_TYPE_FOLDER) {
                if (set_ids_only) {
                    m->temp_id = folder_id; // saves a temporary ID for matching in the project file
                    folder_id++;
                } else {
                    // if we're saving folders, save the folder
                    stream.writeStartElement("folder");
                    stream.writeAttribute("name", m->get_name());
                    stream.writeAttribute("id", QString::number(m->temp_id));
                    if (!item.parent().isValid()) {
                        stream.writeAttribute("parent", "0");
                    } else {
                        stream.writeAttribute("parent", QString::number(project_model.getItem(item.parent())->temp_id));
                    }
                    stream.writeEndElement();
                }
				// save_folder(stream, item, type, set_ids_only);
            } else {
                int folder = root ? 0 : project_model.getItem(parent)->temp_id;
                if (type == MEDIA_TYPE_FOOTAGE) {
                    Footage* f = m->to_footage();
                    f->save_id = media_id;
                    stream.writeStartElement("footage");
                    stream.writeAttribute("id", QString::number(media_id));
                    stream.writeAttribute("folder", QString::number(folder));
                    stream.writeAttribute("name", f->name);
                    stream.writeAttribute("url", proj_dir.relativeFilePath(f->url));
                    stream.writeAttribute("duration", QString::number(f->length));
                    stream.writeAttribute("using_inout", QString::number(f->using_inout));
                    stream.writeAttribute("in", QString::number(f->in));
                    stream.writeAttribute("out", QString::number(f->out));
                    for (int j=0;j<f->video_tracks.size();j++) {
                        FootageStream* ms = f->video_tracks.at(j);
                        stream.writeStartElement("video");
                        stream.writeAttribute("id", QString::number(ms->file_index));
                        stream.writeAttribute("width", QString::number(ms->video_width));
                        stream.writeAttribute("height", QString::number(ms->video_height));
						stream.writeAttribute("framerate", QString::number(ms->video_frame_rate, 'f', 10));
                        stream.writeAttribute("infinite", QString::number(ms->infinite_length));
                        stream.writeEndElement();
                    }
                    for (int j=0;j<f->audio_tracks.size();j++) {
                        FootageStream* ms = f->audio_tracks.at(j);
                        stream.writeStartElement("audio");
                        stream.writeAttribute("id", QString::number(ms->file_index));
                        stream.writeAttribute("channels", QString::number(ms->audio_channels));
                        stream.writeAttribute("layout", QString::number(ms->audio_layout));
                        stream.writeAttribute("frequency", QString::number(ms->audio_frequency));
                        stream.writeEndElement();
                    }
                    stream.writeEndElement();
                    media_id++;
                } else if (type == MEDIA_TYPE_SEQUENCE) {
                    Sequence* s = m->to_sequence();
                    if (set_ids_only) {
                        s->save_id = sequence_id;
                        sequence_id++;
                    } else {
                        stream.writeStartElement("sequence");
                        stream.writeAttribute("id", QString::number(s->save_id));
                        stream.writeAttribute("folder", QString::number(folder));
                        stream.writeAttribute("name", s->name);
                        stream.writeAttribute("width", QString::number(s->width));
                        stream.writeAttribute("height", QString::number(s->height));
						stream.writeAttribute("framerate", QString::number(s->frame_rate, 'f', 10));
                        stream.writeAttribute("afreq", QString::number(s->audio_frequency));
                        stream.writeAttribute("alayout", QString::number(s->audio_layout));
                        if (s == sequence) {
                            stream.writeAttribute("open", "1");
						}
						stream.writeAttribute("workarea", QString::number(s->using_workarea));
						stream.writeAttribute("workareaIn", QString::number(s->workarea_in));
						stream.writeAttribute("workareaOut", QString::number(s->workarea_out));

                        for (int j=0;j<s->transitions.size();j++) {
                            Transition* t = s->transitions.at(j);
                            if (t != NULL) {
                                stream.writeStartElement("transition");
                                stream.writeAttribute("id", QString::number(j));
                                stream.writeAttribute("length", QString::number(t->get_true_length()));
                                t->save(stream);
                                stream.writeEndElement(); // transition
                            }
                        }

                        for (int j=0;j<s->clips.size();j++) {
                            Clip* c = s->clips.at(j);
                            if (c != NULL) {
                                stream.writeStartElement("clip"); // clip
                                stream.writeAttribute("id", QString::number(j));
								stream.writeAttribute("enabled", QString::number(c->enabled));
                                stream.writeAttribute("name", c->name);
                                stream.writeAttribute("clipin", QString::number(c->clip_in));
                                stream.writeAttribute("in", QString::number(c->timeline_in));
                                stream.writeAttribute("out", QString::number(c->timeline_out));
                                stream.writeAttribute("track", QString::number(c->track));
                                stream.writeAttribute("opening", QString::number(c->opening_transition));
                                stream.writeAttribute("closing", QString::number(c->closing_transition));

                                stream.writeAttribute("r", QString::number(c->color_r));
                                stream.writeAttribute("g", QString::number(c->color_g));
                                stream.writeAttribute("b", QString::number(c->color_b));

                                stream.writeAttribute("autoscale", QString::number(c->autoscale));
								stream.writeAttribute("speed", QString::number(c->speed, 'f', 10));
                                stream.writeAttribute("maintainpitch", QString::number(c->maintain_audio_pitch));
                                stream.writeAttribute("reverse", QString::number(c->reverse));

								if (c->media != NULL) {
									stream.writeAttribute("type", QString::number(c->media->get_type()));
									switch (c->media->get_type()) {
									case MEDIA_TYPE_FOOTAGE:
										stream.writeAttribute("media", QString::number(c->media->to_footage()->save_id));
										stream.writeAttribute("stream", QString::number(c->media_stream));
										break;
									case MEDIA_TYPE_SEQUENCE:
										stream.writeAttribute("sequence", QString::number(c->media->to_sequence()->save_id));
										break;
									}
								}

                                stream.writeStartElement("linked"); // linked
                                for (int k=0;k<c->linked.size();k++) {
                                    stream.writeStartElement("link"); // link
                                    stream.writeAttribute("id", QString::number(c->linked.at(k)));
                                    stream.writeEndElement(); // link
                                }
                                stream.writeEndElement(); // linked

                                for (int k=0;k<c->effects.size();k++) {
									stream.writeStartElement("effect"); // effect
									c->effects.at(k)->save(stream);
									stream.writeEndElement(); // effect
                                }

                                stream.writeEndElement(); // clip
                            }
                        }
						for (int j=0;j<s->markers.size();j++) {
							stream.writeStartElement("marker");
							stream.writeAttribute("frame", QString::number(s->markers.at(j).frame));
							stream.writeAttribute("name", s->markers.at(j).name);
							stream.writeEndElement();
						}
                        stream.writeEndElement();
                    }
                }
            }
        }

        if (m->get_type() == MEDIA_TYPE_FOLDER) {
            save_folder(stream, type, set_ids_only, item);
		}
    }
}

void Project::save_project(bool autorecovery) {
    folder_id = 1;
    media_id = 1;
    sequence_id = 1;

    QFile file(autorecovery ? autorecovery_filename : project_url);
    if (!file.open(QIODevice::WriteOnly/* | QIODevice::Text*/)) {
		dout << "[ERROR] Could not open file";
        return;
    }

    QXmlStreamWriter stream(&file);
    stream.setAutoFormatting(true);
    stream.writeStartDocument(); // doc

    stream.writeStartElement("project"); // project

	stream.writeTextElement("version", QString::number(SAVE_VERSION));

    stream.writeTextElement("url", project_url);
    proj_dir = QFileInfo(project_url).absoluteDir();

    save_folder(stream, MEDIA_TYPE_FOLDER, true);

    stream.writeStartElement("folders"); // folders
    save_folder(stream, MEDIA_TYPE_FOLDER, false);
    stream.writeEndElement(); // folders

    stream.writeStartElement("media"); // media
    save_folder(stream, MEDIA_TYPE_FOOTAGE, false);
    stream.writeEndElement(); // media

    save_folder(stream, MEDIA_TYPE_SEQUENCE, true);

    stream.writeStartElement("sequences"); // sequences
    save_folder(stream, MEDIA_TYPE_SEQUENCE, false);
    stream.writeEndElement();// sequences

    stream.writeEndElement(); // project

    stream.writeEndDocument(); // doc

    file.close();

    if (!autorecovery) {
        add_recent_project(project_url);
		mainWindow->setWindowModified(false);
    }
}

void Project::save_recent_projects() {
    // save to file
    QFile f(recent_proj_file);
    if (f.open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) {
        QTextStream out(&f);
        for (int i=0;i<recent_projects.size();i++) {
            if (i > 0) {
                out << "\n";
            }
            out << recent_projects.at(i);
        }
        f.close();
    } else {
		dout << "[WARNING] Could not save recent projects";
    }
}

void Project::clear_recent_projects() {
    recent_projects.clear();
	save_recent_projects();
}

void Project::add_recent_project(QString url) {
    bool found = false;
    for (int i=0;i<recent_projects.size();i++) {
        if (url == recent_projects.at(i)) {
            found = true;
            recent_projects.move(i, 0);
            break;
        }
    }
    if (!found) {
        recent_projects.insert(0, url);
        if (recent_projects.size() > MAXIMUM_RECENT_PROJECTS) {
            recent_projects.removeLast();
        }
    }
    save_recent_projects();
}

void Project::list_all_sequences_worker(QVector<Media*>* list, Media* parent) {
    for (int i=0;i<project_model.childCount(parent);i++) {
        Media* item = project_model.child(i, parent);
        switch (item->get_type()) {
        case MEDIA_TYPE_SEQUENCE:
            list->append(item);
            break;
        case MEDIA_TYPE_FOLDER:
            list_all_sequences_worker(list, item);
            break;
        }
    }
}

QVector<Media*> Project::list_all_project_sequences() {
    QVector<Media*> list;
    list_all_sequences_worker(&list, NULL);
    return list;
}

#define THROBBER_LIMIT 20
#define THROBBER_SIZE 50

MediaThrobber::MediaThrobber(Media *i) : pixmap(":/icons/throbber.png"), animation(0), item(i), animator(NULL) {}

void MediaThrobber::start() {
    // set up throbber
    animation_update();
    animator = new QTimer(this);
    animator->setInterval(20);
    connect(animator, SIGNAL(timeout()), this, SLOT(animation_update()));
    animator->start();
}

void MediaThrobber::animation_update() {
    if (animation == THROBBER_LIMIT) {
        animation = 0;
    }
    project_model.set_icon(item, QIcon(pixmap.copy(THROBBER_SIZE*animation, 0, THROBBER_SIZE, THROBBER_SIZE)));
	animation++;
}

void MediaThrobber::stop(int icon_type, bool replace) {
    if (animator != NULL) {
        animator->stop();
        delete animator;
    }

    switch (icon_type) {
    case ICON_TYPE_VIDEO: project_model.set_icon(item, QIcon(":/icons/videosource.png")); break;
    case ICON_TYPE_AUDIO: project_model.set_icon(item, QIcon(":/icons/audiosource.png")); break;
    case ICON_TYPE_IMAGE: project_model.set_icon(item, QIcon(":/icons/imagesource.png")); break;
    case ICON_TYPE_ERROR: project_model.set_icon(item, QIcon(":/icons/error.png")); break;
    }

	// refresh all clips
    QVector<Media*> sequences = panel_project->list_all_project_sequences();
    for (int i=0;i<sequences.size();i++) {
        Sequence* s = sequences.at(i)->to_sequence();
        for (int j=0;j<s->clips.size();j++) {
            Clip* c = s->clips.at(j);
			if (c != NULL) {
				c->refresh();
            }
        }
	}

    // redraw clips
	update_ui(replace);

    panel_project->source_table->viewport()->update();
    item->throbber = NULL;
    deleteLater();
}
