/*
 * Copyright (c) 2006-2008, Dennis M. Sosnoski All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this list of conditions and the following
 * disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 * following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of
 * JiBX nor the names of its contributors may be used to endorse or promote products derived from this software without
 * specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.jibx.schema.codegen;

import java.util.ArrayList;
import java.util.HashSet;

/**
 * Utility methods for working with Java names.
 * 
 * @author Dennis M. Sosnoski
 */
public class NameConverter
{
    /** Reserved words for Java (keywords and literals). */
    private static final HashSet s_reservedWords = new HashSet();
    static {
        
        // keywords
        s_reservedWords.add("abstract");
        s_reservedWords.add("assert");
        s_reservedWords.add("boolean");
        s_reservedWords.add("break");
        s_reservedWords.add("byte");
        s_reservedWords.add("case");
        s_reservedWords.add("catch");
        s_reservedWords.add("char");
        s_reservedWords.add("class");
        s_reservedWords.add("const");
        s_reservedWords.add("continue");
        
        s_reservedWords.add("default");
        s_reservedWords.add("do");
        s_reservedWords.add("double");
        s_reservedWords.add("else");
        s_reservedWords.add("enum");
        s_reservedWords.add("extends");
        s_reservedWords.add("final");
        s_reservedWords.add("finally");
        s_reservedWords.add("float");
        s_reservedWords.add("for");
        s_reservedWords.add("goto");
        
        s_reservedWords.add("if");
        s_reservedWords.add("implements");
        s_reservedWords.add("import");
        s_reservedWords.add("instanceof");
        s_reservedWords.add("int");
        s_reservedWords.add("interface");
        s_reservedWords.add("long");
        s_reservedWords.add("native");
        s_reservedWords.add("new");
        s_reservedWords.add("package");
        
        s_reservedWords.add("private");
        s_reservedWords.add("protected");
        s_reservedWords.add("public");
        s_reservedWords.add("return");
        s_reservedWords.add("short");
        s_reservedWords.add("static");
        s_reservedWords.add("strictfp");
        s_reservedWords.add("super");
        s_reservedWords.add("switch");
        s_reservedWords.add("synchronized");
        
        s_reservedWords.add("this");
        s_reservedWords.add("throw");
        s_reservedWords.add("throws");
        s_reservedWords.add("transient");
        s_reservedWords.add("try");
        s_reservedWords.add("void");
        s_reservedWords.add("volatile");
        s_reservedWords.add("while");
        
        // literals
        s_reservedWords.add("true");
        s_reservedWords.add("false");
        s_reservedWords.add("null");
    }
    
    /** Camelcase field names flag. */
    private boolean m_camelCase;
    
    /** Prefix used for normal field names (non-<code>null</code>, may be empty). */
    private String m_fieldPrefix;
    
    /** Suffix used for normal field names (non-<code>null</code>, may be empty). */
    private String m_fieldSuffix;
    
    /** Prefix used for static field names (non-<code>null</code>, may be empty). */
    private String m_staticPrefix;
    
    /** Suffix used for static field names (non-<code>null</code>, may be empty). */
    private String m_staticSuffix;
    
    /**
     * Use underscores in field names flag (as substitute for special characters, and to split words).
     */
    private boolean m_underscore;
    
    /** Uppercase initial letter of field names flag. */
    private boolean m_upperInitial;
    
    /** Set of XML name prefixes to be discarded in conversions. */
    private String[] m_discardPrefixSet;
    
    /** Set of XML name suffixes to be discarded in conversions. */
    private String[] m_discardSuffixSet;
    
    /** Reusable array for words in name. */
    private ArrayList m_wordList;
    
    /**
     * Constructor.
     */
    public NameConverter() {
        m_fieldPrefix = "";
        m_fieldSuffix = "";
        m_staticPrefix = "";
        m_staticSuffix = "";
        m_camelCase = true;
        m_discardPrefixSet = new String[0];
        if (ClassHolder.TRIM_COMMON_SUFFIXES) {
            m_discardSuffixSet = new String[] { "Type", "Group", "Union" };
        } else {
            m_discardSuffixSet = new String[0];
        }
        m_wordList = new ArrayList();
    }
    
    /**
     * Check if a name is reserved in Java.
     * 
     * @param name
     * @return is reserved
     */
    public static boolean isReserved(String name) {
        return s_reservedWords.contains(name);
    }
    
    /**
     * Get prefix text for normal field names.
     * 
     * @return field prefix (non-<code>null</code>, may be empty)
     */
    public String getFieldPrefix() {
        return m_fieldPrefix;
    }
    
