#pragma once

#include "BsCorePrerequisites.h"
#include "BsFontDesc.h"
#include "BsVector2I.h"

namespace BansheeEngine
{
	/**
	 * @brief	This object takes as input a string, a font and optionally some constraints (like word wrap)
	 *			and outputs a set of character data you may use for rendering or metrics.
	 */
	class TextDataBase
	{
	protected:
		/**
		 * @brief	Represents a single word as a set of characters, or optionally just a blank space
		 *			of a certain length.
		 *
		 * @note	Due to the way allocation is handled, this class is not allowed to have a destructor.
		 */
		class TextWord
		{
		public:
			/**
			 * @brief	Initializes the word and signals if it just a space (or multiple spaces), or 
			 *			an actual word with letters.
			 */
			void init(bool spacer);

			/**
			 * @brief	Appends a new character to the word.
			 *
			 * @param	charIdx		Sequential index of the character in the original string.
			 * @param	desc		Character description from the font.
			 *
			 * @returns		How many pixels did the added character expand the word by.
			 */
			UINT32 addChar(UINT32 charIdx, const CHAR_DESC& desc);

			/**
			 * @brief	Adds a space to the word. Word must have previously have been declared as
			 *			a "spacer".
			 */
			void addSpace(UINT32 spaceWidth);

			/**
			 * @brief	Returns the width of the word in pixels.
			 */
			UINT32 getWidth() const { return mWidth; }

			/**
			 * @brief	Returns height of the word in pixels.
			 */
			UINT32 getHeight() const { return mHeight; }

			/**
			 * @brief	Calculates new width of the word if we were to add the provided character, 
			 *			without actually adding it.
			 *
			 * @param	desc	Character description from the font.
			 *
			 * @returns	Width of the word in pixels with the character appended to it.
			 */
			UINT32 calcWidthWithChar(const CHAR_DESC& desc);

			/**
			 * @brief	Returns true if word is a spacer. Spacers contain just a space 
			 *			of a certain length with no actual characters.
			 */
			bool isSpacer() const { return mSpacer; }

			/**
			 * @brief	Returns the number of characters in the word.
			 */
			UINT32 getNumChars() const { return mLastChar == nullptr ? 0 : (mCharsEnd - mCharsStart + 1); }

			/**
			 * @brief	Returns the index of the starting character in the word.
			 */
			UINT32 getCharsStart() const { return mCharsStart; }

			/**
			 * @brief	Returns the index of the last character in the word.
			 */
			UINT32 getCharsEnd() const { return mCharsEnd; }

			/**
			 * @brief	Calculates width of the character by which it would expand the width of the word
			 *			if it was added to it.
			 *
			 * @param	prevDesc	Descriptor of the character preceding the one we need the width for. Can be null.
			 * @param	desc		Character description from the font.
			 *
			 * @returns	How many pixels would the added character expand the word by.
			 */
			static UINT32 calcCharWidth(const CHAR_DESC* prevDesc, const CHAR_DESC& desc);

		private:
			UINT32 mCharsStart, mCharsEnd;
			UINT32 mWidth;
			UINT32 mHeight;

			const CHAR_DESC* mLastChar;

			bool mSpacer;
			UINT32 mSpaceWidth;
		};

		/**
		 * @brief	Contains information about a single texture page that contains rendered
		 *			character data.
		 *
		 * @note	Due to the way allocation is handled, this class is not allowed to have a destructor.
		 */
		struct PageInfo
		{
			UINT32 numQuads;
			HTexture texture;
		};

	public:
		/**
		 * @brief	Represents a single line as a set of words.
		 *
		 * @note	Due to the way allocation is handled, this class is not allowed to have a destructor.
		 */
		class BS_CORE_EXPORT TextLine
		{
		public:
			/**
			 * @brief	Returns width of the line in pixels.
			 */
			UINT32 getWidth() const { return mWidth; }

			/**
			 * @brief	Returns height of the line in pixels.
			 */
			UINT32 getHeight() const { return mHeight; }

			/**
			 * @brief	Returns an offset used to separate two lines.
			 */
			UINT32 getYOffset() const { return mTextData->getLineHeight(); }

			/**
			 * @brief	Calculates new width of the line if we were to add the provided character, 
			 *			without actually adding it.
			 *
			 * @param	desc		Character description from the font.
			 *
			 * @returns	Width of the line in pixels with the character appended to it.
			 */
			UINT32 calcWidthWithChar(const CHAR_DESC& desc);

