#pragma once
#ifndef ES_APP_COMPONENTS_TEXT_LIST_COMPONENT_H
#define ES_APP_COMPONENTS_TEXT_LIST_COMPONENT_H

#include "components/IList.h"
#include "math/Misc.h"
#include "utils/StringUtil.h"
#include "Log.h"
#include "Settings.h"
#include "Sound.h"
#include <memory>

class TextCache;

struct TextListData
{
	unsigned int colorId;
	std::shared_ptr<TextCache> textCache;
};

//A graphical list. Supports multiple colors for rows and scrolling.
template <typename T>
class TextListComponent : public IList<TextListData, T>
{
protected:
	using IList<TextListData, T>::mEntries;
	using IList<TextListData, T>::listUpdate;
	using IList<TextListData, T>::listInput;
	using IList<TextListData, T>::listRenderTitleOverlay;
	using IList<TextListData, T>::getTransform;
	using IList<TextListData, T>::mSize;
	using IList<TextListData, T>::mCursor;
	using IList<TextListData, T>::mViewportTop;
	using IList<TextListData, T>::mEntry;

public:
	using IList<TextListData, T>::size;
	using IList<TextListData, T>::isScrolling;
	using IList<TextListData, T>::stopScrolling;

	// flag to re-evaluate list cursor position in visible list section
	static constexpr int REFRESH_LIST_CURSOR_POS = -1;

	TextListComponent(Window* window);

	bool input(InputConfig* config, Input input) override;
	void update(int deltaTime) override;
	void render(const Transform4x4f& parentTrans) override;
	void applyTheme(const std::shared_ptr<ThemeData>& theme, const std::string& view, const std::string& element, unsigned int properties) override;

	void add(const std::string& name, const T& obj, unsigned int colorId);

	enum Alignment
	{
		ALIGN_LEFT,
		ALIGN_CENTER,
		ALIGN_RIGHT
	};

	inline void setAlignment(Alignment align) { mAlignment = align; }

	inline void setCursorChangedCallback(const std::function<void(CursorState state)>& func) { mCursorChangedCallback = func; }
	inline void setFavoriteIndicatorCallback(const std::function<bool(const T&)>& func) { mFavoriteIndicatorCallback = func; }

	inline void setFont(const std::shared_ptr<Font>& font)
	{
		mFont = font;
		for(auto it = mEntries.begin(); it != mEntries.end(); it++)
			it->data.textCache.reset();
	}

	inline void setUppercase(bool uppercase)
	{
		mUppercase = uppercase;
		for(auto it = mEntries.begin(); it != mEntries.end(); it++)
			it->data.textCache.reset();
	}

	inline void setSelectorHeight(float selectorScale) { mSelectorHeight = selectorScale; }
	inline void setSelectorOffsetY(float selectorOffsetY) { mSelectorOffsetY = selectorOffsetY; }
	inline void setSelectorColor(unsigned int color) { mSelectorColor = color; }
	inline void setSelectorColorEnd(unsigned int color) { mSelectorColorEnd = color; }
	inline void setSelectorColorGradientHorizontal(bool horizontal) { mSelectorColorGradientHorizontal = horizontal; }
	inline void setSelectedColor(unsigned int color) { mSelectedColor = color; }
	inline void setColor(unsigned int id, unsigned int color) { mColors[id] = color; }
	inline void setLineSpacing(float lineSpacing) { mLineSpacing = lineSpacing; }

protected:
	virtual void onScroll(int /*amt*/) override { if(!mScrollSound.empty()) Sound::get(mScrollSound)->play(); }
	virtual void onCursorChanged(const CursorState& state) override;

private:
	int mMarqueeOffset;
	int mMarqueeOffset2;
	int mMarqueeTime;

	Alignment mAlignment;
	float mHorizontalMargin;

	int viewportTop();
	std::function<void(CursorState state)> mCursorChangedCallback;

