/*
 * Copyright (c) 2007, 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.custom;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.jibx.runtime.IUnmarshallingContext;
import org.jibx.runtime.JiBXException;

/**
 * Command line processor for all types of customizable tools. This just provides the basic handling of a customizations
 * file, target directory, and overrides of values in the customizations root object.
 * 
 * @author Dennis M. Sosnoski
 */
public abstract class CustomizationCommandLineBase
{
    /** Array of method parameter classes for single String parameter. */
    public static final Class[] STRING_PARAMETER_ARRAY = new Class[] { String.class };
    
    /** Array of classes for String and unmarshaller parameters. */
    public static final Class[] STRING_UNMARSHALLER_PARAMETER_ARRAY =
        new Class[] { String.class, IUnmarshallingContext.class };
    
    /** Ordered array of usage lines. */
    protected static final String[] COMMON_USAGE_LINES =
        new String[] { " -c       clean target generation directory (ignored if current directory)",
            " -f path  input customizations file",
            " -t path  target directory for generated output (default is current directory)",
            " -v       verbose output flag" };
    
    /** List of specified classes or files. */
    protected List m_extraArgs;
    
    /** Target directory for output. */
    protected File m_generateDirectory;
    
    /**
     * Process command line arguments array.
     * 
     * @param args
     * @return <code>true</code> if valid, <code>false</code> if not
     * @throws JiBXException
     * @throws IOException
     */
    public boolean processArgs(String[] args) throws JiBXException, IOException {
        boolean clean = false;
        boolean verbose = false;
        String custom = null;
        String genpath = null;
        Map overrides = new HashMap();
        ArgList alist = new ArgList(args);
        m_extraArgs = new ArrayList();
        while (alist.hasNext()) {
            String arg = alist.next();
            if ("-c".equalsIgnoreCase(arg)) {
                clean = true;
            } else if ("-f".equalsIgnoreCase(arg)) {
                custom = alist.next();
            } else if ("-t".equalsIgnoreCase(arg)) {
                genpath = alist.next();
            } else if ("-v".equalsIgnoreCase(arg)) {
                verbose = true;
            } else if (arg.startsWith("--") && arg.length() > 2 && Character.isLetter(arg.charAt(2))) {
                if (!putKeyValue(arg.substring(2), overrides)) {
                    alist.setValid(false);
                }
            } else if (!checkParameter(alist)) {
                if (arg.startsWith("-")) {
                    System.err.println("Unknown option flag '" + arg + '\'');
                    alist.setValid(false);
                } else {
                    m_extraArgs.add(alist.current());
                    break;
                }
            }
        }
        
        // collect the extra arguments at end
        while (alist.hasNext()) {
            String arg = alist.next();
            if (arg.startsWith("-")) {
                System.err.println("Command line options must precede all other arguments: error on '" + arg + '\'');
                alist.setValid(false);
                break;
            } else {
                m_extraArgs.add(arg);
            }
        }
        
        // check for valid command line arguments
        if (alist.isValid()) {
            
            // set output directory
            if (genpath == null) {
                m_generateDirectory = new File(".");
                clean = false;
            } else {
                m_generateDirectory = new File(genpath);
            }
            if (!m_generateDirectory.exists()) {
                m_generateDirectory.mkdirs();
                clean = false;
            }
            if (!m_generateDirectory.canWrite()) {
                System.err.println("Target directory " + m_generateDirectory.getPath() + " is not writable");
                alist.setValid(false);
            } else {
                
                // finish the command line processing
                finishParameters(alist);
                
                // report on the configuration
                if (verbose) {
                    verboseDetails();
                    System.out.println("Output to directory " + m_generateDirectory);
                }
                
                // clean generate directory if requested
                if (clean) {
                    CustomUtils.clean(m_generateDirectory);
                }
                
                // load customizations and check for errors
                if (!loadCustomizations(custom)) {
                    alist.setValid(false);
                } else {
                    
                    // apply command line overrides to customizations
                    Map unknowns = applyOverrides(overrides);
                    if (!unknowns.isEmpty()) {
                        for (Iterator iter = unknowns.keySet().iterator(); iter.hasNext();) {
                            String key = (String)iter.next();
                            System.err.println("Unknown override key '" + key + '\'');
                        }
                        alist.setValid(false);
                    }
                    
                }
            }
            
        } else {
            printUsage();
        }
        return alist.isValid();
    }
    
