//
// Created by Supakorn on 5/13/2021.
//

#if HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef HAVE_LSP
#include "lspserv.h"

#include <LibLsp/JsonRpc/stream.h>

#include <string>
#include <cstdlib>
#include <vector>
#include <memory>
#include <queue>

#include <thread>

#include "common.h"
#include "dec.h"
#include "asyprocess.h"
#include "locate.h"

#define REGISTER_REQ_FN(typ, fn) remoteEndPoint->registerHandler(\
 [this](typ::request const& req) { return this->fn(req); });
#define REGISTER_NOTIF_FN(typ, handler) remoteEndPoint->registerHandler(\
 [this](typ::notify& notif) { this->handler(notif); });

namespace AsymptoteLsp
{
 using std::unique_ptr;
 using std::shared_ptr;
 using absyntax::block;
 using Level=lsp::Log::Level;

 class SearchPathAddition
 {
 public:
   SearchPathAddition(mem::string const& dir)
   {
     settings::searchPath.push_back(dir);
   }

   SearchPathAddition(SearchPathAddition const&) = delete;
   SearchPathAddition& operator=(SearchPathAddition const&) = delete;

   SearchPathAddition(SearchPathAddition&&) = delete;
   SearchPathAddition& operator=(SearchPathAddition&&) = delete;

   ~SearchPathAddition()
   {
     settings::searchPath.pop_back();
   }
 };

std::string wslDos2Unix(std::string const& dosPath)
 {
#ifndef _WIN32
   bool isDrivePath=false;
   char drive;
   if (dosPath.length() >= 3)
   {
     if (dosPath[0] == '/' and dosPath[2] == ':')
     {
       isDrivePath=true;
       drive=dosPath[1];
     }
   }

   if (isDrivePath)
   {
     std::stringstream sstream;
     sstream << "/mnt/" << (char) tolower(drive) << dosPath.substr(3);
     return sstream.str();
   } else
#endif
   {
     return dosPath;
   }
 }

std::string wslUnix2Dos(std::string const& unixPath)
 {
#ifndef _WIN32
   bool isMntPath=false;
   char drive;

#ifdef __GNU__
#define PATH_MAX 4096
#endif
   char actPath[PATH_MAX];
   if(!realpath(unixPath.c_str(), actPath))
     return "";
   std::string fullPath(actPath);

   if (fullPath.length() >= 7) // /mnt/
   {
     if (fullPath.find("/mnt/") == 0)
     {
       isMntPath=true;
       drive=fullPath[5];
     }
   }

   if (isMntPath)
   {
     std::stringstream sstream;
     sstream << "/" << (char) tolower(drive) << ":" << fullPath.substr(6);
     return sstream.str();
   } else
   {
     return fullPath;
   }
#else
     return unixPath;
#endif
 }

 TextDocumentHover::Either fromString(std::string const& str)
 {
   auto strobj=std::make_pair(make_optional(str), optional<lsMarkedString>());
   std::vector<decltype(strobj)> vec{strobj};
   return std::make_pair(vec, nullopt);
 }

 TextDocumentHover::Either fromMarkedStr(lsMarkedString const& markedString)
 {
   auto strobj=std::make_pair((optional<std::string>) nullopt, make_optional(markedString));
   std::vector<decltype(strobj)> vec{strobj};
   return std::make_pair(vec, nullopt);
 }

 TextDocumentHover::Either fromMarkedStr(std::vector<std::string> const& stringList, std::string const& language)
 {
   std::vector<std::pair<optional<std::string>, optional<lsMarkedString>>> vec;
   std::transform(stringList.begin(), stringList.end(), std::back_inserter(vec),
                  [&language](std::string const& str)
                  {
                    lsMarkedString lms;
                    lms.language=language;
                    lms.value=str;
                    return std::make_pair((optional<std::string>) nullopt, make_optional(lms));
                  });

   return std::make_pair(vec, nullopt);
 }

