#pragma once

#include <string>
#include <vector>
#include <array>
#include <memory>
#include <unordered_set>
#include <unordered_map>
#include <map>
#include <optional>
#include "imgui.h"
#include "csh.h"

class TextEditor
{

public:
	static constexpr ImU32 mColorBackground = IM_COL32_WHITE;
	static constexpr ImU32 mColorLineNumber = IM_COL32(0, 0, 0, 255);
	static constexpr ImU32 mColorLineCursor = IM_COL32(0, 0, 0, 255);
	static constexpr ImU32 mColorCurrentLineLine = IM_COL32(192, 192, 192, 64);
	static constexpr ImU32 mColorCurrentLineFill = IM_COL32(192, 192, 192, 64);
	static constexpr ImU32 mColorCurrentLineFillInactive = IM_COL32(224, 224, 224, 64);
	static constexpr ImU32 mColorSelection = IM_COL32(128, 128, 128, 64);
	static constexpr ImU32 mColorErrorMarker = IM_COL32(128, 0, 0, 128);
	static constexpr ImU32 mColorSelectionInActive = IM_COL32(192, 192, 192, 64);
	static constexpr int mTabSize = 4;

	enum class SelectionMode
	{
		Normal,
		Word,
		Line
	};
	using clock = std::chrono::steady_clock;
	using PaletteIndex = EColorSyntaxType;

	// Represents a character coordinate from the user's point of view,
	// i. e. consider an uniform grid (assuming fixed-width font) on the
	// screen as it is rendered, and each cell has its own coordinate, starting from 0.
	struct Coordinates
	{
		int mLine = 0, mColumn = 0;
		static constexpr Coordinates Invalid() noexcept { return {-1, -1}; }
		constexpr bool IsInvalid() const noexcept { return mLine<0 || mColumn<0; }
		friend constexpr auto operator<=>(const Coordinates &, const Coordinates &) noexcept = default;
	};

	struct GlyphStyle {
		ImU32 color;
		uint8_t bold : 1;
		uint8_t italic : 1;
		uint8_t underlined : 1;
		bool operator==(const GlyphStyle &) const noexcept = default;
	};

	typedef std::string String;
	typedef std::map<Coordinates, std::pair<int, std::string>> ErrorMarkers; //single line errors only
	typedef std::array<GlyphStyle, (unsigned)COLOR_MAX> Palette;
	typedef uint8_t Char;

	struct Glyph { //should fit in two bytes
		Char mChar;
		PaletteIndex mColorIndex = COLOR_NORMAL;
	};
	static_assert(sizeof(Glyph)==2);

	struct Line : public std::vector<Glyph> {
		unsigned num_chars() const noexcept;
		void invalidate_num_chars() noexcept { num_char = -1; }
	private:
		mutable int num_char = -1; //The number of UTF8 characters, without the terminating newline
		unsigned calculate_num_chars() const noexcept;
	};

	typedef std::vector<Line> Lines;

	//tab: shift is meaningful. from-till are the lines to indent (from.col==0, till.col=max col in that line)
	//     When this is called the text has not been changed
	//     return 1 if the text has changed
	//bkspace: shift and till are not meaningful.
	//         when this is called no deletion has been made
	//         return the number of chars to delete - or zero to have the regular bkspace behaviour
	//other: shift and till is not meaningful. from is the character pos "after" inserting the char.
	//       the char is already inserted
	//       returns the number of spaces inserted/deleted (negative) (to change cursor)
	typedef int(*IndentCallback)(void *data, Lines &lines, char character, bool shift,
								  const Coordinates &from, const Coordinates &till);
	IndentCallback mIndent = nullptr;
	void *mIndentData = nullptr;
	std::string mIndentChars;

	typedef void(*ColorizeCallback)(Lines &lines, void *data);
	ColorizeCallback mColorize = nullptr;
	void *mColorizeData = nullptr;

	struct UndoBufferIndex {
		int mIndex = 0;      //=0 means no undo buffer at all
		int mGeneration = 0;
		constexpr bool operator==(const UndoBufferIndex &o) const noexcept = default;
	};

	struct UndoRecordSummary {
		std::string added;
		std::string removed;
	};

	TextEditor();

	const Palette& GetPalette() const { return mPaletteBase; }
	void SetPalette(ColorSyntaxAppearance (&)[COLOR_MAX]);

	void SetFonts(ImFont *regular, ImFont *bold, ImFont *italic, ImFont *bold_italic, float height,
				  ImFont *tooltip) noexcept;
	void SetErrorMarkers(ErrorMarkers &&e) { mErrorMarkers = std::move(e); }

	void Render(const char* aTitle, const ImVec2& aSize = ImVec2(), bool aBorder = false);
	void SetText(const std::string& aText);
	std::string GetText() const;

	void SetTextLines(const std::vector<std::string>& aLines);
	std::vector<std::string> GetTextLines() const;

	std::string GetSelectedText() const;
	std::string GetCurrentLineText()const;

	int GetTotalLines() const { return (int)mLines.size(); }
	bool IsOverwrite() const { return mOverwrite; }

