#include "OSDWidget.hh"
#include "OutputSurface.hh"
#include "CommandException.hh"
#include "TclObject.hh"
#include "StringOp.hh"
#include "GLUtil.hh"
#include "memory.hh"
#include "stl.hh"
#include <SDL.h>
#include <algorithm>
#include <limits>

using std::string;
using std::vector;
using std::unique_ptr;
using namespace gl;

namespace openmsx {

// intersect two rectangles
static void intersect(int xa, int ya, int wa, int ha,
                      int xb, int yb, int wb, int hb,
                      int& x, int& y, int& w, int& h)
{
	int x1 = std::max<int>(xa, xb);
	int y1 = std::max<int>(ya, yb);
	int x2 = std::min<int>(xa + wa, xb + wb);
	int y2 = std::min<int>(ya + ha, yb + hb);
	x = x1;
	y = y1;
	w = std::max(0, x2 - x1);
	h = std::max(0, y2 - y1);
}

////

static void normalize(int& x, int& w)
{
	if (w < 0) {
		w = -w;
		x -= w;
	}
}

class SDLScopedClip
{
public:
	SDLScopedClip(OutputSurface& output, int x, int y, int w, int h);
	~SDLScopedClip();
private:
	SDL_Surface* surface;
	SDL_Rect origClip;
};


SDLScopedClip::SDLScopedClip(OutputSurface& output, int x, int y, int w, int h)
	: surface(output.getSDLSurface())
{
	normalize(x, w); normalize(y, h);
	SDL_GetClipRect(surface, &origClip);

	int xn, yn, wn, hn;
	intersect(origClip.x, origClip.y, origClip.w, origClip.h,
	          x,  y,  w,  h,
	          xn, yn, wn, hn);
	SDL_Rect newClip = { Sint16(xn), Sint16(yn), Uint16(wn), Uint16(hn) };
	SDL_SetClipRect(surface, &newClip);
}

SDLScopedClip::~SDLScopedClip()
{
	SDL_SetClipRect(surface, &origClip);
}

////

#if COMPONENT_GL

class GLScopedClip
{
public:
	GLScopedClip(OutputSurface& output, int x, int y, int w, int h);
	~GLScopedClip();
private:
	GLint box[4]; // x, y, w, h;
	GLboolean wasEnabled;
};


GLScopedClip::GLScopedClip(OutputSurface& output, int x, int y, int w, int h)
{
	normalize(x, w); normalize(y, h);
	y = output.getHeight() - y - h; // openGL sets (0,0) in LOWER-left corner

	wasEnabled = glIsEnabled(GL_SCISSOR_TEST);
	if (wasEnabled == GL_TRUE) {
		glGetIntegerv(GL_SCISSOR_BOX, box);
		int xn, yn, wn, hn;
		intersect(box[0], box[1], box[2], box[3],
		          x,  y,  w,  h,
		          xn, yn, wn, hn);
		glScissor(xn, yn, wn, hn);
	} else {
		glScissor(x, y, w, h);
		glEnable(GL_SCISSOR_TEST);
	}
}

GLScopedClip::~GLScopedClip()
{
	if (wasEnabled == GL_TRUE) {
		glScissor(box[0], box[1], box[2], box[3]);
	} else {
		glDisable(GL_SCISSOR_TEST);
	}
}

#endif

////

OSDWidget::OSDWidget(const TclObject& name_)
	: parent(nullptr)
	, name(name_)
	, z(0.0)
	, scaled(false)
	, clip(false)
	, suppressErrors(false)
{
}

OSDWidget::~OSDWidget()
{
}

void OSDWidget::addWidget(unique_ptr<OSDWidget> widget)
{
	widget->setParent(this);

	// Insert the new widget in the correct place (sorted on ascending Z)
	// heuristic: often we have either
	//  - many widgets with all the same Z
	//  - only a few total number of subwidgets (possibly with different Z)
	// In the former case we can simply append at the end. In the latter
	// case a linear search is probably faster than a binary search. Only
	// when there are many sub-widgets with not all the same Z (and not
	// created in sorted Z-order) a binary search would be faster.
	float z = widget->getZ();
	if (subWidgets.empty() || (subWidgets.back()->getZ() <= z)) {
		subWidgets.push_back(std::move(widget));
	} else {
		auto it = begin(subWidgets);
		while ((*it)->getZ() <= z) ++it;
		subWidgets.insert(it, std::move(widget));

	}
}

void OSDWidget::deleteWidget(OSDWidget& widget)
{
	auto it = find_if_unguarded(subWidgets,
		[&](const std::unique_ptr<OSDWidget>& p) { return p.get() == &widget; });
	subWidgets.erase(it);
}

#ifdef DEBUG
struct AscendingZ {
	bool operator()(const unique_ptr<OSDWidget>& lhs,
	                const unique_ptr<OSDWidget>& rhs) const {
		return lhs->getZ() < rhs->getZ();
	}
};
#endif
void OSDWidget::resortUp(OSDWidget* elem)
{
	// z-coordinate was increased, first search for elements current position
	auto it1 = begin(subWidgets);
	while (it1->get() != elem) ++it1;
	// next search for the position were it belongs
	float z = elem->getZ();
	auto it2 = it1;
	++it2;
	while ((it2 != end(subWidgets)) && ((*it2)->getZ() < z)) ++it2;
	// now move elements to correct position
	rotate(it1, it1 + 1, it2);
#ifdef DEBUG
	assert(std::is_sorted(begin(subWidgets), end(subWidgets), AscendingZ()));
#endif
}
void OSDWidget::resortDown(OSDWidget* elem)
{
	// z-coordinate was decreased, first search for new position
	auto it1 = begin(subWidgets);
	float z = elem->getZ();
	while ((*it1)->getZ() <= z) {
		++it1;
		if (it1 == end(subWidgets)) return;
	}
	// next search for the elements current position
	auto it2 = it1;
	if ((it2 != begin(subWidgets)) && ((it2 - 1)->get() == elem)) return;
	while (it2->get() != elem) ++it2;
	// now move elements to correct position
	rotate(it1, it2, it2 + 1);
#ifdef DEBUG
	assert(std::is_sorted(begin(subWidgets), end(subWidgets), AscendingZ()));
#endif
}

vector<string_ref> OSDWidget::getProperties() const
{
	static const char* const vals[] = {
		"-type", "-x", "-y", "-z", "-relx", "-rely", "-scaled",
		"-clip", "-mousecoord", "-suppressErrors",
	};
	return vector<string_ref>(std::begin(vals), std::end(vals));
}

void OSDWidget::setProperty(
	Interpreter& interp, string_ref name, const TclObject& value)
{
	if (name == "-type") {
		throw CommandException("-type property is readonly");
	} else if (name == "-mousecoord") {
		throw CommandException("-mousecoord property is readonly");
	} else if (name == "-x") {
		pos[0] = value.getDouble(interp);
	} else if (name == "-y") {
		pos[1] = value.getDouble(interp);
	} else if (name == "-z") {
		float z2 = value.getDouble(interp);
		if (z != z2) {
			bool up = z2 > z; // was z increased?
			z = z2;
			if (OSDWidget* parent = getParent()) {
				// TODO no need for a full sort: instead remove and re-insert in the correct place
				if (up) {
					parent->resortUp(this);
				} else {
					parent->resortDown(this);
				}
			}
		}
	} else if (name == "-relx") {
		relPos[0] = value.getDouble(interp);
	} else if (name == "-rely") {
		relPos[1] = value.getDouble(interp);
	} else if (name == "-scaled") {
		bool scaled2 = value.getBoolean(interp);
		if (scaled != scaled2) {
			scaled = scaled2;
			invalidateRecursive();
		}
	} else if (name == "-clip") {
		clip = value.getBoolean(interp);
	} else if (name == "-suppressErrors") {
		suppressErrors = value.getBoolean(interp);
	} else {
		throw CommandException("No such property: " + name);
	}
}

void OSDWidget::getProperty(string_ref name, TclObject& result) const
{
	if (name == "-type") {
		result.setString(getType());
	} else if (name == "-x") {
		result.setDouble(pos[0]);
	} else if (name == "-y") {
		result.setDouble(pos[1]);
	} else if (name == "-z") {
		result.setDouble(z);
	} else if (name == "-relx") {
		result.setDouble(relPos[0]);
	} else if (name == "-rely") {
		result.setDouble(relPos[1]);
	} else if (name == "-scaled") {
		result.setBoolean(scaled);
	} else if (name == "-clip") {
		result.setBoolean(clip);
	} else if (name == "-mousecoord") {
		vec2 coord = getMouseCoord();
		result.addListElement(coord[0]);
		result.addListElement(coord[1]);
	} else if (name == "-suppressErrors") {
		result.setBoolean(suppressErrors);
	} else {
		throw CommandException("No such property: " + name);
	}
}

float OSDWidget::getRecursiveFadeValue() const
{
	return 1.0f; // fully opaque
}

void OSDWidget::invalidateRecursive()
{
	invalidateLocal();
	invalidateChildren();
}

void OSDWidget::invalidateChildren()
{
	for (auto& s : subWidgets) {
		s->invalidateRecursive();
	}
}

bool OSDWidget::needSuppressErrors() const
{
	if (suppressErrors) return true;
	if (const OSDWidget* parent = getParent()) {
		return parent->needSuppressErrors();
	}
	return false;
}

void OSDWidget::paintSDLRecursive(OutputSurface& output)
{
	paintSDL(output);

	std::unique_ptr<SDLScopedClip> scopedClip;
	if (clip) {
		ivec2 pos, size;
		getBoundingBox(output, pos, size);
		scopedClip = make_unique<SDLScopedClip>(
			output, pos[0], pos[1], size[0], size[1]);
	}

	for (auto& s : subWidgets) {
		s->paintSDLRecursive(output);
	}
}

void OSDWidget::paintGLRecursive (OutputSurface& output)
{
	(void)output;
#if COMPONENT_GL
	paintGL(output);

	std::unique_ptr<GLScopedClip> scopedClip;
	if (clip) {
		ivec2 pos, size;
		getBoundingBox(output, pos, size);
		scopedClip = make_unique<GLScopedClip>(
			output, pos[0], pos[1], size[0], size[1]);
	}

	for (auto& s : subWidgets) {
		s->paintGLRecursive(output);
	}
#endif
}

int OSDWidget::getScaleFactor(const OutputRectangle& output) const
{
	if (scaled) {
		return output.getOutputWidth() / 320;;
	} else if (getParent()) {
		return getParent()->getScaleFactor(output);
	} else {
		return 1;
	}
}

vec2 OSDWidget::transformPos(const OutputRectangle& output,
                             vec2 pos, vec2 relPos) const
{
	vec2 out = pos
	         + (float(getScaleFactor(output)) * getPos())
		 + (relPos * getSize(output));
	if (const OSDWidget* parent = getParent()) {
		out = parent->transformPos(output, out, getRelPos());
	}
	return out;
}

vec2 OSDWidget::transformReverse(const OutputRectangle& output, vec2 pos) const
{
	if (const OSDWidget* parent = getParent()) {
		pos = parent->transformReverse(output, pos);
		return pos
		       - (getRelPos() * parent->getSize(output))
		       - (getPos() * float(getScaleFactor(output)));
	} else {
		return pos;
	}
}

vec2 OSDWidget::getMouseCoord() const
{
	if (SDL_ShowCursor(SDL_QUERY) == SDL_DISABLE) {
		// Host cursor is not visible. Return dummy mouse coords for
		// the OSD cursor position.
		// The reason for doing this is that otherwise (e.g. when using
		// the mouse in an MSX program) it's possible to accidentally
		// click on the reversebar. This will also block the OSD mouse
		// in other Tcl scripts (e.g. vampier's nemesis script), but
		// almost always those scripts will also not be useful when the
		// host mouse cursor is not visible.
		//
		// We need to return coordinates that lay outside any
		// reasonable range. Initially we returned (NaN, NaN). But for
		// some reason that didn't work on dingoo: Dingoo uses
		// softfloat, in c++ NaN seems to behave as expected, but maybe
		// there's a problem on the tcl side? Anyway, when we return
		// +inf instead of NaN it does work.
		return vec2(std::numeric_limits<float>::infinity());
	}

	SDL_Surface* surface = SDL_GetVideoSurface();
	if (!surface) {
		throw CommandException(
			"Can't get mouse coordinates: no window visible");
	}
	DummyOutputRectangle output(surface->w, surface->h);

	int mouseX, mouseY;
	SDL_GetMouseState(&mouseX, &mouseY);

	vec2 out = transformReverse(output, vec2(mouseX, mouseY));

	vec2 size = getSize(output);
	if ((size[0] == 0.0f) || (size[1] == 0.0f)) {
		throw CommandException(
			"-can't get mouse coordinates: "
			"widget has zero width or height");
	}
	return out / size;
}

void OSDWidget::getBoundingBox(const OutputRectangle& output,
                               ivec2& pos, ivec2& size)
{
	vec2 topLeft     = transformPos(output, vec2(), vec2(0.0f));
	vec2 bottomRight = transformPos(output, vec2(), vec2(1.0f));
	pos  = round(topLeft);
	size = round(bottomRight - topLeft);
}

} // namespace openmsx