    /**
     * Set prefix text for normal field names.
     * 
     * @param pref field prefix (non-<code>null</code>, may be empty)
     */
    public void setFieldPrefix(String pref) {
        m_fieldPrefix = pref;
    }
    
    /**
     * Get suffix text for normal field names.
     * 
     * @return field suffix (non-<code>null</code>, may be empty)
     */
    public String getFieldSuffix() {
        return m_fieldPrefix;
    }
    
    /**
     * Set suffix text for normal field names.
     * 
     * @param suff field suffix (non-<code>null</code>, may be empty)
     */
    public void setFieldSuffix(String suff) {
        m_fieldPrefix = suff;
    }
    
    /**
     * Get prefix text for static field names.
     * 
     * @return field prefix (non-<code>null</code>, may be empty)
     */
    public String getStaticPrefix() {
        return m_staticPrefix;
    }
    
    /**
     * Set prefix text for static field names.
     * 
     * @param pref field prefix (non-<code>null</code>, may be empty)
     */
    public void setStaticPrefix(String pref) {
        m_staticPrefix = pref;
    }
    
    /**
     * Get suffix text for static field names.
     * 
     * @return field suffix (non-<code>null</code>, may be empty)
     */
    public String getStaticSuffix() {
        return m_staticPrefix;
    }
    
    /**
     * Set suffix text for static field names.
     * 
     * @param suff field suffix (non-<code>null</code>, may be empty)
     */
    public void setStaticSuffix(String suff) {
        m_staticPrefix = suff;
    }

    /**
     * Get the prefixes to be stripped when converting XML names.
     *
     * @return prefixes
     */
    public String[] getDiscardPrefixSet() {
        return m_discardPrefixSet;
    }

    /**
     * Set the prefixes to be stripped when converting XML names.
     *
     * @param prefixes
     */
    public void setDiscardPrefixSet(String[] prefixes) {
        m_discardPrefixSet = prefixes;
    }

    /**
     * Get the suffixes to be stripped when converting XML names.
     *
     * @return suffixes
     */
    public String[] getDiscardSuffixSet() {
        return m_discardSuffixSet;
    }

    /**
     * Set the suffixes to be stripped when converting XML names.
     *
     * @param suffixes
     */
    public void setDiscardSuffixSet(String[] suffixes) {
        m_discardSuffixSet = suffixes;
    }

    /**
     * Trim specified prefixes and/or suffixes from an XML name.
     *
     * @param xname XML name
     * @return trimmed name, with specified prefixes and/or suffixes removed
     */
    public String trimXName(String xname) {
        
        // first trim off a prefix on name
        for (int i = 0; i < m_discardPrefixSet.length; i++) {
            String prefix = m_discardPrefixSet[i];
            if (xname.startsWith(prefix) && xname.length() > prefix.length()) {
                xname = xname.substring(prefix.length());
                break;
            }
        }
        
        // next trim off a suffix on name
        for (int i = 0; i < m_discardSuffixSet.length; i++) {
            String suffix = m_discardSuffixSet[i];
            if (xname.endsWith(suffix) && xname.length() > suffix.length()) {
                xname = xname.substring(0, xname.length() - suffix.length());
                break;
            }
        }
        return xname;
    }
    
    /**
     * Split an XML name into words. This splits first on the basis of separator characters ('.', '-', and '_') in the
     * name, and secondly based on case (an uppercase character immediately followed by one or more lowercase characters
     * is considered a word, and multiple uppercase characters not followed immediately by a lowercase character are
     * also considered a word). Characters which are not valid as parts of identifiers in Java are dropped from the XML
     * name before it is split, and words starting with initial uppercase characters have the upper case dropped for
     * consistency. Note that this method is not threadsafe.
     * 
     * @param name
     * @return array of words
     */
    public String[] splitXMLWords(String name) {
        
        // start by finding a valid start character
        int offset = 0;
        while (!Character.isJavaIdentifierStart(name.charAt(offset))) {
            if (++offset >= name.length()) {
                return new String[0];
            }
        }
        
        // accumulate the list of words
        StringBuffer word = new StringBuffer();
        m_wordList.clear();
        boolean lastupper = false;
        while (offset < name.length()) {
            
            // check next character of name
            char chr = name.charAt(offset++);
            if (chr == '-' || chr == '.' || chr == '_') {
                
                // force split at each splitting character
                if (word.length() > 0) {
                    m_wordList.add(word.toString());
                    word.setLength(0);
                }
                
            } else if (Character.isJavaIdentifierPart(chr)) {
                
                // check case of valid identifier part character
                if (Character.isUpperCase(chr)) {
                    if (!lastupper && word.length() > 0) {
                        
                        // upper after lower, split before upper
                        m_wordList.add(word.toString());
                        word.setLength(0);
                        
                    }
                    lastupper = true;
                    
                } else {
                    if (lastupper) {
                        if (word.length() > 1) {
                            
                            // multiple uppers followed by lower, split before last
                            int split = word.length() - 1;
                            m_wordList.add(word.substring(0, split));
                            char start = Character.toLowerCase(word.charAt(split));
                            word.setLength(0);
                            word.append(start);
                            
                        } else if (word.length() > 0) {
                            
                            // single upper followed by lower, convert upper to lower
                            word.setCharAt(0, Character.toLowerCase(word.charAt(0)));
                            
                        }
                    }
                    lastupper = false;
                }
                word.append(chr);
            }
        }
        if (word.length() > 0) {
            m_wordList.add(word.toString());
        }
        
        // return array of words
        return (String[])m_wordList.toArray(new String[m_wordList.size()]);
    }
    