	std::shared_ptr<Font> mFont;
	bool mUppercase;
	float mLineSpacing;
	float mSelectorHeight;
	float mSelectorOffsetY;
	unsigned int mSelectorColor;
	unsigned int mSelectorColorEnd;
	bool mSelectorColorGradientHorizontal = true;
	unsigned int mSelectedColor;
	std::string mScrollSound;
	std::function<bool(const T&)> mFavoriteIndicatorCallback;
	static const unsigned int COLOR_ID_COUNT = 2;
	unsigned int mColors[COLOR_ID_COUNT];
	int mViewportHeight;
	int mCursorPrev = -1;

	ImageComponent mSelectorImage;
	ImageComponent mFavoriteIcon;
	bool mFavoriteIconVisible;
};

template <typename T>
TextListComponent<T>::TextListComponent(Window* window) :
	IList<TextListData, T>(window), mSelectorImage(window), mFavoriteIcon(window)
{
	mMarqueeOffset = 0;
	mMarqueeOffset2 = 0;
	mMarqueeTime = 0;

	mHorizontalMargin = 0;
	mAlignment = ALIGN_CENTER;

	mFont = Font::get(FONT_SIZE_MEDIUM);
	mUppercase = false;
	mLineSpacing = 1.5f;
	mSelectorHeight = mFont->getSize() * mLineSpacing;
	mSelectorOffsetY = 0;
	mSelectorColor = 0x000000FF;
	mSelectorColorEnd = 0x000000FF;
	mSelectorColorGradientHorizontal = true;
	mSelectedColor = 0;
	mColors[0] = 0x0000FFFF;
	mColors[1] = 0x00FF00FF;

	mFavoriteIcon.setImage(":/heart_filled.svg");
	mFavoriteIconVisible = true;
}

