/*
 * This file is part of OpenTTD.
 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
 */

/** @file gfx_layout.h Functions related to laying out the texts. */

#ifndef GFX_LAYOUT_H
#define GFX_LAYOUT_H

#include "misc/lrucache.hpp"
#include "fontcache.h"
#include "gfx_func.h"
#include "core/math_func.hpp"

#include <string_view>

/**
 * Text drawing parameters, which can change while drawing a line, but are kept between multiple parts
 * of the same text, e.g. on line breaks.
 */
struct FontState {
	FontSize fontsize; ///< Current font size.
	ExtendedTextColour cur_colour; ///< Current text colour.
	std::vector<ExtendedTextColour> colour_stack; ///< Stack of colours to assist with colour switching.

	/** Create the font state with an invalid state. */
	FontState() : fontsize(FontSize::End), cur_colour(TextColour::Invalid) {}
	/**
	 * Create the font state.
	 * @param colour The colour of the font.
	 * @param fontsize The size of the font.
	 */
	FontState(ExtendedTextColour colour, FontSize fontsize) : fontsize(fontsize), cur_colour(colour) {}

	auto operator<=>(const FontState &) const = default;

	/**
	 * Switch to new colour \a c.
	 * @param c New colour to use.
	 */
	inline void SetColour(ExtendedTextColour c)
	{
		assert((c.colour >= TextColour::Begin && c.colour < TextColour::End) || c.colour == TextColour::Invalid);
		assert(!c.flags.Test(ExtendedTextColourFlag::IsPaletteColour));
		if (!this->cur_colour.flags.Test(ExtendedTextColourFlag::Forced)) this->cur_colour = c;
	}

	/**
	 * Switch to and pop the last saved colour on the stack.
	 */
	inline void PopColour()
	{
		if (colour_stack.empty()) return;
		SetColour(colour_stack.back());
		colour_stack.pop_back();
	}

	/**
	 * Push the current colour on to the stack.
	 */
	inline void PushColour()
	{
		colour_stack.push_back(this->cur_colour);
	}

	/**
	 * Switch to using a new font \a f.
	 * @param f New font to use.
	 */
	inline void SetFontSize(FontSize f)
	{
		this->fontsize = f;
	}
};

template <typename T> struct std::hash<std::vector<T>> {
	size_t operator()(const std::vector<T> &vec) const
	{
		/* This is not an optimal hash algorithm but in most cases this is empty and therefore the same anyway. */
		return std::transform_reduce(std::begin(vec), std::end(vec),
			std::hash<size_t>{}(std::size(vec)),
			[](const size_t &a, const size_t &b) -> size_t { return a ^ b; },
			[](const T &x) -> size_t { return std::hash<T>{}(x); });
	}
};

/** Instantiation of a hash for an ExtendedTextColour. */
template <> struct std::hash<ExtendedTextColour> {
	/**
	 * Hash the text colour.
	 * @param tc The colour to hash.
	 * @return The hashed value.
	 */
	std::size_t operator()(const ExtendedTextColour &tc) const noexcept
	{
		size_t h1 = std::hash<TextColour>{}(tc.colour);
		size_t h2 = std::hash<uint8_t>{}(tc.flags.base());
		return h1 ^ (h2 << 1);
	}
};

template <> struct std::hash<FontState> {
	std::size_t operator()(const FontState &state) const noexcept
	{
		size_t h1 = std::hash<FontSize>{}(state.fontsize);
		size_t h2 = std::hash<ExtendedTextColour>{}(state.cur_colour);
		size_t h3 = std::hash<std::vector<ExtendedTextColour>>{}(state.colour_stack);
		return h1 ^ (h2 << 1) ^ (h3 << 2);
	}
};

/**
 * Container with information about a font.
 */
class Font {
public:
	FontCache *fc;     ///< The font we are using.
	ExtendedTextColour colour; ///< The colour this font has to be.

	Font(FontSize size, ExtendedTextColour colour);
};

/** Mapping from index to font. The pointer is owned by FontColourMap. */
using FontMap = std::vector<std::pair<int, Font *>>;

/**
 * Interface to glue fallback and normal layouter into one.
 */
class ParagraphLayouter {
public:
	/** Ensure the destructor of the sub classes are called as well. */
	virtual ~ParagraphLayouter() = default;

	/** Position of a glyph within a VisualRun. */
	class Position {
	public:
		int16_t left; ///< Left-most position of glyph.
		int16_t right; ///< Right-most position of glyph.
		int16_t top; ///< Top-most position of glyph.

		constexpr inline Position(int16_t left, int16_t right, int16_t top) : left(left), right(right), top(top) { }

		/**
		 * Conversion from a single point to a Position.
		 * @param pt The point to create the position for.
		 */
		constexpr inline Position(const Point &pt) : left(pt.x), right(pt.x), top(pt.y) { }
	};

	/** Visual run contains data about the bit of text with the same font. */
	class VisualRun {
	public:
		/** Ensure the destructor of the sub classes are called as well. */
		virtual ~VisualRun() = default;

		/**
		 * Get the font.
		 * @return The font used for this run.
		 */
		virtual const Font *GetFont() const = 0;

		/**
		 * Get the number of glyphs.
		 * @return The number of glyphs for this run.
		 */
		virtual size_t GetGlyphCount() const = 0;

		/**
		 * Get the glyphs to draw.
		 * @return The glyphs.
		 */
		virtual std::span<const GlyphID> GetGlyphs() const = 0;

		/**
		 * Get the positions for each of the glyphs.
		 * @return The glyph positions.
		 */
		virtual std::span<const Position> GetPositions() const = 0;

		/**
		 * Get the font leading, or distance between the baselines of consecutive lines.
		 * @return The leading in pixels.
		 */
		virtual int GetLeading() const = 0;

