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

#include "project.h"

#include "ass_dialogue.h"
#include "ass_file.h"
#include "async_video_provider.h"
#include "audio_controller.h"
#include "audio_provider_factory.h"
#include "base_grid.h"
#include "charset_detect.h"
#include "compat.h"
#include "dialog_progress.h"
#include "dialogs.h"
#include "format.h"
#include "include/aegisub/context.h"
#include "include/aegisub/video_provider.h"
#include "mkv_wrap.h"
#include "options.h"
#include "selection_controller.h"
#include "subs_controller.h"
#include "utils.h"
#include "video_controller.h"
#include "video_display.h"

#include <libaegisub/audio/provider.h>
#include <libaegisub/format_path.h>
#include <libaegisub/fs.h>
#include <libaegisub/keyframe.h>
#include <libaegisub/log.h>
#include <libaegisub/path.h>

#include <boost/algorithm/string/case_conv.hpp>
#include <wx/msgdlg.h>

Project::Project(agi::Context *c) : context(c) {
	BindConnection(OPT_SUB("Audio/Cache/Type", &Project::ReloadAudio, this));
	BindConnection(OPT_SUB("Audio/Provider", &Project::ReloadAudio, this));
	BindConnection(OPT_SUB("Provider/Audio/FFmpegSource/Decode Error Handling", &Project::ReloadAudio, this));
	BindConnection(OPT_SUB("Provider/Avisynth/Allow Ancient", &Project::ReloadVideo, this));
	BindConnection(OPT_SUB("Provider/Avisynth/Memory Max", &Project::ReloadVideo, this));
	BindConnection(OPT_SUB("Provider/Video/FFmpegSource/Decoding Threads", &Project::ReloadVideo, this));
	BindConnection(OPT_SUB("Provider/Video/FFmpegSource/Unsafe Seeking", &Project::ReloadVideo, this));
	BindConnection(OPT_SUB("Subtitle/Provider", &Project::ReloadVideo, this));
	BindConnection(OPT_SUB("Video/Provider", &Project::ReloadVideo, this));
}

Project::~Project() { }

void Project::UpdateRelativePaths() {
	using namespace std::string_view_literals;
	context->ass->Properties.audio_file     = context->path->MakeRelative(audio_file, "?script"sv).generic_string();
	context->ass->Properties.video_file     = context->path->MakeRelative(video_file, "?script"sv).generic_string();
	context->ass->Properties.timecodes_file = context->path->MakeRelative(timecodes_file, "?script"sv).generic_string();
	context->ass->Properties.keyframes_file = context->path->MakeRelative(keyframes_file, "?script"sv).generic_string();
}

void Project::ReloadAudio() {
	if (audio_provider)
		LoadAudio(audio_file);
}

void Project::ReloadVideo() {
	if (video_provider) {
		DoLoadVideo(video_file);
		context->videoController->JumpToFrame(context->videoController->GetFrameN());
	}
}

void Project::ShowError(wxString const& message) {
	wxMessageBox(message, _("Error loading file"), wxOK | wxICON_ERROR | wxCENTER, context->parent);
}

void Project::ShowError(std::string const& message) {
	ShowError(to_wx(message));
}

void Project::SetPath(agi::fs::path& var, const char *token, const char *mru, agi::fs::path const& value) {
	var = value;
	if (*token)
		context->path->SetToken(token, value);
	if (*mru)
		config::mru->Add(mru, value);
	UpdateRelativePaths();
}

bool Project::DoLoadSubtitles(agi::fs::path const& path, std::string encoding, ProjectProperties &properties) {
	try {
		if (encoding.empty())
			encoding = CharSetDetect::GetEncoding(path);
	}
	catch (agi::UserCancelException const&) {
		return false;
	}
	catch (agi::fs::FileNotFound const&) {
		config::mru->Remove("Subtitle", path);
		ShowError(fmt_tl("%s not found.", path.string()));
		return false;
	}

	if (encoding != "binary") {
		// Try loading as timecodes and keyframes first since we can't
		// distinguish them based on filename alone, and just ignore failures
		// rather than trying to differentiate between malformed timecodes
		// files and things that aren't timecodes files at all
		try { DoLoadTimecodes(path); return false; } catch (...) { }
		try { DoLoadKeyframes(path); return false; } catch (...) { }
	}

	try {
		properties = context->subsController->Load(path, encoding.c_str());
	}
	catch (agi::UserCancelException const&) { return false; }
	catch (agi::fs::FileNotFound const&) {
		config::mru->Remove("Subtitle", path);
		ShowError(fmt_tl("%s not found.", path.string()));
		return false;
	}
	catch (agi::Exception const& e) {
		ShowError(e.GetMessage());
		return false;
	}
	catch (std::exception const& e) {
		ShowError(std::string(e.what()));
		return false;
	}
	catch (...) {
		ShowError(_("Unknown error"));
		return false;
	}

	Selection sel;
	AssDialogue *active_line = nullptr;
	if (!context->ass->Events.empty()) {
		int row = mid<int>(0, properties.active_row, context->ass->Events.size() - 1);
		active_line = &*std::next(context->ass->Events.begin(), row);
		sel.insert(active_line);
	}
	context->selectionController->SetSelectionAndActive(std::move(sel), active_line);
	context->subsGrid->ScrollTo(properties.scroll_position);

	return true;
}

