/*
 * Copyright (c) 1998-2007 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *   Free SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Emil Ong
 */

package com.caucho.jaxb;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAnyAttribute;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlID;
import javax.xml.bind.annotation.XmlSchema;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlValue;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventFactory;
import javax.xml.ws.Holder;

import static java.lang.Character.*;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import com.caucho.util.L10N;

/**
 * JAXB utilities.
 */
public class JAXBUtil {
  private static final L10N L = new L10N(JAXBUtil.class);
  private static final Logger log = Logger.getLogger(JAXBUtil.class.getName());

  private static final Map<Class,QName> _datatypeMap
    = new HashMap<Class,QName>();
  private static final Map<QName,Class> _classMap
    = new HashMap<QName,Class>();

  private static DatatypeFactory _datatypeFactory;

  public static final XMLEventFactory EVENT_FACTORY 
    = XMLEventFactory.newInstance();

  public static final String XML_SCHEMA_NS = "http://www.w3.org/2001/XMLSchema";
  public static final String XML_SCHEMA_PREFIX = "xsd";

  public static DatatypeFactory getDatatypeFactory()
    throws JAXBException
  {
    if (_datatypeFactory == null) {
      try {
        _datatypeFactory = DatatypeFactory.newInstance();
      }
      catch (DatatypeConfigurationException e) {
        throw new JAXBException(e);
      }
    }

    return _datatypeFactory;
  }

  public static QName qnameFromNode(Node node)
  {
    if (node == null)
      return null;

    int colon = node.getNodeName().indexOf(':');

    if (colon > 0) { 
      String prefix = node.getNodeName().substring(0, colon);
      String localName = node.getNodeName().substring(colon + 1);

      return new QName(node.getNamespaceURI(), localName, prefix);
    } 
    else if (node.getNamespaceURI() != null)
      return new QName(node.getNamespaceURI(), node.getNodeName());
    else
      return new QName(node.getNodeName());
  }

  public static Element elementFromQName(QName name, Node node)
  {
    Document doc = node.getOwnerDocument();

    if (name.getPrefix() != null)
      return doc.createElementNS(name.getNamespaceURI(),
                                 name.getPrefix() + ':' + name.getLocalPart());
    else if (name.getNamespaceURI() != null)
      return doc.createElementNS(name.getNamespaceURI(), name.getLocalPart());
    else
      return doc.createElement(name.getLocalPart());
  }

  // skip all the whitespace and comments
  public static Node skipIgnorableNodes(Node node) 
  {
    while (node != null) {
      if (node.getNodeType() == Node.TEXT_NODE) {
        String text = node.getTextContent();

        boolean whitespace = true;

        for (int i = 0; i < text.length(); i++) {
          if (! Character.isWhitespace(text.charAt(i))) {
            whitespace = false;
            break;
          }
        }

        if (! whitespace)
          break;
      }
      else if (node.getNodeType() != Node.COMMENT_NODE) 
        break;

      node = node.getNextSibling();
    }

    return node;
  }

  /**
   * Gets the type of a parameter.  If the type is something like Holder<T>,
   * it return T, otherwise, it returns the passed type.
   *
   **/
  public static Type getActualParameterType(Type type)
    throws JAXBException
  {
    if (type instanceof ParameterizedType) {
      ParameterizedType ptype = (ParameterizedType) type;

      if (ptype.getRawType().equals(Holder.class)) {
        Type[] arguments = ptype.getActualTypeArguments();

        if (arguments.length != 1)
          throw new JAXBException("Holder has incorrect number of arguments");

        return arguments[0];
      }
    }

    return type;
  }

  public static void introspectClass(Class cl, Collection<Class> jaxbClasses)
    throws JAXBException
  {
    log.finest("Introspecting class " + cl.getName());

    Method[] methods = cl.getMethods();

    for (Method method : methods)
      introspectMethod(method, jaxbClasses);
  }

  /**
   * Finds all the classes mentioned in a method signature (return type and
   * parameters) and adds them to the passed in classList.  Pass in a set if
   * you expect multiple references.
   */
  public static void introspectMethod(Method method, 
                                      Collection<Class> jaxbClasses)
    throws JAXBException
  {
    log.finest("Introspecting method " + method.getName());

    introspectType(method.getReturnType(), jaxbClasses);

    Type[] params = method.getGenericParameterTypes();

    for (Type param : params) {
      if (param.equals(Holder.class))
        continue;

      introspectType(getActualParameterType(param), jaxbClasses);
    }

    // XXX: Check for @WebFault annotation

    /*
    Type[] exceptions = method.getGenericExceptionTypes();

    for (Type exception : exceptions) {
      if (! exception.toString().endsWith("_Exception"))
        introspectType(exception, jaxbClasses);
    }*/
  }

