/*
 * Copyright (c) 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.binding.model;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

import org.jibx.runtime.IBindingFactory;
import org.jibx.runtime.IMarshallable;
import org.jibx.runtime.IMarshallingContext;
import org.jibx.runtime.JiBXException;
import org.jibx.util.InsertionOrderedMap;

/**
 * Organizer for a set of bindings under construction. This tracks the individual bindings by default namespace, and
 * manages the relationships between the different bindings.
 * 
 * @author Dennis M. Sosnoski
 */
public class BindingDirectory
{
    //
    // Flags to initialize constructed bindings
    private final boolean m_forceClasses;
    private final boolean m_trackSource;
    private final boolean m_addConstructors;
    private final boolean m_inBinding;
    private final boolean m_outBinding;
    
    /** Map from namespace URI to binding holder. */
    private final InsertionOrderedMap m_uriBindingMap;
    
    /**
     * Constructor taking flags used with constructed bindings.
     * 
     * @param force force classes flag
     * @param track track source flag
     * @param addcon add constructors flag
     * @param in input binding flag
     * @param out output binding flag
     */
    public BindingDirectory(boolean force, boolean track, boolean addcon, boolean in, boolean out) {
        m_forceClasses = force;
        m_trackSource = track;
        m_addConstructors = addcon;
        m_inBinding = in;
        m_outBinding = out;
        m_uriBindingMap = new InsertionOrderedMap();
    }

    /**
     * Find the binding to be used for a particular namespace. If this is the first time a particular namespace was
     * requested, a new binding will be created for that namespace and returned.
     * 
     * @param uri namespace URI (<code>null</code> if no namespace)
     * @return binding holder
     */
    public BindingHolder findBinding(String uri) {
        BindingHolder hold = (BindingHolder)m_uriBindingMap.get(uri);
        if (hold == null) {
            hold = new BindingHolder(uri, m_uriBindingMap.size() + 1, this);
            m_uriBindingMap.put(uri, hold);
            BindingElement binding = hold.getBinding();
            binding.setForceClasses(m_forceClasses);
            binding.setTrackSource(m_trackSource);
            binding.setAddConstructors(m_addConstructors);
            binding.setInBinding(m_inBinding);
            binding.setOutBinding(m_outBinding);
        }
        return hold;
    }
    
    /**
     * General object comparison method. Don't know why Sun hasn't seen fit to include this somewhere, but at least it's
     * easy to write (over and over again).
     * 
     * @param a first object to be compared
     * @param b second object to be compared
     * @return <code>true</code> if both objects are <code>null</code>, or if <code>a.equals(b)</code>;
     * <code>false</code> otherwise
     */
    public static boolean isEqual(Object a, Object b) {
        return (a == null) ? b == null : a.equals(b);
    }
    
    /**
     * Add dependency on another binding.
     * 
     * @param uri namespace for binding of referenced component
     * @param hold binding holder
     */
    public void addDependency(String uri, BindingHolder hold) {
        if (!isEqual(uri, hold.getNamespace())) {
            BindingHolder tohold = findBinding(uri);
            hold.addReference(tohold);
        }
    }
    
    /**
     * Fix all references between bindings. This needs to be called after the generation of binding components is
     * completed, in order to make sure that required namespace definitions are included in the output bindings.
     */
    public void finish() {
        ArrayList uris = getNamespaces();
        for (int i = 0; i < uris.size(); i++) {
            String uri = (String)uris.get(i);
            BindingHolder holder = (BindingHolder)m_uriBindingMap.get(uri);
            holder.finish();
        }
    }
    
    /**
     * Get the existing binding definition for a namespace.
     * 
     * @param uri
     * @return binding holder, or <code>null</code> if none
     */
    public BindingHolder getBinding(String uri) {
        return (BindingHolder)m_uriBindingMap.get(uri);
    }
    
    /**
     * Get the list of binding namespace URIs.
     * 
     * @return namespaces
     */
    public ArrayList getNamespaces() {
        return m_uriBindingMap.keyList();
    }
    
    /**
     * Add namespace declarations to binding. This is used to define any namespaces which are not directly used by the
     * binding(s) but need to be present in the binding definition.
     *
     * @param adduris
     * @param binding
     */
    private void addNamespaceDeclarations(String[] adduris, BindingElement binding) {
        int offset = -1;
        int nsnum = m_uriBindingMap.size();
        for (int i = 0; i < adduris.length; i++) {
            String adduri = adduris[i];
            if (!m_uriBindingMap.containsKey(adduri)) {
                ArrayList childs = binding.topChildren();
                if (offset < 0) {
                    while (++offset < childs.size() && (childs.get(offset) instanceof NamespaceElement));
                }
                NamespaceElement ns = new NamespaceElement();
                ns.setDefaultName("none");
                ns.setUri(adduri);
                ns.setPrefix("ns" + ++nsnum);
                childs.add(offset++, ns);
            }
        }
    }
    