template <typename T>
void TextListComponent<T>::render(const Transform4x4f& parentTrans)
{
	Transform4x4f trans = parentTrans * getTransform();

	std::shared_ptr<Font>& font = mFont;

	if(size() == 0)
		return;

	const float entrySize = Math::max(font->getHeight(1.0), (float)font->getSize()) * mLineSpacing;

	// number of listentries that can fit on the screen
	mViewportHeight = (int)(mSize.y() / entrySize);

	if(mViewportTop == REFRESH_LIST_CURSOR_POS)
	{
		// returning from screen saver activated game launch or screensaver press 'A'
		mViewportTop = mCursor - mViewportHeight/2;
		mCursorPrev = -1; // reset to pristine to calc viewportTop() right when jumping to game with 'A' pressed
	}
	if(mCursor != mCursorPrev)
	{
		mViewportTop = (size() > mViewportHeight) ? viewportTop() : 0;
		mCursorPrev = mCursor;
	}

	unsigned int listCutoff = mViewportTop + mViewportHeight;
	if(listCutoff > size())
		listCutoff = size();

	float y = (mSize.y() - (mViewportHeight * entrySize)) * 0.5f;

	if (mSelectorImage.hasImage()) {
		mSelectorImage.setPosition(0.f, y + (mCursor - mViewportTop)*entrySize + mSelectorOffsetY, 0.f);
		mSelectorImage.render(trans);
	} else {
		Renderer::setMatrix(trans);
		Renderer::drawRect(0.0f, y + (mCursor - mViewportTop)*entrySize + mSelectorOffsetY, mSize.x(),
			mSelectorHeight, mSelectorColor, mSelectorColorEnd, mSelectorColorGradientHorizontal);
	}

	// clip to inside margins
	Vector3f dim(mSize.x(), mSize.y(), 0);
	dim = trans * dim - trans.translation();
	Renderer::pushClipRect(Vector2i((int)(trans.translation().x() + mHorizontalMargin), (int)trans.translation().y()),
		Vector2i((int)(dim.x() - mHorizontalMargin*2), (int)dim.y()));

	for(int i = mViewportTop; i < listCutoff; i++)
	{
		typename IList<TextListData, T>::Entry& entry = mEntries.at((unsigned int)i);

		unsigned int color;
		if(mCursor == i && mSelectedColor)
			color = mSelectedColor;
		else
			color = mColors[entry.data.colorId];

		if(!entry.data.textCache)
			entry.data.textCache = std::unique_ptr<TextCache>(font->buildTextCache(mUppercase ? Utils::String::toUpper(entry.name) : entry.name, 0, 0, 0x000000FF));

		entry.data.textCache->setColor(color);

		const bool showFavorite = mFavoriteIconVisible && mFavoriteIcon.hasImage() && (mFavoriteIndicatorCallback ? mFavoriteIndicatorCallback(entry.object) : false);
		const float iconSize = entrySize * 0.60f;
		const float iconGap = showFavorite ? (iconSize * 0.35f) : 0.0f;
		const float textWidth = entry.data.textCache->metrics.size.x();
		const float lineWidth = showFavorite ? (iconSize + iconGap + textWidth) : textWidth;

		Vector3f offset(0, y, 0);

		switch(mAlignment)
		{
		case ALIGN_LEFT:
			offset[0] = mHorizontalMargin;
			break;
		case ALIGN_CENTER:
			offset[0] = (int)((mSize.x() - lineWidth) / 2);
			if(offset[0] < mHorizontalMargin)
				offset[0] = mHorizontalMargin;
			break;
		case ALIGN_RIGHT:
			offset[0] = (mSize.x() - lineWidth);
			offset[0] -= mHorizontalMargin;
			if(offset[0] < mHorizontalMargin)
				offset[0] = mHorizontalMargin;
			break;
		}

		float rowScrollOffset = 0.0f;
		if((mCursor == i) && (mMarqueeOffset > 0))
			rowScrollOffset = (float)mMarqueeOffset;

		if(showFavorite)
		{
			Transform4x4f iconTrans = trans;
			iconTrans.translate(offset - Vector3f(rowScrollOffset, 0, 0));
			mFavoriteIcon.setPosition(0, (entrySize - iconSize) * 0.5f, 0);
			mFavoriteIcon.setResize(iconSize, iconSize);
			mFavoriteIcon.render(iconTrans);
		}

		Vector3f textOffset = offset + Vector3f(showFavorite ? (iconSize + iconGap) : 0.0f, 0, 0);

		// render text
		Transform4x4f drawTrans = trans;

		// currently selected item text might be scrolling
		if((mCursor == i) && (mMarqueeOffset > 0))
			drawTrans.translate(textOffset - Vector3f((float)mMarqueeOffset, 0, 0));
		else
			drawTrans.translate(textOffset);

		Renderer::setMatrix(drawTrans);
		font->renderTextCache(entry.data.textCache.get());

		// render currently selected item text again if
		// marquee is scrolled far enough for it to repeat
		if((mCursor == i) && (mMarqueeOffset2 < 0))
		{
			// also render the favorite icon alongside the wrap-around copy of the text
			if(showFavorite)
			{
				Transform4x4f iconTrans2 = trans;
				iconTrans2.translate(offset - Vector3f((float)mMarqueeOffset2, 0, 0));
				mFavoriteIcon.render(iconTrans2);
			}

			drawTrans = trans;
			drawTrans.translate(textOffset - Vector3f((float)mMarqueeOffset2, 0, 0));
			Renderer::setMatrix(drawTrans);
			font->renderTextCache(entry.data.textCache.get());
		}

		y += entrySize;
	}

	Renderer::popClipRect();

	listRenderTitleOverlay(trans);

	GuiComponent::renderChildren(trans);
}


template <typename T>
int TextListComponent<T>::viewportTop()
{
	int viewportTopMax = size() - mViewportHeight;
	int topNew = mViewportTop;

	if (mCursorPrev == -1)
		mCursorPrev = mCursor;

	int delta = mCursor - mCursorPrev;

	if(Settings::getInstance()->getBool("UseFullscreenPaging"))
	{
		// delta may be greater/less than +/-mViewportHeight on re-sorting of list
		if (delta <= -mViewportHeight || delta >= mViewportHeight
			// keep cursor sticky at position unless the user navigates
			// to the middle of the viewport
			|| delta < 0 && mCursor - mViewportTop < mViewportHeight/2
			|| delta > 0 && mCursor - mViewportTop > mViewportHeight/2)
		{
			topNew += delta;
		}
		// no match above will place the cursor more towards the middle
	}
	else
		// put cursor in middle of visible list
		topNew = mCursor - mViewportHeight/2;

	if (mCursor <= mViewportHeight/2)
		topNew = 0;
	else if (mCursor >= viewportTopMax + mViewportHeight/2)
		topNew = viewportTopMax;

	return topNew;
}

