//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

package de.ugoe.cs.autoquest.eventcore.guimodel;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Properties;
import java.util.logging.Level;

import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * Creates {@link IGUIElement}s from a given specification. Implemented as singleton.
 * </p>
 * 
 * @version 1.0
 * @author Patrick Harms
 */
public class GUIElementFactory implements IGUIElementFactory {

    /**
     * <p>
     * Instance of the class (singleton)
     * </p>
     */
    private static GUIElementFactory instance = new GUIElementFactory();

    /**
     * <p>
     * Constructor. Creates a new GUIElementFactory. Private to preserve singleton property.
     * </p>
     */
    private GUIElementFactory() {}

    /**
     * <p>
     * Returns the instance of this class.
     * </p>
     * 
     * @return the instance
     */
    public static synchronized GUIElementFactory getInstance() {
        return instance;
    }

    /**
     * <p>
     * A property mapping that defines to which Java class is created given the type of the GUI
     * element found in the specification.
     * </p>
     */
    private Properties mappingsFromConfiguration;

    
    /*
     * (non-Javadoc)
     * 
     * @see
     * de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElementFactory#instantiateGUIElement(de.ugoe.cs.autoquest
     * .eventcore.guimodel.IGUIElementSpec, de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement)
     */
    @Override
    public IGUIElement instantiateGUIElement(IGUIElementSpec specification, IGUIElement parent)
        throws GUIModelConfigurationException
    {
        Properties mappings = getMappingsFromConfiguration();
        IGUIElement guiElement = null;
        String[] typeHierarchy = specification.getTypeHierarchy();
        int i = 0;
        String className = null;
        
        while ((className == null) && (i < typeHierarchy.length)) {
            className = mappings.getProperty(typeHierarchy[i]);
            i++;
        }
        
        if (className != null) {
            try {
                Class<?> clazz = this.getClass().getClassLoader().loadClass(className);

                if (!IGUIElement.class.isAssignableFrom(clazz)) {
                    Console.traceln(Level.WARNING, "configured GUI element representing class " +
                        className + " is no valid GUIElement derivate.");

                    return null;
                }

                Constructor<?> constructor = null;
                Class<?> parentClass = (parent == null) ? null : parent.getClass();

                // search for a constructor, that perfectly matches the types
                for (Constructor<?> candidate : clazz.getConstructors()) {
                    if ((parentClass != null) && (candidate.getParameterTypes().length == 2) &&
                        (candidate.getParameterTypes()[0].equals(specification.getClass())) &&
                        (candidate.getParameterTypes()[1].equals(parentClass)))
                    {
                        constructor = candidate;
                        break;
                    }
                    else if (parentClass == null) {
                        if ((candidate.getParameterTypes().length >= 1) &&
                            (candidate.getParameterTypes()[0].equals(specification.getClass())))
                        {
                            constructor = candidate;
                            break;
                        }
                    }
                }

                if (constructor == null) {
                    // search for an assignable constructor
                    for (Constructor<?> candidate : clazz.getConstructors()) {
                        if ((candidate.getParameterTypes().length == 2) &&
                            (candidate.getParameterTypes()[0].isInstance(specification)) &&
                            (candidate.getParameterTypes()[1].isInstance(parent)))
                        {
                            constructor = candidate;
                            break;
                        }
                    }

                }
                
                if (constructor != null) {
                    guiElement = (IGUIElement) constructor.newInstance(specification, parent);
                }
                else {
                    throw new NoSuchMethodException
                        ("no constructor with two parameters and assignable parameter types for " +
                         specification.getClass() + " and " +
                         (parent != null ? parent.getClass() : "null") + " found in class " +
                         clazz);
                }

            }
            catch (ClassNotFoundException e) {
                Console.traceln(Level.WARNING, "configured GUI element representing class " +
                                className + " can not be loaded.");
                throw new GUIModelConfigurationException
                    ("configured GUI element representing class " + className +
                     " can not be loaded.", e);
            }
            catch (SecurityException e) {
                Console.traceln(Level.WARNING, "configured GUI element representing class " +
                                className + " can not be instantiated due to security reasons.");
                throw new GUIModelConfigurationException
                    ("configured GUI element representing class " + className +
                     " can not be instantiated due to security reasons.", e);
            }
            catch (NoSuchMethodException e) {
                Console.traceln(Level.WARNING, "configured GUI element representing class " +
                                className + " does not provide an appropriate constructor.");
                throw new GUIModelConfigurationException
                    ("configured GUI element representing class " + className +
                     " does not provide an appropriate constructor.", e);
            }
            catch (IllegalArgumentException e) {
                Console.traceln(Level.WARNING, "configured GUI element representing class " +
                                className + " does not provide an appropriate constructor " +
                                "accepting the provided parameters.");
                throw new GUIModelConfigurationException
                    ("configured GUI element representing class " + className + " does not " +
                     "provide an appropriate constructor accepting the provided parameters.", e);
            }
            catch (InstantiationException e) {
                Console.traceln(Level.WARNING, "configured GUI element representing class " +
                                className + " can not be instantiated.");
                throw new GUIModelConfigurationException
                    ("configured GUI element representing class " + className +
                     " can not be instantiated.", e);
            }
            catch (IllegalAccessException e) {
                Console.traceln(Level.WARNING, "configured GUI element representing class " +
                                className + " can not be instantiated.");
                throw new GUIModelConfigurationException
                    ("configured GUI element representing class " + className +
                     " can not be instantiated.", e);
            }
            catch (InvocationTargetException e) {
                Console.traceln(Level.WARNING, "configured GUI element representing class " +
                                className + " can not be instantiated.");
                throw new GUIModelConfigurationException
                    ("configured GUI element representing class " + className +
                     " can not be instantiated.", e);
            }
        }
        
        if (guiElement == null ) {
            Console.traceln(Level.WARNING, "no class representing GUI elements of type " +
                            specification.getType() + " found. Please extend GUI element " +
                            "mapping files.");
            throw new GUIModelConfigurationException
                ("no class representing GUI elements of type " + specification.getType() +
                 " found. Please extend GUI element mapping files");
        }

        return guiElement;
    }