void Project::LoadSubtitles(agi::fs::path path, std::string encoding, bool load_linked) {
	ProjectProperties properties;
	if (DoLoadSubtitles(path, encoding, properties) && load_linked)
		LoadUnloadFiles(properties);
}

void Project::CloseSubtitles() {
	context->subsController->Close();
	context->path->SetToken("?script", "");
	LoadUnloadFiles(context->ass->Properties);
	auto line = &*context->ass->Events.begin();
	context->selectionController->SetSelectionAndActive({line}, line);
}

void Project::LoadUnloadFiles(ProjectProperties properties) {
	auto load_linked = OPT_GET("App/Auto/Load Linked Files")->GetInt();
	if (!load_linked) return;

	auto audio     = context->path->MakeAbsolute(properties.audio_file, "?script");
	auto video     = context->path->MakeAbsolute(properties.video_file, "?script");
	auto timecodes = context->path->MakeAbsolute(properties.timecodes_file, "?script");
	auto keyframes = context->path->MakeAbsolute(properties.keyframes_file, "?script");

	// There are TOCTOU races here but they should not cause any actual harm.
	if (!agi::fs::Exists(audio) && !context->path->IsDummyPath(audio))
		audio = "";

	if (!agi::fs::Exists(video) && !context->path->IsDummyPath(video))
		video = "";

	if (!agi::fs::Exists(timecodes))
		timecodes = "";

	if (!agi::fs::Exists(keyframes))
		keyframes = "";

	if (video == video_file && audio == audio_file && keyframes == keyframes_file && timecodes == timecodes_file)
		return;

	if (load_linked == 2) {
		wxString str = _("Do you want to load/unload the associated files?");
		str += "\n";

		auto append_file = [&](agi::fs::path const& p, wxString const& unload, wxString const& load) {
			if (p.empty())
				str += "\n" + unload;
			else
				str += "\n" + agi::wxformat(load, p);
		};

		if (audio != audio_file)
			append_file(audio, _("Unload audio"), _("Load audio file: %s"));
		if (video != video_file)
			append_file(video, _("Unload video"), _("Load video file: %s"));
		if (timecodes != timecodes_file)
			append_file(timecodes, _("Unload timecodes"), _("Load timecodes file: %s"));
		if (keyframes != keyframes_file)
			append_file(keyframes, _("Unload keyframes"), _("Load keyframes file: %s"));

		if (wxMessageBox(str, _("(Un)Load files?"), wxYES_NO | wxCENTRE, context->parent) != wxYES)
			return;
	}

	bool loaded_video = false;
	if (video != video_file) {
		if (video.empty())
			CloseVideo();
		else if ((loaded_video = DoLoadVideo(video))) {
			auto vc = context->videoController.get();
			vc->JumpToFrame(properties.video_position);

			auto ar_mode = static_cast<AspectRatio>(properties.ar_mode);
			if (ar_mode == AspectRatio::Custom)
				vc->SetAspectRatio(properties.ar_value);
			else
				vc->SetAspectRatio(ar_mode);
			context->videoDisplay->SetWindowZoom(properties.video_zoom);
		}
	}

	if (!timecodes.empty()) LoadTimecodes(timecodes);
	if (!keyframes.empty()) LoadKeyframes(keyframes);

	if (audio != audio_file) {
		if (audio.empty())
			CloseAudio();
		else
			DoLoadAudio(audio, false);
	}
	else if (loaded_video && OPT_GET("Video/Open Audio")->GetBool() && audio_file != video_file && video_provider->HasAudio())
		DoLoadAudio(video, true);
}