template <typename T>
bool TextListComponent<T>::input(InputConfig* config, Input input)
{
	bool isSingleStep = config->isMappedLike("down", input) || config->isMappedLike("up", input);
	bool isPageStep = config->isMappedLike("rightshoulder", input) || config->isMappedLike("leftshoulder", input);

	if(size() > 0 && (isSingleStep || isPageStep))
	{
		if(input.value != 0)
		{
			int delta;
			mCursorPrev = mCursor;
			if(isSingleStep)
				delta = config->isMappedLike("down", input) ? 1 : -1;
			else
			{
				delta = Settings::getInstance()->getBool("UseFullscreenPaging") ? mViewportHeight : 10;
				if (config->isMappedLike("leftshoulder", input))
					delta = -delta;
			}
			listInput(delta);
			return true;
		}else{
			stopScrolling();
		}
	}

	return GuiComponent::input(config, input);
}

template <typename T>
void TextListComponent<T>::update(int deltaTime)
{
	listUpdate(deltaTime);

	if(!isScrolling() && size() > 0)
	{
		// always reset the marquee offsets
		mMarqueeOffset  = 0;
		mMarqueeOffset2 = 0;

		// if we're not scrolling and this object's text goes outside our size, marquee it!
		auto name = mEntries.at((unsigned int)mCursor).name;
		const float textLength = mFont->sizeText(mUppercase ? Utils::String::toUpper(name) : name).x();
		const float limit      = mSize.x() - mHorizontalMargin * 2;

		if(textLength > limit)
		{
			// loop
			// pixels per second ( based on nes-mini font at 1920x1080 to produce a speed of 200 )
			const float speed        = mFont->sizeText("ABCDEFGHIJKLMNOPQRSTUVWXYZ").x() * 0.247f;
			const float delay        = 3000;
			const float scrollLength = textLength;
			const float returnLength = speed * 1.5f;
			const float scrollTime   = (scrollLength * 1000) / speed;
			const float returnTime   = (returnLength * 1000) / speed;
			const int   maxTime      = (int)(delay + scrollTime + returnTime);

			mMarqueeTime += deltaTime;
			while(mMarqueeTime > maxTime)
				mMarqueeTime -= maxTime;

			mMarqueeOffset = (int)(Math::Scroll::loop(delay, scrollTime + returnTime, (float)mMarqueeTime, scrollLength + returnLength));

			if(mMarqueeOffset > (scrollLength - (limit - returnLength)))
				mMarqueeOffset2 = (int)(mMarqueeOffset - (scrollLength + returnLength));
		}
	}

	GuiComponent::update(deltaTime);
}

//list management stuff
template <typename T>
void TextListComponent<T>::add(const std::string& name, const T& obj, unsigned int color)
{
	assert(color < COLOR_ID_COUNT);

	typename IList<TextListData, T>::Entry entry;
	entry.name = name;
	entry.object = obj;
	entry.data.colorId = color;
	static_cast<IList< TextListData, T >*>(this)->add(entry);
}

template <typename T>
void TextListComponent<T>::onCursorChanged(const CursorState& state)
{
	mMarqueeOffset = 0;
	mMarqueeOffset2 = 0;
	mMarqueeTime = 0;

	if(mCursorChangedCallback)
		mCursorChangedCallback(state);
}