 TextDocumentHover::Either fromMarkedStr(std::string const& str, std::string const& language)
 {
   lsMarkedString lms;
   lms.language=language;
   lms.value=str;
   return fromMarkedStr(lms);
 }

 std::string getDocIdentifierRawPath(lsTextDocumentIdentifier const& textDocIdentifier)
 {
   lsDocumentUri fileUri(textDocIdentifier.uri);
   std::string rawPath=std::string(fileUri.GetRawPath());
   if(settings::getSetting<bool>("wsl")) rawPath=wslDos2Unix(rawPath);
   return static_cast<std::string>(rawPath);
 }

 void AsymptoteLspServer::generateMissingTrees(std::string const& inputFile)
 {
   using extRefMap=std::unordered_map<std::string, SymbolContext*>;
   //using extRefMapLoc = std::pair<extRefMap*, std::string>;
   std::queue<extRefMap::iterator> procList;
   std::unordered_set<std::string> processing;

   processing.emplace(inputFile);
   SymbolContext* ctx=symmapContextsPtr->at(inputFile).get();

   for (auto const& locPair : ctx->getEmptyRefs())
   {
     procList.emplace(locPair);
   }

   // standard BFS algorithm
   while (not procList.empty())
   {
     auto it=procList.front();
     procList.pop();

     std::string filename(it->first);
     processing.emplace(filename);

     auto mapIt=symmapContextsPtr->find(filename);
     if (mapIt != symmapContextsPtr->end())
     {
       it->second=mapIt->second.get();
     }
     else
     {
       block* blk=ifile(mem::string(filename.c_str())).getTree();
       auto s=symmapContextsPtr->emplace(
               filename,
               make_unique<SymbolContext>(posInFile(1, 1), filename));
       auto fit=std::get<0>(s);

       if(blk == nullptr)
       {
         // dead end. file cannot be parsed. no new paths.
         continue;
       }

       blk->createSymMap(fit->second.get()); // parse symbol from there.
       // set plain.asy to plain
       if (plainCtx != nullptr)
       {
         fit->second->extRefs.extFileRefs[plainFile]=plainCtx;
       }

       // also parse its neighbors
       for (auto const& sit : fit->second->getEmptyRefs())
       {
         if (processing.find(sit->first) == processing.end())
         {
           procList.emplace(sit);
         }
         else
         {
           // import cycles detected!
           logWarning("Import cycles detected!");
         }
       }
       it->second=fit->second.get();
     }
   }
 }

 void LspLog::log(Level level, std::string&& msg)
 {
   if ((uint32_t)Level::WARNING + settings::verbose >= (uint32_t)level || level == Level::ALL)
   {
     cerr << msg << std::endl;
   }
 }

 void LspLog::log(Level level, std::wstring&& msg)
 {
   if ((uint32_t)Level::WARNING + settings::verbose >= (uint32_t)level || level == Level::ALL)
   {
     std::wcerr << msg << std::endl;
   }
 }

 void LspLog::log(Level level, const std::string& msg)
 {
   if ((uint32_t)Level::WARNING + settings::verbose >= (uint32_t)level || level == Level::ALL)
   {
     cerr << msg << std::endl;
   }
 }

 void LspLog::log(Level level, const std::wstring& msg)
 {
   if ((uint32_t)Level::WARNING + settings::verbose >= (uint32_t)level || level == Level::ALL)
   {
     std::wcerr << msg << std::endl;
   }
 }

 AsymptoteLspServer::AsymptoteLspServer(
         shared_ptr<lsp::ProtocolJsonHandler> const& jsonHandler,
         shared_ptr<GenericEndpoint> const& endpoint, LspLog& log) :
         internalREP(make_unique<RemoteEndPoint>(jsonHandler, endpoint, log)), remoteEndPoint(internalREP.get()),
         pjh(jsonHandler), ep(endpoint), _log(log)
 {
   initializeRequestFn();
   initializeNotifyFn();
 }

