/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.xml.binding;

import org.jboss.util.Classes;

/**
 * Various utilities for XML binding.
 *
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 * @version <tt>$Revision: 1.1.2.6 $</tt>
 */
public final class Util
{
   /**
    * Characters that are considered to be word separators while convertinging XML names to Java identifiers
    * according to JAXB 2.0 spec.
    */
   public static final char HYPHEN_MINUS = '\u002D';
   public static final char FULL_STOP = '\u002E';
   public static final char COLLON = '\u003A';
   public static final char LOW_LINE = '\u005F';
   public static final char MIDDLE_DOT = '\u00B7';
   public static final char GREEK_ANO_TELEIA = '\u0387';
   public static final char ARABIC_END_OF_AYAH = '\u06DD';
   public static final char ARABIC_START_OF_RUB_EL_HIZB = '\u06DE';

   /**
    * Converts XML name to Java class name according to
    * Binding XML Names to Java Identifiers
    * C.2. The Name to Identifier Mapping Algorithm
    * jaxb-2_0-edr-spec-10_jun_2004.pdf
    *
    * @param name          XML name
    * @param ignoreLowLine whether low lines should not be parts of Java identifiers
    * @return Java class name
    */
   public static String xmlNameToClassName(String name, boolean ignoreLowLine)
   {
      return XMLNameToJavaIdentifierConverter.PARSER.parse(XMLNameToJavaIdentifierConverter.CLASS_NAME,
         name,
         ignoreLowLine
      );
   }

   /**
    * Converts XML name to Java getter method name according to
    * Binding XML Names to Java Identifiers
    * C.2. The Name to Identifier Mapping Algorithm
    * jaxb-2_0-edr-spec-10_jun_2004.pdf
    *
    * @param name          XML name
    * @param ignoreLowLine whether low lines should not be parts of Java identifiers
    * @return Java getter method name
    */
   public static String xmlNameToGetMethodName(String name, boolean ignoreLowLine)
   {
      return "get" + xmlNameToClassName(name, ignoreLowLine);
   }

   /**
    * Converts XML name to Java setter method name according to
    * Binding XML Names to Java Identifiers
    * C.2. The Name to Identifier Mapping Algorithm
    * jaxb-2_0-edr-spec-10_jun_2004.pdf
    *
    * @param name          XML name
    * @param ignoreLowLine whether low lines should not be parts of Java identifiers
    * @return Java setter method name
    */
   public static String xmlNameToSetMethodName(String name, boolean ignoreLowLine)
   {
      return "set" + xmlNameToClassName(name, ignoreLowLine);
   }

   /**
    * Converts XML name to Java constant name according to
    * Binding XML Names to Java Identifiers
    * C.2. The Name to Identifier Mapping Algorithm
    * jaxb-2_0-edr-spec-10_jun_2004.pdf
    *
    * @param name XML name
    * @return Java constant name
    */
   public static String xmlNameToConstantName(String name)
   {
      return XMLNameToJavaIdentifierConverter.PARSER.parse(XMLNameToJavaIdentifierConverter.CONSTANT_NAME,
         name,
         true
      );
   }

