/**
 * Copyright 2013 Google Inc. All Rights Reserved.
 */

package com.google.appengine.endpoints;

import com.google.api.server.spi.config.Api;
import com.google.appengine.repackaged.com.google.common.io.Files;
import com.google.common.base.Joiner;
import eu.infomas.annotation.AnnotationDetector;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

/**
 * Process Endpoints annotations and change web.xml accordingly.
 */
public class WebXmlProcessing {

  Log log;
  String webXmlSourcePath;
  String outputDirectory;
  MavenProject project;
  String userSpecifiedServiceClassNames;


  public WebXmlProcessing(Log log, String webXmlSourcePath,
      String outputDirectory, MavenProject project,
      String userSpecifiedServiceClassNames) {
    this.log = log;
    this.webXmlSourcePath = webXmlSourcePath;
    this.outputDirectory = outputDirectory;
    this.project = project;
    this.userSpecifiedServiceClassNames = userSpecifiedServiceClassNames;

  }

  private Log getLog() {
    return log;
  }

  public List<String> getAPIServicesClasses() {
    List<String> classes;
    if (userSpecifiedServiceClassNames != null) {
      classes = Arrays.asList(userSpecifiedServiceClassNames.split(","));
    } else {
      ApiReporter reporter = new ApiReporter();
      String targetDir = project.getBuild().getOutputDirectory();

      final AnnotationDetector cf = new AnnotationDetector(reporter);
      try {
        cf.detect(new File(targetDir));
      } catch (IOException ex) {
        getLog().info(ex);
      }
      classes = reporter.getClasses();
    }
    XmlUtil util = new XmlUtil();
    try {
      util.updateWebXml(classes, webXmlSourcePath);
    } catch (Exception ex) {
      getLog().info("Error: " + ex);
    }
    return classes;
  }

  class ApiReporter implements AnnotationDetector.TypeReporter {

    private final List<String> classes = new ArrayList<>();

    @SuppressWarnings("unchecked")
    @Override
    public Class<? extends Annotation>[] annotations() {
      return new Class[]{Api.class};
    }

    @Override
    public void reportTypeAnnotation(Class<? extends Annotation> annotation, String className) {
      classes.add(className);

    }

    public List<String> getClasses() {
      return classes;
    }
  }

  /**
   * Xml Manipulation Utility Class for modifying web.xml for generated APIs.
   */
  class XmlUtil {

    private static final String COMMA = ",";
    private static final String WEB_APP = "web-app";
    private static final String INIT_PARAM = "init-param";
    private static final String SERVLET = "servlet";
    private static final String SERVLET_NAME = "servlet-name";
    private static final String SERVLET_MAPPING = "servlet-mapping";
    private static final String SERVLET_CLASS = "servlet-class";
    private static final String URL_PATTERN = "url-pattern";
    private static final String SPI_URL_PATTERN = "/_ah/spi/*";
    private static final String PARAM_NAME = "param-name";
    private static final String PARAM_VALUE = "param-value";
    private static final String SERVICES = "services";
    private static final String SYSTEM_SERVICE_SERVLET = "SystemServiceServlet";
    private static final String SYSTEM_SERVICE_SERVLET_CLASS =
        "com.google.api.server.spi.SystemServiceServlet";

    /**
     * Finds the WebApp node in web.xml document. Then tries to find SystemServiceServlet node and
     * returns it. If not found, returns WebApp node. The returned type is of type Element
     *
     * @return SystemServiceServlet node if found, else WebApp node.
     */
    private Node findSystemServiceServlet(Document doc) {
      Node webAppNode;
      for (webAppNode = doc.getFirstChild(); webAppNode != null;
          webAppNode = webAppNode.getNextSibling()) {

        if (isElementAndNamed(webAppNode, WEB_APP)) {
          break;
        }
      }
      if (webAppNode == null) {
        getLog().info("Not a valid web.xml document");
        return null;
      }

      Node systemServiceServletNode;
      for (systemServiceServletNode = webAppNode.getFirstChild();
          systemServiceServletNode != null;
          systemServiceServletNode = systemServiceServletNode.getNextSibling()) {
        if (isElementAndNamed(systemServiceServletNode, SERVLET)) {
          for (Node n3 = systemServiceServletNode.getFirstChild();
              n3 != null; n3 = n3.getNextSibling()) {
            if (isElementAndNamed(n3, SERVLET_NAME)
                && n3.getTextContent().equals(SYSTEM_SERVICE_SERVLET)) {

              return systemServiceServletNode;
            }
          }
        }
      }
      return webAppNode;
    }