 AsymptoteLspServer::AsymptoteLspServer(
         RemoteEndPoint* remoteEndPt,
         shared_ptr<lsp::ProtocolJsonHandler> const& jsonHandler,
         shared_ptr<GenericEndpoint> const& endpoint, LspLog& log) :
         internalREP(nullptr), remoteEndPoint(remoteEndPt),
         pjh(jsonHandler), ep(endpoint), _log(log)
 {
   initializeRequestFn();
   initializeNotifyFn();
 }

 void AsymptoteLspServer::initializeRequestFn()
 {
   REGISTER_REQ_FN(td_initialize, handleInitailizeRequest);
   REGISTER_REQ_FN(td_hover, handleHoverRequest);
   REGISTER_REQ_FN(td_shutdown, handleShutdownRequest);
   REGISTER_REQ_FN(td_definition, handleDefnRequest);
   REGISTER_REQ_FN(td_documentColor, handleDocColorRequest);
   REGISTER_REQ_FN(td_colorPresentation, handleColorPresRequest);
 }

 void AsymptoteLspServer::initializeNotifyFn()
 {
   REGISTER_NOTIF_FN(Notify_InitializedNotification, onInitialized);
   REGISTER_NOTIF_FN(Notify_TextDocumentDidChange, onChange);
   REGISTER_NOTIF_FN(Notify_TextDocumentDidOpen, onOpen);
   REGISTER_NOTIF_FN(Notify_TextDocumentDidSave, onSave);
   REGISTER_NOTIF_FN(Notify_TextDocumentDidClose, onClose);
   REGISTER_NOTIF_FN(Notify_Exit, onExit);
 }

//#pragma region notifications

 void AsymptoteLspServer::onInitialized(Notify_InitializedNotification::notify& notify)
 {
   logInfo("server initialized notification");
 }

 void AsymptoteLspServer::onExit(Notify_Exit::notify& notify)
 {
   logInfo("server exit notification");
   serverClosed.notify(make_unique<bool>(true));
 }

 void AsymptoteLspServer::onChange(Notify_TextDocumentDidChange::notify& notify)
 {
   logInfo("text change notification");

   auto& fileChange = notify.params.contentChanges;
   if (not fileChange.empty())
   {
     bool updatable = true;
     block* codeBlk;
     try
     {
       codeBlk=istring(mem::string(fileChange[0].text.c_str())).getTree();
     }
     catch (handled_error const&)
     {
       updatable = false;
     }

     if (updatable)
     {
       std::string rawPath=getDocIdentifierRawPath(notify.params.textDocument.AsTextDocumentIdentifier());
       std::istringstream iss(fileChange[0].text);
       updateFileContentsTable(rawPath, iss);
       reloadFileRaw(codeBlk, rawPath);
     }
   }

   logInfo("changed text data");
 }

 void AsymptoteLspServer::onOpen(Notify_TextDocumentDidOpen::notify& notify)
 {
   logInfo("onOpen notification");
   lsDocumentUri fileUri(notify.params.textDocument.uri);
   reloadFile(fileUri.GetRawPath());
 }

 void AsymptoteLspServer::onSave(Notify_TextDocumentDidSave::notify& notify)
 {
   logInfo("onSave notification");
//    lsDocumentUri fileUri(notify.params.textDocument.uri);
//    reloadFile(fileUri.GetRawPath());
 }

 void AsymptoteLspServer::onClose(Notify_TextDocumentDidClose::notify& notify)
 {
   logInfo("onClose notification");
 }

//#pragma endregion

//#pragma region requests
 td_initialize::response AsymptoteLspServer::handleInitailizeRequest(td_initialize::request const& req)
 {
   clearVariables();

   symmapContextsPtr=make_unique<SymContextFilemap>();
   fileContentsPtr=make_unique<
           std::remove_reference<decltype(*fileContentsPtr)>::type>();
   plainFile=settings::locateFile("plain", true).c_str();
   plainCtx=reloadFileRaw(plainFile, false);
   generateMissingTrees(plainFile);

   td_initialize::response rsp;
   rsp.id=req.id;
   rsp.result.capabilities.hoverProvider=true;

   lsTextDocumentSyncOptions tdso;
   tdso.openClose=true;
   tdso.change=lsTextDocumentSyncKind::Full;
   lsSaveOptions so;
   so.includeText=true;
   tdso.save=so;
   rsp.result.capabilities.textDocumentSync=opt_right<lsTextDocumentSyncKind>(tdso);
   rsp.result.capabilities.definitionProvider=std::make_pair(true, nullopt);
   rsp.result.capabilities.colorProvider=std::make_pair(true, nullopt);
   return rsp;
 }

