// Copyright (c) 2012, 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 "ass_file.h"
#include "async_video_provider.h"
#include "compat.h"
#include "help_button.h"
#include "include/aegisub/context.h"
#include "libresrc/libresrc.h"
#include "project.h"
#include "resolution_resampler.h"
#include "validators.h"

#include <boost/range/size.hpp>
#include <wx/checkbox.h>
#include <wx/combobox.h>
#include <wx/dialog.h>
#include <wx/sizer.h>
#include <wx/spinctrl.h>
#include <wx/statbox.h>
#include <wx/stattext.h>
#include <wx/valgen.h>

namespace {

// Intentionally kept separate from agi::ycbcr::valid_header_strings:
// Resampling to or from "None" does not immediately make sense as the
// behavior of "None" depends on the (real) color space of the loaded video.
// Instead, allow choosing either explicit matrices for source and destination
// or a blank option to opt out of resampling the matrix.
constexpr std::string_view MatrixOptions[] = {
	"",
	"TV.601", "PC.601",
	"TV.709", "PC.709",
	"TV.FCC", "PC.FCC",
	"TV.240M", "PC.240M",
};

int MatrixOptionFromHeader(agi::ycbcr::Header header) {
	std::string header_string = header.to_existing_string();
	if (auto pos = std::ranges::find(MatrixOptions, header_string); pos != std::end(MatrixOptions))
		return std::distance(std::begin(MatrixOptions), pos);
	return 0;
}

/// @class DialogResample
/// @brief Configuration dialog for resolution resampling
///
/// Populate a ResampleSettings structure with data from the user
struct DialogResample {
	wxDialog d;
	agi::Context *c; ///< Project context

	ResampleSettings &settings;

	int script_w;
	int script_h;
	agi::ycbcr::Header script_mat;
	int video_w = 0;
	int video_h = 0;
	agi::ycbcr::header_colorspace video_mat;

	wxSpinCtrl *source_x;
	wxSpinCtrl *source_y;
	wxSpinCtrl *dest_x;
	wxSpinCtrl *dest_y;
	wxComboBox *source_matrix;
	wxComboBox *dest_matrix;
	wxCheckBox *symmetrical;
	wxRadioBox *ar_mode;
	wxSpinCtrl *margin_ctrl[4];

	wxButton *from_script;
	wxButton *from_video;