			/**
			 * @brief	Fills the vertex/uv/index buffers for the specified page, with all the character data
			 * 			needed for rendering.
			 *
			 * @param	page			The page.
			 * @param [out]	vertices	Pre-allocated array where character vertices will be written.
			 * @param [out]	uvs			Pre-allocated array where character uv coordinates will be written.
			 * @param [out]	indexes 	Pre-allocated array where character indices will be written.
			 * @param	offset			Offsets the location at which the method writes to the buffers.
			 * 							Counted as number of quads.
			 * @param	size			Total number of quads that can fit into the specified buffers.
			 *
			 * @return	Number of quads that were written.
			 */
			UINT32 fillBuffer(UINT32 page, Vector2* vertices, Vector2* uvs, UINT32* indexes, UINT32 offset, UINT32 size) const;

			/**
			 * @brief	Checks are we at a word boundary (i.e. next added character will start a new word).
			 */
			bool isAtWordBoundary() const;

			/**
			 * @brief	Returns the total number of characters on this line.
			 */
			UINT32 getNumChars() const;

			/**
			 * @brief	Query if this line was created explicitly due to a newline character.
			 * 			As opposed to a line that was created because a word couldn't fit on the previous line.
			 */
			bool hasNewlineChar() const { return mHasNewline; }
		private:
			friend class TextDataBase;

			/**
			 * @brief	Appends a new character to the line.
			 *
			 * @param	charIdx		Sequential index of the character in the original string.
			 * @param	desc		Character description from the font.
			 */
			void add(UINT32 charIdx, const CHAR_DESC& charDesc);

			/**
			 * @brief	Appends a space to the line.
			 */
			void addSpace(UINT32 spaceWidth);

			/**
			 * @brief	Adds a new word to the line.
			 *
			 * @param	wordIdx		Sequential index of the word in the original string. 
			 *						Spaces are counted as words as well.
			 * @param	word		Description of the word.
			 */
			void addWord(UINT32 wordIdx, const TextWord& word);

			/**
			 * @brief	Initializes the line. Must be called after construction.
			 */
			void init(TextDataBase* textData);

			/**
			 * @brief	Finalizes the line. Do not add new characters/words after a line has
			 *			been finalized.
			 *
			 * @param	hasNewlineChar	Set to true if line was create due to an explicit newline char.
			 *							As opposed to a line that was created because a word couldn't fit 
			 *							on the previous line.
			 */
			void finalize(bool hasNewlineChar);

			/**
			 * @brief	Returns true if the line contains no words.
			 */
			bool isEmpty() const { return mIsEmpty; }

			/**
			 * @brief	Removes last word from the line and returns its sequential index.
			 */
			UINT32 removeLastWord();

			/**
			 * @brief	Calculates the line width and height in pixels.
			 */
			void calculateBounds();

		private:
			TextDataBase* mTextData;
			UINT32 mWordsStart, mWordsEnd;

			UINT32 mWidth;
			UINT32 mHeight;

			bool mIsEmpty;
			bool mHasNewline;
		};

	public:
		/**
		 * @brief	Initializes a new text data using the specified string and font. Text will attempt to fit into
		 *			the provided area. If enabled it will wrap words to new line when they don't fit. Individual words
		 *			will be broken into multiple pieces if they don't fit on a single line when word break
		 *			is enabled, otherwise they will be clipped. If the specified area is zero size then the text
		 *			will not be clipped or word wrapped in any way.
		 *
		 *			After this object is constructed you may call various getter methods to get needed information.
		 */
		BS_CORE_EXPORT TextDataBase(const WString& text, const HFont& font, UINT32 fontSize,
			UINT32 width = 0, UINT32 height = 0, bool wordWrap = false, bool wordBreak = true);
		BS_CORE_EXPORT virtual ~TextDataBase() { }

		/**
		 * @brief	Returns the number of lines that were generated.
		 */
		BS_CORE_EXPORT UINT32 getNumLines() const { return mNumLines; }

		/**
		 * @brief	Returns the number of font pages references by the used characters.
		 */
		BS_CORE_EXPORT UINT32 getNumPages() const { return mNumPageInfos; }

		/**
		 * @brief	Returns the height of a line in pixels.
		 */
		BS_CORE_EXPORT UINT32 getLineHeight() const;

		/**
		 * @brief	Gets information describing a single line at the specified index.
		 */
		BS_CORE_EXPORT const TextLine& getLine(UINT32 idx) const { return mLines[idx]; }

		/**
		 * @brief	Returns font texture for the provided page index. 
		 */
		BS_CORE_EXPORT const HTexture& getTextureForPage(UINT32 page) const;

		/**
		 * @brief	Returns the number of quads used by all the characters in the provided page.
		 */
		BS_CORE_EXPORT UINT32 getNumQuadsForPage(UINT32 page) const { return mPageInfos[page].numQuads; }

