/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.myfaces.tobago.util;

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

import javax.faces.application.ViewHandler;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.servlet.ServletContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class WebXmlUtils {

  private static final Map<Class<Throwable>, String> ERROR_PAGE_LOCATIONS = new HashMap<>();

  public static String getErrorPageLocation(final Throwable exception) {
    if (ERROR_PAGE_LOCATIONS.size() <= 0) {
      init();
    }

    String location = null;

    Class<?> exceptionClass = exception.getClass();
    while (exceptionClass != null && location == null) {
      location = ERROR_PAGE_LOCATIONS.get(exceptionClass);
      exceptionClass = exceptionClass.getSuperclass();
    }

    if (location == null) {
      location = ERROR_PAGE_LOCATIONS.get(null);
    }

    return location;
  }

  private static void init() {
    final FacesContext facesContext = FacesContext.getCurrentInstance();
    final ExternalContext externalContext = facesContext.getExternalContext();

    try {
      final List<Document> webXmls = getWebXmls(facesContext);

      String locationDefault = null;
      String location500 = null;

      for (final Document document : webXmls) {
        final NodeList errorPages = document.getElementsByTagName("error-page");

        for (int i = 0; i < errorPages.getLength(); i++) {
          final Node errorPage = errorPages.item(i);

          String errorCode = null;
          String exceptionType = null;
          String location = null;

          final NodeList children = errorPage.getChildNodes();
          for (int j = 0; j < children.getLength(); j++) {
            final Node child = children.item(j);
            final String name = child.getNodeName();

            if ("error-code".equals(name)) {
              errorCode = child.getFirstChild().getNodeValue().trim();
            } else if ("exception-type".equals(name)) {
              exceptionType = child.getFirstChild().getNodeValue().trim();
            } else if ("location".equals(name)) {
              location = child.getFirstChild().getNodeValue().trim();
            }
          }

          if (exceptionType != null) {
            final Class<Throwable> key = (Class<Throwable>) Class.forName(exceptionType);
            final String value = normalizePath(externalContext, location);
            ERROR_PAGE_LOCATIONS.put(key, value);
          } else if ("500".equals(errorCode)) {
            location500 = location;
          } else if (errorCode == null && exceptionType == null) {
            locationDefault = location;
          }
        }
      }

      if (!ERROR_PAGE_LOCATIONS.containsKey(null)) {
        final String value = normalizePath(externalContext, location500 != null ? location500 : locationDefault);
        ERROR_PAGE_LOCATIONS.put(null, value);
      }
    } catch (IOException | ParserConfigurationException | ClassNotFoundException | SAXException e) {
      throw new UnsupportedOperationException(e);
    }
  }

  private static List<Document> getWebXmls(final FacesContext facesContext)
      throws ParserConfigurationException, IOException, SAXException {
    final List<Document> webXmls = new ArrayList<>();

    final DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
    for (final URL url : getWebXmlUrls(facesContext)) {
      webXmls.add(getWebXml(documentBuilder, url));
    }

    return webXmls;
  }

  private static List<URL> getWebXmlUrls(final FacesContext facesContext) throws IOException {
    final List<URL> urls = new ArrayList<>();
    final ServletContext servletContext = (ServletContext) facesContext.getExternalContext().getContext();
    urls.add(servletContext.getResource("/WEB-INF/web.xml"));

    final Enumeration<URL> webFragments = Thread.currentThread().getContextClassLoader()
        .getResources("META-INF/web-fragment.xml");
    while (webFragments.hasMoreElements()) {
      urls.add(webFragments.nextElement());
    }

    return urls;
  }

  private static Document getWebXml(final DocumentBuilder documentBuilder, final URL url)
      throws ParserConfigurationException, IOException, SAXException {
    if (url != null) {
      final URLConnection connection = url.openConnection();
      connection.setUseCaches(false);

      try (final InputStream input = connection.getInputStream()) {
        final Document document = documentBuilder.parse(input);
        document.getDocumentElement().normalize();
        return document;
      }
    } else {
      return null;
    }
  }

  private static String normalizePath(final ExternalContext externalContext, final String path) {
    if (path == null) {
      return null;
    }

    if (externalContext.getRequestPathInfo() != null) {
      final String prefix = externalContext.getRequestServletPath();
      if (path.startsWith(prefix)) {
        return path.substring(prefix.length());
      } else {
        return path;
      }
    } else {
      final String suffixInitParam = externalContext.getInitParameter(ViewHandler.FACELETS_SUFFIX_PARAM_NAME);
      final String suffix = suffixInitParam != null ? suffixInitParam : ViewHandler.DEFAULT_FACELETS_SUFFIX;

      if (path.endsWith(suffix)) {
        return path;
      } else {
        return path.substring(0, path.lastIndexOf('.')) + suffix;
      }
    }
  }
}