	void OnMatrixChange();
	void SetSourceFromScript(wxCommandEvent &);
	/// Set the destination resolution to the video's resolution
	void SetDestFromVideo(wxCommandEvent &);
	/// Symmetrical checkbox toggle handler
	void OnSymmetrical(wxCommandEvent &);
	/// Copy margin values over if symmetrical is enabled
	void OnMarginChange(wxSpinCtrl *src, wxSpinCtrl *dst);
	void UpdateButtons();

public:
	/// Constructor
	/// @param context Project context
	/// @param[out] settings Settings struct to populate
	DialogResample(agi::Context *context, ResampleSettings &settings);
};

enum {
	LEFT = 0,
	RIGHT = 1,
	TOP = 2,
	BOTTOM = 3
};

DialogResample::DialogResample(agi::Context *c, ResampleSettings &settings)
: d(c->parent, -1, _("Resample Resolution"))
, c(c)
, settings(settings)
, script_mat(agi::ycbcr::Header(std::string(c->ass->GetScriptInfo("YCbCr Matrix"))).to_effective())
{
	d.SetIcons(GETICONS(resample_toolbutton));

	c->ass->GetResolution(script_w, script_h);
	settings.source_x = script_w;
	settings.source_y = script_h;

	if (auto provider = c->project->VideoProvider()) {
		settings.dest_x = video_w = provider->GetWidth();
		settings.dest_y = video_h = provider->GetHeight();
		video_mat = provider->GetRealColorSpace();
	}
	else {
		settings.dest_x = script_w;
		settings.dest_y = script_h;
	}

	// Create static box sizers
	auto source_res_box_sizer = new wxStaticBoxSizer(wxVERTICAL, &d, _("Source Resolution"));
	auto dest_res_box_sizer = new wxStaticBoxSizer(wxVERTICAL, &d, _("Destination Resolution"));
	auto margin_box_sizer = new wxStaticBoxSizer(wxVERTICAL, &d, _("Margin offset"));

	wxWindow *source_res_box = source_res_box_sizer->GetStaticBox();
	wxWindow *dest_res_box = dest_res_box_sizer->GetStaticBox();
	wxWindow *margin_box = margin_box_sizer->GetStaticBox();

	// Create all controls and set validators
	for (size_t i = 0; i < 4; ++i) {
		margin_ctrl[i] = new wxSpinCtrl(margin_box, -1, "0", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, -9999, 9999, 0);
		margin_ctrl[i]->SetValidator(wxGenericValidator(&settings.margin[i]));
	}

	symmetrical = new wxCheckBox(margin_box, -1, _("&Symmetrical"));
	symmetrical->SetValue(true);

	margin_ctrl[RIGHT]->Enable(false);
	margin_ctrl[BOTTOM]->Enable(false);

	source_x = new wxSpinCtrl(source_res_box, -1, "", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 1, 999999);
	source_y = new wxSpinCtrl(source_res_box, -1, "", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 1, 999999);
	source_matrix = new wxComboBox(source_res_box, -1, "", wxDefaultPosition,
		wxDefaultSize, to_wx(MatrixOptions), wxCB_READONLY);
	dest_x = new wxSpinCtrl(dest_res_box, -1, "", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 1, 999999);
	dest_y = new wxSpinCtrl(dest_res_box, -1, "", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 1, 999999);
	dest_matrix = new wxComboBox(dest_res_box, -1, "", wxDefaultPosition, wxDefaultSize,
		to_wx(MatrixOptions), wxCB_READONLY);

	source_x->SetValidator(wxGenericValidator(&settings.source_x));
	source_y->SetValidator(wxGenericValidator(&settings.source_y));
	dest_x->SetValidator(wxGenericValidator(&settings.dest_x));
	dest_y->SetValidator(wxGenericValidator(&settings.dest_y));

	from_video = new wxButton(dest_res_box, -1, _("From &video"));
	from_video->Enable(false);
	from_script = new wxButton(source_res_box, -1, _("From s&cript"));
	from_script->Enable(false);

	wxString ar_modes[] = {_("Stretch"), _("Add borders"), _("Remove borders"), _("Manual")};
	ar_mode = new wxRadioBox(&d, -1, _("Aspect Ratio Handling"), wxDefaultPosition,
		wxDefaultSize, std::size(ar_modes), ar_modes, 1, 4, MakeEnumBinder(&settings.ar_mode));

	// Position the controls
	auto margin_sizer = new wxGridSizer(3, 3, 5, 5);
	margin_sizer->AddSpacer(1);
	margin_sizer->Add(margin_ctrl[TOP], wxSizerFlags(1).Expand());
	margin_sizer->AddSpacer(1);
	margin_sizer->Add(margin_ctrl[LEFT], wxSizerFlags(1).Expand());
	margin_sizer->Add(symmetrical, wxSizerFlags(1).Expand());
	margin_sizer->Add(margin_ctrl[RIGHT], wxSizerFlags(1).Expand());
	margin_sizer->AddSpacer(1);
	margin_sizer->Add(margin_ctrl[BOTTOM], wxSizerFlags(1).Expand());
	margin_sizer->AddSpacer(1);

	margin_box_sizer->Add(margin_sizer, wxSizerFlags(1).Expand().Border(wxBOTTOM));

	auto source_res_sizer = new wxBoxSizer(wxHORIZONTAL);
	source_res_sizer->Add(source_x, wxSizerFlags(1).Border(wxRIGHT).Align(wxALIGN_CENTER_VERTICAL));
	source_res_sizer->Add(new wxStaticText(source_res_box, -1, _(L"\u00D7")), wxSizerFlags().Center().Border(wxRIGHT)); // U+00D7 multiplication sign
	source_res_sizer->Add(source_y, wxSizerFlags(1).Border(wxRIGHT).Align(wxALIGN_CENTER_VERTICAL));
	source_res_sizer->Add(from_script);

	auto source_matrix_sizer = new wxBoxSizer(wxHORIZONTAL);
	source_matrix_sizer->Add(new wxStaticText(source_res_box, -1, _("YCbCr Matrix:")), wxSizerFlags().Border(wxRIGHT).Center());
	source_matrix_sizer->Add(source_matrix, wxSizerFlags(1).Center());

	source_res_box_sizer->Add(source_res_sizer, wxSizerFlags(1).Expand().Border(wxBOTTOM));
	source_res_box_sizer->Add(source_matrix_sizer, wxSizerFlags(1).Expand());

	auto dest_res_sizer = new wxBoxSizer(wxHORIZONTAL);
	dest_res_sizer->Add(dest_x, wxSizerFlags(1).Border(wxRIGHT).Align(wxALIGN_CENTER_VERTICAL));
	dest_res_sizer->Add(new wxStaticText(dest_res_box, -1, _(L"\u00D7")), wxSizerFlags().Center().Border(wxRIGHT));
	dest_res_sizer->Add(dest_y, wxSizerFlags(1).Border(wxRIGHT).Align(wxALIGN_CENTER_VERTICAL));
	dest_res_sizer->Add(from_video);

	auto dest_matrix_sizer = new wxBoxSizer(wxHORIZONTAL);
	dest_matrix_sizer->Add(new wxStaticText(dest_res_box, -1, _("YCbCr Matrix:")), wxSizerFlags().Border(wxRIGHT).Center());
	dest_matrix_sizer->Add(dest_matrix, wxSizerFlags(1).Center());

	dest_res_box_sizer->Add(dest_res_sizer, wxSizerFlags(1).Expand().Border(wxBOTTOM));
	dest_res_box_sizer->Add(dest_matrix_sizer, wxSizerFlags(1).Expand());

	auto main_sizer = new wxBoxSizer(wxVERTICAL);
	main_sizer->Add(source_res_box_sizer, wxSizerFlags().Expand().Border());
	main_sizer->Add(dest_res_box_sizer, wxSizerFlags().Expand().Border());
	main_sizer->Add(ar_mode, wxSizerFlags().Expand().Border());
	main_sizer->Add(margin_box_sizer, wxSizerFlags(1).Expand().Border());
	main_sizer->Add(d.CreateStdDialogButtonSizer(wxOK | wxCANCEL | wxHELP), wxSizerFlags().Expand().Border(wxALL & ~wxTOP));
	d.SetSizerAndFit(main_sizer);
	d.CenterOnParent();

	d.TransferDataToWindow();
	UpdateButtons();

	// Bind events
	using std::bind;
	d.Bind(wxEVT_BUTTON, bind(&HelpButton::OpenPage, "Resample resolution"), wxID_HELP);
	d.Bind(wxEVT_SPINCTRL, [this](wxCommandEvent&) { UpdateButtons(); });
	d.Bind(wxEVT_RADIOBOX, [this](wxCommandEvent&) { UpdateButtons(); });
	from_video->Bind(wxEVT_BUTTON, &DialogResample::SetDestFromVideo, this);
	from_script->Bind(wxEVT_BUTTON, &DialogResample::SetSourceFromScript, this);
	symmetrical->Bind(wxEVT_CHECKBOX, &DialogResample::OnSymmetrical, this);
	margin_ctrl[LEFT]->Bind(wxEVT_SPINCTRL, bind(&DialogResample::OnMarginChange, this, margin_ctrl[LEFT], margin_ctrl[RIGHT]));
	margin_ctrl[TOP]->Bind(wxEVT_SPINCTRL, bind(&DialogResample::OnMarginChange, this, margin_ctrl[TOP], margin_ctrl[BOTTOM]));

	source_matrix->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { OnMatrixChange(); });
	dest_matrix->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { OnMatrixChange(); });

	// Find out if we want to suggest resampling colors.
	// If the script matrix is None, we do not know what matrix to resample from, so we don't suggest to resample.
	// If the video matrix is unspecified or does not correspond to a valid YCbCr Header value, we cannot currently resample to it.
	// -> FIXME: The latter is not a theoretical reason but only due to the fact that resampling is not currently implemented for any other matrices in ycbcr_conv.h:
	//    We could in theory have a situation where a script with no YCbCr Header (so effectively TV.601) was authored on a BT.2020 video using color matching,
	//    and the user now wants to adjust the colors so that they can set the matrix to None instead.
	// Otherwise, i.e. if e.g. the script has a TV.601 header but the video uses TV.709, we suggest to resample.
	// (The assumption here being that, just like when resampling PlayRes, the user only uses the resample dialog when the script *currently* looks correct on the current video.)
	agi::ycbcr::Header video_mat_header(video_mat);
	if (video_mat_header != script_mat
			&& video_mat_header.valid()		//< (Not necessary in theory, see FIXME above)
			&& script_mat.valid()			//< (Should always be true since we use to_effective() above)
			&& std::holds_alternative<agi::ycbcr::header_colorspace>(video_mat_header)
			&& std::holds_alternative<agi::ycbcr::header_colorspace>(script_mat)
	) {
		source_matrix->SetSelection(MatrixOptionFromHeader(script_mat));
		dest_matrix->SetSelection(MatrixOptionFromHeader(video_mat_header));
	}
	OnMatrixChange();
}

