#include "lspdocumentclient.hpp"
#include "lspclientplugin.hpp"
#include "lspclientserver.hpp"
#include "lspclientservermanager.hpp"
#include <eepp/system/filesystem.hpp>
#include <eepp/system/iostreamstring.hpp>
#include <eepp/system/log.hpp>
#include <eepp/ui/doc/syntaxhighlighter.hpp>
#include <eepp/window/engine.hpp>

using namespace EE::System;

namespace ecode {

LSPDocumentClient::LSPDocumentClient( LSPClientServer* server, TextDocument* doc ) :
	mServer( server ), mServerManager( server->getManager() ), mDoc( doc ) {
	refreshTag();
	notifyOpen();
	requestSymbolsDelayed();
	requestSemanticHighlightingDelayed();
	if ( mServer->isReady() )
		setupFoldRangeService();
}

void LSPDocumentClient::onServerInitialized() {
	requestSymbols();
	requestSemanticHighlighting();
	setupFoldRangeService();
	// requestCodeLens();
}

void LSPDocumentClient::setupFoldRangeService() {
	mDoc->getFoldRangeService().setProvider( this );
	if ( mDoc->getFoldRangeService().isEnabled() )
		tryRequestFoldRanges( true );
}

LSPDocumentClient::~LSPDocumentClient() {
	mDoc->getFoldRangeService().setProvider( nullptr );
	mDoc = nullptr;
	UISceneNode* sceneNode = getUISceneNode();
	if ( nullptr != sceneNode && 0 != mTag )
		sceneNode->removeActionsByTag( mTag );
	if ( nullptr != sceneNode && 0 != mTagSemanticTokens )
		sceneNode->removeActionsByTag( mTagSemanticTokens );
	mShutdown = true;
	while ( mRunningSemanticTokens )
		Sys::sleep( Milliseconds( 0.1f ) );
}

bool LSPDocumentClient::tryRequestFoldRanges( bool requestFolds ) {
	bool ret = mServer->getCapabilities().foldingRangeProvider;
	if ( ret && requestFolds )
		requestFoldRange();
	return ret;
}

bool LSPDocumentClient::foldingRangeProvider() const {
	return mServer->getCapabilities().foldingRangeProvider;
}

void LSPDocumentClient::onDocumentLoaded( TextDocument* ) {
	refreshTag();
	requestSemanticHighlightingDelayed();
	// requestCodeLens();
}

void LSPDocumentClient::onDocumentTextChanged( const DocumentContentChange& change ) {
	++mVersion;
	// If several change event are being fired, the thread pool can't guaranteed that it will be
	// executed in FIFO. Se we accumulate the events in a queue and fire them in correct order.
	mServer->queueDidChange( mDoc->getURI(), mVersion, "", { change } );
	mServer->getThreadPool()->run( [this, change]() { mServer->processDidChangeQueue(); } );
	requestSymbolsDelayed();
	requestSemanticHighlightingDelayed();
}

void LSPDocumentClient::onDocumentUndoRedo( const TextDocument::UndoRedo& /*eventType*/ ) {}

void LSPDocumentClient::onDocumentCursorChange( const TextPosition& ) {}

void LSPDocumentClient::onDocumentSelectionChange( const TextRange& ) {}

void LSPDocumentClient::onDocumentLineCountChange( const size_t& /*lastCount*/,
												   const size_t& /*newCount*/ ) {}

void LSPDocumentClient::onDocumentLineChanged( const Int64& /*lineIndex*/ ) {}

void LSPDocumentClient::onDocumentSaved( TextDocument* ) {
	mServer->getThreadPool()->run( [this]() { mServer->didSave( mDoc ); } );
}

void LSPDocumentClient::onDocumentClosed( TextDocument* ) {
	URI uri = mDoc->getURI();
	LSPClientServer* server = mServer;
	LSPClientServerManager* manager = mServerManager;
	mServer->getThreadPool()->run( [server, manager, uri]() {
		if ( manager->isServerRunning( server ) )
			server->didClose( uri );
	} );
	mServer->removeDoc( mDoc );
}

void LSPDocumentClient::onDocumentDirtyOnFileSystem( TextDocument* ) {}

void LSPDocumentClient::onDocumentMoved( TextDocument* ) {
	refreshTag();
}

void LSPDocumentClient::onDocumentReloaded( TextDocument* ) {
	URI uri = mDoc->getURI();
	TextDocument* doc = mDoc;
	LSPClientServer* server = mServer;
	auto version = ++mVersion;
	mServer->getThreadPool()->run( [this, server, doc, uri, version]() {
		server->didClose( uri );
		server->didOpen( doc, version );
		requestSemanticHighlighting( true );
	} );
	refreshTag();
}

void LSPDocumentClient::onDocumentReset( TextDocument* doc ) {
	onDocumentReloaded( doc );
}

TextDocument* LSPDocumentClient::getDoc() const {
	return mDoc;
}

LSPClientServer* LSPDocumentClient::getServer() const {
	return mServer;
}

int LSPDocumentClient::getVersion() const {
	return mVersion;
}

void LSPDocumentClient::refreshTag() {
	String::HashType oldTag = mTag;
	mTag = String::hash( mDoc->getURI().toString() );
	mTagSemanticTokens = String::hash( mDoc->getURI().toString() + ":semantictokens" );
	UISceneNode* sceneNode = getUISceneNode();
	if ( nullptr != sceneNode && 0 != oldTag )
		sceneNode->removeActionsByTag( oldTag );
}

void LSPDocumentClient::requestSemanticHighlighting( bool reqFull ) {
	if ( !mServer || !mServer->getManager()->getPlugin()->semanticHighlightingEnabled() ||
		 !mServer->getManager()->getPlugin()->langSupportsSemanticHighlighting(
			 mServer->getDefinition().language ) )
		return;
	const auto& cap = mServer->getCapabilities();
	if ( !cap.semanticTokenProvider.full && !cap.semanticTokenProvider.fullDelta &&
		 !cap.semanticTokenProvider.range )
		return;

	mWaitingSemanticTokensResponse = true;
	TextRange range;
	std::string reqId;
	bool delta = false;
	if ( cap.semanticTokenProvider.fullDelta ) {
		delta = true;
		reqId = reqFull ? "" : mSemanticeResultId;
	} else if ( cap.semanticTokenProvider.range && !mFirstHighlight && !reqFull ) {
		range = mDoc->getActiveClientVisibleRange();
	} else if ( mFirstHighlight || reqFull ) {
		mFirstHighlight = false;
	}

	LSPDocumentClient* docClient = this;
	URI uri = mDoc->getURI();
	LSPClientServer* server = mServer;
	Uint64 docModId = mDoc->getModificationId();
	mServer->documentSemanticTokensFull(
		mDoc->getURI(), delta, reqId, range,
		[docClient, uri, server, docModId]( const auto&, LSPSemanticTokensDelta&& deltas ) {
			if ( server->hasDocument( uri ) ) {
				docClient->mWaitingSemanticTokensResponse = false;
				docClient->processTokens( std::move( deltas ), docModId );
			}
		} );
}

void LSPDocumentClient::requestSemanticHighlightingDelayed( bool reqFull ) {
	if ( !mServer || !mServer->getManager()->getPlugin()->semanticHighlightingEnabled() ||
		 !mServer->getManager()->getPlugin()->langSupportsSemanticHighlighting(
			 mServer->getDefinition().language ) )
		return;
	const auto& cap = mServer->getCapabilities();
	if ( !cap.semanticTokenProvider.full && !cap.semanticTokenProvider.fullDelta &&
		 !cap.semanticTokenProvider.range )
		return;
	UISceneNode* sceneNode = getUISceneNode();
	if ( sceneNode ) {
		mWaitingSemanticTokensResponse = true;
		sceneNode->removeActionsByTag( mTagSemanticTokens );
		sceneNode->runOnMainThread( [this, reqFull]() { requestSemanticHighlighting( reqFull ); },
									Seconds( 0.5f ), mTagSemanticTokens );
	}
}

bool LSPDocumentClient::isRunningSemanticTokens() const {
	return mRunningSemanticTokens;
}

bool LSPDocumentClient::isWaitingSemanticTokensResponse() const {
	return mWaitingSemanticTokensResponse;
}

void LSPDocumentClient::requestCodeLens() {
	if ( !mServer || !mServer->getCapabilities().codeLensProvider.supported )
		return;

	URI uri = mDoc->getURI();
	LSPClientServer* server = mServer;
	LSPDocumentClient* docClient = this;
	mServer->documentCodeLens(
		uri, [uri, server, docClient]( const auto&, const std::vector<LSPCodeLens>& codeLens ) {
			if ( server->hasDocument( uri ) ) {
				docClient->mCodeLens = codeLens;
			}
		} );
}

UISceneNode* LSPDocumentClient::getUISceneNode() {
	LSPClientServer* server = mServer;
	if ( !server || !server->getManager() || !server->getManager()->getPluginManager() ||
		 !server->getManager()->getPluginManager()->getUISceneNode() )
		return nullptr;
	return server->getManager()->getPluginManager()->getUISceneNode();
}

static SyntaxStyleType semanticTokenTypeToSyntaxType( const std::string& type ) {
	switch ( String::hash( type ) ) {
		case SemanticTokenTypes::Namespace:
		case SemanticTokenTypes::Type:
		case SemanticTokenTypes::Class:
		case SemanticTokenTypes::Enum:
		case SemanticTokenTypes::Interface:
		case SemanticTokenTypes::Struct:
		case SemanticTokenTypes::TypeParameter:
			return "keyword2"_sst;
		case SemanticTokenTypes::Parameter:
			return "keyword3"_sst;
		case SemanticTokenTypes::Variable:
			return "symbol"_sst;
		case SemanticTokenTypes::Property:
			return "symbol"_sst;
		case SemanticTokenTypes::EnumMember:
		case SemanticTokenTypes::Event:
			return "keyword2"_sst;
		case SemanticTokenTypes::Function:
		case SemanticTokenTypes::Method:
		case SemanticTokenTypes::Member:
			return "function"_sst;
		case SemanticTokenTypes::Macro:
			return "keyword2"_sst;
		case SemanticTokenTypes::Keyword:
		case SemanticTokenTypes::Modifier:
			return "keyword"_sst;
		case SemanticTokenTypes::Comment:
			return "comment"_sst;
		case SemanticTokenTypes::Str:
			return "string"_sst;
		case SemanticTokenTypes::Number:
		case SemanticTokenTypes::Regexp:
			return "number"_sst;
		case SemanticTokenTypes::Operator:
			return "operator"_sst;
		case SemanticTokenTypes::Decorator:
			return "literal"_sst;
		case SemanticTokenTypes::Unknown:
			break;
	};
	return SyntaxStyleTypes::Normal;
}

void LSPDocumentClient::processTokens( LSPSemanticTokensDelta&& tokens,
									   const Uint64& docModificationId ) {
	if ( mDoc == nullptr || mServer == nullptr )
		return;

	// If the document has already being modified after requesting the semantic highlighting,
	// re-request the changes
	if ( docModificationId != mDoc->getModificationId() )
		return requestSemanticHighlightingDelayed();

	mRunningSemanticTokens = true;

	if ( !tokens.resultId.empty() )
		mSemanticeResultId = tokens.resultId;

	for ( const auto& edit : tokens.edits ) {
		auto& curTokens = mSemanticTokens.data;
		if ( edit.deleteCount > 0 ) {
			curTokens.erase( curTokens.begin() + edit.start,
							 curTokens.begin() + edit.start + edit.deleteCount );
		}
		curTokens.insert( curTokens.begin() + edit.start, edit.data.begin(), edit.data.end() );
	}

	if ( !tokens.data.empty() ) {
		mSemanticTokens = std::move( tokens );
	}

	highlight();

	mRunningSemanticTokens = false;
}

void LSPDocumentClient::highlight() {
	if ( mShutdown )
		return;
	const auto& data = mSemanticTokens.data;

	if ( data.size() % 5 != 0 ) {
		Log::warning( "LSPDocumentClient::highlight bad data format for doc: %s",
					  mDoc->getURI().toString().c_str() );
		return;
	}
	Clock clock;
	const auto& caps = mServer->getCapabilities().semanticTokenProvider;
	Int64 firstLine = !data.empty() ? data[0] : -1;
	Int64 currentLine = 0;
	Uint32 start = 0;
	std::unordered_map<size_t, TokenizedLine> tokenizerLines;
	Int64 lastLine = 0;
	TokenizedLine* lastLinePtr = nullptr;
	Time diff;

	for ( size_t i = 0; i < data.size(); i += 5 ) {
		if ( mShutdown )
			return;
		const Uint32 deltaLine = data[i];
		const Uint32 deltaStart = data[i + 1];
		const Uint32 len = data[i + 2];
		const int type = data[i + 3];
		// const Uint32 mod = data[i + 4];
		currentLine += deltaLine;

		if ( deltaLine == 0 ) {
			start += deltaStart;
		} else {
			start = deltaStart;
		}

		auto* line = &tokenizerLines[currentLine];
		if ( type >= 0 && type < (int)caps.legend.tokenTypes.size() ) {
			const auto& ltype = caps.legend.tokenTypes[type];
			line->tokens.push_back( { semanticTokenTypeToSyntaxType( ltype ), start, len } );
		} else {
			line->tokens.push_back( { SyntaxStyleTypes::Normal, start, len } );
		}
		line->hash = mDoc->line( currentLine ).getHash();
		line->updateSignature();

		auto curSignature = mDoc->getHighlighter()->getTokenizedLineSignature( lastLine );
		if ( lastLine != currentLine && lastLinePtr && lastLinePtr->signature == curSignature ) {
			tokenizerLines.erase( lastLine );
		}

		lastLine = currentLine;
		lastLinePtr = line;
	}

	diff = clock.getElapsedTime();

	for ( auto& tline : tokenizerLines ) {
		if ( mShutdown )
			return;

		mDoc->getHighlighter()->mergeLine( tline.first, tline.second );
	}

	if ( firstLine != -1 && lastLine >= firstLine && mDoc ) {
		getServer()->getManager()->getPlugin()->getManager()->getSplitter()->forEachEditor(
			[this, firstLine, lastLine]( UICodeEditor* editor ) {
				if ( editor->isVisible() && &editor->getDocument() == mDoc &&
					 editor->getVisibleRange().intersectsLineRange( firstLine, lastLine ) ) {
					editor->invalidateDraw();
				}
			} );
	}

	if ( !mServer->isSilent() ) {
		Log::debug(
			"LSPDocumentClient::highlight took: %.2f ms. Diff analysis took: %.2f ms. Updated "
			"%lld elements",
			clock.getElapsedTime().asMilliseconds(), diff.asMilliseconds(), tokenizerLines.size() );
	}
}

void LSPDocumentClient::notifyOpen() {
	eeASSERT( mDoc );
	if ( Engine::instance()->isMainThread() ) {
		mServer->getThreadPool()->run( [this]() { mServer->didOpen( mDoc, ++mVersion ); } );
	} else {
		mServer->didOpen( mDoc, ++mVersion );
	}
}

void LSPDocumentClient::requestSymbols() {
	eeASSERT( mDoc );
	LSPClientServer* server = mServer;
	if ( !server->getCapabilities().documentSymbolProvider )
		return;
	URI uri = mDoc->getURI();
	if ( Engine::instance()->isMainThread() ) {
		mServer->getThreadPool()->run(
			[server, uri]() { server->documentSymbolsBroadcast( uri ); } );
	} else {
		server->documentSymbolsBroadcast( uri );
	}
}

void LSPDocumentClient::requestFoldRange() {
	eeASSERT( mDoc );
	LSPClientServer* server = mServer;
	if ( !server->getCapabilities().foldingRangeProvider )
		return;
	URI uri = mDoc->getURI();
	TextDocument* doc = mDoc;
	auto handler = [uri, server, doc]( const PluginIDType&,
									   const std::vector<LSPFoldingRange>& res ) {
		if ( !server->hasDocument( uri ) )
			return;
		std::vector<TextRange> regions;
		regions.reserve( res.size() );
		for ( const auto& region : res ) {
			if ( region.endLine - region.startLine > 1 )
				regions.push_back( { { region.startLine, 0 }, { region.endLine, 0 } } );
		}
		doc->getFoldRangeService().setFoldingRegions( regions );
	};

	if ( Engine::instance()->isMainThread() ) {
		server->getThreadPool()->run(
			[server, uri, handler]() { server->documentFoldingRange( uri, handler ); } );
	} else {
		server->documentFoldingRange( uri, handler );
	}
}

void LSPDocumentClient::requestSymbolsDelayed() {
	if ( !mServer || !mServer->getCapabilities().documentSymbolProvider )
		return;
	UISceneNode* sceneNode = getUISceneNode();
	if ( sceneNode ) {
		sceneNode->removeActionsByTag( mTag );
		LSPDocumentClient* docClient = this;
		URI uri = mDoc->getURI();
		LSPClientServer* server = mServer;
		sceneNode->runOnMainThread(
			[docClient, server, uri]() {
				if ( server->hasDocument( uri ) )
					docClient->requestSymbols();
			},
			Seconds( 1.f ), mTag );
	}
}

} // namespace ecode