 SymbolContext* AsymptoteLspServer::fromRawPath(lsTextDocumentIdentifier const& identifier)
 {
   std::string rawPath=getDocIdentifierRawPath(identifier);
   auto fileSymIt=symmapContextsPtr->find(rawPath);

   return fileSymIt != symmapContextsPtr->end() ? fileSymIt->second.get() : nullptr;
 }

 td_hover::response AsymptoteLspServer::handleHoverRequest(td_hover::request const& req)
 {
   td_hover::response rsp;
   SymbolContext* fileSymPtr=fromRawPath(req.params.textDocument);
   std::vector<std::pair<optional<std::string>, optional<lsMarkedString>>> nullVec;

   if (!fileSymPtr)
   {
     rsp.result.contents.first=nullVec;
     return rsp;
   }

     auto s=fileSymPtr->searchSymbol(fromLsPosition(req.params.position));
     auto st=std::get<0>(s);
     auto ctx=std::get<1>(s);

   if (not st.has_value())
   {
     rsp.result.contents.first=nullVec;
     return rsp;
   }

   auto v=st.value();
   auto symText=std::get<0>(v);
   auto startPos=std::get<1>(v);
   auto endPos=std::get<2>(v);

   rsp.result.range=make_optional(lsRange(toLsPosition(startPos), toLsPosition(endPos)));

   auto typ=ctx->searchLitSignature(symText);
   std::list<std::string> endResultList;
   if (typ.has_value())
   {
     endResultList.push_back(typ.value());
   }
   endResultList.splice(endResultList.end(), ctx->searchLitFuncSignature(symText));

   std::vector<std::string> endResult;
   std::copy(endResultList.begin(), endResultList.end(), std::back_inserter(endResult));

   rsp.result.contents=endResult.empty() ?
                       fromMarkedStr("<decl-unknown> " + symText.name + ";") :
                       fromMarkedStr(endResult);
   return rsp;
 }

 td_documentColor::response AsymptoteLspServer::handleDocColorRequest(td_documentColor::request const& req)
 {
   td_documentColor::response rsp;

   if (SymbolContext* fileSymPtr=fromRawPath(req.params.textDocument))
   {
     logInfo("Got Document color request.");
     auto& colorsInfo = fileSymPtr->colorInformation;
     for (auto const& colorPtr : colorsInfo)
     {
       ColorInformation cif;

       cif.color = static_cast<TextDocument::Color>(*colorPtr);
       cif.range.start=toLsPosition(colorPtr->rangeBegin);

       auto s=colorPtr->lastArgPosition;
       auto& line=std::get<0>(s);
       auto& colm=std::get<1>(s);
       size_t offset = 0;
       size_t lineOffset = 0;

       auto& strLines = fileContentsPtr->at(getDocIdentifierRawPath(req.params.textDocument));
       char ch=strLines[line + lineOffset - 1][colm - 1 + offset];

       while (
               ch != ')' and ch != ';'
               and line + lineOffset <= strLines.size()
               )
       {
         ++offset;
         if (offset > strLines[line+lineOffset-1].size())
         {
           ++lineOffset;
           offset = 0;
         }
         if (line+lineOffset <= strLines.size())
         {
           ch=strLines[line + lineOffset - 1][colm - 1 + offset];
         }
       }

       if (ch != ')' or line + lineOffset > strLines.size())
       {
         continue;
       }

       cif.range.end=toLsPosition(make_pair(line+lineOffset, colm+offset+1));
       rsp.result.emplace_back(cif);
     }
   }

   return rsp;
 }

