package nxm.sys.prim;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;

import nxm.sys.lib.FileName;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.TextFile;

/** This class will filter a TOC file. Used along with the TOC file for the
    <i>NeXtMidas User's Guide</i>.

    @author  J. Czechowski IV / Neon Ngo
 */
public class makedocs extends Primitive {
  /** The number of spaces that constitute a tab. */ private static final int    TAB_SPACES = 2;
  /** Default doc title.                          */ private static final String DEF_TITLE  = "Index";
  /** Max length in TOC.                          */ private static final int    MAX_TOC_LENGTH = 28;
  /** FYI: zero-length arrays are immutable. For more info see Effective Java,
      Item 27: Return zero-length arrays, not null. */
  private static final String[] EMPTY_STRING_ARRAY = new String[0];

  /** The files from the TOC listing */ private FileInfo[] files;
  /** The content directory          */ private File       srcDir;
  /** The destination directory      */ private File       destDir;
  /** Index of next file to process  */ private int        fileIndex;
  /** Other options.                 */ private Table      options;
  /** stylesheet filename            */ private String     style;
  /** print stylesheet filename      */ private String     printStyle;
  /** Blank space (no arrow)         */ private String     noarrow;
  /** Left arrow                     */ private String     larrow;
  /** Right arrow                    */ private String     rarrow;
  /** Document title                 */ private String     docTitle;

  /** default constructor */
  public makedocs () {}

  public int open () {
    options = MA.getTable("OPTIONS", new Table());

    String defSrc  = fixPath(options.getS("SRCDIR",    ""));
    String defDest = fixPath(options.getS("DESTDIR",   ""));
    String defToc  = fixPath(options.getS("TOCFILE",   ""));
    // String style   = fixPath(options.getS("STYLESHEET","")); // old
    style   = options.getS("STYLESHEET","style.css"); // default to use relative style.css
    // default to use relative print.css for print view
    printStyle   = options.getS("PRINTSTYLESHEET","print.css");

    FileName src  = MA.getFileName("SRCDIR",  defSrc);
    FileName dest = MA.getFileName("DESTDIR", defDest);
    FileName toc  = MA.getFileName("TOCFILE", defToc);

    TextFile tocFile  = new TextFile(this, toc);
    tocFile.open(TextFile.FORCEABORT);
    String[] tocLines = tocFile.readAll().split("\n");
    tocFile.close();

    srcDir     = new File(src.toString());
    destDir    = new File(dest.toString());
    files      = getFileInfo(tocLines);
    fileIndex  = 0;
    noarrow    = options.getS("NOARROW", null);
    larrow     = options.getS("LARROW",  null);
    rarrow     = options.getS("RARROW",  null);
    docTitle   = options.getS("TITLE",   null);

    if (!srcDir.exists() || !srcDir.isDirectory()) {
      throw new MidasException("MAKEDOCS: Can not find directory "+srcDir);
    }
    if (!destDir.exists() || !destDir.isDirectory()) {
      throw new MidasException("MAKEDOCS: Can not find directory "+destDir);
    }

    return NORMAL;
  }

  public int process () {
    int status = NORMAL;

    if (fileIndex >= files.length) {
      try {
        writeIndexPage();
      } catch(Exception e) {
        M.printStackTrace("MAKEDOCS: Error while creating index.html file.", e);
      }
      try {
        writeScmIgnore();
      } catch(Exception e) {
        M.printStackTrace("MAKEDOCS: Error while creating .gitignore file.", e);
      }
      status = FINISH;
    } else {
      try {
        processContentPage(fileIndex);
      } catch(Exception e) {
        M.printStackTrace("MAKEDOCS: Error while processing "+files[fileIndex], e);
      }
      fileIndex++;
    }

    return status;
  }

  public int close () {
    return NORMAL;
  }

  /** Fixes a path substituting the value of NMROOT for $NMROOT and fixes slashes on Windows. */
  private static String fixPath (String str) {
    if (str.startsWith("$NMROOT")) {
      str = Shell.getNmRoot() + str.substring(7);
    }

    return str.replace('/', File.separatorChar);
  }

  /** Creates an html link for a given file. */
  private String getLink (FileInfo file) {
    return getLink(file, file.getTitle());
  }

  /** Creates an html link for a given file. */
  private String getLink (FileInfo file, String title) {
    return getLink(file, title, -1);
  }

