#ifndef ECODE_PLUGINMANAGER_HPP
#define ECODE_PLUGINMANAGER_HPP

#include "../projectsearch.hpp"
#include "lsp/lspprotocol.hpp"
#include <eepp/ui/models/filesystemmodel.hpp>
#include <eepp/ui/models/model.hpp>
#include <eepp/ui/tools/uicodeeditorsplitter.hpp>
#include <eepp/ui/uicodeeditor.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uiwindow.hpp>
#include <limits>
#include <memory>
#include <nlohmann/json.hpp>
#include <string>

using namespace EE;
using namespace EE::System;
using namespace EE::UI;
using namespace EE::UI::Models;
using namespace EE::UI::Tools;

namespace ecode {

class PluginManager;
class FileSystemListener;

typedef std::function<UICodeEditorPlugin*( PluginManager* pluginManager )> PluginCreatorFn;

#ifdef minor
#undef minor
#endif
#ifdef major
#undef major
#endif
struct PluginVersion {
	PluginVersion() {}

	PluginVersion( Uint8 major, Uint8 minor, Uint8 patch ) :
		major( major ),
		minor( minor ),
		patch( patch ),
		string( String::format( "%d.%d.%d", major, minor, patch ) ) {}

	Uint8 major; /**< major version */
	Uint8 minor; /**< minor version */
	Uint8 patch; /**< update version */
	std::string string;

	Uint32 getVersion() const { return major * 1000 + minor * 100 + patch; }

	const std::string& getVersionString() const { return string; }
};

struct PluginDefinition {
	std::string id;
	std::string name;
	std::string description;
	PluginCreatorFn creatorFn;
	PluginVersion version;
	PluginCreatorFn creatorSyncFn{ nullptr };
};

enum class PluginCapability {
	WorkspaceSymbol,
	TextDocumentSymbol,
	Max
};

enum class PluginMessageType {
	WorkspaceFolderChanged, // Broadcast the workspace folder from the application to the plugins
	Diagnostics,			// Broadcast a document diagnostics from the LSP Client
	CodeCompletion, // Request the LSP Server to start a code completion in the requested document
					// and position
	LanguageServerCapabilities, // Request the language server capabilities of a language if there
								// is any available, it will be returned as a broadcast
	SignatureHelp,				// Request the LSP Server to provide function/method signature help
	CancelRequest,				// Cancel a request ID
	FindAndOpenClosestURI,		// Request a component to find and open the closest path from an URI
	DocumentFormatting,			// Request the LSP Server to format a document
	SymbolReference,			// Request the LSP Server to find a symbol reference in the project
	ShowMessage,		// The LSP server sends a request to the client to show a message on screen
	ShowDocument,		// The LSP server sends a request to the client to show a document
	WorkspaceSymbol,	// Request to the LSP server to query workspace symbols
	TextDocumentSymbol, // Request to the LSP server the document symbols
	TextDocumentFlattenSymbol, // Request to the LSP server the document symbols flattened
	DiagnosticsCodeAction,	   // Request a code action to anyone that can handle it
	FileSystemListenerReady,   // Broadcast to inform the plugins that the file system listener is
							   // available
	GetErrorOrWarning, // Request a component to provide the information of an error or warning in a
					   // particular document location
	GetDiagnostics,	   // Request the diagnostic information from a cursor position
	QueryPluginCapability, // Requests / queries if a plugin providers a capability
	Undefined
};

enum class PluginMessageFormat {
	Empty,
	JSON,
	Diagnostics,
	CodeCompletion,
	LanguageServerCapabilities,
	SignatureHelp,
	ProjectSearchResult,
	ShowMessage,
	ShowDocument,
	SymbolInformation,
	DiagnosticsCodeAction
};

class PluginIDType {
  public:
	enum class Type { Integer, String, Invalid };

	PluginIDType() {}

	PluginIDType( Int64 val ) : mType( Type::Integer ), mInt( val ) {}

	PluginIDType( const std::string& val ) : mType( Type::String ), mString( val ) {}