void DialogResample::OnMatrixChange() {
	agi::ycbcr::Header src_mat(from_wx(source_matrix->GetValue()));
	agi::ycbcr::Header dst_mat(from_wx(dest_matrix->GetValue()));

	if (std::holds_alternative<agi::ycbcr::header_colorspace>(src_mat) && std::holds_alternative<agi::ycbcr::header_colorspace>(dst_mat)) {
		settings.matrix_conversion = std::make_pair(std::get<agi::ycbcr::header_colorspace>(src_mat), std::get<agi::ycbcr::header_colorspace>(dst_mat));
	} else {
		settings.matrix_conversion = std::nullopt;
	}
}

void DialogResample::SetDestFromVideo(wxCommandEvent &) {
	dest_x->SetValue(video_w);
	dest_y->SetValue(video_h);
	dest_matrix->SetSelection(MatrixOptionFromHeader(agi::ycbcr::Header(video_mat)));
}

void DialogResample::SetSourceFromScript(wxCommandEvent&) {
	source_x->SetValue(script_w);
	source_y->SetValue(script_h);
	source_matrix->SetSelection(MatrixOptionFromHeader(script_mat));
}

void DialogResample::UpdateButtons() {
	from_video->Enable(c->project->VideoProvider() &&
		(dest_x->GetValue() != video_w || dest_y->GetValue() != video_h));
	from_script->Enable(source_x->GetValue() != script_w || source_y->GetValue() != script_h);

	auto source_ar = double(source_x->GetValue()) / source_y->GetValue();
	auto dest_ar = double(dest_x->GetValue()) / dest_y->GetValue();
	bool ar_changed = std::abs(source_ar - dest_ar) / dest_ar > .01;

	ar_mode->Enable(ar_changed);

	bool margins = ar_changed && ar_mode->GetSelection() == (int)ResampleARMode::Manual;
	symmetrical->Enable(margins);
	margin_ctrl[LEFT]->Enable(margins);
	margin_ctrl[TOP]->Enable(margins);
	margin_ctrl[RIGHT]->Enable(margins && !symmetrical->GetValue());
	margin_ctrl[BOTTOM]->Enable(margins && !symmetrical->GetValue());
}

void DialogResample::OnSymmetrical(wxCommandEvent &) {
	bool state = !symmetrical->IsChecked();

	margin_ctrl[RIGHT]->Enable(state);
	margin_ctrl[BOTTOM]->Enable(state);

	if (!state) {
		margin_ctrl[RIGHT]->SetValue(margin_ctrl[LEFT]->GetValue());
		margin_ctrl[BOTTOM]->SetValue(margin_ctrl[TOP]->GetValue());
	}
}

void DialogResample::OnMarginChange(wxSpinCtrl *src, wxSpinCtrl *dst) {
	if (symmetrical->IsChecked())
		dst->SetValue(src->GetValue());
}
}

bool PromptForResampleSettings(agi::Context *c, ResampleSettings &settings) {
	return DialogResample(c, settings).d.ShowModal() == wxID_OK;
}