    /**
     * Insert a SystemServiceServlet node in web.xml inside webApp node.
     *
     * @return The inserted SystemServiceServlet node.
     */
    private Node insertSystemServiceServlet(Document doc, Node webAppNode, String spc,
        String delimiter) {
      Node n2, n3, n4, n5;
      n5 = doc.createTextNode(spc);
      webAppNode.appendChild(n5);
      n2 = doc.createElement(SERVLET);
      webAppNode.appendChild(n2);
      n5 = doc.createTextNode(delimiter + spc);
      webAppNode.appendChild(n5);
      n3 = doc.createElement(SERVLET_MAPPING);
      webAppNode.appendChild(n3);
      n5 = doc.createTextNode(delimiter);
      webAppNode.appendChild(n5);

      n5 = doc.createTextNode("\n" + spc + spc);
      n2.appendChild(n5);
      n5 = doc.createElement(SERVLET_NAME);
      n5.setTextContent(SYSTEM_SERVICE_SERVLET);
      n2.appendChild(n5);
      n5 = doc.createTextNode("\n" + spc + spc);
      n2.appendChild(n5);
      n5 = doc.createElement(SERVLET_CLASS);
      n5.setTextContent(SYSTEM_SERVICE_SERVLET_CLASS);
      n2.appendChild(n5);
      n5 = doc.createTextNode("\n" + spc + spc);
      n2.appendChild(n5);
      n4 = doc.createElement(INIT_PARAM);
      n2.appendChild(n4);
      n5 = doc.createTextNode("\n" + spc);
      n2.appendChild(n5);

      n5 = doc.createTextNode("\n" + spc + spc);
      n3.appendChild(n5);
      n5 = doc.createElement(SERVLET_NAME);
      n5.setTextContent(SYSTEM_SERVICE_SERVLET);
      n3.appendChild(n5);
      n5 = doc.createTextNode("\n" + spc + spc);
      n3.appendChild(n5);
      n5 = doc.createElement(URL_PATTERN);
      n5.setTextContent(SPI_URL_PATTERN);
      n3.appendChild(n5);
      n5 = doc.createTextNode("\n" + spc);
      n3.appendChild(n5);

      n5 = doc.createTextNode("\n" + spc + spc + spc);
      n4.appendChild(n5);
      n5 = doc.createElement(PARAM_NAME);
      n5.setTextContent(SERVICES);
      n4.appendChild(n5);
      n5 = doc.createTextNode("\n" + spc + spc + spc);
      n4.appendChild(n5);
      n5 = doc.createElement(PARAM_VALUE);
      n5.setTextContent("");
      n4.appendChild(n5);
      n5 = doc.createTextNode("\n" + spc + spc);
      n4.appendChild(n5);

      return n2;
    }

    /**
     * Checks if a node is an XML element and checks if it has a specific name
     *
     * @return true if matching element, false if name doesn't match OR if node type isn't ELEMENT
     */
    private boolean isElementAndNamed(Node node, String name) {
      if (node == null || name == null) {
        throw new IllegalArgumentException();
      }
      return (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals(name));
    }

    private void saveFile(Document doc, String filePath)
        throws TransformerFactoryConfigurationError, TransformerException,
        IOException {
      Transformer transformer = TransformerFactory.newInstance().newTransformer();
      transformer.transform(new DOMSource(doc), new StreamResult(new File(filePath)));
    }