	bool operator==( const PluginIDType& right ) {
		return mType == right.mType && ( ( mType == Type::Integer && mInt == right.mInt ) ||
										 ( mType == Type::String && mString == right.mString ) );
	}

	bool operator!=( const PluginIDType& right ) { return !( *this == right ); }

	bool is( const Type& type ) const { return type == mType; }

	bool isString() const { return Type::String == mType; }

	bool isInteger() const { return Type::Integer == mType; }

	bool isValid() const { return mType != Type::Invalid; }

	std::string toString() {
		if ( mType == Type::Integer )
			return String::toString( mInt );
		return mString;
	}

	operator Int64() const { return mInt; }

	operator std::string() { return mString; }

	const Int64& asInt() const { return mInt; }

	const std::string& asString() const { return mString; }

  protected:
	Type mType{ Type::Invalid };
	Int64 mInt{ std::numeric_limits<Int64>::max() };
	std::string mString;
};

class LSPClientPlugin;

struct PluginMessage {
	PluginMessageType type{ PluginMessageType::Undefined };
	PluginMessageFormat format{ PluginMessageFormat::Empty };
	const void* data{ nullptr };
	PluginIDType responseID{ 0 }; // 0 if it's not a response;

	const void* asData() const { return data; }

	const nlohmann::json& asJSON() const { return *static_cast<const nlohmann::json*>( data ); }

	bool isJSON() const { return format == PluginMessageFormat::JSON; }

	const LSPPublishDiagnosticsParams& asDiagnostics() const {
		return *static_cast<const LSPPublishDiagnosticsParams*>( data );
	}

	const LSPCompletionList& asCodeCompletion() const {
		return *static_cast<const LSPCompletionList*>( data );
	}

	const LSPServerCapabilities& asLanguageServerCapabilities() const {
		return *static_cast<const LSPServerCapabilities*>( data );
	}

	const LSPSignatureHelp& asSignatureHelp() const {
		return *static_cast<const LSPSignatureHelp*>( data );
	}

	const ProjectSearch::Result& asProjectSearchResult() const {
		return *static_cast<const ProjectSearch::Result*>( data );
	}

	const LSPShowMessageParams& asShowMessage() const {
		return *static_cast<const LSPShowMessageParams*>( data );
	}

	const LSPShowDocumentParams& asShowDocument() const {
		return *static_cast<const LSPShowDocumentParams*>( data );
	}

	const LSPSymbolInformationList& asSymbolInformation() const {
		return *static_cast<const LSPSymbolInformationList*>( data );
	}

	const LSPDiagnosticsCodeAction& asDiasnosticsCodeAction() const {
		return *static_cast<const LSPDiagnosticsCodeAction*>( data );
	}

	const PluginIDType& asPluginID() const { return *static_cast<const PluginIDType*>( data ); }

	bool isResponse() const { return -1 != responseID && 0 != responseID; }

	bool isRequest() const { return 0 == responseID; }

	bool isBroadcast() const { return -1 == responseID; }
};

struct PluginInmediateResponse {
	PluginMessageType type{ PluginMessageType::Undefined };
	nlohmann::json data;
};

class PluginRequestHandle {
  public:
	static PluginRequestHandle broadcast() { return PluginRequestHandle( -1 ); }

	static PluginRequestHandle empty() { return PluginRequestHandle(); }

	PluginRequestHandle() {}

	PluginRequestHandle( PluginIDType id ) : mId( std::move( id ) ) {}

	explicit PluginRequestHandle( PluginInmediateResponse msg ) :
		mId( -2 ), mResponse( std::move( msg ) ) {}

	virtual const PluginIDType& id() const { return mId; }

	virtual void cancel() {}

	bool isEmpty() const { return mId == 0; }

	bool isBroadcast() const { return mId == -1; }

	const PluginInmediateResponse& getResponse() const { return mResponse; }

	bool isResponse() const { return mId == -2 && !mResponse.data.empty(); }

