/*
 * TextEditingTargetCompilePdfHelper.java
 *
 * Copyright (C) 2022 by Posit Software, PBC
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */
package org.rstudio.studio.client.workbench.views.source.editors.text;

import java.util.ArrayList;
import java.util.HashMap;

import org.rstudio.core.client.Debug;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.files.FileSystemItem;
import org.rstudio.core.client.regex.Match;
import org.rstudio.core.client.regex.Pattern;
import org.rstudio.core.client.tex.TexMagicComment;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.application.Desktop;
import org.rstudio.studio.client.common.filetypes.TextFileType;
import org.rstudio.studio.client.common.latex.LatexProgramRegistry;
import org.rstudio.studio.client.common.rnw.RnwWeave;
import org.rstudio.studio.client.common.rnw.RnwWeaveDirective;
import org.rstudio.studio.client.common.rnw.RnwWeaveRegistry;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.workbench.model.Session;
import org.rstudio.studio.client.workbench.model.SessionInfo;
import org.rstudio.studio.client.workbench.model.TexCapabilities;
import org.rstudio.studio.client.workbench.prefs.model.UserPrefs;
import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorPosition;
import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorSelection;
import org.rstudio.studio.client.workbench.views.source.model.RnwChunkOptions;
import org.rstudio.studio.client.workbench.views.source.model.RnwCompletionContext;
import org.rstudio.studio.client.workbench.views.source.model.TexServerOperations;

import com.google.gwt.core.client.GWT;
import com.google.inject.Inject;

