// Copyright (c) 2013, 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 "resolution_resampler.h"

#include "ass_dialogue.h"
#include "ass_file.h"
#include "ass_style.h"
#include "utils.h"

#include <libaegisub/exception.h>
#include <libaegisub/of_type_adaptor.h>
#include <libaegisub/split.h>
#include <libaegisub/util.h>
#include <libaegisub/ycbcr_conv.h>

#include <algorithm>
#include <cmath>
#include <wx/intl.h>

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

namespace {
	std::string transform_drawing(std::string const& drawing, int shift_x, int shift_y, double scale_x, double scale_y) {
		bool is_x = true;
		std::string final;
		final.reserve(drawing.size());

		for (auto cur : agi::Split(drawing, ' ')) {
			double val;
			if (agi::util::try_parse(cur, &val)) {
				if (is_x)
					val = (val + shift_x) * scale_x;
				else
					val = (val + shift_y) * scale_y;
				final += float_to_string(val);
				final += ' ';
				is_x = !is_x;
			}
			else if (cur.size() == 1) {
				char c = tolower(cur[0]);
				if (c == 'm' || c == 'n' || c == 'l' || c == 'b' || c == 's' || c == 'p' || c == 'c') {
					is_x = true;
					final += c;
					final += ' ';
				}
			}
		}

		if (final.size())
			final.pop_back();
		return final;
	}

	struct resample_state {
		const int *margin;
		double rx;
		double ry;
		double rm;
		double ar;
		std::optional<agi::ycbcr_converter> conv;
	};

	void resample_tags(std::string const&, AssOverrideParameter *cur, void *ud) {
		resample_state *state = static_cast<resample_state *>(ud);

		double resizer = 1.0;
		int shift = 0;

		switch (cur->classification) {
			case AssParameterClass::ABSOLUTE_SIZE_X:
				resizer = state->rx;
				break;

			case AssParameterClass::ABSOLUTE_SIZE_Y:
				resizer = state->ry;
				break;

			case AssParameterClass::ABSOLUTE_SIZE_XY:
				resizer = state->rm;
				break;

			case AssParameterClass::ABSOLUTE_POS_X:
				resizer = state->rx;
				shift = state->margin[LEFT];
				break;

			case AssParameterClass::ABSOLUTE_POS_Y:
				resizer = state->ry;
				shift = state->margin[TOP];
				break;

			case AssParameterClass::RELATIVE_SIZE_X:
				resizer = state->ar;
				break;

			case AssParameterClass::RELATIVE_SIZE_Y:
				break;

			case AssParameterClass::DRAWING: {
				cur->Set(transform_drawing(
					cur->Get<std::string>(),
					state->margin[LEFT], state->margin[TOP], state->rx, state->ry));
				return;
			}

			case AssParameterClass::COLOR:
				if (state->conv)
					cur->Set<std::string>(state->conv->rgb_to_rgb(agi::Color{cur->Get<std::string>()}).GetAssOverrideFormatted());
				return;

			default:
				return;
		}

		VariableDataType curType = cur->GetType();
		if (curType == VariableDataType::FLOAT)
			cur->Set((cur->Get<double>() + shift) * resizer);
		else if (curType == VariableDataType::INT)
			cur->Set<int>((cur->Get<int>() + shift) * resizer + 0.5);
	}

	void resample_line(resample_state *state, AssDialogue &diag) {
		if (diag.Comment && (diag.Effect.get().starts_with("template") || diag.Effect.get().starts_with("code")))
			return;

		auto blocks = diag.ParseTags();

		for (auto block : blocks | agi::of_type<AssDialogueBlockOverride>())
			block->ProcessParameters(resample_tags, state);

		for (auto drawing : blocks | agi::of_type<AssDialogueBlockDrawing>())
			drawing->text = transform_drawing(drawing->text, 0, 0, state->rx / state->ar, state->ry);

		for (size_t i = 0; i < 3; ++i) {
			if (diag.Margin[i])
				diag.Margin[i] = int((diag.Margin[i] + state->margin[i]) * (i < 2 ? state->rx : state->ry) + 0.5);
		}

		diag.UpdateText(blocks);
	}