    /**
     * Convert a raw package name to a legal Java package name. The raw package name must be in standard package name
     * form, with periods separating the individual directory components of the package name.
     * 
     * @param raw basic package name, which may include illegal characters
     * @return sanitized package name
     */
    public String sanitizePackageName(String raw) {
        StringBuffer buff = new StringBuffer(raw.length());
        boolean first = true;
        for (int i = 0; i < raw.length();) {
            char chr = buff.charAt(i);
            if (first) {
                if (Character.isJavaIdentifierStart(chr)) {
                    first = false;
                    i++;
                } else {
                    buff.deleteCharAt(i);
                }
            } else if (chr == '.') {
                first = true;
                i++;
            } else if (!Character.isJavaIdentifierPart(chr)) {
                buff.deleteCharAt(i);
            } else {
                i++;
            }
        }
        return buff.toString();
    }
    
    /**
     * Convert an XML name to a legal Java class name.
     * 
     * @param xname XML name
     * @return converted name
     */
    public String toJavaClassName(String xname) {
        
        // split trimed name into a series of words
        String[] words = splitXMLWords(trimXName(xname));
        if (words.length == 0) {
            return "X";
        }
        
        // form name by concatenating words with initial uppercase
        StringBuffer buff = new StringBuffer();
        for (int i = 0; i < words.length; i++) {
            String word = words[i];
            buff.append(Character.toUpperCase(word.charAt(0)));
            if (word.length() > 1) {
                buff.append(word.substring(1, word.length()));
            }
        }
        return buff.toString();
    }
    
    /**
     * Convert a word to a name component. If the supplied word starts with a lowercase letter, this converts it to
     * upper case.
     *
     * @param word
     * @return word with uppercase initial letter
     */
    public static String toNameWord(String word) {
        char chr = word.charAt(0);
        if (Character.isLowerCase(chr)) {
            StringBuffer buff = new StringBuffer(word);
            buff.setCharAt(0, Character.toUpperCase(chr));
            return buff.toString();
        } else {
            return word;
        }
    }
    
    /**
     * Convert a word to a leading name component. If the supplied word starts with an uppercase letter which is not
     * followed by another uppercase letter, this converts the initial uppercase to lowercase.
     *
     * @param word
     * @return word with lowercase initial letter
     */
    public static String toNameLead(String word) {
        char chr = word.charAt(0);
        if (Character.isUpperCase(chr) && (word.length() < 2 || Character.isLowerCase(word.charAt(1)))) {
            StringBuffer buff = new StringBuffer(word);
            buff.setCharAt(0, Character.toLowerCase(chr));
            return buff.toString();
        } else {
            return word;
        }
    }
    
    /**
     * Convert an XML name to a Java value base name. The base name is guaranteed not to match a Java keyword, and is in
     * normalized camelcase form with leading lower case (unless the first word of the name is all uppercase).
     * 
     * @param xname XML name
     * @return converted name
     */
    public String toBaseName(String xname) {
        
        // split trimed name into a series of words
        String[] words = splitXMLWords(trimXName(xname));
        if (words.length == 0) {
            return "x";
        }
        
        // use single underscore for empty result
        if (words.length == 0) {
            words = new String[] { "_" };
        }
        
        // form name by concatenating words with configured style
        StringBuffer buff = new StringBuffer();
        buff.append(m_fieldPrefix);
        for (int i = 0; i < words.length; i++) {
            String word = words[i];
            if (i > 0 && m_underscore) {
                buff.append('_');
            }
            if ((i == 0 && m_upperInitial) || (i > 0 && m_camelCase)) {
                buff.append(Character.toUpperCase(word.charAt(0)));
                if (word.length() > 1) {
                    buff.append(word.substring(1, word.length()));
                }
            } else {
                buff.append(word);
            }
        }
        buff.append(m_fieldSuffix);
        
        // add leading underscore if match to reserved word
        String fname = buff.toString();
        if (s_reservedWords.contains(fname)) {
            fname = "_" + fname;
        }
        return fname;
    }
    