public class TextEditingTargetCompilePdfHelper
      implements RnwCompletionContext
{ 
   public TextEditingTargetCompilePdfHelper(DocDisplay docDisplay)
   {
      docDisplay_ = docDisplay;
      RStudioGinjector.INSTANCE.injectMembers(this);
   }
   
   @Inject
   public void initialize(UserPrefs prefs,
                          Session session,
                          TexServerOperations server,
                          RnwWeaveRegistry rnwWeaveRegistry,
                          LatexProgramRegistry latexProgramRegistry)
   {
      prefs_ = prefs;
      session_ = session;
      server_ = server;
      rnwWeaveRegistry_ = rnwWeaveRegistry;
      latexProgramRegistry_ = latexProgramRegistry;
   }
   
   // get the chunk options which apply to the current document. when 
   // the chunk options are ready the callback command is execute (note
   // that if there are no chunk options available then execute will 
   // never be called). this method caches the results from the server
   // so that chunk options are only retrieved once per session -- we
   // do this not only to save the round-trip but also because knitr takes
   // over 500ms to load and it may need to be loaded to serve the
   // request for chunk options
   public void getChunkOptions(
                   final ServerRequestCallback<RnwChunkOptions> requestCallback)
   {
      // determine the current rnw weave type
      final RnwWeave rnwWeave = getActiveRnwWeave();
      if (rnwWeave == null)
         return;
      
      // look it up in the cache
      if (chunkOptionsCache_.containsKey(rnwWeave.getName()))
      {
         requestCallback.onResponseReceived(
                                    chunkOptionsCache_.get(rnwWeave.getName()));
      }
      else
      {
         server_.getChunkOptions(
            rnwWeave.getName(), 
            new ServerRequestCallback<RnwChunkOptions>() {
               @Override
               public void onResponseReceived(RnwChunkOptions options)
               {
                  chunkOptionsCache_.put(rnwWeave.getName(), options);
                  requestCallback.onResponseReceived(options);
               }

               @Override
               public void onError(ServerError error)
               {
                  requestCallback.onError(error);
               }
            });
      }
   }
   
   public void ensureRnwConcordance()
   {
      RnwWeave rnwWeave = getActiveRnwWeave();
      if ( (rnwWeave != null) && rnwWeave.getInjectConcordance())
      {
         if (!hasConcordanceDirective(docDisplay_.getCode()))
         {    
            InputEditorSelection doc = docDisplay_.search(
                                          "\\\\begin{document}",
                                          false,   // backwards
                                          true,    // wrap
                                          false,   // case sensitive
                                          false,   // whole word
                                          null,    // from selection
                                          null,    // range (search all)
                                          true);   // regexp mode
            if (doc != null)
            {  
               InputEditorPosition pos = doc.getEnd().moveToNextLine();
               docDisplay_.insertCode(pos, "\\SweaveOpts{concordance=TRUE}\n");
            }
         }
      }
   }
   
   
   public void checkCompilers(WarningBarDisplay display, TextFileType fileType)
   {
      // for all tex files we need to parse magic comments and validate
      // any explicit latex program directive
      ArrayList<TexMagicComment> magicComments = null;
      if (fileType.canCompilePDF())
      {
         magicComments = TexMagicComment.parseComments(docDisplay_.getCode());
         String latexProgramDirective = 
                           detectLatexProgramDirective(magicComments);
           
         if (latexProgramDirective != null)
         {
            if (latexProgramRegistry_.findTypeIgnoreCase(latexProgramDirective)
                  == null)
            {
               // show warning and bail
               String message = constants_.checkCompilersUnknownLatexType(
                     latexProgramDirective,
                     latexProgramRegistry_.getPrintableTypeNames());
               showWarning(display, message);
               return;
            }
         }
      }
   
      // for Rnw we first determine the RnwWeave type
      RnwWeave rnwWeave = null;
      RnwWeaveDirective rnwWeaveDirective = null;
      if (fileType.isRnw())
      {
         rnwWeaveDirective = detectRnwWeaveDirective(magicComments);
         if (rnwWeaveDirective != null) 
         {
            rnwWeave = rnwWeaveDirective.getRnwWeave();
            if (rnwWeave == null)
            {
               // show warning and bail 
               String message = constants_.checkCompilersRnWWeaveTypeError(
                     rnwWeaveDirective.getName(),
                     rnwWeaveRegistry_.getPrintableTypeNames());
               showWarning(display, message);
               return;
            }    
         }
         else
         {
            rnwWeave = rnwWeaveRegistry_.findTypeIgnoreCase(
                                    prefs_.defaultSweaveEngine().getValue());
         }     
      }
            
       
      final SessionInfo sessionInfo = session_.getSessionInfo();
      TexCapabilities texCap = sessionInfo.getTexCapabilities();

      final boolean checkForTeX = fileType.canCompilePDF() && 
                                  !texCap.isTexInstalled();
      
      final boolean checkForRnwWeave = (rnwWeave != null) && 
                                       !texCap.isRnwWeaveAvailable(rnwWeave);
                                 
      if (checkForTeX || checkForRnwWeave)
      {
         // alias variables to finals
         final boolean hasRnwWeaveDirective = rnwWeaveDirective != null;
         final RnwWeave fRnwWeave = rnwWeave;
         
         server_.getTexCapabilities(new ServerRequestCallback<TexCapabilities>()
         {
            @Override
            public void onResponseReceived(TexCapabilities response)
            {
               if (checkForTeX && !response.isTexInstalled())
               {
                  String warning;
                  if (Desktop.isDesktop())
                     warning = constants_.checkCompilersDesktopWarning();
                  else
                     warning = constants_.checkCompilersServerWarning();
                  showWarning(display, warning);
               }
               else if (checkForRnwWeave && 
                        !response.isRnwWeaveAvailable(fRnwWeave))
               {
                  String forContext = "";
                  if (hasRnwWeaveDirective)
                     forContext = constants_.thisFile();
                  else if (sessionInfo.getActiveProjectFile() != null)
                     forContext = constants_.rnwFilesForProject();
                  else
                     forContext = constants_.rnwFiles();
                  
                  String message = constants_.checkCompilersRnWPackageNotInstalled(
                        fRnwWeave.getName(),
                        forContext,
                        fRnwWeave.getPackageName());
                  showWarning(display, message);
               }
               else
               {
                  hideWarning(display);
               }
            }

            @Override
            public void onError(ServerError error)
            {
               Debug.logError(error);
            }
         });
      }
      else
      {
         hideWarning(display);
      }
   }
   
   public FileSystemItem getTargetFile(FileSystemItem editorFile)
   {
      ArrayList<TexMagicComment> magicComments = 
                  TexMagicComment.parseComments(docDisplay_.getCode());
      String root = StringUtil.notNull(detectRootDirective(magicComments));
      if (root.length() > 0)
      {
         return FileSystemItem.createFile(
                        editorFile.getParentPath().completePath(root));
      }
      else 
      {
         String rootPref = prefs_.rootDocument().getValue();
         if (rootPref.length() > 0)
         {
            FileSystemItem projDir = 
                           session_.getSessionInfo().getActiveProjectDir();
            if (projDir != null)
               return FileSystemItem.createFile(projDir.completePath(rootPref));
            else
               return editorFile;
         }
         else
         {
            return editorFile;
         }
      }
   }
   
   
   // get the currently active rnw weave method -- note this can return
   // null in the case that there is an embedded directive which is invalid
   public RnwWeave getActiveRnwWeave()
   {
      if (docDisplay_.getFileType().canKnitToHTML())
         return rnwWeaveRegistry_.findTypeIgnoreCase("knitr");

      RnwWeave weave = null;
      ArrayList<TexMagicComment> magicComments = 
            TexMagicComment.parseComments(docDisplay_.getCode());
      RnwWeaveDirective rnwWeaveDirective = detectRnwWeaveDirective(
                                                             magicComments);
      if (rnwWeaveDirective != null)
      {
         weave = rnwWeaveDirective.getRnwWeave();
      }
      else
      {
         weave = rnwWeaveRegistry_.findTypeIgnoreCase(
                                    prefs_.defaultSweaveEngine().getValue());
      }
      
      // look for the 'driver' directive and don't inject 
      // concocordance if there is a custom driver
      String driver = detectRnwDriverDirective(magicComments);
      if (driver != null)
         return RnwWeave.withNoConcordance(weave);
      else
         return weave;
   }

   @Override
   public int getRnwOptionsStart(String line, int cursorPos)
   {
      return getRnwOptionsStart(line, 
            cursorPos, 
            docDisplay_.getFileType().getRnwStartPatternBegin(),
            docDisplay_.getFileType().getRnwStartPatternEnd());
   }
   
   public static int getRnwOptionsStart(String line, int cursorPos, 
         Pattern startPattern, Pattern endPattern)
   {
      if (startPattern == null)
         return -1;
      
      String linePart = StringUtil.substring(line, 0, cursorPos);
      Match match = startPattern.match(linePart, 0);
      if (match == null)
         return -1;

      // See if the cursor is already past the end of the chunk header,
      // for example <<foo>>=[CURSOR].
      if (endPattern != null && endPattern.match(linePart, 0) != null)
         return -1;

      return match.getValue().length();
   }

   // get the currently active rnw weave name -- arranges to always return
   // a valid string by returning the pref if the directive is invalid
   public String getActiveRnwWeaveName()
   {
      if (docDisplay_.getFileType().canKnitToHTML() || 
          docDisplay_.getFileType().isRpres())
         return "knitr";

      RnwWeaveDirective rnwWeaveDirective = detectRnwWeaveDirective(
                         TexMagicComment.parseComments(docDisplay_.getCode()));
      if (rnwWeaveDirective != null)
      {
         RnwWeave rnwWeave = rnwWeaveDirective.getRnwWeave();
         if (rnwWeave != null)
            return rnwWeave.getName();
      }
        
      return rnwWeaveRegistry_.findTypeIgnoreCase(
                           prefs_.defaultSweaveEngine().getValue()).getName();
   }
   
   private boolean hasConcordanceDirective(String code)
   {
      Iterable<String> lines = StringUtil.getLineIterator(code);
      
      for (String line : lines)
      {
         line = line.trim();
         if (line.length() == 0)
         {
            continue;
         }
         else if (line.startsWith("\\SweaveOpts"))
         {
            Match match = concordancePattern_.match(line, 0);
            if (match != null)
               return true;
         }
      }
    
      return false;  
   }
   
   private RnwWeaveDirective detectRnwWeaveDirective(
                                    ArrayList<TexMagicComment> magicComments)
   {
      for (TexMagicComment comment : magicComments)
      {
         RnwWeaveDirective rnwWeaveDirective = 
                           RnwWeaveDirective.fromTexMagicComment(comment);
         if (rnwWeaveDirective != null)
            return rnwWeaveDirective;
      }
      
      return null;
   }
   
   private String detectRnwDriverDirective(
                                    ArrayList<TexMagicComment> magicComments)
   {
      for (TexMagicComment comment : magicComments)
      {
         if (comment.getScope().equalsIgnoreCase("rnw") &&
             comment.getVariable().equalsIgnoreCase("driver"))
         {
            return comment.getValue();
         }
      }
      
      return null;
   }
   
   private String detectLatexProgramDirective(
                     ArrayList<TexMagicComment> magicComments)
   {
      for (TexMagicComment comment : magicComments)
      {
         if (comment.getScope().equalsIgnoreCase("tex") &&
             (comment.getVariable().equalsIgnoreCase("program") ||
              comment.getVariable().equalsIgnoreCase("ts-program")))
         { 
            return comment.getValue();
         }
      }
      
      return null;
   }
   
   private String detectRootDirective(ArrayList<TexMagicComment> magicComments)
   {
      for (TexMagicComment comment : magicComments)
      {
         String scope = comment.getScope().toLowerCase();
         if ((scope.equals("rnw") || scope.equals("tex")) &&
             comment.getVariable().equalsIgnoreCase("root"))
         {
            return comment.getValue();
         }
      }
      
      return null;
   }
   
   private void showWarning(WarningBarDisplay display, String message)
   {
      isWarningShowing_ = true;
      display.showWarningBar(message);
   }
   
   private void hideWarning(WarningBarDisplay display)
   {
      if (isWarningShowing_)
      {
         isWarningShowing_ = false;
         display.hideWarningBar();
      }
   }
   
   private final DocDisplay docDisplay_;
   private boolean isWarningShowing_ = false;
   
   private UserPrefs prefs_;
   private Session session_;
   private TexServerOperations server_;
   private RnwWeaveRegistry rnwWeaveRegistry_;
   private LatexProgramRegistry latexProgramRegistry_;
   
   private static final Pattern concordancePattern_ = Pattern.create(
                     "\\\\[\\s]*SweaveOpts[\\s]*{.*concordance[\\s]*=.*}");
   
   private static HashMap<String, RnwChunkOptions> chunkOptionsCache_ = new HashMap<>();
   private static final EditorsTextConstants constants_ = GWT.create(EditorsTextConstants.class);
}
