/*
   Copyright (C) 2013-2020 Nicola L.C. Talbot
   www.dickimaw-books.com

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/
package com.dickimawbooks.makeglossariesgui;

import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.*;

public class Glossary
{
  public Glossary(MakeGlossariesInvoker invoker, String label, String transExt,
     String glsExt, String gloExt)
  {
     this.invoker = invoker;
     this.label = label;
     this.transExt = transExt;
     this.glsExt = glsExt;
     this.gloExt = gloExt;

     entryTable = new Hashtable<String,GlossaryEntry>();
  }

  public void xindy(File dir, String baseName,
     boolean isWordOrder, String istName)
     throws IOException,InterruptedException,GlossaryException
  {
     File xindyApp = new File(invoker.getXindyApp());

     if (!xindyApp.exists())
     {
        throw new GlossaryException(invoker.getLabelWithValues(
           "error.no_indexer_app", "xindy", xindyApp.getAbsolutePath()),
           invoker.getLabelWithValues("diagnostics.no_indexer", "xindy"));
     }

     String transFileName = baseName+"."+transExt;

     if (language == null || language.equals(""))
     {
        language = invoker.getDefaultLanguage();
     }

     if (codepage == null || codepage.equals(""))
     {
        codepage = invoker.getDefaultCodePage();
     }

     if (!invoker.getProperties().isOverride())
     {
        XindyModule mod = XindyModule.getModule(language);

        if (mod != null)
        {
           String variant = mod.getDefaultVariant();

           if (variant != null && !mod.hasCodePage(codepage))
           {
              addDiagnosticMessage(invoker.getLabelWithValues(
                "diagnostics.variant", language, codepage, variant));
              codepage = variant+"-"+codepage;
           }
        }
     }

     String style = istName;

     int idx = istName.lastIndexOf(".");

     if (idx != -1)
     {
        style = style.substring(0, idx);
     }

     File gloFile = new File(dir, baseName+"."+gloExt);

     String[] cmdArray;

     if (isWordOrder)
     {
        cmdArray = new String[]
        {
           xindyApp.getAbsolutePath(),
           "-L", language,
           "-C", codepage,
           "-I", "xindy",
           "-M", style,
           "-t", transFileName,
           "-o", baseName+"."+glsExt,
           gloFile.getName()
        };
     }
     else
     {
        cmdArray = new String[]
        {
           xindyApp.getAbsolutePath(),
           "-L", language,
           "-C", codepage,
           "-I", "xindy",
           "-M", style,
           "-M", "ord/letorder",
           "-t", transFileName,
           "-o", baseName+"."+glsExt,
           gloFile.getName()
        };
     }

     int exitCode = 0;
     BufferedReader in=null;

     Charset charset = null;

     try
     {
        charset = invoker.getCharset(codepage);
        invoker.setEncoding(charset);
     }
     catch (Exception e)
     {
        invoker.getMessageSystem().error(e);
     }

     if (charset == null)
     {
        addDiagnosticMessage(invoker.getLabelWithValues(
         "diagnostics.unknown.encoding", codepage));
        charset = invoker.getEncoding();
     }

     File transFile = new File(dir, transFileName);

     invoker.getMessageSystem().aboutToExec(cmdArray, dir);

     if (invoker.isDryRunMode())
     {
        if (transFile.exists())
        {
           in = new BufferedReader(new InputStreamReader(
             new FileInputStream(transFile), charset));
        }
     }
     else
     {
        Process p = Runtime.getRuntime().exec(cmdArray, null, dir);

        exitCode = p.waitFor();

        in = new BufferedReader(new InputStreamReader(p.getErrorStream(),
               charset));
     }

     String line;

     StringBuilder processErrors = null;

     String unknownMod = null;
     boolean emptySortFound = false;

     while (in != null && (line = in.readLine()) != null)
     {
        if (processErrors == null)
        {
           processErrors = new StringBuilder();
        }

        processErrors.append(String.format("%n%s", line));

        Matcher matcher = xindyModulePattern.matcher(line);

        if (matcher.matches())
        {
           unknownMod = invoker.getLabelWithValues(
             "diagnostics.unknown_language_or_codepage",
              matcher.group(1), matcher.group(2));
        }
        else
        {
           matcher = emptySortPattern.matcher(line);

           if (matcher.matches())
           {
              emptySortFound = true;
           }
           else
           {
              matcher = collapsedSortPattern.matcher(line);

              if (matcher.matches())
              {
                 emptySortFound = true;
              }
           }
        }
     }

     if (in != null)
     {
        in.close();
     }

     String unknownVar = null;

     if (transFile.exists())
     {
        in = new BufferedReader(new InputStreamReader(
               new FileInputStream(transFile), charset));

        while ((line = in.readLine()) != null)
        {
           Matcher matcher = xindyIstPattern.matcher(line);

           if (matcher.matches())
           {
              unknownVar = matcher.group();
           }
        }

        in.close();
     }

     if (emptySortFound)
     {
        addErrorMessage(invoker.getLabel("error.empty_sort"));
     }

     boolean deprecated = false;
     boolean depCheck = false;

     if (gloFile.exists())
     {
        in = new BufferedReader(new InputStreamReader(
               new FileInputStream(gloFile), charset));

        while ((line = in.readLine()) != null)
        {
           Matcher matcher;

           boolean retry = false;

           if (depCheck)
           {
              matcher = (deprecated ? makeindexOldEntryPattern.matcher(line):
                         xindyEntryPattern.matcher(line));
           }
           else
           {
              matcher = xindyEntryPattern.matcher(line);

              depCheck = true;

              if (!matcher.matches())
              {
                 matcher = xindyOldEntryPattern.matcher(line);
                 retry = true;
              }
           }

           if (matcher.matches())
           {
              if (retry)
              {
                 deprecated = true;
              }

              String sort = matcher.group(1);
              String key = matcher.group(2);

              GlossaryEntry entry = entryTable.get(key);

              if (entry == null)
              {
                 sort = sort.replaceAll("\\\\\\\\", "\\\\");
                 entry = new GlossaryEntry(key, sort);
                 entryTable.put(key, entry);

                 if (emptySortFound)
                 {
                    matcher = xindyEmptySortPattern.matcher(sort);

                    if (matcher.matches())
                    {
                       addDiagnosticMessage(invoker.getLabelWithValues(
                         "diagnostics.empty_sort",
                          sort, key));
                       entry.setHasProblem(true);
                    }
                 }
              }

              entry.increment();
           }
        }

        in.close();
     }

     if (deprecated)
     {
        addDiagnosticMessage(invoker.getLabel("diagnostics.deprecated"));
     }

     if (exitCode > 0)
     {
        addErrorMessage(invoker.getLabelWithValues("error.app_failed",
           "Xindy", ""+exitCode));

        if (unknownVar != null)
        {
           addDiagnosticMessage(invoker.getLabelWithValues
              ("diagnostics.bad_attributes", istName, "xindy"));
        }
        else if (unknownMod != null)
        {
           addDiagnosticMessage(unknownMod);
        }
        else if (processErrors != null)
        {
           addDiagnosticMessage(invoker.getLabelWithValues(
              "diagnostics.app_err",
              "Xindy", processErrors.toString()));
        }
        else
        {
           addDiagnosticMessage(invoker.getLabel("diagnostics.app_err_null"));
        }
     }
     else if (entryTable.size() == 0)
     {
        addDiagnosticMessage(invoker.getLabelWithValues(
           "diagnostics.no_entries", label));
     }
  }

  public void makeindex(File dir, String baseName,
      boolean isWordOrder, String istName, Vector<String> extra)
     throws IOException,InterruptedException,GlossaryException
  {
     File makeindexApp = new File(invoker.getMakeIndexApp());

     if (!makeindexApp.exists())
     {
        throw new GlossaryException(invoker.getLabelWithValues(
           "error.no_indexer_app",
           "makeindex", makeindexApp.getAbsolutePath()),
           invoker.getLabelWithValues("diagnostics.no_indexer", "makeindex"));
     }

     String transFileName = baseName+"."+transExt;

     File gloFile = new File(dir, baseName+"."+gloExt);

     String[] cmdArray;

     int n = (extra == null ? 0 : extra.size());

     if (isWordOrder)
     {
        cmdArray = new String[8+n];
        int idx = 0;
        cmdArray[idx++] = makeindexApp.getAbsolutePath();
        cmdArray[idx++] = "-s";
        cmdArray[idx++] = istName;
        cmdArray[idx++] = "-t";
        cmdArray[idx++] = transFileName;
        cmdArray[idx++] = "-o";
        cmdArray[idx++] = baseName+"."+glsExt;

        for (int i = 0; i < n; i++)
        {
           cmdArray[idx++] = extra.get(i);
        }

        cmdArray[idx++] = gloFile.getName();
     }
     else
     {
        cmdArray = new String[9+n];
        int idx = 0;
        cmdArray[idx++] = makeindexApp.getAbsolutePath();
        cmdArray[idx++] = "-l";
        cmdArray[idx++] = "-s";
        cmdArray[idx++] = istName;
        cmdArray[idx++] = "-t";
        cmdArray[idx++] = transFileName;
        cmdArray[idx++] = "-o";
        cmdArray[idx++] = baseName+"."+glsExt;

        for (int i = 0; i < n; i++)
        {
           cmdArray[idx++] = extra.get(i);
        }

        cmdArray[idx++] = gloFile.getName();
     }

     int exitCode = 0;
     BufferedReader in = null;

     invoker.getMessageSystem().aboutToExec(cmdArray, dir);

     // makeindex is limited to the range 1 ... 255
     Charset charset = StandardCharsets.ISO_8859_1;
     invoker.setEncoding(charset);

     if (invoker.isDryRunMode())
     {
        File transFile = new File(dir, transFileName);

        if (transFile.exists())
        {
           in = new BufferedReader(new InputStreamReader(
                  new FileInputStream(transFile), charset));
        }
     }
     else
     {
        Process p = Runtime.getRuntime().exec(cmdArray, null, dir);

        exitCode = p.waitFor();

        in = new BufferedReader(new InputStreamReader(p.getErrorStream(),
              charset));
     }

     String line;

     StringBuilder processErrors = null;

     while (in != null && (line = in.readLine()) != null)
     {
        if (processErrors == null)
        {
           processErrors = new StringBuilder();
        }

        processErrors.append(String.format("%n%s", line));
     }

     in.close();

     in = new BufferedReader(new InputStreamReader(
        new FileInputStream(new File(dir, transFileName)), charset));

     int numAccepted = 0;
     int numRejected = 0;
     String rejected = "";

     int numAttributes = 0;
     int numIgnored = 0;
     int numTooLong = 0;

     while ((line = in.readLine()) != null)
     {
        Matcher matcher = makeindexAcceptedPattern.matcher(line);

        if (matcher.matches())
        {
           try
           {
              numAccepted = Integer.parseInt(matcher.group(1));
              rejected = matcher.group(2);
              numRejected = Integer.parseInt(rejected);
           }
           catch (NumberFormatException e)
           {
           }

           continue;
        }

        matcher = makeindexIstAttributePattern.matcher(line);

        if (matcher.matches())
        {
           try
           {
              numAttributes = Integer.parseInt(matcher.group(1));
              numIgnored = Integer.parseInt(matcher.group(2));
           }
           catch (NumberFormatException e)
           {
           }

           continue;
        }

        matcher = makeindexTooLongPattern.matcher(line);

        if (matcher.matches())
        {
           numTooLong++;
        }
     }

     in.close();

     boolean deprecated = false;
     boolean depCheck = false;

     if (gloFile.exists())
     {
        in = new BufferedReader(new InputStreamReader(
           new FileInputStream(gloFile), charset));

        while ((line = in.readLine()) != null)
        {
           Matcher matcher;
           boolean depRetry = false;

           if (depCheck)
           {
              matcher = (deprecated ?
                         makeindexOldEntryPattern.matcher(line) :
                         makeindexEntryPattern.matcher(line));
           }
           else
           {
              matcher = makeindexEntryPattern.matcher(line);

              depCheck = true;

              if (!matcher.matches())
              {
                 matcher = makeindexOldEntryPattern.matcher(line);

                 depRetry = true;
              }
           }

           if (matcher.matches())
           {
              if (depRetry)
              {
                 deprecated = true;
              }

              String sort = matcher.group(1);
              String key = matcher.group(2);

              GlossaryEntry entry = entryTable.get(key);

              if (entry == null)
              {
                 entry = new GlossaryEntry(key, sort);
                 entryTable.put(key, entry);
              }

              entry.increment();
           }
        }

        in.close();
     }

     if (exitCode > 0)
     {
        addErrorMessage(invoker.getLabelWithValues("error.app_failed",
           "Makeindex", exitCode));

        if (processErrors != null)
        {
           addDiagnosticMessage(invoker.getLabelWithValues(
              "diagnostics.app_err",
              "Makeindex", processErrors.toString()));
        }
        else
        {
           addDiagnosticMessage(invoker.getLabel("diagnostics.app_err_null"));
        }
     }
     else if (numRejected > 0)
     {
        addErrorMessage(invoker.getLabelWithValues("error.entries_rejected",
          numRejected));

        if (numAccepted == 0)
        {
           addDiagnosticMessage(invoker.getLabelWithValues(
             "diagnostics.makeindex_reject_all", label));
        }

        if (numIgnored > 0)
        {
           addDiagnosticMessage(invoker.getLabelWithValues
              ("diagnostics.bad_attributes", istName, "makeindex"));
        }

        if (numTooLong > 0)
        {
           if (deprecated)
           {
              addDiagnosticMessage(invoker.getLabelWithValues(
               "diagnostics.old_too_long", numTooLong));
           }
           else
           {
              addDiagnosticMessage(invoker.getLabelWithValues(
               "diagnostics.too_long", numTooLong));
           }
        }
     }
     else if (numAccepted == 0)
     {
        if (label.equals("main"))
        {
           addDiagnosticMessage(invoker.getLabel(
              "diagnostics.no_entries_main"));
        }
        else
        {
           addDiagnosticMessage(invoker.getLabelWithValues(
              "diagnostics.no_entries", label));
        }

        addErrorMessage(invoker.getLabelWithValues("error.no_entries", label));
     }
  }

  public int getNumEntries()
  {
     return entryTable.size();
  }

  public String[] getEntryLabels()
  {
     int n = entryTable.size();

     String[] array = new String[n];

     int i = 0;

     for (Enumeration<String> en = entryTable.keys(); en.hasMoreElements();)
     {
        array[i] = en.nextElement();
        i++;
     }

     return array;
  }

  public Integer getEntryCount(String key)
  {
     GlossaryEntry entry = entryTable.get(key);

     return entry == null ? 0 : entry.getCount();
  }

  public String getEntrySort(String key)
  {
     GlossaryEntry entry = entryTable.get(key);

     return entry == null ? null : entry.getSort();
  }

  public boolean hasProblem(String key)
  {
     GlossaryEntry entry = entryTable.get(key);

     return entry == null ? false : entry.hasProblem();
  }

  public Integer getEntryCount(int entryIdx)
  {
     int i = 0;

     for (Enumeration<GlossaryEntry> en = entryTable.elements();
        en.hasMoreElements();)
     {
        GlossaryEntry val = en.nextElement();

        if (i == entryIdx) return val.getCount();

        i++;
     }

     return 0;
  }

  public String getEntrySort(int entryIdx)
  {
     int i = 0;

     for (Enumeration<GlossaryEntry> en = entryTable.elements();
        en.hasMoreElements();)
     {
        GlossaryEntry val = en.nextElement();

        if (i == entryIdx) return val.getSort();

        i++;
     }

     return null;
  }

  public String getEntryLabel(int entryIdx)
  {
     int i = 0;

     for (Enumeration<String> en = entryTable.keys(); en.hasMoreElements();)
     {
        String key = en.nextElement();

        if (i == entryIdx) return key;

        i++;
     }

     return null;
  }

  public int getEntryIdx(String label)
  {
     int i = 0;

     for (Enumeration<String> en = entryTable.keys(); en.hasMoreElements();)
     {
        String key = en.nextElement();

        if (key.equals(label)) return i;

        i++;
     }

     return -1;
  }

  public void setLanguage(String language)
  {
     String mappedLang = invoker.getLanguage(language);

     if (!mappedLang.equals(language))
     {
        addDiagnosticMessage(invoker.getLabelWithValues(
          "diagnostics.mapped_lang", language, mappedLang));
        this.language = mappedLang;
     }
     else
     {
        this.language = language;
     }
  }

  public void setCodePage(String codepage)
  {
     this.codepage = codepage;
  }

  public String displayCodePage()
  {
     return codepage == null ?
        "<font class=error>"+invoker.getLabel("error.unknown")+"</font>" :
        codepage;
  }

  public String displayLanguage()
  {
     return language == null ?
        "<font class=error>"+invoker.getLabel("error.unknown")+"</font>" :
        language;
  }

  public void addErrorMessage(String mess)
  {
     if (errorMessage == null)
     {
        errorMessage = new StringBuilder(mess);
     }
     else
     {
        errorMessage.append(String.format("%n%s", mess));
     }
  }

  public String getErrorMessages()
  {
     return errorMessage == null ? null : errorMessage.toString();
  }

  public String getLanguage()
  {
     return language;
  }

  public String getCodePage()
  {
     return codepage;
  }

  public String getDiagnosticMessages()
  {
     return diagnosticMessage == null ? null : diagnosticMessage.toString();
  }

  public void addDiagnosticMessage(String mess)
  {
     if (diagnosticMessage == null)
     {
        diagnosticMessage = new StringBuilder(mess);
     }
     else
     {
        diagnosticMessage.append(String.format("<p>%s", mess));
     }
  }


  public void checkNonAsciiLabels()
  {
     int numFound = 0;
     StringBuilder builder = null;

     for (Enumeration<String> en=entryTable.keys(); en.hasMoreElements();)
     {
        String key = en.nextElement();

        for (int i = 0, n = key.length(); i < n; i++)
        {
           int c = key.charAt(i);

           if (c > '|' || c < '!' || c == '#' || c == '$' || c == '&'
            || c == '\\' || c == '^' || c == '_')
           {
              numFound++;

              if (builder == null)
              {
                 builder = new StringBuilder(key);
              }
              else
              {
                 builder.append(", ");
                 builder.append(key);
              }

              break;
           }
        }
     }

     if (numFound > 0)
     {
        addDiagnosticMessage(invoker.getLabelWithValues(
          "diagnostics.labels_with_problem_char",
             numFound, label, builder.toString()));
     }
  }

  public String label, transExt, glsExt, gloExt;

  private String language, codepage;

  private StringBuilder errorMessage, diagnosticMessage;

  private Hashtable<String,GlossaryEntry> entryTable;

  private MakeGlossariesInvoker invoker;

  private static final Pattern makeindexAcceptedPattern
     = Pattern.compile(".*?(\\d+)\\s+entries\\s+accepted.*(\\d+)\\s+rejected.*");

  private static final Pattern makeindexIstAttributePattern
     = Pattern.compile(".*?(\\d+)\\s+attributes\\s+redefined.*(\\d+).*ignored.*");

  private static final Pattern makeindexTooLongPattern
     = Pattern.compile("\\s+-- First argument too long \\(max \\d+\\)\\.");

  private static final Pattern xindyIstPattern
     = Pattern.compile(".*variable (.*) has no value.*");

  private static final Pattern emptySortPattern
     = Pattern.compile(".*Would replace complete index key by empty string.*");

  private static final Pattern collapsedSortPattern
     = Pattern.compile(".*index 0 should be less than the length of the string.*");

  private static final Pattern xindyModulePattern
     = Pattern.compile(".*Cannot\\s+locate\\s+xindy\\s+module\\s+for\\s+language\\s+([a-zA-Z0-9\\-]+)\\s+in\\s+codepage\\s+([a-zA-Z0-9\\-]+)..*");

  private static final Pattern makeindexOldEntryPattern
     = Pattern.compile("\\\\glossaryentry\\{(.*?)\\?\\\\glossaryentryfield\\{(.*?)\\}.*");

  private static final Pattern makeindexEntryPattern
     = Pattern.compile("\\\\glossaryentry\\{(.*?)\\?(?:\\\\gls(?:no)?nextpages\\s)?\\\\glossentry\\{(.*?)\\}.*");

  private static final Pattern xindyOldEntryPattern = Pattern.compile(
  "\\(indexentry\\s+:tkey\\s*\\(\\s*\\(\\s*\"(.*?)\"\\s+\"\\\\\\\\glossaryentryfield\\{(.*?)\\}.*\".*");

  private static final Pattern xindyEntryPattern = Pattern.compile(
  "\\(indexentry\\s+:tkey\\s*\\(\\s*\\(\\s*\"(.*?)\"\\s+\"(?:\\\\\\\\gls(?:no)?nextpages\\s)?\\\\\\\\glossentry\\{(.*?)\\}.*\".*");

  private static final Pattern xindyEmptySortPattern = Pattern.compile(
  "(?:\\$|\\{\\\\[a-zA-Z@]+ *\\}|\\\\[a-zA-Z@]+ *)+");
}

class GlossaryEntry
{
  public GlossaryEntry(String label, String sort)
  {
     this.label = label;
     this.sort = sort;
     this.count = 0;
     this.hasProblem = false;
  }

  public void increment()
  {
     count++;
  }

  public int getCount()
  {
     return count;
  }

  public String getSort()
  {
     return sort;
  }

  public String getLabel()
  {
     return label;
  }

  public boolean hasProblem()
  {
     return hasProblem;
  }

  public void setHasProblem(boolean hasProblem)
  {
     this.hasProblem = hasProblem;
  }

  private int count = 0;
  private String label, sort;
  private boolean hasProblem;
}