    /**
     * Apply a key/value map to a customization object instance. This uses reflection to match the keys to either set
     * methods (with names of the form setZZZText, or setZZZ, taking a single String parameter) or String fields (named
     * m_ZZZ). The ZZZ in the names is based on the key name, with hyphenation converted to camel case (leading upper
     * camel case, for the method names).
     * 
     * @param map
     * @param obj
     * @return map for key/values not found in the supplied object
     */
    public static Map applyKeyValueMap(Map map, Object obj) {
        Map missmap = new HashMap();
        for (Iterator iter = map.keySet().iterator(); iter.hasNext();) {
            String key = (String)iter.next();
            String value = (String)map.get(key);
            boolean fail = true;
            Throwable t = null;
            try {
                StringBuffer buff = new StringBuffer(key);
                for (int i = 0; i < buff.length(); i++) {
                    char chr = buff.charAt(i);
                    if (chr == '-') {
                        buff.deleteCharAt(i);
                        buff.setCharAt(i, Character.toUpperCase(buff.charAt(i)));
                    }
                }
                String fname = "m_" + buff.toString();
                buff.setCharAt(0, Character.toUpperCase(buff.charAt(0)));
                String mname = "set" + buff.toString();
                Method method = null;
                Class clas = obj.getClass();
                int argcnt = 1;
                while (!clas.getName().equals("java.lang.Object")) {
                    try {
                        method = clas.getDeclaredMethod(mname + "Text", STRING_UNMARSHALLER_PARAMETER_ARRAY);
                        argcnt = 2;
                        break;
                    } catch (NoSuchMethodException e) {
                        try {
                            method = clas.getDeclaredMethod(mname, STRING_PARAMETER_ARRAY);
                            break;
                        } catch (NoSuchMethodException e1) {
                            clas = clas.getSuperclass();
                        }
                    }
                }
                if (method == null) {
                    clas = obj.getClass();
                    while (!clas.getName().equals("java.lang.Object")) {
                        try {
                            Field field = clas.getDeclaredField(fname);
                            try {
                                field.setAccessible(true);
                            } catch (SecurityException e) { /* deliberately empty */
                            }
                            String type = field.getType().getName();
                            if ("java.lang.String".equals(type)) {
                                field.set(obj, value);
                                fail = false;
                            } else if ("boolean".equals(type) || "java.lang.Boolean".equals(type)) {
                                Boolean bval = null;
                                if ("true".equals(value) || "1".equals(value)) {
                                    bval = Boolean.TRUE;
                                } else if ("false".equals(value) || "0".equals(value)) {
                                    bval = Boolean.FALSE;
                                }
                                if (bval == null) {
                                    throw new IllegalArgumentException("Unknown value '" + value
                                        + "' for boolean parameter " + key);
                                }
                                field.set(obj, bval);
                                fail = false;
                            } else if ("[Ljava.lang.String;".equals(type)) {
                                try {
                                    field.set(obj, org.jibx.runtime.Utility.deserializeTokenList(value));
                                    fail = false;
                                } catch (JiBXException e) {
                                    throw new IllegalArgumentException("Error processing list value + '" + value +
                                        "': " + e.getMessage());
                                }
                            } else {
                                throw new IllegalArgumentException("Cannot handle field of type " + type);
                            }
                            break;
                        } catch (NoSuchFieldException e) {
                            clas = clas.getSuperclass();
                        }
                    }
                } else {
                    try {
                        method.setAccessible(true);
                    } catch (SecurityException e) { /* deliberately empty */
                    }
                    Object[] args = new Object[argcnt];
                    args[0] = value;
                    method.invoke(obj, args);
                    fail = false;
                }
            } catch (IllegalAccessException e) {
                t = e;
            } catch (SecurityException e) {
                t = e;
            } catch (IllegalArgumentException e) {
                t = e;
            } catch (InvocationTargetException e) {
                t = e;
            } finally {
                if (t != null) {
                    t.printStackTrace();
                    System.exit(1);
                }
            }
            if (fail) {
                missmap.put(key, value);
            }
        }
        return missmap;
    }
    
    /**
     * Get generate directory.
     * 
     * @return directory
     */
    public File getGeneratePath() {
        return m_generateDirectory;
    }
    