 td_colorPresentation::response AsymptoteLspServer::handleColorPresRequest(td_colorPresentation::request const& req)
 {
   td_colorPresentation::response rsp;

   if (SymbolContext* fileSymPtr=fromRawPath(req.params.textDocument))
   {
     logInfo("Got color presentation request.");
     ColorPresentation clp;

     for (auto& colPtr : fileSymPtr->colorInformation)
     {
       auto& incomingColor = req.params.color;
       std::ostringstream ssargs;
       std::ostringstream labelargs;

       bool opaque=std::fabs(incomingColor.alpha - 1) < std::numeric_limits<double>::epsilon();
       std::string fnName = opaque ? "rgb" : "rgba";

       labelargs << std::setprecision(3) << incomingColor.red << "," << incomingColor.green << "," <<
                                         incomingColor.blue;
       ssargs << incomingColor.red << "," << incomingColor.green << "," <<
          incomingColor.blue;
       if (!opaque)
       {
         ssargs << "," << incomingColor.alpha;
         labelargs << "," << incomingColor.alpha;
       }
       std::ostringstream ss;
       ss << fnName << "(" << ssargs.str() << ")";
       clp.textEdit.newText = ss.str();

       std::ostringstream lss;
       lss << fnName << "(" << labelargs.str() << ")";
       clp.label = lss.str();

       if (colPtr->rangeBegin == fromLsPosition(req.params.range.start))
       {
         clp.textEdit.range = req.params.range;
         rsp.result.emplace_back(std::move(clp));
         break;
       }
     }
   }
   return rsp;
 }

 td_shutdown::response AsymptoteLspServer::handleShutdownRequest(td_shutdown::request const& req)
 {
   logInfo("got shut down request");
   td_shutdown::response rsp;
   JsonNull nullrsp;
   lsp::Any anyrsp;
   anyrsp.Set(nullrsp);
   rsp.result = make_optional<lsp::Any>(std::move(anyrsp));

   serverClosed.notify(make_unique<bool>(true));
   return rsp;
 }

 td_definition::response AsymptoteLspServer::handleDefnRequest(td_definition::request const& req)
 {
   td_definition::response rsp;
   std::list<posRangeInFile> posRanges;
   if (SymbolContext* fileSymPtr=fromRawPath(req.params.textDocument))
   {
     posInFile pos = fromLsPosition(req.params.position);
     auto s=fileSymPtr->searchSymbol(pos);
     auto st=std::get<0>(s);
     auto ctx=std::get<1>(s);
     if (st.has_value())
     {
       optional<posRangeInFile> posRange=ctx->searchLitPosition(std::get<0>(st.value()), pos);
       if (posRange.has_value())
       {
         posRanges.push_back(posRange.value());
       }

       posRanges.splice(posRanges.begin(), ctx->searchLitFuncPositions(std::get<0>(st.value()), pos));
     }
   }
   rsp.result.first=make_optional(std::vector<lsLocation>());
   std::transform(
           posRanges.begin(), posRanges.end(), std::back_inserter(rsp.result.first.value()),
           [](posRangeInFile const& posRange)
           {
             auto& fil=std::get<0>(posRange);
             auto& posBegin=std::get<1>(posRange);
             auto& posEnd=std::get<2>(posRange);
             lsRange rng(toLsPosition(posBegin), toLsPosition(posEnd));

             std::string filReturn(
               settings::getSetting<bool>("wsl") ?
               static_cast<std::string>(wslUnix2Dos(fil)) : fil);

             lsDocumentUri uri(filReturn);
             return lsLocation(uri, rng);
           });
   return rsp;
 }

//#pragma endregion
 void AsymptoteLspServer::reloadFile(std::string const& filename)
 {
   std::string rawPath(filename);
   if(settings::getSetting<bool>("wsl")) rawPath=wslDos2Unix(filename);
   reloadFileRaw(static_cast<std::string>(rawPath));
 }