template <typename T>
void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme, const std::string& view, const std::string& element, unsigned int properties)
{
	if(Settings::getInstance()->getBool("UseFullscreenPaging"))
	{
		mViewportTop = REFRESH_LIST_CURSOR_POS;
	}

	GuiComponent::applyTheme(theme, view, element, properties);

	const ThemeData::ThemeElement* elem = theme->getElement(view, element, "textlist");
	if(!elem)
		return;

	using namespace ThemeFlags;
	if(properties & COLOR)
	{
		if(elem->has("selectorColor"))
		{
			setSelectorColor(elem->get<unsigned int>("selectorColor"));
			setSelectorColorEnd(elem->get<unsigned int>("selectorColor"));
		}
		if (elem->has("selectorColorEnd"))
			setSelectorColorEnd(elem->get<unsigned int>("selectorColorEnd"));
		if (elem->has("selectorGradientType"))
			setSelectorColorGradientHorizontal(!(elem->get<std::string>("selectorGradientType").compare("horizontal")));
		if(elem->has("selectedColor"))
			setSelectedColor(elem->get<unsigned int>("selectedColor"));
		if(elem->has("primaryColor"))
			setColor(0, elem->get<unsigned int>("primaryColor"));
		if(elem->has("secondaryColor"))
			setColor(1, elem->get<unsigned int>("secondaryColor"));
	}

	setFont(Font::getFromTheme(elem, properties, mFont));
	const float selectorHeight = Math::max(mFont->getHeight(1.0), (float)mFont->getSize()) * mLineSpacing;
	setSelectorHeight(selectorHeight);

	if(properties & SOUND && elem->has("scrollSound"))
		mScrollSound = elem->get<std::string>("scrollSound");

	if(properties & ALIGNMENT)
	{
		if(elem->has("alignment"))
		{
			const std::string& str = elem->get<std::string>("alignment");
			if(str == "left")
				setAlignment(ALIGN_LEFT);
			else if(str == "center")
				setAlignment(ALIGN_CENTER);
			else if(str == "right")
				setAlignment(ALIGN_RIGHT);
			else
				LOG(LogError) << "Unknown TextListComponent alignment \"" << str << "\"!";
		}
		if(elem->has("horizontalMargin"))
		{
			mHorizontalMargin = elem->get<float>("horizontalMargin") * (this->mParent ? this->mParent->getSize().x() : (float)Renderer::getScreenWidth());
		}
	}

	if(properties & FORCE_UPPERCASE && elem->has("forceUppercase"))
		setUppercase(elem->get<bool>("forceUppercase"));

	if(properties & LINE_SPACING)
	{
		if(elem->has("lineSpacing"))
			setLineSpacing(elem->get<float>("lineSpacing"));
		if(elem->has("selectorHeight"))
		{
			setSelectorHeight(elem->get<float>("selectorHeight") * Renderer::getScreenHeight());
		}
		if(elem->has("selectorOffsetY"))
		{
			float scale = this->mParent ? this->mParent->getSize().y() : (float)Renderer::getScreenHeight();
			setSelectorOffsetY(elem->get<float>("selectorOffsetY") * scale);
		} else {
			setSelectorOffsetY(0.0);
		}
	}

	if (elem->has("selectorImagePath"))
	{
		std::string path = elem->get<std::string>("selectorImagePath");
		bool tile = elem->has("selectorImageTile") && elem->get<bool>("selectorImageTile");
		mSelectorImage.setImage(path, tile);
		mSelectorImage.setSize(mSize.x(), mSelectorHeight);
		mSelectorImage.setColorShift(mSelectorColor);
		mSelectorImage.setColorShiftEnd(mSelectorColorEnd);
	} else {
		mSelectorImage.setImage("");
	}

	if (elem->has("favoriteIconPath"))
		mFavoriteIcon.setImage(elem->get<std::string>("favoriteIconPath"));
	else
		mFavoriteIcon.setImage(":/heart_filled.svg");

	if (elem->has("favoriteIconColor"))
		mFavoriteIcon.setColorShift(elem->get<unsigned int>("favoriteIconColor"));
	else
		mFavoriteIcon.setColorShift(0xFFFFFFFF);

	{
		std::string var = theme->getVariable("favoriteIconVisible");
		mFavoriteIconVisible = (var == "true");
	}
}

#endif // ES_APP_COMPONENTS_TEXT_LIST_COMPONENT_H