void Project::DoLoadAudio(agi::fs::path const& path, bool quiet) {
	if (!progress)
		progress = new DialogProgress(context->parent);

	try {
		try {
			audio_provider = GetAudioProvider(path, *context->path, progress);
		}
		catch (agi::UserCancelException const&) { return; }
		catch (...) {
			config::mru->Remove("Audio", path);
			throw;
		}
	}
	catch (agi::fs::FileNotFound const& e) {
		return ShowError(_("The audio file was not found: ") + to_wx(e.GetMessage()));
	}
	catch (agi::AudioDataNotFound const& e) {
		if (quiet) {
			LOG_D("video/open/audio") << "File " << video_file << " has no audio data: " << e.GetMessage();
			return;
		}
		else
			return ShowError(_("None of the available audio providers recognised the selected file as containing audio data.\n\nThe following providers were tried:\n") + to_wx(e.GetMessage()));
	}
	catch (agi::AudioProviderError const& e) {
		return ShowError(_("None of the available audio providers have a codec available to handle the selected file.\n\nThe following providers were tried:\n") + to_wx(e.GetMessage()));
	}
	catch (agi::Exception const& e) {
		return ShowError(e.GetMessage());
	}

	SetPath(audio_file, "?audio", "Audio", path);
	AnnounceAudioProviderModified(audio_provider.get());
}

void Project::LoadAudio(agi::fs::path path) {
	DoLoadAudio(path, false);
}

void Project::CloseAudio() {
	AnnounceAudioProviderModified(nullptr);
	audio_provider.reset();
	SetPath(audio_file, "?audio", "", "");
}

bool Project::DoLoadVideo(agi::fs::path const& path) {
	if (!progress)
		progress = new DialogProgress(context->parent);

	try {
		video_provider = std::make_unique<AsyncVideoProvider>(path, context->ass->GetYCbCrMatrix(), context->videoController.get(), progress);
	}
	catch (agi::UserCancelException const&) { return false; }
	catch (agi::fs::FileSystemError const& err) {
		config::mru->Remove("Video", path);
		ShowError(to_wx(err.GetMessage()));
		return false;
	}
	catch (VideoProviderError const& err) {
		ShowError(to_wx(err.GetMessage()));
		return false;
	}

	AnnounceVideoProviderModified(video_provider.get());

	UpdateVideoProperties(context->ass.get(), video_provider.get(), context->parent);
	video_provider->LoadSubtitles(context->ass.get());

	timecodes = video_provider->GetFPS();
	keyframes = video_provider->GetKeyFrames();

	timecodes_file.clear();
	keyframes_file.clear();
	SetPath(video_file, "?video", "Video", path);

	std::string warning = video_provider->GetWarning();
	if (!warning.empty())
		wxMessageBox(to_wx(warning), _("Warning"), wxICON_WARNING | wxOK);

	video_has_subtitles = false;
	if (agi::fs::HasExtension(path, "mkv"))
		video_has_subtitles = MatroskaWrapper::HasSubtitles(path);

	AnnounceKeyframesModified(keyframes);
	AnnounceTimecodesModified(timecodes);
	return true;
}

void Project::LoadVideo(agi::fs::path path) {
	if (path.empty()) return;
	if (!DoLoadVideo(path)) return;
	if (OPT_GET("Video/Open Audio")->GetBool() && audio_file != video_file && video_provider->HasAudio())
		DoLoadAudio(video_file, true);

	double dar = video_provider->GetDAR();
	if (dar > 0)
		context->videoController->SetAspectRatio(dar);
	else
		context->videoController->SetAspectRatio(AspectRatio::Default);
	context->videoController->JumpToFrame(0);
}

void Project::CloseVideo() {
	AnnounceVideoProviderModified(nullptr);
	video_provider.reset();
	SetPath(video_file, "?video", "", "");
	video_has_subtitles = false;
	context->ass->Properties.ar_mode = 0;
	context->ass->Properties.ar_value = 0.0;
	context->ass->Properties.video_position = 0;
}

void Project::DoLoadTimecodes(agi::fs::path const& path) {
	timecodes = agi::vfr::Framerate(path);
	SetPath(timecodes_file, "", "Timecodes", path);
	AnnounceTimecodesModified(timecodes);
}

void Project::LoadTimecodes(agi::fs::path path) {
	try {
		DoLoadTimecodes(path);
	}
	catch (agi::fs::FileSystemError const& e) {
		ShowError(e.GetMessage());
		config::mru->Remove("Timecodes", path);
	}
	catch (agi::vfr::Error const& e) {
		ShowError(_("Failed to parse timecodes file: ") + to_wx(e.GetMessage()));
		config::mru->Remove("Timecodes", path);
	}
}