		/**
		 * The offset for each of the glyphs to the character run that was passed to the #Layouter.
		 * @return The offsets.
		 */
		virtual std::span<const int> GetGlyphToCharMap() const = 0;
	};

	/** A single line worth of VisualRuns. */
	class Line {
	public:
		/** Ensure the destructor of the sub classes are called as well. */
		virtual ~Line() = default;

		/**
		 * Get the font leading, or distance between the baselines of consecutive lines.
		 * @return The leading in pixels, which is generally the maximum of all runs..
		 */
		virtual int GetLeading() const = 0;

		/**
		 * Get the width of this line.
		 * @return The width of the line.
		 */
		virtual int GetWidth() const = 0;

		/**
		 * Get the number of runs in this line.
		 * @return The number of runs.
		 */
		virtual size_t CountRuns() const = 0;

		/**
		 * Get a reference to the given run.
		 * @param run The index into the runs.
		 * @return The reference to the run.
		 */
		virtual const VisualRun &GetVisualRun(size_t run) const = 0;

		/**
		 * Get the number of elements the given character occupies in the underlying text buffer of the Layouter.
		 * Many use UTF-16 internally, meaning a character larger than MAX_UINT16 occupies two places in its buffer.
		 * @param c The character to get the length for.
		 * @return The length.
		 */
		virtual int GetInternalCharLength(char32_t c) const = 0;
	};

	/**
	 * Reset the position to the start of the paragraph.
	 */
	virtual void Reflow() = 0;

	/**
	 * Construct a new line with a maximum width.
	 * @param max_width The maximum width of the string.
	 * @return A Line, or \c nullptr when at the end of the paragraph.
	 */
	virtual std::unique_ptr<const Line> NextLine(int max_width) = 0;
};

/**
 * The layouter performs all the layout work.
 *
 * It also accounts for the memory allocations and frees.
 */
class Layouter : public std::vector<const ParagraphLayouter::Line *> {
	std::string_view string; ///< Pointer to the original string.

	/** Key into the linecache */
	struct LineCacheKey {
		FontState state_before;  ///< Font state at the beginning of the line.
		std::string str;         ///< Source string of the line (including colour and font size codes).
	};

	struct LineCacheQuery {
		const FontState &state_before; ///< Font state at the beginning of the line.
		std::string_view str;    ///< Source string of the line (including colour and font size codes).
	};

	friend struct std::hash<Layouter::LineCacheQuery>;
	struct LineCacheHash;

	struct LineCacheEqualTo {
		using is_transparent = void;

		template <typename Tlhs, typename Trhs>
		bool operator()(const Tlhs &lhs, const Trhs &rhs) const
		{
			return lhs.state_before == rhs.state_before && lhs.str == rhs.str;
		}
	};

public:
	/** Item in the linecache */
	struct LineCacheItem {
		/** Due to the type of data in the buffer differing depending on the Layouter, we need to pass our own deleter routine. */
		using Buffer = std::unique_ptr<void, void(*)(void *)>;
		/* Stuff that cannot be freed until the ParagraphLayout is freed */
		Buffer buffer{nullptr, [](void *){}}; ///< Accessed by our ParagraphLayout::nextLine.
		FontMap runs;              ///< Accessed by our ParagraphLayout::nextLine.

		FontState state_after;     ///< Font state after the line.
		std::unique_ptr<ParagraphLayouter> layout = nullptr; ///< Layout of the line.

		std::vector<std::unique_ptr<const ParagraphLayouter::Line>> cached_layout{}; ///< Cached results of line layouting.
		int cached_width = 0; ///< Width used for the cached layout.
	};
private:
	using LineCache = LRUCache<LineCacheKey, LineCacheItem, LineCacheHash, LineCacheEqualTo>;
	static std::unique_ptr<LineCache> linecache;

	static LineCacheItem &GetCachedParagraphLayout(std::string_view str, const FontState &state);

	using FontColourMap = std::map<ExtendedTextColour, std::unique_ptr<Font>>;
	static EnumIndexArray<FontColourMap, FontSize, FontSize::End> fonts; ///< The colour mapping of each of the fonts.
public:
	static Font *GetFont(FontSize size, ExtendedTextColour colour);

	Layouter(std::string_view str, int maxw = INT32_MAX, FontSize fontsize = FontSize::Normal);
	Dimension GetBounds();
	ParagraphLayouter::Position GetCharPosition(std::string_view::const_iterator ch) const;
	ptrdiff_t GetCharAtPosition(int x, size_t line_index) const;

	static void Initialize();
	static void ResetFontCache(FontSize size);
	static void ResetLineCache();
};

ParagraphLayouter::Position GetCharPosInString(std::string_view str, size_t pos, FontSize start_fontsize = FontSize::Normal);
ptrdiff_t GetCharAtPosition(std::string_view str, int x, FontSize start_fontsize = FontSize::Normal);

template <> struct std::hash<Layouter::LineCacheQuery> {
	std::size_t operator()(const Layouter::LineCacheQuery &state) const noexcept
	{
		size_t h1 = std::hash<std::string_view>{}(state.str);
		size_t h2 = std::hash<FontState>{}(state.state_before);
		return h1 ^ (h2 << 1);
	}
};

struct Layouter::LineCacheHash {
	using is_transparent = void;

	std::size_t operator()(const Layouter::LineCacheKey &query) const { return std::hash<Layouter::LineCacheQuery>{}(LineCacheQuery{query.state_before, query.str}); }
	std::size_t operator()(const Layouter::LineCacheQuery &query) const { return std::hash<Layouter::LineCacheQuery>{}(query); }
};

#endif /* GFX_LAYOUT_H */