   /**
    * Converts XML namespace to Java package name.
    * The base algorithm is described in JAXB-2.0 spec in 'C.5 Generating a Java package name'.
    *
    * @param namespace XML namespace
    * @return Java package name
    */
   public static String xmlNamespaceToJavaPackage(String namespace)
   {
      if(namespace.length() == 0)
      {
         return namespace;
      }

      char[] src = namespace.toLowerCase().toCharArray();
      char[] dst = new char[namespace.length()];

      int srcInd = 0;
      // skip protocol part, i.e. http://, urn://
      while(src[srcInd++] != ':')
      {
         ;
      }

      while(src[srcInd] == '/')
      {
         ++srcInd;
      }

      // skip www part
      if(src[srcInd] == 'w' && src[srcInd + 1] == 'w' && src[srcInd + 2] == 'w')
      {
         srcInd += 4;
      }

      // find domain start and end indexes
      int domainStart = srcInd;
      while(srcInd < src.length && src[srcInd] != '/')
      {
         ++srcInd;
      }

      int dstInd = 0;
      // copy domain parts in the reverse order
      for(int start = srcInd - 1, end = srcInd; true; --start)
      {
         if(start == domainStart)
         {
            System.arraycopy(src, start, dst, dstInd, end - start);
            dstInd += end - start;
            break;
         }

         if(src[start] == '.')
         {
            System.arraycopy(src, start + 1, dst, dstInd, end - start - 1);
            dstInd += end - start;
            dst[dstInd - 1] = '.';
            end = start;
         }
      }

      // copy the rest
      while(srcInd < src.length)
      {
         char c = src[srcInd++];
         if(c == '/')
         {
            if(srcInd < src.length)
            {
               dst = append(dst, dstInd++, '.');
               if(!Character.isJavaIdentifierStart(src[srcInd]))
               {
                  dst = append(dst, dstInd++, '_');
               }
            }
         }
         else if(c == '.')
         {
            // for now assume it's an extention, i.e. '.xsd'
            break;
         }
         else
         {
            dst = append(dst, dstInd++, Character.isJavaIdentifierPart(c) ? c : '_');
         }
      }

      return String.valueOf(dst, 0, dstInd);
   }

   /**
    * Converts XML namespace URI and local name to fully qualified class name.
    *
    * @param namespaceUri  namespace URI
    * @param localName     local name
    * @param ignoreLowLine should low lines be ignored in the class name
    * @return fully qualified class name
    */
   public static String xmlNameToClassName(String namespaceUri, String localName, boolean ignoreLowLine)
   {
      return namespaceUri == null || namespaceUri.length() == 0 ?
         xmlNameToClassName(localName, ignoreLowLine) :
         xmlNamespaceToJavaPackage(namespaceUri) + '.' + xmlNameToClassName(localName, ignoreLowLine);
   }

   public static boolean isAttributeType(final Class type)
   {
      return Classes.isPrimitive(type) ||
         type == String.class ||
         type == java.util.Date.class;
   }

   // Private

   /**
    * Sets an array character's element with the given index to a character value.
    * If index is more or equal to the length of the array, a new array is created with enough length to set
    * the element.
    *
    * @param buf   array of characters
    * @param index index of the element to set
    * @param ch    character to set
    * @return if the index parameter is less then array's length then the original array is returned,
    *         otherwise a new array is returned
    */
   private static char[] append(char[] buf, int index, char ch)
   {
      if(index >= buf.length)
      {
         char[] tmp = buf;
         buf = new char[index + 4];
         System.arraycopy(tmp, 0, buf, 0, tmp.length);
      }
      buf[index] = ch;
      return buf;
   }

   // Inner

   /**
    * An interface for XML name to Java identifier (class name, get/set methods, constant names) converter.
    * The following rules and algorithms should be supported
    * <ul>
    * <li>Binding XML Names to Java Identifiers,
    * C.2. The Name to Identifier Mapping Algorithm,
    * jaxb-2_0-edr-spec-10_jun_2004.pdf</li>
    * <li>http://www.w3.org/TR/soap12-part2/#namemap</li>
    * </ul>
    * <p/>
    * But these are not guaranteed to work yet. Instead, a simplified implementation is provided.
    * Incompatabilities should be fixed.
    */
   interface XMLNameToJavaIdentifierConverter
   {
      // commands indicating what should be done with the next character from the XML name
      byte IGNORE = 0;
      byte APPEND = 1;
      byte APPEND_WITH_LOW_LINE = 2;
      byte APPEND_UPPER_CASED = 3;
      byte APPEND_UPPER_CASED_WITH_LOW_LINE = 4;

      /**
       * Returns a command for the next character given the previous character.
       *
       * @param prev          previous character
       * @param next          next character
       * @param ignoreLowLine whether low lines are allowed in the Java identifier or should be ignored
       * @return command for the next character
       */
      byte commandForNext(char prev, char next, boolean ignoreLowLine);