  protected:
	PluginIDType mId{ 0 };
	PluginInmediateResponse
		mResponse; //! Some requests can be responded inmediatly, so the message comes in the handle
};

class PluginManager {
  public:
	static constexpr int versionNumber( int major, int minor, int patch ) {
		return ( (major)*1000 + (minor)*100 + ( patch ) );
	}

	static std::string versionString( int major, int minor, int patch ) {
		return String::format( "%d.%d.%.d", major, minor, patch );
	}

	using OnFileLoadedCb = std::function<void( UICodeEditor*, const std::string& )>;
	using OnLoadFileCb = std::function<void( const std::string&, const OnFileLoadedCb& )>;

	PluginManager( const std::string& resourcesPath, const std::string& pluginsPath,
				   std::shared_ptr<ThreadPool> pool, const OnLoadFileCb& loadFileCb );

	~PluginManager();

	void registerPlugin( const PluginDefinition& def );

	UICodeEditorPlugin* get( const std::string& id );

	bool setEnabled( const std::string& id, bool enable, bool sync = false );

	bool isEnabled( const std::string& id ) const;

	bool reload( const std::string& id );

	const std::string& getResourcesPath() const;

	const std::string& getPluginsPath() const;

	const std::map<std::string, bool>& getPluginsEnabled() const;

	void onNewEditor( UICodeEditor* editor );

	void setPluginsEnabled( const std::map<std::string, bool>& pluginsEnabled, bool sync );

	const std::shared_ptr<ThreadPool>& getThreadPool() const;

	std::function<void( UICodeEditorPlugin* )> onPluginEnabled;

	const std::map<std::string, PluginDefinition>& getDefinitions() const;

	const PluginDefinition* getDefinitionIndex( const Int64& index ) const;

	UICodeEditorSplitter* getSplitter() const;

	UISceneNode* getUISceneNode() const;

	const std::string& getWorkspaceFolder() const;

	void setWorkspaceFolder( const std::string& workspaceFolder );

	PluginRequestHandle sendRequest( PluginMessageType type, PluginMessageFormat format,
									 const void* data );

	PluginRequestHandle sendRequest( UICodeEditorPlugin* pluginWho, PluginMessageType type,
									 PluginMessageFormat format, const void* data );

	void sendResponse( UICodeEditorPlugin* pluginWho, PluginMessageType type,
					   PluginMessageFormat format, const void* data,
					   const PluginIDType& responseID );

	void sendBroadcast( UICodeEditorPlugin* pluginWho, PluginMessageType, PluginMessageFormat,
						const void* data );

	void sendBroadcast( const PluginMessageType& notification, const PluginMessageFormat& format,
						void* data );

	void subscribeMessages( UICodeEditorPlugin* plugin,
							std::function<PluginRequestHandle( const PluginMessage& )> cb );

	void unsubscribeMessages( UICodeEditorPlugin* plugin );

	void subscribeMessages( const std::string& uniqueComponentId,
							std::function<PluginRequestHandle( const PluginMessage& )> cb );

	void unsubscribeMessages( const std::string& uniqueComponentId );

	FileSystemListener* getFileSystemListener() const { return mFileSystemListener; };

	const OnLoadFileCb& getLoadFileFn() const;

  protected:
	using SubscribedPlugins =
		std::map<std::string, std::function<PluginRequestHandle( const PluginMessage& )>>;
	friend class App;
	std::string mResourcesPath;
	std::string mPluginsPath;
	std::string mWorkspaceFolder;
	std::map<std::string, UICodeEditorPlugin*> mPlugins;
	std::map<std::string, bool> mPluginsEnabled;
	std::map<std::string, PluginDefinition> mDefinitions;
	std::shared_ptr<ThreadPool> mThreadPool;
	UICodeEditorSplitter* mSplitter{ nullptr };
	FileSystemListener* mFileSystemListener{ nullptr };
	Mutex mSubscribedPluginsMutex;
	SubscribedPlugins mSubscribedPlugins;
	OnLoadFileCb mLoadFileFn;
	bool mClosing{ false };