    /**
     * <p>
     * Loads the mappings for GUI elements. All files that start with &quot;guimapping&quot;, end
     * with &quot;.txt&quot;, and are located in the folder &quot;data/guimappings&quot; (relative
     * to the working directory) are loaded.
     * </p>
     * 
     * @return loaded GUI mappings
     */
    private synchronized Properties getMappingsFromConfiguration()
        throws GUIModelConfigurationException
    {
        if (mappingsFromConfiguration != null) {
            return mappingsFromConfiguration;
        }
        else {
            mappingsFromConfiguration = new Properties();

            File mappingsFolder = new File("data/guimappings");
            File[] children = mappingsFolder.listFiles();

            if (children != null) {
                for (File mappingsFile : children) {
                    if (!mappingsFile.isDirectory() &&
                        mappingsFile.getName().startsWith("guimapping") &&
                        mappingsFile.getName().endsWith(".txt"))
                    {
                        InputStream inStream = null;
                        try {
                            inStream = new FileInputStream(mappingsFile);
                            mappingsFromConfiguration.load(inStream);
                        }
                        catch (FileNotFoundException e) {
                            throw new GUIModelConfigurationException(
                                                                     "could not read mapping configuration file " +
                                                                         mappingsFile, e);
                        }
                        catch (IOException e) {
                            throw new GUIModelConfigurationException(
                                                                     "could not read mapping configuration file " +
                                                                         mappingsFile, e);
                        }
                        finally {
                            if (inStream != null) {
                                try {
                                    inStream.close();
                                }
                                catch (IOException e) {
                                    // ignore
                                }
                            }
                        }
                    }
                }
            }
            else {
                throw new GUIModelConfigurationException(
                                                         "no GUI mappings file provided in folder " +
                                                             mappingsFolder);
            }

            return mappingsFromConfiguration;
        }
    }
}