    /**
     * Get extra arguments from command line. These extra arguments must follow all parameter flags.
     * 
     * @return args
     */
    public List getExtraArgs() {
        return m_extraArgs;
    }
    
    /**
     * Set a key=value definition in a map. This is a command line processing assist method that prints an error message
     * directly if the expected format is not found.
     * 
     * @param def
     * @param map
     * @return <code>true</code> if successful, <code>false</code> if error
     */
    public static boolean putKeyValue(String def, Map map) {
        int split = def.indexOf('=');
        if (split >= 0) {
            String key = def.substring(0, split);
            if (map.containsKey(key)) {
                System.err.println("Repeated key item: '" + def + '\'');
                return false;
            } else {
                map.put(key, def.substring(split + 1));
                return true;
            }
        } else {
            System.err.println("Missing '=' in expected key=value item: '" + def + '\'');
            return false;
        }
    }
    
    /**
     * Merge two arrays of strings, returning an ordered array containing all the strings from both provided arrays.
     * 
     * @param base
     * @param adds
     * @return ordered merged
     */
    protected String[] mergeUsageLines(String[] base, String[] adds) {
        if (adds.length == 0) {
            return base;
        } else {
            String fulls[] = new String[base.length + adds.length];
            System.arraycopy(base, 0, fulls, 0, base.length);
            System.arraycopy(adds, 0, fulls, base.length, adds.length);
            Arrays.sort(fulls);
            return fulls;
        }
    }
    
    /**
     * Check extension parameter. This method may be overridden by subclasses to process parameters beyond those known
     * to this base class.
     * 
     * @param alist argument list
     * @return <code>true</code> if parameter processed, <code>false</code> if unknown
     */
    protected boolean checkParameter(ArgList alist) {
        return false;
    }
    
    /**
     * Finish processing of command line parameters. This method may be overridden by subclasses to implement any added
     * processing after all the command line parameters have been handled.
     * 
     * @param alist 
     */
    protected void finishParameters(ArgList alist) {}
    
    /**
     * Print any extension details. This method may be overridden by subclasses to print extension parameter values for
     * verbose output.
     */
    protected void verboseDetails() {}
    
    /**
     * Load the customizations file. This method must load the specified customizations file, or create a default
     * customizations instance, of the appropriate type.
     *
     * @param path customization file path, <code>null</code> if none
     * @return <code>true</code> if successful, <code>false</code> if an error
     * @throws JiBXException 
     * @throws IOException 
     */
    protected abstract boolean loadCustomizations(String path) throws JiBXException, IOException;
    
    /**
     * Apply map of override values to customizations read from file or created as default.
     * 
     * @param overmap override key-value map
     * @return map for key/values not recognized
     */
    protected abstract Map applyOverrides(Map overmap);
    
    /**
     * Print usage information.
     */
    public abstract void printUsage();
    
    /**
     * Wrapper class for command line argument list.
     */
    protected static class ArgList
    {
        private int m_offset;
        
        private final String[] m_args;
        
        private boolean m_valid;
        
        /**
         * Constructor.
         * 
         * @param args
         */
        protected ArgList(String[] args) {
            m_offset = -1;
            m_args = args;
            m_valid = true;
        }
        
        /**
         * Check if another argument value is present.
         * 
         * @return <code>true</code> if argument present, <code>false</code> if all processed
         */
        public boolean hasNext() {
            return m_args.length - m_offset > 1;
        }
        
        /**
         * Get current argument value.
         * 
         * @return argument, or <code>null</code> if none
         */
        public String current() {
            return (m_offset >= 0 && m_offset < m_args.length) ? m_args[m_offset] : null;
        }
        
        /**
         * Get next argument value. If this is called with no argument value available it sets the argument list
         * invalid.
         * 
         * @return argument, or <code>null</code> if none
         */
        public String next() {
            if (++m_offset < m_args.length) {
                return m_args[m_offset];
            } else {
                m_valid = false;
                return null;
            }
        }
        
        /**
         * Set valid state.
         * 
         * @param valid
         */
        public void setValid(boolean valid) {
            m_valid = valid;
        }
        
        /**
         * Check if argument list valid.
         * 
         * @return <code>true</code> if valid, <code>false</code> if not
         */
        public boolean isValid() {
            return m_valid;
        }
    }
}