	bool hasDefinition( const std::string& id );

	void setSplitter( UICodeEditorSplitter* splitter );

	void setFileSystemListener( FileSystemListener* listener );
};

class PluginsModel : public Model {
  public:
	enum Columns { Id, Title, Enabled, Description, Version };

	static std::shared_ptr<PluginsModel> New( PluginManager* manager );

	PluginsModel( PluginManager* manager ) : mManager( manager ) {}

	virtual ~PluginsModel() {}

	virtual size_t rowCount( const ModelIndex& ) const;

	virtual size_t columnCount( const ModelIndex& ) const { return mColumnNames.size(); }

	virtual std::string columnName( const size_t& col ) const;

	virtual void setColumnName( const size_t& index, const std::string& name ) {
		eeASSERT( index <= Columns::Version );
		mColumnNames[index] = name;
	}

	virtual Variant data( const ModelIndex& index, ModelRole role = ModelRole::Display ) const;

	virtual void update() { onModelUpdate(); }

	PluginManager* getManager() const;

  protected:
	PluginManager* mManager;
	std::vector<std::string> mColumnNames{ "Id", "Title", "Enabled", "Description", "Version" };
};

class UIPluginManager {
  public:
	static UIWindow* New( UISceneNode* sceneNode, PluginManager* manager,
						  std::function<void( const std::string& )> loadFileCb );
};

class Plugin : public UICodeEditorPlugin {
  public:
	explicit Plugin( PluginManager* manager );

	void subscribeFileSystemListener();

	void unsubscribeFileSystemListener();

	bool isReady() const;

	bool isLoading() const { return mLoading; }

	bool isShuttingDown() const;

	virtual bool hasFileConfig();

	virtual std::string getFileConfigPath();

	PluginManager* getManager() const;

	virtual String::HashType getConfigFileHash() { return 0; }

  protected:
	PluginManager* mManager{ nullptr };
	std::shared_ptr<ThreadPool> mThreadPool;
	Uint64 mFileSystemListenerCb{ 0 };
	std::string mConfigPath;
	FileInfo mConfigFileInfo;

	bool mReady{ false };
	bool mLoading{ false };
	bool mShuttingDown{ false };
};

class PluginBase : public Plugin {
  public:
	explicit PluginBase( PluginManager* manager ) : Plugin( manager ) {}

	virtual ~PluginBase();

	virtual void onRegister( UICodeEditor* ) override;

	virtual void onUnregister( UICodeEditor* ) override;

	virtual String::HashType getConfigFileHash() override { return mConfigHash; }

  protected:
	//! Keep track of the registered editors + all the listeners registered to each editor
	std::unordered_map<UICodeEditor*, std::vector<Uint32>> mEditors;
	//! Keep track of the documents opened
	std::set<TextDocument*> mDocs;
	//! Documents and Editors mutex
	Mutex mMutex;
	//! Keep track of the document pointer of each editor
	std::unordered_map<UICodeEditor*, TextDocument*> mEditorDocs;
	//! Keep track of the key bindings managed by the plugin
	std::map<std::string, std::string> mKeyBindings; /* cmd, shortcut */
	//! If the configuration is stored in a file, keep track of the config hash
	String::HashType mConfigHash{ 0 };

	virtual void onDocumentLoaded( TextDocument* ){};

	virtual void onDocumentClosed( TextDocument* ){};

	virtual void onDocumentChanged( UICodeEditor*, TextDocument* /*oldDoc*/ ){};

	virtual void onRegisterListeners( UICodeEditor*, std::vector<Uint32>& /*listeners*/ ){};

	//! Usually used to remove keybindings in an editor
	virtual void onBeforeUnregister( UICodeEditor* ){};

	virtual void onRegisterDocument( TextDocument* ){};

	virtual void onUnregisterEditor( UICodeEditor* ){};

	//! Usually used to unregister commands in a document
	virtual void onUnregisterDocument( TextDocument* ){};
};

} // namespace ecode

#endif // ECODE_PLUGINMANAGER_HPP