    /**
     * Check if a character is an ASCII alpha character.
     * 
     * @param chr
     * @return alpha character flag
     */
    private static boolean isAsciiAlpha(char chr) {
        return (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z');
    }
    
    /**
     * Check if a character is an ASCII numeric character.
     * 
     * @param chr
     * @return numeric character flag
     */
    private static boolean isAsciiNum(char chr) {
        return chr >= '0' && chr <= '9';
    }
    
    /**
     * Check if a character is an ASCII alpha or numeric character.
     * 
     * @param chr
     * @return alpha or numeric character flag
     */
    private static boolean isAsciiAlphaNum(char chr) {
        return isAsciiAlpha(chr) || isAsciiNum(chr);
    }
    
    /**
     * Configure the names to be used for writing bindings to files. If only one binding has been defined, it just gets
     * the supplied name. If multiple bindings have been defined, a single root binding is constructed which includes
     * all the other bindings, and that root binding is given the supplied name while the other bindings are given
     * unique names within the same directory.
     * 
     * @param name file name for root or singleton binding definition
     * @param adduris list of namespaces to be added to bindings, if not already defined
     * @param pack target package for binding
     * @return root or singleton binding holder
     */
    public BindingHolder configureFiles(String name, String[] adduris, String pack) {
        BindingHolder rhold;
        BindingElement root;
        ArrayList uris = getNamespaces();
        if (uris.size() == 1) {
            
            // single binding, just write it using supplied name and added namespaces
            rhold = (BindingHolder)m_uriBindingMap.get(uris.get(0));
            root = rhold.getBinding();
            
        } else {
            
            // get or create no namespace binding
            rhold = findBinding(null);
            root = rhold.getBinding();
            
            // set file names and add to root binding
            Set nameset = new HashSet();
            for (int i = 0; i < uris.size(); i++) {
                String uri = (String)uris.get(i);
                BindingHolder holder = (BindingHolder)m_uriBindingMap.get(uri);
                if (holder != rhold) {
                    
                    // get last part of namespace URI as file name candidate
                    String bindname;
                    String raw = holder.getNamespace();
                    if (raw == null) {
                        bindname = "nonamespaceBinding";
                    } else {
                        
                        // strip off protocol and any trailing slash
                        raw = raw.replace('\\', '/');
                        int split = raw.indexOf("://");
                        if (split >= 0) {
                            raw = raw.substring(split + 3);
                        }
                        while (raw.endsWith("/")) {
                            raw = raw.substring(0, raw.length()-1);
                        }
                        
                        // strip off host portion if present and followed by path
                        split = raw.indexOf('/');
                        if (split > 0 && raw.substring(0, split).indexOf('.') > 0) {
                            raw = raw.substring(split+1);
                        }
                        
                        // eliminate any invalid characters in name
                        StringBuffer buff = new StringBuffer();
                        int index = 0;
                        char chr = raw.charAt(0);
                        if (isAsciiAlpha(chr)) {
                            buff.append(chr);
                            index = 1;
                        } else {
                            buff.append('_');
                        }
                        boolean toupper = false;
                        while (index < raw.length()) {
                            chr = raw.charAt(index++);
                            if (isAsciiAlphaNum(chr)) {
                                if (toupper) {
                                    chr = Character.toUpperCase(chr);
                                    toupper = false;
                                }
                                buff.append(chr);
                            } else if (chr == '.') {
                                toupper = true;
                            }
                        }
                        buff.append("Binding");
                        bindname = buff.toString();
                    }
                    
                    // ensure uniqueness of the name
                    String uname = bindname.toLowerCase();
                    int pass = 0;
                    while (nameset.contains(uname)) {
                        bindname = bindname + pass;
                        uname = bindname.toLowerCase();
                    }
                    nameset.add(uname);
                    holder.setFileName(bindname + ".xml");
                    
                    // include within the root binding
                    IncludeElement include = new IncludeElement();
                    include.setIncludePath(holder.getFileName());
                    rhold.addInclude(include);
                    
                }
            }
        }
        
        // add any necessary namespace declarations to binding root
        addNamespaceDeclarations(adduris, root);
        
        // set the file name on the singleton or root binding
        rhold.setFileName(name);
        
        // set the binding name based on the file name
        int split = name.lastIndexOf('.');
        if (split > 0) {
            root.setName(name.substring(0, split));
        } else {
            root.setName(name);
        }
        
        // set the target package for code generation
        root.setTargetPackage(pack);
        return rhold;
    }
    
    /**
     * Write the bindings to supplied destination path. This first finalizes the binding defintions with a call to
     * {@link #finish()}, then writes the completed bindings. The binding file names must be set before calling this
     * method, generally by a call to {@link #configureFiles(String, String[], String)}.
     * 
     * @param dir target directory for writing binding definitions
     * @throws JiBXException
     * @throws IOException
     */
    public void writeBindings(File dir) throws JiBXException, IOException {
        finish();
        IBindingFactory fact = org.jibx.runtime.BindingDirectory.getFactory(BindingElement.class);
        IMarshallingContext ictx = fact.createMarshallingContext();
        ictx.setIndent(2);
        ArrayList uris = getNamespaces();
        for (int i = 0; i < uris.size(); i++) {
            String uri = (String)uris.get(i);
            BindingHolder holder = (BindingHolder)m_uriBindingMap.get(uri);
            File file = new File(dir, holder.getFileName());
            ictx.setOutput(new FileOutputStream(file), null);
            ((IMarshallable)holder.getBinding()).marshal(ictx);
            ictx.getXmlWriter().flush();
        }
    }
}