/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.apphosting.utils.config;

import com.google.appengine.repackaged.com.google.common.io.Files;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.SchemaFactory;

/**
 * Utility functions for processing XML.
 */
public class XmlUtils {

  /* Returns the trimmed text from the passed XmlParser.Node in node or an empty
   * if the passed in node does not contain any text.
   */
  static String getText(Element node) throws AppEngineConfigException {
    String content = node.getTextContent();
    if (content == null) {
      return "";
    }
    return content.trim();
  }

  static Document parseXml(InputStream inputStream) {
    return parseXml(inputStream, null);
  }

  static Document parseXml(InputStream inputStream, String filename) {
    try {
      DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
      DocumentBuilder docBuilder = dbFactory.newDocumentBuilder();
      Document doc = docBuilder.parse(inputStream);
      doc.getDocumentElement().normalize();
      return doc;
    } catch (IOException e) {
      String msg = "Received IOException parsing the input stream" + maybeFilename(filename);
      throw new AppEngineConfigException(msg, e);
    } catch (SAXException e) {
      String msg = "Received SAXException parsing the input stream" + maybeFilename(filename);
      throw new AppEngineConfigException(msg, e);
    } catch (ParserConfigurationException e) {
      String msg =
          "Received ParserConfigurationException parsing the input stream"
              + maybeFilename(filename);
      throw new AppEngineConfigException(msg, e);
    }
  }

  private static String maybeFilename(String filename) {
    if (filename == null) {
      return ".";
    } else {
      return " for " + filename;
    }
  }

  /**
   * Validates a given XML document against a given schema.
   *
   * @param xmlFilename filename with XML document.
   * @param schema XSD schema to validate with.
   *
   * @throws AppEngineConfigException for malformed XML, or IO errors
   */
  public static void validateXml(String xmlFilename, File schema) {
    File xml = new File(xmlFilename);
    if (!xml.exists()) {
      throw new AppEngineConfigException("Xml file: " + xml.getPath() + " does not exist.");
    }
    if (!schema.exists()) {
      throw new AppEngineConfigException("Schema file: " + schema.getPath() + " does not exist.");
    }
    try {
      validateXmlContent(Files.toString(xml, StandardCharsets.UTF_8), schema);
    } catch (IOException ex) {
      throw new AppEngineConfigException(
          "IO error validating " + xmlFilename + " against " + schema.getPath(), ex);
    }
  }

  /**
   * Validates a given XML document against a given schema.
   *
   * @param content a String containing the entire XML to validate.
   * @param schema XSD schema to validate with.
   *
   * @throws AppEngineConfigException for malformed XML, or IO errors
   */
  static void validateXmlContent(String content, File schema) {
    if (!schema.exists()) {
      throw new AppEngineConfigException("Schema file: " + schema.getPath() + " does not exist.");
    }
    try {
      SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
      try {
        factory
            .newSchema(schema)
            .newValidator()
            .validate(
                new StreamSource(
                    new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))));
      } catch (SAXException ex) {
        throw new AppEngineConfigException(
            "XML error validating " + content + " against " + schema.getPath(), ex);
      }
    } catch (IOException ex) {
      throw new AppEngineConfigException(
          "IO error validating " + content + " against " + schema.getPath(), ex);
    }
  }

  static String getRequiredChildElementBody(Element element, String tagName) {
    return getChildElementBody(element, tagName, true);
  }

  static String getOptionalChildElementBody(Element element, String tagName) {
    return getChildElementBody(element, tagName, false);
  }

  static String getChildElementBody(Element element, String tagName, boolean required) {
    Element elt = getChildElement(element, tagName, required);
    if (elt == null) {
      return null;
    }
    String result = getText(elt);
    return result.isEmpty() ? null : result;
  }

  static Element getOptionalChildElement(Element parent, String tagName) {
    return getChildElement(parent, tagName, false);
  }

  static Element getChildElement(Element parent, String tagName, boolean required) {
    NodeList nodes = parent.getElementsByTagName(tagName);
    if (nodes == null || nodes.getLength() == 0) {
      if (required) {
        throw new IllegalStateException(
            String.format("Missing tag %s in element %s.", tagName, parent));
      } else {
        return null;
      }
    }
    return (Element) nodes.item(0);
  }

  static String getAttributeOrNull(Element element, String name) {
    if (!element.hasAttribute(name)) {
      return null;
    } else {
      return element.getAttribute(name);
    }
  }

  static List<Element> getChildren(Element element) {
    return getChildren(element, null);
  }

  /**
   * Returns the immediate children of the given element that have the given {@code tagName}.
   * If the {@code tagName} is null, all immediate children are returned.
   */
  static List<Element> getChildren(Element element, String tagName) {
    NodeList nodes = element.getChildNodes();

    List<Element> elements = new ArrayList<>(nodes.getLength());
    for (int i = 0; i < nodes.getLength(); i++) {
      Node item = nodes.item(i);
      if (item instanceof Element) {
        Element itemElement = (Element) item;
        if (tagName == null || tagName.equals(itemElement.getTagName())) {
          elements.add(itemElement);
        }
      }
    }
    return elements;
  }
}