	void resample_style(resample_state *state, AssStyle &style) {
		style.fontsize = int(style.fontsize * state->ry + 0.5);
		style.outline_w *= state->ry;
		style.shadow_w *= state->ry;
		style.spacing *= state->ry;  // gets multiplied by scalex (and hence by ar) during rendering
		style.scalex *= state->ar;
		for (int i = 0; i < 3; i++)
			style.Margin[i] = int((style.Margin[i] + state->margin[i]) * (i < 2 ? state->rx : state->ry) + 0.5);
		if (state->conv) {
			style.primary = state->conv->rgb_to_rgb(style.primary);
			style.secondary = state->conv->rgb_to_rgb(style.secondary);
			style.outline = state->conv->rgb_to_rgb(style.outline);
			style.shadow = state->conv->rgb_to_rgb(style.shadow);
		}
		style.UpdateData();
	}
}

void ResampleResolution(AssFile *ass, ResampleSettings settings) {
	auto horizontal_stretch = 1.0;
	auto old_ar = double(settings.source_x) / settings.source_y;
	auto new_ar = double(settings.dest_x) / settings.dest_y;
	bool border_horizontally = new_ar > old_ar;
	// Don't convert aspect ratio if it's very close to correct
	// (for reference, 848x480 <-> 1280x720 is .006)
	if (std::abs(old_ar - new_ar) / new_ar > .01) {
		switch (settings.ar_mode) {
		case ResampleARMode::RemoveBorder:
			border_horizontally = !border_horizontally;
			[[fallthrough]];
		case ResampleARMode::AddBorder:
			if (border_horizontally) // Wider/Shorter
				settings.margin[LEFT] = settings.margin[RIGHT] = (settings.source_y * new_ar - settings.source_x) / 2;
			else // Taller/Narrower
				settings.margin[TOP] = settings.margin[BOTTOM] = (settings.source_x / new_ar - settings.source_y) / 2;
			break;
		case ResampleARMode::Stretch:
			horizontal_stretch = new_ar / old_ar;
			break;
		case ResampleARMode::Manual:
			old_ar =
				double(settings.source_x + settings.margin[LEFT] + settings.margin[RIGHT]) /
				double(settings.source_y + settings.margin[TOP] + settings.margin[BOTTOM]);

			if (std::abs(old_ar - new_ar) / new_ar > .01)
				horizontal_stretch = new_ar / old_ar;
			break;
		}
	}

	// Add margins to original resolution
	settings.source_x += settings.margin[LEFT] + settings.margin[RIGHT];
	settings.source_y += settings.margin[TOP] + settings.margin[BOTTOM];

	double rx = double(settings.dest_x) / double(settings.source_x);
	double ry = double(settings.dest_y) / double(settings.source_y);

	resample_state state = {
		settings.margin,
		rx,
		ry,
		rx == ry ? rx : std::sqrt(rx * ry),
		horizontal_stretch,
		settings.matrix_conversion && settings.matrix_conversion->first != settings.matrix_conversion->second ?	    // FIXME: Use transform once C++23 can be used
		std::make_optional(agi::ycbcr_converter{
			settings.matrix_conversion->first,
			settings.matrix_conversion->second,
		}) : std::nullopt,
	};

	for (auto& line : ass->Styles)
		resample_style(&state, line);
	for (auto& line : ass->Events)
		resample_line(&state, line);

	ass->SetScriptInfo("PlayResX", std::to_string(settings.dest_x));
	ass->SetScriptInfo("PlayResY", std::to_string(settings.dest_y));
	if (settings.matrix_conversion)
		ass->SetScriptInfo("YCbCr Matrix", agi::ycbcr::Header(settings.matrix_conversion->second).to_best_practice_string());

	ass->Commit(_("resolution resampling"), AssFile::COMMIT_SCRIPTINFO | AssFile::COMMIT_DIAG_FULL);
}