  /** Creates an html link for a given file. */
  private String getLink (FileInfo file, int maxLength) {
    return getLink(file, file.getTitle(), maxLength);
  }

  /** Creates an html link for a given file. */
  private String getLink (FileInfo file, String title, int maxLength) {
    title = fixTitle(title, maxLength);

    return "<a href='"+file.getRootName()+".html'>"+title+"</a>";
  }

  /** Fixes a title for display in the TOC. */
  private String fixTitle (FileInfo file) {
    return fixTitle(file, -1);
  }

  /** Fixes a title for display in the TOC. */
  private String fixTitle (FileInfo file, int maxLength) {
    return fixTitle(file.getTitle(), maxLength);
  }

  /** Fixes a title for display in the TOC. */
  private String fixTitle (String title, int maxLength) {
    if (title.startsWith("\"") && title.endsWith("\"")) {
      title = title.substring(1, title.length()-1);
    }
//    if ((maxLength > 3) && (title.length() > maxLength)) {
//      title = title.substring(0,maxLength-3) + "...";
//    }
    return title;
  }

  /** Creates a .gitignore file for the destination directory. */
  private void writeScmIgnore () throws IOException {
    File outFile = new File(destDir, ".gitignore");
    PrintStream out = new PrintStream(new FileOutputStream(outFile));

    if (verbose) { M.info(StringUtil.padRight("Writing "+outFile, 60)); }
    out.println(".gitignore");
    out.println("index.html");
    for (int i = 0; i < files.length; i++) {
      FileInfo fileInfo = files[i];
      if (fileInfo!=null) out.println("/"+fileInfo.getRootName() + ".html"); // add '/' so rawfiles subdirectory is not ignored NXM-3618
    }
    out.close();
  }

  /** Creates an index page for the destination directory. */
  private void writeIndexPage () throws IOException {
    File outFile = new File(destDir, "index.html");
    ArrayList<String> lines = new ArrayList<>();
    String title = (docTitle == null)? DEF_TITLE: docTitle;

    lines.add("        <h1>"+title+"</h1>");
    lines.add("        <ul class='top'><li>Modules");
    lines.add("          <ul>");
    for (int i = 0; i < files.length; i++) {
      FileInfo fileInfo = files[i];
      if (fileInfo!=null && fileInfo.getDepth() == 0) {
        lines.add("            <li>"+getLink(fileInfo)+"</li>");
      }
    }
    lines.add("          </ul></li>");
    lines.add("        </ul>");

    String[] content = lines.toArray(EMPTY_STRING_ARRAY);
    writePage(outFile, title, getNav(-1), getToc(-1), content);
  }

  /** Creates one of the output files.
      @param pageNum The index (0..N-1) of the file to write.
   */
  private void processContentPage (int pageNum) throws IOException {
    File outFile = new File(destDir, files[pageNum].getRootName()+".html");

    String title = files[pageNum].getTitle();

    if (docTitle != null) { title = docTitle + ": " + title; }

    writePage(outFile, title, getNav(pageNum), getToc(pageNum), readHtmlBody(files[pageNum]));
  }