		/**
		 * @brief	Returns the width of the actual text in pixels.
		 */
		BS_CORE_EXPORT UINT32 getWidth() const;

		/**
		 * @brief	Returns the height of the actual text in pixels.
		 */
		BS_CORE_EXPORT UINT32 getHeight() const;

	protected:
		/**
		 * @brief	Copies internally stored data in temporary buffers to a persistent buffer.
		 *
		 * @param	text			Text originally used for creating the internal temporary buffer data.
		 * @param	buffer			Memory location to copy the data to. If null then no data will be copied
		 *							and the parameter \p size will contain the required buffer size.
		 * @param	size			Size of the provided memory buffer, or if the buffer is null, this will
		 *							contain the required buffer size after method exists.
		 * @param	freeTemporary	If true the internal temporary data will be freed after copying.
		 *
		 * @note	Must be called after text data has been constructed and is in the temporary buffers.
		 */
		BS_CORE_EXPORT void generatePersistentData(const WString& text, UINT8* buffer, UINT32& size, bool freeTemporary = true);
	private:
		friend class TextLine;

		/**
		 * @brief	Returns Y offset that determines the line on which the characters are placed.
		 *			In pixels.
		 */
		INT32 getBaselineOffset() const;

		/**
		 * @brief	Returns the width of a single space in pixels.
		 */
		UINT32 getSpaceWidth() const;

		/**
		 * @brief	Gets a description of a single character referenced by its sequential index
		 *			based on the original string.
		 */
		const CHAR_DESC& getChar(UINT32 idx) const { return *mChars[idx]; }

		/**
		 * @brief	Gets a description of a single word referenced by its sequential index
		 *			based on the original string.
		 */
		const TextWord& getWord(UINT32 idx) const { return mWords[idx]; }

	protected:
		const CHAR_DESC** mChars;
		UINT32 mNumChars;

		TextWord* mWords;
		UINT32 mNumWords;

		TextLine* mLines;
		UINT32 mNumLines;

		PageInfo* mPageInfos;
		UINT32 mNumPageInfos;

		HFont mFont;
		SPtr<const FontBitmap> mFontData;

		// Static buffers used to reduce runtime memory allocation
	protected:
		/**
		 * @brief	Stores per-thread memory buffers used to reduce memory allocation.
		 */
		// Note: I could replace this with the global frame allocator to avoid the extra logic
		struct BufferData
		{
			BufferData();
			~BufferData();

			/**
			 * @brief	Allocates a new word and adds it to the buffer. Returns index of the word
			 *			in the word buffer.
			 *
			 * @param	spacer	Specify true if the word is only to contain spaces. (Spaces are considered
			 *					a special type of word).
			 */
			UINT32 allocWord(bool spacer);

			/**
			 * @brief	Allocates a new line and adds it to the buffer. Returns index of the line
			 *			in the line buffer.
			 */
			UINT32 allocLine(TextDataBase* textData);

			/**
			 * @brief	Increments the count of characters for the referenced page, and optionally
			 *			creates page info if it doesn't already exist.
			 */
			void addCharToPage(UINT32 page, const FontBitmap& fontData);

			/**
			 * @brief	Resets all allocation counters, but doesn't actually release memory.
			 */
			void deallocAll();

			TextWord* WordBuffer;
			UINT32 WordBufferSize;
			UINT32 NextFreeWord;

			TextLine* LineBuffer;
			UINT32 LineBufferSize;
			UINT32 NextFreeLine;

			PageInfo* PageBuffer;
			UINT32 PageBufferSize;
			UINT32 NextFreePageInfo;
		};

		static BS_THREADLOCAL BufferData* MemBuffer;

		/**
		 * @brief	Allocates an initial set of buffers that will be reused while parsing
		 *			text data.
		 */
		static void initAlloc();
	};

	/**
	 * @copydoc	TextDataBase
	 */
	template<class Alloc = GenAlloc>
	class TextData : public TextDataBase
	{
	public:
		/**
		 * @copydoc	TextDataBase::TextDataBase
		 */
		TextData(const WString& text, const HFont& font, UINT32 fontSize,
			UINT32 width = 0, UINT32 height = 0, bool wordWrap = false, bool wordBreak = true)
			:TextDataBase(text, font, fontSize, width, height, wordWrap, wordBreak), mData(nullptr)
		{
			UINT32 totalBufferSize = 0;
			generatePersistentData(text, nullptr, totalBufferSize);

			mData = (UINT8*)bs_alloc<Alloc>(totalBufferSize);
			generatePersistentData(text, (UINT8*)mData, totalBufferSize);
		}

		~TextData()
		{
			if (mData != nullptr)
				bs_free<Alloc>(mData);
		}

	private:
		UINT8* mData;
	};
}