	void SetReadOnly(bool aValue);
	bool IsReadOnly() const noexcept { return mReadOnly; }
	void SetAlwaysRenderCursor(bool aValue) noexcept { mAlwaysRenderCursor = aValue; }
	bool AlwaysRendersCursor() const noexcept { return mAlwaysRenderCursor; }
	bool IsTextChanged() const noexcept { return mTextChanged; }
	bool IsCursorPositionChanged() const noexcept { return mCursorPositionChanged; }
	bool IsScrollChanged() const noexcept { return mScrollHasChanged; }
	UndoBufferIndex GetUndoBufferIndex() const noexcept { return {mUndoIndex, mUndoIndex>0 ? mUndoBuffer[mUndoIndex-1].mGeneration : 0}; }
	int UndoBufferClearedFrom() const noexcept { return mUndoBufferCleared;  } // Does not report undo buffer change (ie reset of all undo buffers) due to SetText() and SetTextLine() (only reports things happening in Render())
	void ReColor(bool force=true) { if (force) Colorize();  ColorizeInternal(); } //Can be called if we need to re-color between Render() calls.

	Coordinates GetCursorPosition() const { return GetActualCursorCoordinates(); }
	void SetCursorPosition(const Coordinates& aPosition);

	void SetHandleMouseInputs(bool aValue) { mHandleMouseInputs = aValue; }
	bool IsHandleMouseInputsEnabled() const { return mHandleMouseInputs; }

	void SetHandleKeyboardInputs (bool aValue){ mHandleKeyboardInputs = aValue;}
	bool IsHandleKeyboardInputsEnabled() const { return mHandleKeyboardInputs; }

	void SetImGuiChildIgnored(bool aValue) { mIgnoreImGuiChild = aValue; }
	bool IsImGuiChildIgnored() const { return mIgnoreImGuiChild; }

	void SetShowWhitespaces(bool aValue) { mShowWhitespaces = aValue; }
	bool IsShowingWhitespaces() const { return mShowWhitespaces; }

	void InsertText(const std::string& aValue);
	void InsertText(const char* aValue);

	void MoveUp(int aAmount = 1, bool aSelect = false);
	void MoveDown(int aAmount = 1, bool aSelect = false);
	void MoveLeft(int aAmount = 1, bool aSelect = false, bool aWordMode = false);
	void MoveRight(int aAmount = 1, bool aSelect = false, bool aWordMode = false);
	void MoveTop(bool aSelect = false);
	void MoveBottom(bool aSelect = false);
	void MoveHome(bool aSelect = false);
	void MoveEnd(bool aSelect = false);

	void SetSelectionStart(const Coordinates& aPosition);
	void SetSelectionEnd(const Coordinates& aPosition);
	void SetSelection(const Coordinates& aStart, const Coordinates& aEnd, SelectionMode aMode = SelectionMode::Normal);
	void SelectWordUnderCursor();
	void SelectAll();
	bool HasSelection() const;

	void Copy();
	void Cut();
	void Paste();
public:
	/** Prevent the next editor change to be merged to a previous undo record.
	 * Used to isolate Hint substitutions as a standalone Undo record.*/
	void DoNotMergeNextChangeForUndo() noexcept { if (mUndoIndex>0 && (int)mUndoBuffer.size()>=mUndoIndex) mUndoBuffer[mUndoIndex-1].prevent_combine = true; }
	void Insert(const char *txt);
	void Delete(bool aWordMode);
private:

	bool CanUndo() const;
	bool CanRedo() const;
	void Undo(int aSteps = 1);
	void Redo(int aSteps = 1);

private:
	struct EditorState
	{
		Coordinates mSelectionStart;
		Coordinates mSelectionEnd;
		Coordinates mCursorPosition;
		bool operator==(const EditorState &) const noexcept = default;
	};

	class UndoRecord
	{
	public:
		UndoRecord() {}
		~UndoRecord() {}

		UndoRecord(
			const std::string& aAdded,
			const TextEditor::Coordinates aAddedStart,
			const TextEditor::Coordinates aAddedEnd,

			const std::string& aRemoved,
			const TextEditor::Coordinates aRemovedStart,
			const TextEditor::Coordinates aRemovedEnd,

			TextEditor::EditorState& aBefore,
			TextEditor::EditorState& aAfter);

		void Undo(TextEditor* aEditor);
		void Redo(TextEditor* aEditor);

		std::string mAdded;
		Coordinates mAddedStart;
		Coordinates mAddedEnd;

		std::string mRemoved;
		Coordinates mRemovedStart;
		Coordinates mRemovedEnd;

		EditorState mBefore;
		EditorState mAfter;

		int mGeneration = 0; //When an UndoRecord is combined, we increase it.
		bool prevent_combine = false; //when set, the next change after us will never be merged with us.
		bool Combine(const UndoRecord &o);
	};

	typedef std::vector<UndoRecord> UndoBuffer;