      /**
       * An XML name parser class that parses the XML name and asks the outer interface implementation
       * what to do with the next parsed character from the XML name.
       */
      final class PARSER
      {
         /**
          * Parses an XML name, asks the converter for a command for the next parsed character,
          * applies the command, composed the resulting Java identifier.
          *
          * @param converter     an implementation of XMLNameToJavaIdentifierConverter
          * @param xmlName       XML name
          * @param ignoreLowLine indicated whether low lines are allowed as part of the Java identifier or
          *                      should be ignored
          * @return Java identifier
          */
         static String parse(XMLNameToJavaIdentifierConverter converter, String xmlName, boolean ignoreLowLine)
         {
            if(xmlName == null || xmlName.length() == 0)
            {
               throw new IllegalArgumentException("Bad XML name: " + xmlName);
            }

            char c = xmlName.charAt(0);
            int i = 1;
            if(!Character.isJavaIdentifierStart(c) || (c == LOW_LINE && ignoreLowLine))
            {
               while(i < xmlName.length())
               {
                  c = xmlName.charAt(i++);
                  if(Character.isJavaIdentifierStart(c) && !(c == LOW_LINE && ignoreLowLine))
                  {
                     break;
                  }
               }

               if(i == xmlName.length())
               {
                  throw new IllegalArgumentException(
                     "XML name contains no valid character to start Java identifier: " + xmlName
                  );
               }
            }

            char[] buf = new char[xmlName.length() - i + 1];
            buf[0] = Character.toUpperCase(c);
            int bufInd = 1;
            while(i < xmlName.length())
            {
               char prev = c;
               c = xmlName.charAt(i++);
               byte command = converter.commandForNext(prev, c, ignoreLowLine);
               switch(command)
               {
                  case IGNORE:
                     break;
                  case APPEND:
                     buf = Util.append(buf, bufInd++, c);
                     break;
                  case APPEND_WITH_LOW_LINE:
                     buf = Util.append(buf, bufInd++, LOW_LINE);
                     buf = Util.append(buf, bufInd++, c);
                     break;
                  case APPEND_UPPER_CASED:
                     buf = Util.append(buf, bufInd++, Character.toUpperCase(c));
                     break;
                  case APPEND_UPPER_CASED_WITH_LOW_LINE:
                     buf = Util.append(buf, bufInd++, LOW_LINE);
                     buf = Util.append(buf, bufInd++, Character.toUpperCase(c));
                     break;
                  default:
                     throw new IllegalArgumentException("Unexpected command: " + command);
               }
            }

            return new String(buf, 0, bufInd);
         }
      }

      /**
       * XML name to Java class name converter
       */
      XMLNameToJavaIdentifierConverter CLASS_NAME = new XMLNameToJavaIdentifierConverter()
      {
         public byte commandForNext(char prev, char next, boolean ignoreLowLine)
         {
            byte command;
            if(Character.isDigit(next))
            {
               command = APPEND;
            }
            else if(next == LOW_LINE)
            {
               command = ignoreLowLine ? IGNORE : APPEND;
            }
            else if(Character.isJavaIdentifierPart(next))
            {
               if(Character.isJavaIdentifierPart(prev) && !Character.isDigit(prev))
               {
                  command = prev == LOW_LINE ? APPEND_UPPER_CASED : APPEND;
               }
               else
               {
                  command = APPEND_UPPER_CASED;
               }
            }
            else
            {
               command = IGNORE;
            }
            return command;
         }
      };

      /**
       * XML name to Java constant name converter
       */
      XMLNameToJavaIdentifierConverter CONSTANT_NAME = new XMLNameToJavaIdentifierConverter()
      {
         public byte commandForNext(char prev, char next, boolean ignoreLowLine)
         {
            byte command;
            if(Character.isDigit(next))
            {
               command = Character.isDigit(prev) ? APPEND : APPEND_UPPER_CASED_WITH_LOW_LINE;
            }
            else if(Character.isJavaIdentifierPart(next))
            {
               if(Character.isDigit(prev))
               {
                  command = APPEND_UPPER_CASED_WITH_LOW_LINE;
               }
               else if(Character.isJavaIdentifierPart(prev))
               {
                  command = Character.isUpperCase(next) ? APPEND_WITH_LOW_LINE : APPEND_UPPER_CASED;
               }
               else
               {
                  command = APPEND_UPPER_CASED_WITH_LOW_LINE;
               }
            }
            else
            {
               command = IGNORE;
            }
            return command;
         }
      };
   }
}