void Project::CloseTimecodes() {
	timecodes = video_provider ? video_provider->GetFPS() : agi::vfr::Framerate{};
	SetPath(timecodes_file, "", "", "");
	AnnounceTimecodesModified(timecodes);
}

void Project::DoLoadKeyframes(agi::fs::path const& path) {
	keyframes = agi::keyframe::Load(path);
	SetPath(keyframes_file, "", "Keyframes", path);
	AnnounceKeyframesModified(keyframes);
}

void Project::LoadKeyframes(agi::fs::path path) {
	try {
		DoLoadKeyframes(path);
	}
	catch (agi::fs::FileSystemError const& e) {
		ShowError(e.GetMessage());
		config::mru->Remove("Keyframes", path);
	}
	catch (agi::keyframe::KeyframeFormatParseError const& e) {
		ShowError(_("Failed to parse keyframes file: ") + to_wx(e.GetMessage()));
		config::mru->Remove("Keyframes", path);
	}
	catch (agi::keyframe::UnknownKeyframeFormatError const& e) {
		ShowError(_("Keyframes file in unknown format: ") + to_wx(e.GetMessage()));
		config::mru->Remove("Keyframes", path);
	}
}

void Project::CloseKeyframes() {
	keyframes = video_provider ? video_provider->GetKeyFrames() : std::vector<int>{};
	SetPath(keyframes_file, "", "", "");
	AnnounceKeyframesModified(keyframes);
}

void Project::LoadList(std::vector<agi::fs::path> const& files) {
	// Keep these lists sorted

	// Video formats
	const char *videoList[] = {
		".asf",
		".avi",
		".avs",
		".d2v",
		".h264",
		".hevc",
		".m2ts",
		".m4v",
		".mkv",
		".mov",
		".mp4",
		".mpeg",
		".mpg",
		".ogm",
		".rm",
		".rmvb",
		".ts",
		".webm",
		".wmv",
		".y4m",
		".yuv"
	};

	// Subtitle formats
	const char *subsList[] = {
		".ass",
		".srt",
		".ssa",
		".sub",
		".ttxt"
	};

	// Audio formats
	const char *audioList[] = {
		".aac",
		".ac3",
		".ape",
		".dts",
		".eac3",
		".flac",
		".m4a",
		".mka",
		".mp3",
		".ogg",
		".opus",
		".w64",
		".wav",
		".wma"
	};

	auto search = [](const char **begin, const char **end, std::string const& str) {
		return std::binary_search(begin, end, str.c_str(), [](const char *a, const char *b) {
			return strcmp(a, b) < 0;
		});
	};

	agi::fs::path audio, video, subs, timecodes, keyframes;
	for (auto file : files) {
		if (file.is_relative()) file = agi::fs::Absolute(file);
		if (!agi::fs::FileExists(file)) continue;

		auto ext = file.extension().string();
		boost::to_lower(ext);

		// Could be subtitles, keyframes or timecodes, so try loading as each
		if (ext == ".txt" || ext == ".log") {
			if (timecodes.empty()) {
				try {
					DoLoadTimecodes(file);
					timecodes = file;
					continue;
				} catch (...) { }
			}

			if (keyframes.empty()) {
				try {
					DoLoadKeyframes(file);
					keyframes = file;
					continue;
				} catch (...) { }
			}

			if (subs.empty() && ext != ".log")
				subs = file;
			continue;
		}

		if (subs.empty() && search(std::begin(subsList), std::end(subsList), ext))
			subs = file;
		if (video.empty() && search(std::begin(videoList), std::end(videoList), ext))
			video = file;
		if (audio.empty() && search(std::begin(audioList), std::end(audioList), ext))
			audio = file;
	}

	ProjectProperties properties;
	if (!subs.empty()) {
		if (!DoLoadSubtitles(subs, "", properties))
			subs.clear();
	}

	if (!video.empty() && DoLoadVideo(video)) {
		double dar = video_provider->GetDAR();
		if (dar > 0)
			context->videoController->SetAspectRatio(dar);
		else
			context->videoController->SetAspectRatio(AspectRatio::Default);
		context->videoController->JumpToFrame(0);

		// We loaded these earlier, but loading video unloaded them
		// Non-Do version of Load in case they've vanished or changed between
		// then and now
		if (!timecodes.empty())
			LoadTimecodes(timecodes);
		if (!keyframes.empty())
			LoadKeyframes(keyframes);
	}

	if (!audio.empty())
		DoLoadAudio(audio, false);
	else if (OPT_GET("Video/Open Audio")->GetBool() && audio_file != video_file)
		DoLoadAudio(video_file, true);

	if (!subs.empty())
		LoadUnloadFiles(properties);
}