  /**
   * Add all classes referenced by type to jaxbClasses.
   */
  private static void introspectType(Type type, Collection<Class> jaxbClasses)
  {
    log.finest("  Introspecting type " + type);

    if (type instanceof Class) {
      Class cl = (Class) type;

      if (! cl.isInterface())
        jaxbClasses.add((Class) type);
    }
    else if (type instanceof ParameterizedType) {
      ParameterizedType pType = (ParameterizedType) type;

      introspectType(pType.getRawType(), jaxbClasses);
      introspectType(pType.getOwnerType(), jaxbClasses);

      Type[] arguments = pType.getActualTypeArguments();

      for (Type argument : arguments)
        introspectType(argument, jaxbClasses);
    }
    else if (type instanceof GenericArrayType) {
      Type component = ((GenericArrayType) type).getGenericComponentType();
      introspectType(component, jaxbClasses);
    }
    else if (type != null) {
      // Type variables must be instantiated
      throw new UnsupportedOperationException(L.l("Method arguments cannot have uninstantiated type variables or wildcards ({0} of type {1})", type, type.getClass().getName()));
    }
  }

  public static String primitiveToWrapperName(Class cl)
  {
    if (cl.getName().equals("int"))
      return "Integer";
    else if (cl.getName().equals("char"))
      return "Character";
    else
      return toUpperCase(cl.getName().charAt(0)) + cl.getName().substring(1);
  }

  /**
   * Tests for punctuation according to JAXB page 334.
   */
  private static boolean isPunctuation(char ch)
  {
    return "-.:\u00B7\u0387\u06DD\u06dd\u06de_".indexOf((int) ch) >= 0;
  }

  /**
   * Tests for "uncased" characters.
   */
  private static boolean isUncased(char ch)
  {
    return (! isLowerCase(ch)) && (! isUpperCase(ch));
  }

  /**
   * Splits a string into XML "words" as defined by the JAXB standard.
   * (see page 162 and appendix D)
   *
   */
  private static List<StringBuilder> splitIdentifier(String identifier)
  {
    List<StringBuilder> words = new ArrayList<StringBuilder>();
    StringBuilder word = new StringBuilder();
    char lastCh = 0;

    for (int i = 0; i < identifier.length(); i++) {
      char ch = identifier.charAt(i);

      // punctuation shouldn't be common for the java -> xml direction
      if (word.length() > 0 && isPunctuation(ch)) {
        words.add(word);
        word = new StringBuilder();
      } 
      else if (isDigit(ch)) {
        if (word.length() > 0 && ! isDigit(lastCh)) {
          words.add(word);
          word = new StringBuilder();
        }

        word.append(ch);
      }
      else if (i > 0) { // all of the following need lastCh
        if (isLowerCase(lastCh) && isUpperCase(ch)) {
          words.add(word);
          word = new StringBuilder();
          word.append(ch);
        }
        else if (isUpperCase(lastCh) && isLowerCase(ch)) {
          // need to steal the last character from the current word 
          // for the next word (e.g. FOOBar -> { "FOO", "Bar" })

          if (word.length() > 1) {
            word.deleteCharAt(word.length() - 1);
            words.add(word);
          }

          word = new StringBuilder();
          word.append(lastCh);
          word.append(ch);
        }
        else if (isLetter(lastCh) != isLetter(ch)) {
          words.add(word);
          word = new StringBuilder();
          word.append(ch);
        }
        else if (isUncased(lastCh) != isUncased(ch)) {
          words.add(word);
          word = new StringBuilder();
          word.append(ch);
        }
        else
          word.append(ch);
      }
      else
        word.append(ch);

      lastCh = ch;
    }

    if (word.length() > 0)
      words.add(word);

    return words;
  }

  public static String identifierToXmlName(Class cl)
  {
    List<StringBuilder> words = splitIdentifier(cl.getSimpleName());
    StringBuilder xmlName = new StringBuilder();

    xmlName.append(toLowerCase(words.get(0).charAt(0)));
    xmlName.append(words.get(0).substring(1));

    for (int i = 1; i < words.size(); i++) {
      if (words.get(i).length() > 0) {
        xmlName.append(toUpperCase(words.get(i).charAt(0)));
        xmlName.append(words.get(i).substring(1));
      }
    }

    return xmlName.toString();
  }

  public static String xmlNameToClassName(String name)
  {
    // XXX FIXME

    if (name.length() > 1)
      return toUpperCase(name.charAt(0)) + name.substring(1);
    else
      return toUpperCase(name.charAt(0)) + "";
  }

  /// XXX this is sooooooooo wrong.  It belongs in JAXBContextImpl
  public static QName getXmlSchemaDatatype(Class cl, JAXBContextImpl context)
  {
    if (_datatypeMap.containsKey(cl))
      return _datatypeMap.get(cl);

    String name = null;
    String namespace = context.getTargetNamespace();

    Package pkg = cl.getPackage();

    // look at package defaults first...
    XmlSchema schema = (XmlSchema) pkg.getAnnotation(XmlSchema.class);

    if (schema != null && ! "".equals(schema.namespace()))
      namespace = schema.namespace();

    if (cl.isAnnotationPresent(XmlType.class)) {
      XmlType xmlType = (XmlType) cl.getAnnotation(XmlType.class);

      if (! "##default".equals(xmlType.name()))
        name = xmlType.name();

      if (! "##default".equals(xmlType.namespace()))
        namespace = xmlType.namespace();
    }

    if (name == null)
      name = identifierToXmlName(cl);

    QName qname = null;
    
    if (namespace == null)
      qname = new QName(name);
    else
      qname = new QName(namespace, name);

    _datatypeMap.put(cl, qname);

    return qname;
  }