    /**
     * Convert text to constant name.
     *
     * @param text raw text to be converted
     * @return constant name
     */
    public String toConstantName(String text) {
        
        // insert underscores to separate words of name, and convert invalid characters
        StringBuffer buff = new StringBuffer(text.length());
        boolean lastup = false;
        boolean multup = false;
        char lastchar = 0;
        for (int index = 0; index < text.length(); index++) {
            char chr = text.charAt(index);
            if (index == 0 && !Character.isJavaIdentifierStart(chr)) {
                buff.append('_');
            }
            if (Character.isJavaIdentifierPart(chr)) {
                if (lastup) {
                    if (Character.isUpperCase(chr)) {
                        multup = true;
                    } else {
                        lastup = false;
                        if (chr == '_') {
                            multup = false;
                        } else if (multup) {
                            buff.insert(buff.length()-1, '_');
                            multup = false;
                        }
                    }
                } else if (Character.isUpperCase(chr)) {
                    if (index > 0 && lastchar != '_') {
                        buff.append('_');
                    }
                    lastup = true;
                    multup = false;
                }
                lastchar = Character.toUpperCase(chr);
                buff.append(lastchar);
            }
        }
        return buff.toString();
    }

    /**
     * Build a field name using supplied prefix and/or suffix.
     *
     * @param base normalized camelcase base name
     * @param prefix text to be added at start of name
     * @param suffix text to be added at end of name
     * @return field name
     */
    private String buildFieldName(String base, String prefix, String suffix) {
        
        // check for any change needed
        int added = prefix.length() + suffix.length();
        boolean toupper = m_upperInitial && !Character.isUpperCase(base.charAt(0));
        if (added == 0) {
            
            // not adding prefix or suffix, check for case conversion
            if (toupper) {
                
                // convert first character to uppercase
                StringBuffer buff = new StringBuffer(base);
                buff.setCharAt(0, Character.toUpperCase(buff.charAt(0)));
                return buff.toString();
                
            } else {
                
                // no change needed, just use base name directly
                return base;
                
            }
            
        } else {
            
            // append prefix and/or suffix, with case conversion if needed
            StringBuffer buff = new StringBuffer(base.length() + added);
            buff.append(prefix);
            int offset = buff.length();
            buff.append(base);
            if (toupper) {
                buff.setCharAt(offset, Character.toUpperCase(base.charAt(0)));
            }
            buff.append(suffix);
            return buff.toString();
            
        }
    }
    
    /**
     * Convert base name to normal field name.
     * 
     * @param base normalized camelcase base name
     * @return field name
     */
    public String toFieldName(String base) {
        String prefix = m_fieldPrefix;
        String suffix = m_fieldSuffix;
        return buildFieldName(base, prefix, suffix);
    }
    
    /**
     * Convert base name to static field name.
     * 
     * @param base normalized camelcase base name
     * @return field name
     */
    public String toStaticFieldName(String base) {
        String prefix = m_staticPrefix;
        String suffix = m_staticSuffix;
        return buildFieldName(base, prefix, suffix);
    }

    /**
     * Convert base name to property name (used for all method names). The property name is always in initial-upper
     * camelcase form.
     *
     * @param base normalized camelcase base name
     * @return property name in initial-upper camelcase form
     */
    public String toPropertyName(String base) {
        if (!Character.isUpperCase(base.charAt(0))) {
            
            // convert first character to uppercase
            StringBuffer buff = new StringBuffer(base);
            buff.setCharAt(0, Character.toUpperCase(buff.charAt(0)));
            return buff.toString();
            
        } else {
            
            // no change needed, just use base name directly
            return base;
            
        }
    }
    
    /**
     * Convert property name to read access method name.
     *
     * @param prop property name in initial-upper camelcase form
     * @return read access method name
     */
    public String toReadAccessMethodName(String prop) {
        return "get" + prop;
    }
    
    /**
     * Convert property name to write access method name.
     *
     * @param prop property name in initial-upper camelcase form
     * @return write access method name
     */
    public String toWriteAccessMethodName(String prop) {
        return "set" + prop;
    }
    
    /**
     * Convert property name to test access method name (for boolean value).
     *
     * @param prop property name in initial-upper camelcase form
     * @return test access method name
     */
    public String toTestAccessMethodName(String prop) {
        return "is" + prop;
    }
    
    /**
     * Convert property name to if set access method name (for value in set of alternatives).
     *
     * @param prop property name in initial-upper camelcase form
     * @return if set access method name
     */
    public String toIfSetAccessMethodName(String prop) {
        return "if" + prop;
    }
}