/*
 * Copyright (c) 2003-2016, KNOPFLERFISH project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials
 *   provided with the distribution.
 *
 * - Neither the name of the KNOPFLERFISH project nor the names of its
 *   contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.knopflerfish.ant.taskdefs.bundle;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.selectors.FilenameSelector;
import org.apache.tools.ant.util.FileUtils;

import org.osgi.framework.Version;

import org.knopflerfish.ant.taskdefs.bundle.BundleArchives.BundleArchive;

/**
 * <p>
 *  This task is used when building distributions of Knopflerfish.
 *  If you don't intend to create a new distribution type of
 *  Knopflerfish then you're in the wrong place.
 * </p>
 *
 * <p>
 *  Task that creates web sites given a template and a source file.
 *  Currently used to create parts of the <code>docs</code> directory in the KF
 *  distribution.  It does this by simply replacing certain text strings with
 *  others. For more information on which text strings this is please
 *  check the source code.
 * </p>
 *
 * <p>
 *  Here is a outline of how to use the task and a description
 *  of different parameters and used system properties.
 * </p>
 *
 * <p>
 *
 * <table border=1>
 *  <tr>
 *   <td valign=top><b>Attribute</b></td>
 *   <td valign=top><b>Description</b></td>
 *   <td valign=top><b>Required</b></td>
 *  </tr>
 *  <tr>
 *   <td>
 *    outdir
 *   </td>
 *   <td>
 *    What dir to put the actual the generated file
 *   </td>
 *   <td>
 *    Yes
 *   </td>
 *  </tr>
 *   <td>
 *    tofile
 *   </td>
 *   <td>
 *    The relative path to where the generated file should be
 *    copied. That is the actual location of the generated file
 *    will be <code>outdir</code>/<code>tofile</code>
 *   </td>
 *   <td>
 *    Yes
 *   </td>
 *  </tr>
 *  <tr>
 *   <td>
 *    template
 *   </td>
 *   <td>
 *    The file which describes what the page should look like
 *   </td>
 *   <td>
 *    Yes
 *   </td>
 *  </tr>
 *  <tr>
 *   <td>
 *    title
 *   </td>
 *   <td>
 *    The page's title
 *   </td>
 *   <td>
 *    No, default is ""
 *   </td>
 *  </tr>
 *  <tr>
 *   <td>
 *    description
 *   </td>
 *   <td>
 *    The page's description
 *   </td>
 *   <td>
 *    No, default is ""
 *   </td>
 *  </tr>
 *  <tr>
 *   <td>
 *    disable
 *   </td>
 *   <td>
 *    Allows you to disable certain links. This attribute is very ad hoc.
 *    It will use the properties <code>htdocs.link.disabled.class</code>
 *    and <code>htdocs.link.enabled.class</code>. The task will then use the
 *    values of these properties to generate the file.
 *   </td>
 *   <td>
 *    No
 *   </td>
 *  </tr>
 *  <tr>
 *   <td>
 *    javadocRelPath
 *   </td>
 *   <td>
 *    Relative path (from outdir) to javadocs.
 *   </td>
 *   <td>
 *    ../../javadoc
 *   </td>
 *  </tr>
 * </table>
 * <p>
 *  <b>Note:</b> instead of using the attributes <code>fromfile</code> and
 *  <code>tofile</code> one can use filesets. It will simply run through
 *  and perform the task on all given files.
 * </p>
 *
 */