 void AsymptoteLspServer::updateFileContentsTable(std::string const& filename)
 {
   std::ifstream fil(filename, std::ifstream::in);
   return updateFileContentsTable(filename, fil);
 }

 void AsymptoteLspServer::updateFileContentsTable(std::string const& filename, std::istream& in)
 {
   auto& fileContents = *fileContentsPtr;
   fileContents[filename].clear();

   std::string line;
   while (std::getline(in, line))
   {
     fileContents[filename].emplace_back(line);
   }
 }

 SymbolContext* AsymptoteLspServer::reloadFileRaw(block* blk, std::string const& rawPath, bool const& fillTree)
 {
   if (blk != nullptr)
   {
     SearchPathAddition sp(stripFile(string(rawPath.c_str())));
     auto it=symmapContextsPtr->find(rawPath);
     if (it != symmapContextsPtr->end())
     {
       *(it->second)=SymbolContext(posInFile(1, 1), rawPath);
     }
     else
     {
       auto s = symmapContextsPtr->emplace(
               rawPath, make_unique<SymbolContext>(posInFile(1, 1), rawPath));
       it=std::get<0>(s);
     }

     SymbolContext* newPtr=it->second.get();

     cerr << rawPath << endl;

     blk->createSymMap(newPtr);

     if (plainCtx != nullptr)
     {
       it->second->extRefs.extFileRefs[plainFile]=plainCtx;
     }
     else if (rawPath == plainFile)
     {
       it->second->extRefs.extFileRefs[plainFile]=newPtr;
     }

     if (fillTree)
     {
       generateMissingTrees(rawPath);
     }
     return it->second.get();
   }
   else
   {
     return nullptr;
   }
 }

 SymbolContext* AsymptoteLspServer::reloadFileRaw(std::string const& rawPath, bool const& fillTree)
 {
   updateFileContentsTable(rawPath);
   block* blk=ifile(mem::string(rawPath.c_str())).getTree();
   return reloadFileRaw(blk, rawPath, fillTree);
 }

 void AsymptoteLspServer::start()
 {
   return startIO(cin, cout);
 }

 AsymptoteLspServer::~AsymptoteLspServer()
 {
 }

 void AsymptoteLspServer::startIO(std::istream& in, std::ostream& out)
 {
   auto inPtr=make_shared<AsymptoteLsp::istream>(in);
   auto outPtr=make_shared<AsymptoteLsp::ostream>(out);
   remoteEndPoint->startProcessingMessages(inPtr,outPtr);
   serverClosed.wait();
 }

 void AsymptoteLspServer::log(lsp::Log::Level const& level, std::string const& message)
 {
   _log.log(level, message);
 }

 void AsymptoteLspServer::logError(std::string const& message)
 {
   log(lsp::Log::Level::SEVERE, message);
 }

 void AsymptoteLspServer::logWarning(std::string const& message)
 {
   log(lsp::Log::Level::WARNING, message);
 }

 void AsymptoteLspServer::logInfo(std::string const& message)
 {
   log(lsp::Log::Level::INFO, message);
 }

 void AsymptoteLspServer::clearVariables()
 {
 }

 // TCP Asymptote Server

 TCPAsymptoteLSPServer::TCPAsymptoteLSPServer(
         std::string const& addr, std::string const& port, shared_ptr<lsp::ProtocolJsonHandler> const& jsonHandler,
         shared_ptr<GenericEndpoint> const& endpoint, LspLog& log) :
         lsp::TcpServer(addr, port, jsonHandler, endpoint, log),
         AsymptoteLspServer(&point, jsonHandler, endpoint, log)
 {
 }

 TCPAsymptoteLSPServer::~TCPAsymptoteLSPServer()
 {
   logInfo("Destroying server...");
   this->stop();
 }


 void TCPAsymptoteLSPServer::start()
 {
   std::thread([this]() {this->run();}).detach();
   serverClosed.wait();
   logInfo("Got server closed notification.");
 }
}

#endif