  /** Creates one of the output files.
      @param file  The file to write to.
      @param title The page title.
      @param nav   The navigation page content.
      @param toc   The table of contents content.
      @param body  The body of the page.
   */
  private void writePage (File file, String title, String[] nav, String[] toc, String[] body)
                        throws IOException {
    if (verbose) { M.info(StringUtil.padRight("Writing "+file, 60)); }
    PrintStream out = new PrintStream(new FileOutputStream(file));

    out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">");
    out.println("<html>");
    out.println("  <!-- ************************************************************** -->");
    out.println("  <!-- ** WARNING: This file is autogenerated any modifications to ** -->");
    out.println("  <!-- **          this file may be lost.                          ** -->");
    out.println("  <!-- ************************************************************** -->");
    out.println("  <head>");
    out.println("    <title>"+title+"</title>");
    out.println("    <link rel='stylesheet' type='text/css' href='"+style+"' media='all'>");
    out.println("    <link rel='stylesheet' type='text/css' href='"+printStyle+"' media='print'>");
    out.println("    <link rel='shortcut icon' href='../NXM.ico'>");
    out.println("    <script type='text/javascript'>");
    out.println("    function toggleToc() {");
    out.println("     if (document.getElementById) {");
    out.println("       var toccol = document.getElementById('toccol');");
    out.println("       var toc = document.getElementById('toc');");
    out.println("       var tocimg_top = document.getElementById('tocimgtop');");
    out.println("       var tocimg_bottom = document.getElementById('tocimgbottom');");
    out.println("        if (toc.style.display == 'none') { toc.style.display = 'block';  toccol.width = '200'; tocimg_top.src = '../back.png'; tocimg_bottom.src = '../back.png'; }");
    out.println("          else { toc.style.display = 'none'; toccol.width = '15'; tocimg_top.src = '../next.png'; tocimg_bottom.src = '../next.png'; } } }");
    out.println("    </script>");
    out.println("  </head>");
    out.println("  <body bgcolor='#330066'>");
    out.println("  <table width='95%' border='1' id='maintable'>");
    out.println("    <tr>");
    out.println("      <td class='maintable' valign='top' colspan='2' bgcolor='#FFFFCC'>");
    out.println("<!-- ******************** BEGIN NAVIGATION AREA ******************** -->");
    for (int j = 0; j < nav.length; j++) { out.println(nav[j]); }
    out.println("<!-- ******************** END NAVIGATION AREA ******************** -->");
    out.println("      </td>");
    out.println("    </tr>");
    out.println("    <tr>");
    out.println("      <td id='toccol' class='maintable noprint' valign='top' bgcolor='#FFFFCC' width='200'>");
    out.println("       <div style='display:inline;float:right;'>");
    out.println("        <a onclick='javascript:toggleToc()' href='#' class='noprint' title='Toggle Table of Contents'>");
    out.println("         <img id='tocimgtop' src='../back.png' border='0' alt='back'>");
    out.println("        </a>");
    out.println("       </div>");
    out.println("       <div id='toc'>");
    out.println("        <code>");
    out.println("<!-- ******************** BEGIN TOC AREA ******************** -->");
    for (int j = 0; j < toc.length; j++)  { out.println(toc[j]);           }
    out.println("          <br>");
    out.println("          <br>");
    out.println("<!-- ******************** END TOC AREA ******************** -->");
    out.println("        </code>");
    out.println("       </div>");
    out.println("      </td>");
    out.println("      <td class='maintable' valign='top' bgcolor='#FFFFFF'>");
    out.println("<!-- ******************** BEGIN CONTENT AREA ******************** -->");
    for (int j = 0; j < body.length; j++) { out.println(body[j]); }
    out.println("<!-- ******************** END CONTENT AREA ******************** -->");
    out.println("        <br>");
    out.println("      </td>");
    out.println("    </tr>");
    out.println("<!-- ******************** BEGIN FOOTER NAVIGATION AREA ******************** -->");
    out.println("    <tr>");
    out.println("    <td class='maintable noprint' valign='top' bgcolor='#FFFFCC'>");
    out.println("      <div style='display:inline;float:right;'>");
    out.println("        <a onclick='javascript:toggleToc()' href='#' class='noprint' title='Toggle Table of Contents'>");
    out.println("          <img id='tocimgbottom' src='../back.png' border='0' alt='back'>");
    out.println("        </a>");
    out.println("      </div>");
    out.println("    </td>");
    out.println("    <td class='maintable' valign='top' bgcolor='#FFFFFF'>");
    out.println("    </td>");
    out.println("    </tr>");
    out.println("<!-- ******************** END FOOTER NAVIGATION AREA ******************** -->");
    out.println("  </table>");
    out.println("  </body>");
    out.println("</html>");
    out.close();
  }

  private String[] getNav (int index) {
    ArrayList<String> lines = new ArrayList<>();
    java.util.Stack<FileInfo>  depth = new java.util.Stack<>();
    String rootTitle = (docTitle == null)? DEF_TITLE: docTitle;

    if (index == -1) {
      lines.add("           <b>"+rootTitle+"</b>");
    } else {
      for (int i = 0; (i < files.length) && (i <= index); i++) {
        while ((depth.size() > files[i].getDepth()) && !depth.empty()) {
          depth.pop();
        }
        depth.push(files[i]);

        if (i == index) {
          if (larrow != null) {
            if ((i-1) >= 0) {
              lines.add(getLink(files[i-1], "<img src='"+larrow+"' class='navtable noprint' alt='Previous'>")+" ");
            } else if (noarrow != null) {
              lines.add("<img src='"+noarrow+"' class='navtable noprint' alt='Previous'>");
            }
          } else {
            if ((i-1) >= 0) {
              lines.add(getLink(files[i-1], "&lt;-- Previous")+" ");
            } else {
              lines.add("<font color='gray'>&lt;-- Previous</font> ");
            }
          }
          if (rarrow != null) {
            if ((i+1) < files.length) {
              lines.add(getLink(files[i+1], "<img src='"+rarrow+"' class='navtable noprint' alt='Next'>")+" ");
            } else if (noarrow != null) {
              lines.add("<img src='"+noarrow+"' class='navtable noprint' alt='Next'>");
            }
          } else {
            if ((i+1) < files.length) {
              lines.add(getLink(files[i+1], "Next --&gt;")+" ");
            } else {
              lines.add("<font color='gray'>Next --&gt;</font> ");
            }
          }
          lines.add("<span class='noprint'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><i>Current Page:</i>");
          if (depth.size() == 0) {
            lines.add("           <b>"+rootTitle+"</b>");
          } else {
            lines.add("           <a href='index.html'>"+rootTitle+"</a>");
          }
          for (int j = 0; j < depth.size(); j++) {
            if (rarrow == null) {
              lines.add("           / ");
            } else {
              lines.add("           <img src='"+rarrow+"' class='navtable' alt='/'> ");
            }
            if (j == (depth.size() - 1)) {
              lines.add("           <b>"+fixTitle(depth.get(j))+"</b>");
            } else {
              lines.add("           "+getLink(depth.get(j)));
            }
          }
        }
      }
    }

    return lines.toArray (EMPTY_STRING_ARRAY);
  }

  private String[] getToc (int pageNum) {
    ArrayList<FileInfo> pages = new ArrayList<>(Arrays.asList(files)); // make a copy
    ArrayList<String> lines = new ArrayList<>();

    boolean inSect    = true;
    boolean inSubSect = true;
    for (int i = pageNum+1; i < pages.size(); i++) {
      FileInfo page = pages.get(i);

      inSect    = inSect    && (page.getDepth() > 0);
      inSubSect = inSubSect && (page.getDepth() > 1);

      if ((inSubSect) || (inSect && (page.getDepth() == 1)) || (page.getDepth() == 0)) {
        // keep page
      } else {
        pages.set(i, null);
      }
    }

    inSect    = true;
    inSubSect = true;
    for (int i = pageNum; i >= 0; i--) {
      FileInfo page = pages.get(i);

      if ((inSubSect) || (inSect && (page.getDepth() == 1)) || (page.getDepth() == 0)) {
        // keep page
      } else {
        pages.set(i, null);
      }

      inSect    = inSect    && (page.getDepth() > 0);
      inSubSect = inSubSect && (page.getDepth() > 1);
    }

    for (int i = 0; i < pages.size(); i++) {
      FileInfo page = pages.get(i);
      FileInfo next = (i+1 < pages.size())? pages.get(i+1): null;

      if (page == null) {
        // skip
      } else {
        StringBuffer str = new StringBuffer("          <span class=\"nobr\">");
        int length = MAX_TOC_LENGTH - page.getDepth()*5 - 4;

        for (int k = 0; k < page.getDepth(); k++) { str.append("&nbsp;&nbsp;&nbsp;"); }
        if (next == null) {
          str.append("+&nbsp;");
        } else {
          str.append("-&nbsp;");
        }

        if (i == pageNum) {
          str.append("<b>").append(fixTitle(page,length)).append("</b>");
        } else {
          str.append(getLink(page,length));
        }
        str.append("&nbsp;&nbsp;&nbsp;</span><br>");
        lines.add(str.toString());
      }
    }

    return lines.toArray(EMPTY_STRING_ARRAY);
  }

  /** This will grab the names of all the files to be used from a TOC file.
      @return The lines from a TOC file.
   */
  private static FileInfo[] getFileInfo (final String[] lines) {
    ArrayList<FileInfo> files = new ArrayList<>();

    for(int i = 0; i < lines.length; i++) {
      try {
        final String line = lines[i].trim();

        if((line.length() == 0) || line.startsWith("#")) {
          /* Skip Line */
        } else {
          int     index    = (line.indexOf(' ') > 0)? line.indexOf(' '): line.length();
          String  name     = line.substring(0, index);
          String  title    = line.substring(index).trim();
          int     depth    = getDepth(lines[i]);
          boolean hidden   = name.startsWith("*");

          if (hidden) { name = name.substring(1); }
          int     pthidx   = Math.max(0, name.lastIndexOf('/'));
          int     extidx   = Math.max(0, name.lastIndexOf('.'));
          String  rootName = name.substring(pthidx, extidx);

          if(extidx < 0) {
            throw new RuntimeException("No extension given for file name, "
                                     + "expected '.htm' or '.html'.");
          }

          files.add(new FileInfo(name, rootName, title, depth, hidden));
        }
      } catch(Exception e) {
        throw new RuntimeException("Error reading line " + (i+1) + ": " + e.getMessage());
      }
    }

    return files.toArray(new FileInfo[0]);
  }

  /** Gets the depth associated with a file.
      @param line The line from the TOC file.
      @return The depth of the file.
   */
  private static int getDepth (final String line) {
    int spaces = 0;

    while((spaces < line.length()) && (line.charAt(spaces) == ' ')) {
      spaces++;
    }

    if((spaces % TAB_SPACES) != 0) {
      throw new RuntimeException("Number of leading spaces in line must be a "
                               + "multiple of " + TAB_SPACES + ".");
    } else {
      return (spaces / TAB_SPACES);
    }
  }

  /** Reads a lines from a file.
      @param file The input file.
      @return The lines in the file.
      @throws IOException If unable to read the file.
   */
  private static String[] readFile (final File file) throws IOException {
    ArrayList<String> lines = new ArrayList<>();
    BufferedReader in = new BufferedReader(new FileReader(file));

    String line;

    while((line = in.readLine()) != null) {
      lines.add(line);
    }
    in.close();

    return lines.toArray(EMPTY_STRING_ARRAY);
  }

  /** Reads a lines from an HTML file discarding all non-body content.
      @param file The input file.
      @return The lines in the file.
      @throws IOException If unable to read the file.
   */
  private String[] readHtmlBody (FileInfo file)  throws IOException {
    return readHtmlBody(new File(srcDir, file.getName()));
  }

  /** Reads a lines from an HTML file discarding all non-body content.
      @param file The input file.
      @return The lines in the file.
      @throws IOException If unable to read the file.
   */
  private String[] readHtmlBody (File file)  throws IOException {
    ArrayList<String> lines = new ArrayList<>();
    BufferedReader in = new BufferedReader(new FileReader(file));

    String line;
    boolean inBody = false;

    while ((line = in.readLine()) != null) {
      if (inBody) {
        int index = line.toLowerCase().indexOf("</body>");

        if (index >= 0) {
          lines.add(line.substring(0,index));
          inBody = false;
        } else {
          lines.add(line);
        }
      } else {
        int index = line.toLowerCase().indexOf("<body>");

        if (index >= 0) {
          lines.add(line.substring(index+6));
          inBody = true;
        }
      }
    }
    in.close();

    return lines.toArray(EMPTY_STRING_ARRAY);
  }


  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Nested FileInfo class.
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Information about a file in the TOC */
  private static class FileInfo {
    private final String  name;     // The name of the file.
    private final String  rootName; // The root name of the file.
    private final String  title;    // The title of the file.
    private final int     depth;    // The depth of the file.
    private final boolean hidden;   // Is it hidden in the TOC?

    /** Creates a new instance.
        @param name      The name of the file.
        @param rootName  The name of the file without extension.
        @param title     The title of the file.
        @param depth     The file depth.
        @param hidden    Is it hidden in the TOC?
     */
    public FileInfo(String name, String rootName, String title, int depth, boolean hidden) {
      if (title.startsWith("\"") && title.endsWith("\"")) {
        title = title.substring(1, title.length()-1);
      }

      this.name     = name;
      this.rootName = rootName;
      this.title    = title;
      this.depth    = depth;
      this.hidden   = hidden;
    }

    /** Gets the name of the file. */
    public String getName() { return name; }

    /** Gets the root name of the file. */
    public String getRootName() { return rootName; }

    /** Gets the title of the file. */
    public String getTitle() { return title; }

    /** Gets the depth of the file. */
    public int getDepth() { return depth; }

    /** Indicates if the file is hidden in the TOC. */
    public boolean isHidden() { return hidden; }

    /** Returns a debug string. */
    public String toString() { return getRootName(); }
  }
}