public class MakeHTMLTask
  extends Task
{
  private final static String TIMESTAMP
    = new SimpleDateFormat("EE MMMM d yyyy, HH:mm:ss", Locale.ENGLISH)
    .format(new Date());
  private final static String YEAR
    = new SimpleDateFormat("yyyy",  Locale.ENGLISH).format(new Date());

  private String projectName = "";
  private boolean do_manpage = false;
  private String javadocRelPath = "../../javadoc";

  /**
   * The source file
   */
  private String fromFile;

  /**
   * Target directory, where everything will end up
   */
  private File outdir;

  /**
   * The relative path to the target file from output dir
   */
  private File toFile;

  /**
   * File's title
   */
  private String title;

  /**
   * Description
   */
  private String description;

  /**
   * Template file
   */
  private File template;

  /**
   * Bundle list
   */
  private String bundleList;

  private final ArrayList<ResourceCollection> filesets = new ArrayList<ResourceCollection>();
  private String disable;

  private final Map<String, HtmlFragment> fragments = new HashMap<String,HtmlFragment>();

  public void setFromfile(String s) {
    fromFile = s;
  }

  public void setTofile(String s) {
    toFile = new File(s);
  }

  public void setTitle(String title) {
    this.title = title;
  }

  @Override
  public void setDescription(String description) {
    this.description = description;
  }

  public void setOutdir(String s) {
    outdir = new File(s);
  }

  public void setTemplate(String template) {
    this.template = new File(template);
  }

  public void setBundleList(String bundleList) {
    this.bundleList = bundleList;
  }

  public void setManstyle(String manstyle) {
    this.do_manpage = true;
  }


  public void setDisable(String disabled) {
    this.disable = disabled;
  }

  public void setJavadocRelPath(String s) {
    this.javadocRelPath = s;
  }

  public void addFileset(FileSet fs) {
    filesets.add(fs);
  }

  public void addConfiguredHtmlFragment(HtmlFragment fragment) {
    final String name = fragment.getName();
    if (name == null) {
      throw new BuildException("Nested HTML-fragments must have a name");
    }
    if (null==fragment.getFromFile()) {
      throw new BuildException("Nested HTML-fragments must have a from file");
    }
    fragments.put(name, fragment);
  }



  @Override
  public void execute() {
    final Project proj = getProject();
    this.projectName = proj.getName();

    if (template == null) {
      throw new BuildException("template must be set");
    }
    if (title == null) {
      title = "";
    }
    if (description == null) {
      description = "";
    }

    if (filesets.isEmpty() && fromFile == null && toFile == null) {
      throw new BuildException("Need to specify tofile and fromfile or give a fileset");
    }

    if (filesets.isEmpty()) {
      // log("Project base is: " + getProject().getBaseDir());
      // log("Attempting to transform: " + fromFile);
      if (!FileUtils.isAbsolutePath(fromFile)) {
        fromFile = getProject().getBaseDir() + File.separator + fromFile;
      }
      transform(fromFile, toFile.toString());
    } else {
      if (fromFile != null) {
        throw new BuildException("Can not specify fromfile when using filesets");
      }
      if (toFile != null) {
        throw new BuildException("Can not specify tofile when using filesets");
      }

      for (final Object element : filesets) {
        final FileSet fs = (FileSet) element;
        final DirectoryScanner ds = fs.getDirectoryScanner(getProject());
        final String[] files = ds.getIncludedFiles();

        for (final String file : files) {
          transform(new File(fs.getDir(getProject()),
                             file).getAbsolutePath(), file);
        }
      }
    }
  }

  private void transform(String fromFile, String toFile) {
    if (fromFile == null) {
      throw new BuildException("fromfile must be set");
    }
    if (toFile == null) {
      throw new BuildException("tofile must be set");
    }

    try {
      final Project proj = getProject();
      File tmp = new File(outdir, toFile).getParentFile();

      if (!tmp.exists()) {
        if (tmp.exists() || !tmp.mkdirs()) {
          throw new IOException("Could not create " + tmp);
        }
      }

      tmp = new File(toFile);
      String pathToRoot = ".";

      while ((tmp = tmp.getParentFile()) != null) {
        pathToRoot = pathToRoot + "/..";
      }

      // Compute the relative path from directory holding the toFile
      // to the javadoc direcotry.
      String pathToOutDir = "";
      tmp = new File(outdir, toFile).getParentFile();
      while ((tmp = tmp.getParentFile()) != null && tmp.equals(outdir)) {
        pathToOutDir = pathToOutDir + "../";
      }
      final String pathToJavadocDir = pathToOutDir + javadocRelPath;

      String content = FileUtil.loadFile(template.toString());
      content = Util.replace(content, "$(LINKS)", links());
      content = Util.replace(content, "$(MAIN)", FileUtil.loadFile(fromFile));
      for (final Object element : fragments.keySet()) {
        final String key = (String) element;
        final HtmlFragment frag = fragments.get(key);
        final String linkText = frag.getLinkText();
        if (null!=linkText && 0<linkText.length()) {
          final String fragLink = "<a href=\"#" +key +"\">" +linkText +"</a>";
          content = Util.replace(content, "$("+key +"_LINK)", fragLink);
        }
        String fragCont = FileUtil.loadFile(frag.getFromFile().getPath());
        if (null!=fragCont && 0<fragCont.length()) {
          fragCont = "<a name=\"" +key +"\"></a>\n" + fragCont;
        }
        content = Util.replace(content, "$("+key +")", fragCont);
      }
      content = Util.replace(content, "$(TITLE)", title);
      content = Util.replace(content, "$(DESC)", description);
      content = Util.replace(content, "$(TSTAMP)", TIMESTAMP);
      content = Util.replace(content, "$(YEAR)", YEAR);
      content = Util.replace(content, "$(USER)",
                             System.getProperty("user.name"));
      content = Util.replace(content, "$(VERSION)",
                             proj.getProperty("version"));
      content = Util.replace(content, "$(VERSION_PREV)",
                             proj.getProperty("version.previous"));
      content = Util.replace(content, "$(KF_VERSION)",
                             proj.getProperty("kf_version"));
      content = Util.replace(content, "$(BASE_VERSION)",
                             proj.getProperty("base_version"));
      content = Util.replace(content, "$(BASE_URL)",
                             proj.getProperty("base.url"));
      content = Util.replace(content, "$(DISTNAME)",
                             proj.getProperty("distname"));
      content = Util.replace(content, "$(DISTRIB_NAME)",
                             proj.getProperty("distrib.name"));
      content = Util.replace(content, "$(RELEASE_NAME)",
                             proj.getProperty("release.name"));
      content = Util.replace(content, "$(RELEASE_DATE)",
                             proj.getProperty("release.date"));
      content = Util.replace(content, "$(MESSAGE)",
                             proj.getProperty("release"));
      content = Util.replace(content, "$(BUNDLE_LIST)", bundleList);
      content = Util.replace(content, "$(ROOT)", pathToRoot);
      content = Util.replace(content, "$(JAVADOC)",
                             proj.getProperty("JAVADOC"));
      content = Util.replace(content, "$(CLASS_NAVIGATION)",
                             proj.getProperty("css_navigation_enabled"));
      content = Util.replace(content, "$(GIT_REPO_URL)",
                             proj.getProperty("git.repo.url"));
      content = Util.replace(content, "$(GIT_TAG)",proj.getProperty("git.tag"));
      content = Util.replace(content, "$(GIT_BRANCH)",proj.getProperty("git.branch"));
      content = Util.replace(content, "$(GIT_BROWSE_URL)",proj.getProperty("git.browse.url"));
      content = Util.replace(content, "$(JAVADOCPATH)", pathToJavadocDir);
      content = Util.replace(content, "$(JAVADOCLINK)",
                             pathToJavadocDir + "/index.html?");

      // Used for bundle_doc generation
      if (do_manpage) {
        content = Util.replace(content, "$(BUNDLE_NAME)", this.projectName);
        content = Util.replace(content, "$(BUNDLE_VERSION)",
                               proj.getProperty("bmfa.Bundle-Version"));

        final BundleArchives bas = getBundleArchives();

        // Create links to jardoc for bundles built from this project
        content = Util.replace(content, "$(BUNDLE_JARDOCS)", basToString(bas));

        content = Util.replace(content, "$(BUNDLE_EXPORT_PACKAGE)",
                               getExportPkgs(bas, pathToJavadocDir));

        // Replce H1-H3 headers to man page style, if manpage style
        content = Util.replace(content, "<h1", "<h1 class=\"man\"");
        content = Util.replace(content, "<H1", "<h1 class=\"man\"");
        content = Util.replace(content, "<h2", "<h2 class=\"man\"");
        content = Util.replace(content, "<H2", "<h2 class=\"man\"");
        content = Util.replace(content, "<h3", "<h3 class=\"man\"");
        content = Util.replace(content, "<H3", "<h3 class=\"man\"");
      }

      final String s = proj.getProperty("navigation_pages");
      final String navEnabled = proj.getProperty("css_navigation_enabled");
      final String navDisabled = proj.getProperty("css_navigation_disabled");
      // System.out.println("Navigation pages: " + s);
      if (s != null) {
        final String[] navPages = Util.splitwords(s);
        for (final String navPage : navPages) {
          // System.out.println("Checking: " + navPages[i]);
          if (disable != null && disable.equals(navPage)) {
            content = Util.replace(content,
                                   "$(CLASS_NAVIGATION_" + navPage + ")",
                                   navDisabled);
          }
          else {
            content = Util.replace(content,
                                   "$(CLASS_NAVIGATION_" + navPage + ")",
                                   navEnabled);
          }
        }
      }

      FileUtil.writeStringToFile(new File(outdir, toFile), content);
      log("Created: " + new File(outdir, toFile));
    } catch (final IOException e) {
      e.printStackTrace();
      throw new BuildException(e);
    } catch (final Exception e) {
      e.printStackTrace();
      throw new BuildException(e);
    }
  }

  private static final String LINK_BASE = "htdocs.link.";
  private static final String LINK_ID = LINK_BASE + "id.";
  private static final String LINK_TYPE = LINK_BASE + "type.";
  private static final String LINK_NAME = LINK_BASE + "name.";
  private static final String LINK_URL = LINK_BASE + "url.";
  private static final String CSS_CLASS_ENABLED = "htdocs.link.enabled.class";
  private static final String CSS_CLASS_DISABLED = "htdocs.link.disabled.class";

  private String links() {
    final Project proj = getProject();
    final StringBuffer buf = new StringBuffer();

    for (int i = 0; ; i++) {
      final String id   = proj.getProperty(LINK_ID + i);

      if (id == null) {
        break;
      }

      final String type = proj.getProperty(LINK_TYPE + i);
      if (type == null) {
        throw new BuildException("must set htdocs.link.type." + i);
      }

      if (type.equals("separator")) {
        buf.append("<p></p>");

      } else if (type.equals("link")) {

        final String name = proj.getProperty(LINK_NAME + i);
        final String url  = proj.getProperty(LINK_URL + i);
        if (name == null) {
          throw new BuildException("Name not set for htdocs.link.url." + i);
        }

        String cssClass = null;

        if (disable != null && disable.equals(id)) {
          cssClass = getProject().getProperty(CSS_CLASS_DISABLED);
        } else {
          cssClass = getProject().getProperty(CSS_CLASS_ENABLED);
        }

        buf.append("<a class=\"" + cssClass + "\" href=\"" + url
                   + "\">" + name + "</a><br/>\n");
      } else {
        throw new BuildException("Do not recognize type " + type);
      }
    }

    return buf.toString();
  }

  /**
   * Loads the bundle archives built from this project.
   * <p>
   * The jar files to analyze is determined from:
   * <ul>
   * <li>The project property named <code>jarfile</code>. This is used to create
   * a file set with dir set to the directory part of the property value that
   * selects the file named by the file name part of the property value.</li>
   * <li>The project properties <code>jars.dir</code> and
   * <code>jardir.name</code> (set by all projects based on
   * <code>bundlebuild.xml</code>. The file set derived from these properties
   * will have its dir-property set to the value <code>jars.dir</code> and an
   * includes pattern of the form
   * <code>${jardir.name}/&ast;&ast;/&ast;.jar</code>.
   * <li>The file set with id <code>docbuild.jarfiles</code>.
   * </ul>
   *
   * @return bundle archives object holding the bundle archives selected by the
   *         file sets described above or null if no file set was defined.
   */
  private BundleArchives getBundleArchives() {
    final List<ResourceCollection> fileSets = new ArrayList<ResourceCollection>();
    final Project proj = getProject();

    // File set for bundlebuild.xml properties
    final String jarsDir = proj.getProperty("jars.dir");
    if (null != jarsDir && 0 < jarsDir.length()) {
      final File file = new File(jarsDir);
      if (file.exists()) {
        final FileSet fileSet = new FileSet();
        fileSet.setProject(proj);
        fileSet.setDir(file);
        final FilenameSelector fns = new FilenameSelector();
        fns.setName(proj.getProperty("jardir.name") + "/**/*.jar");
        fileSet.add(fns);
        fileSet.setExcludes("**/*-source.jar,**/*-javadoc.jar");
        fileSets.add(fileSet);
        log("Found build results (bundlebuild): " + fileSet, Project.MSG_DEBUG);
      }
    }

    // File set for jarfile-property (e.g., framework.jar)
    final String jarfile = proj.getProperty("jarfile");
    if (null!=jarfile && 0<jarfile.length()) {
      final File file = new File(jarfile);
      if (file.exists()) {
        final FileSet fileSet = new FileSet();
        fileSet.setProject(proj);
        fileSet.setDir(file.getParentFile());
        final FilenameSelector fns = new FilenameSelector();
        fns.setName(file.getName());
        fileSet.add(fns);
        fileSets.add(fileSet);
        log("Found build results (jarfile): " + fileSet, Project.MSG_DEBUG);
      }
    }

    // FileSet defined with id (for bundle overview documentation).
    final FileSet docbuildeFileSet
      = (FileSet) proj.getReference("docbuild.jarfiles");
    if (null!=docbuildeFileSet) {
      fileSets.add(docbuildeFileSet);
      log("Found build results (docbuild.jarfiles): " + docbuildeFileSet,
          Project.MSG_DEBUG);
    }

    if (0 < fileSets.size()) {
      final BundleArchives bas = new BundleArchives(this, fileSets, true);
      bas.doProviders();
      return bas;
    }
    return null;
  }

  static String replace(String src, String a, String b) {
    return Util.replace(src, a, b == null ? "" : b);
  }

  /** Template row for an exported package. */
  private static final String packageListRow
    = " <tr><td>${namelink}</td><td align=\"center\">${version}</td><td>${providers}</td></tr>\n";

  /** The table heading for the list of exported packages. */
  private static final String packageListHeading
    = "<table class=\"man\">\n <tr><th>Package</th><th>Version</th><th>Providers</th></tr>\n";

  /** The table footer for the list of exported packages. */
  private static final String packageListFooter
    = "</table>\n";

  /**
   * Return HTML-formated String with one exported package per line, linking the
   * package name to its javadoc (if present).
   *
   * @param bas
   *          Bundle archives object that holds the set of packages and the
   *          exporters.
   * @param pathToJavadocDir
   *          Relative path from the file we are generating to the javadoc
   *          directory.
   * @return HTML string with all exported packages.
   */
  private String getExportPkgs(final BundleArchives bas, final String pathToJavadocDir) {
    final StringBuffer res = new StringBuffer();

    if (null != bas) {
      final boolean javadocPresent = pathToJavadocDir != null && 0<pathToJavadocDir.length();
      for (final Entry<String, SortedMap<Version, SortedSet<BundleArchive>>> pkgEntry: bas.allExports.entrySet()) {
        final String pkg = pkgEntry.getKey().toString();
        final Map<Version,SortedSet<BundleArchive>> verToProvides = pkgEntry.getValue();

        final String docFile = replace(pkg, ".", "/") +BundleHTMLExtractorTask.PACKAGE_SUMMARY_HTML;
        final String docPath = pathToJavadocDir +"/index.html?" +docFile;
        final File f = new File(outdir +File.separator
                                +pathToJavadocDir.replace('/', File.separatorChar)
                                +File.separator
                                +docFile.replace('/', File.separatorChar));
        for (final Entry<Version,SortedSet<BundleArchive>> vtpEntry : verToProvides.entrySet()) {
          final String version = vtpEntry.getKey().toString();
          final Set<BundleArchive> providers = vtpEntry.getValue();

          String row = packageListRow;
          if(javadocPresent) {
            if (!f.exists()) {
              row = replace(row, "${namelink}", "${name}");
            } else {
              row = replace(row,
                            "${namelink}",
                            "<a target=\"_top\" href=\"${javadoc}\">${name}</a>");
            }
          } else {
            row = replace(row, "${namelink}", "${name}");
          }
          row = replace(row, "${name}", pkg);
          row = replace(row, "${version}", version.toString());
          row = replace(row, "${javadoc}", docPath);
          row = replace(row, "${providers}", providersToString(providers));

          res.append(row);
        }
      }
    }
    if (0<res.length()) {
      return packageListHeading + res.toString() + packageListFooter;
    } else {
      return "No exported packages.";
    }
  }

  private String basToString(final BundleArchives bas) {
    final StringBuffer res = new StringBuffer();

    if (null != bas) {
      for (final Object element : bas.allBundleArchives) {
        final BundleArchive ba = (BundleArchive) element;

        if (0 < res.length()) {
          res.append("<br>\n");
        }
        res.append(providerToString(ba));
      }
    }
    return res.toString();
  }

  private String providersToString(final Set<BundleArchive> providers) {
    final StringBuffer res = new StringBuffer();

    for (final BundleArchive ba : providers) {
      if (0 < res.length()) {
        res.append(", ");
      }
      res.append(providerToString(ba));
    }
    return res.toString();
  }

  private static final String packageListProviderTemplate
    = "<a target=\"_top\" href=\"../../jars/index.html?bundle=${bundledoc.uri}\">${bundle.name.short}</a>";

  private String providerToString(final BundleArchive ba) {
    final String relPath = replace(ba.relPath, ".jar", "");
    final String htmlURI = replace(relPath, "\\", "/")
      + BundleHTMLExtractorTask.HTML;
    final String bundleShortName = replace(ba.file.getName(), ".jar", "");

    String provider = replace(packageListProviderTemplate, "${bundledoc.uri}",
                              htmlURI);
    provider = replace(provider, "${bundle.name.short}", bundleShortName);
    return provider;
  }

}