	void ProcessInputs();
	void Colorize(int aFromLine = 0, int aCount = -1);
	void ColorizeInternal();
	float TextDistanceToLineStart(const Coordinates& aFrom) const;
	void EnsureCursorVisible();
	int GetPageSize() const;
public:
	Coordinates GetCoordinate(int char_pos) noexcept;
    std::pair<int, int> GetCharBytePos(const Coordinates& a) const noexcept;
    int GetCharPos(const Coordinates &a) const noexcept;
	std::pair<int, int> GetSelectionCharPos() const noexcept { return {GetCharPos(mState.mSelectionStart), GetCharPos(mState.mSelectionEnd)}; }
	std::pair<Coordinates, Coordinates> GetSelection() const noexcept { return {mState.mSelectionStart, mState.mSelectionEnd}; }
	std::string GetText(const Coordinates& aStart, const Coordinates& aEnd) const;
	bool Find(std::string_view what, bool case_sensitive, bool whole_words_only, bool fw,
			  const std::optional<std::pair<Coordinates, Coordinates>>& selection,
			  bool last_found_selected);
	std::optional<UndoRecordSummary> GetLastUndoRecord() const {
		if (mUndoIndex == 0) return {};
		return UndoRecordSummary{mUndoBuffer[mUndoIndex - 1].mAdded, mUndoBuffer[mUndoIndex - 1].mRemoved};
	}
private:
	Coordinates GetActualCursorCoordinates() const;
	Coordinates SanitizeCoordinates(const Coordinates& aValue) const;
	void Advance(Coordinates& aCoordinates) const;
	void DeleteRange(const Coordinates& aStart, const Coordinates& aEnd);
	int InsertTextAt(Coordinates& aWhere, const char* aValue);
	void AddUndo(UndoRecord& aValue);
public:
	Coordinates ScreenPosToCoordinates(const ImVec2& aPosition) const;
	std::optional<std::pair<ImVec2, ImVec2>> CoordinatesToScreenPos(const TextEditor::Coordinates &a) const;
	Coordinates FindWordStart(const Coordinates& aFrom) const;
private:
	Coordinates FindWordEnd(const Coordinates& aFrom) const;
	Coordinates FindNextWord(const Coordinates& aFrom) const;
	int GetCharacterIndex(const Coordinates& aCoordinates) const;
	int GetCharacterColumn(int aLine, int aIndex) const;
	int GetLineCharacterCount(int aLine) const;
public:
	int GetLineMaxColumn(int aLine) const;
private:
	bool IsOnWordBoundary(const Coordinates& aAt) const;
	void RemoveLine(int aStart, int aEnd);
	void RemoveLine(int aIndex);
	Line& InsertLine(int aIndex);
	void Backspace(bool aWordMode);
public:
	void EnterCharacter(ImWchar aChar, bool aShift);
	void DeleteSelection();
protected:
	std::string GetWordUnderCursor() const;
	std::string GetWordAt(const Coordinates& aCoords) const;
	GlyphStyle GetGlyphStyle(const Glyph& aGlyph) const;

public:
	void HandleKeyboardInputs(bool only_when_focused = true);
protected:
	void HandleMouseInputs();
	void Render();

	ImFont *GetFont(const GlyphStyle &s) const noexcept {
		return mFonts[(s.bold ? 1 : 0) + (s.italic ? 2 : 0)];
	}

	float mLineSpacing;
	Lines mLines;
	EditorState mState;
    std::string mFullLineOnClipboard; //When copying a full line, we store it here to see that we need to re-insert the whole line when pasting
	UndoBuffer mUndoBuffer;
	int mUndoIndex;
	int mUndoBufferCleared;

	bool mOverwrite;
	bool mReadOnly;
	bool mWithinRender;
	bool mScrollToCursor;
	bool mScrollToTop;
	bool mTextChanged;
	float mTextStart;  // width of the line numbers and some margin
	int  mLeftMargin;
	bool mCursorPositionChanged;
	int mColorRangeMin, mColorRangeMax;
	SelectionMode mSelectionMode;
	bool mHandleKeyboardInputs;
	bool mHandleMouseInputs;
	bool mIgnoreImGuiChild;
	bool mShowWhitespaces;
	bool mAlwaysRenderCursor;
	Palette mPaletteBase;
	Palette mPalette;

	ErrorMarkers mErrorMarkers;
	ImVec2 mCharAdvance;
	Coordinates mInteractiveStart, mInteractiveEnd;
	clock::time_point mStartTime;

	float mLastClick;
	std::array<ImFont *, 5> mFonts;
	ImVec2 mLastOrigin; // The screen corrdinate of char (0:0) (maybe outside the screen if scrolled) as seen in Render(). Used in CoordinatesToEditorPos() and ScreenPosToCoordinates()
	ImVec2 mLastSize;   // The workspace size in Render
	ImVec2 mLastScroll; // The last scroll value seen in Render(). Used in CoordinatesToEditorPos() and ScreenPosToCoordinates()
	bool mScrollHasChanged;
public:
	float mFontSize; //These two should be changed in sync
	float mScale = 1;
};