    /**
     * Update the SystemServiceServlet parameter in web.xml, it doesn't make changes if nothing new
     * is to be added. If changes are required, it will modify the file and save
     */
    private boolean updateSystemServiceServletParam(Document doc,
        Node systemServiceServletNode, List<String> services) {
      Node initParamNode;
      for (initParamNode = systemServiceServletNode.getFirstChild();
          initParamNode != null;
          initParamNode = initParamNode.getNextSibling()) {
        if (isElementAndNamed(initParamNode, INIT_PARAM)) {
          break;
        }
      }
      if (initParamNode == null) {
        getLog().info("Not a valid web.xml document");
        return false;
      }

      Node paramValueNode;
      for (paramValueNode = initParamNode.getFirstChild();
          paramValueNode != null;
          paramValueNode = paramValueNode.getNextSibling()) {
        if (isElementAndNamed(paramValueNode, PARAM_VALUE)) {
          break;
        }
      }
      if (paramValueNode == null) {
        getLog().info("Not a valid web.xml document");
        return false;
      }

      // get all services the file currently lists,
      // put it in a treeset for sorted order, also removes duplicates
      String serviceXMLString = paramValueNode.getTextContent();
      Set<String> servicesOnFile = new TreeSet<>();
      if (serviceXMLString != null && !serviceXMLString.trim().isEmpty()) {
        String[] servicesArray = serviceXMLString.split(",");
        for (String s : servicesArray) {
          servicesOnFile.add(s.trim());
        }
      }

      // find all services we need to remove
      List<String> servicesToRemove = new ArrayList<>();
      for (String s : servicesOnFile) {
        if (!services.contains(s)) {
          servicesToRemove.add(s);
        }
      }

      // find all services we need to add
      List<String> servicesToAdd = new ArrayList<>();
      for (String s : services) {
        if (!servicesOnFile.contains(s)) {
          servicesToAdd.add(s);
        }
      }

      // if we don't need to make any changes, then return false
      if (servicesToAdd.isEmpty() && servicesToRemove.isEmpty()) {
        return false;
      }

      // remove those marked for removal
      for (String s : servicesToRemove) {
        servicesOnFile.remove(s);
      }

      // add those marked for adding
      for (String s : servicesToAdd) {
        servicesOnFile.add(s);
      }

      // write the appropriate data to the file
      if (servicesOnFile.isEmpty()) {
        paramValueNode.setTextContent("");
      } else {
        Joiner joiner = Joiner.on(COMMA);
        paramValueNode.setTextContent(joiner.join(servicesOnFile));
      }

      // indicate that a save is required
      return true;
    }

    public void updateWebXml(List<String> services, String webXmlPath)
        throws ParserConfigurationException, SAXException, IOException,
        TransformerFactoryConfigurationError, TransformerException {
      boolean saveRequired;

      String spc = " ";
      String delimiter = "\n";

      DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
      DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
      Document document = docBuilder.parse(webXmlPath);

      Node systemServiceServletNode = findSystemServiceServlet(document);
      if (systemServiceServletNode == null) {
        getLog().info("Not a valid web.xml document");
        return;
      }
      if (isElementAndNamed(systemServiceServletNode, WEB_APP)) {
        systemServiceServletNode = insertSystemServiceServlet(document,
            systemServiceServletNode, spc, delimiter);
        saveRequired = true;
      }

      saveRequired = updateSystemServiceServletParam(document,
          systemServiceServletNode, services);
      String generatedWebInf = outputDirectory + "/WEB-INF";
      new File(generatedWebInf).mkdirs();
      saveFile(document, generatedWebInf + "/web.xml");
      Files.copy(
          new File(new File(webXmlPath).getParentFile(), "appengine-web.xml"),
          new File(generatedWebInf, "appengine-web.xml"));
    }
  }
}