  public static Class getClassForDatatype(QName qname)
  {
    return _classMap.get(qname);
  }

  public static String qNameToString(QName qName)
  {
    if (qName.getPrefix() == null || "".equals(qName.getPrefix()))
      return qName.getLocalPart();
    else
      return qName.getPrefix() + ':' + qName.getLocalPart();
  }

  public static boolean isJAXBAnnotated(AnnotatedElement element)
  {
    return element.isAnnotationPresent(XmlAnyAttribute.class) ||
           element.isAnnotationPresent(XmlAnyElement.class) ||
           element.isAnnotationPresent(XmlAttribute.class) ||
           element.isAnnotationPresent(XmlElement.class) ||
           element.isAnnotationPresent(XmlElements.class) ||
           element.isAnnotationPresent(XmlElementRef.class) ||
           element.isAnnotationPresent(XmlID.class) ||
           element.isAnnotationPresent(XmlValue.class);
  }

  static {
    QName stringQName = new QName(XML_SCHEMA_NS, "string", XML_SCHEMA_PREFIX);
    _datatypeMap.put(String.class, stringQName);
    _classMap.put(stringQName, String.class);

    QName bigDecimalQName = 
      new QName(XML_SCHEMA_NS, "decimal", XML_SCHEMA_PREFIX);
    _datatypeMap.put(BigDecimal.class, bigDecimalQName);
    _classMap.put(bigDecimalQName, BigDecimal.class);

    QName bigIntegerQName = 
      new QName(XML_SCHEMA_NS, "integer", XML_SCHEMA_PREFIX);
    _datatypeMap.put(BigInteger.class, bigIntegerQName);
    _classMap.put(bigIntegerQName, BigInteger.class);

    QName booleanQName = new QName(XML_SCHEMA_NS, "boolean", XML_SCHEMA_PREFIX);
    _datatypeMap.put(Boolean.class, booleanQName);
    _datatypeMap.put(boolean.class, booleanQName);
    _classMap.put(booleanQName, boolean.class);

    // XXX hexBinary
    QName base64BinaryQName = 
      new QName(XML_SCHEMA_NS, "base64Binary", XML_SCHEMA_PREFIX);
    _datatypeMap.put(Byte[].class, base64BinaryQName); 
    _datatypeMap.put(byte[].class, base64BinaryQName);
    _classMap.put(base64BinaryQName, byte[].class);

    QName byteQName = new QName(XML_SCHEMA_NS, "byte", XML_SCHEMA_PREFIX);
    _datatypeMap.put(Byte.class, byteQName);
    _datatypeMap.put(byte.class, byteQName);
    _classMap.put(byteQName, byte.class);

    QName charQName = 
      new QName(XML_SCHEMA_NS, "unsignedShort", XML_SCHEMA_PREFIX);
    _datatypeMap.put(Character.class, charQName);
    _datatypeMap.put(char.class, charQName);
    _classMap.put(charQName, char.class);

    // XXX time
    QName calendarQName = new QName(XML_SCHEMA_NS, "date", XML_SCHEMA_PREFIX);
    _datatypeMap.put(Calendar.class, calendarQName);
    _classMap.put(calendarQName, Calendar.class);

    QName dateTimeQName = 
      new QName(XML_SCHEMA_NS, "dateTime", XML_SCHEMA_PREFIX);
    _datatypeMap.put(XMLGregorianCalendar.class, dateTimeQName);
    _classMap.put(dateTimeQName, XMLGregorianCalendar.class);

    QName doubleQName = new QName(XML_SCHEMA_NS, "double", XML_SCHEMA_PREFIX);
    _datatypeMap.put(Double.class, doubleQName);
    _datatypeMap.put(double.class, doubleQName);
    _classMap.put(doubleQName, double.class);

    QName floatQName = new QName(XML_SCHEMA_NS, "float", XML_SCHEMA_PREFIX);
    _datatypeMap.put(Float.class, floatQName); 
    _datatypeMap.put(float.class, floatQName);
    _classMap.put(floatQName, float.class);

    QName intQName = new QName(XML_SCHEMA_NS, "int", XML_SCHEMA_PREFIX);
    _datatypeMap.put(Integer.class, intQName);
    _datatypeMap.put(int.class, intQName);
    _classMap.put(intQName, int.class);

    QName longQName = new QName(XML_SCHEMA_NS, "long", XML_SCHEMA_PREFIX);
    _datatypeMap.put(Long.class, longQName);
    _datatypeMap.put(long.class, longQName);
    _classMap.put(longQName, long.class);

    QName shortQName = new QName(XML_SCHEMA_NS, "short", XML_SCHEMA_PREFIX);
    _datatypeMap.put(Short.class, shortQName);
    _datatypeMap.put(short.class, shortQName);
    _classMap.put(shortQName, short.class);
  }
}
