package org.jboss.mockgenerator;

/*
 * Copyright 2001-2005 The Apache Software Foundation.
 *
 * 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.
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.jboss.mockgenerator.config.Mock;
import org.jboss.mockgenerator.config.MockConfig;
import org.jboss.mockgenerator.config.MockMethod;
import org.jboss.mockgenerator.config.io.xpp3.MockConfigXpp3Reader;

/**
 */
public abstract class AbstractMockMojo extends AbstractMojo {
    /**
     * Top maven project.
     * 
     * @parameter expression="${project}"
     * @readonly
     */
    protected MavenProject project;
    /**
     * Location of the compiled java classes.
     * 
     * @parameter expression="${project.build.directory}"
     * @required
     */
    protected File outputDirectory;

    /**
     * <p class="changed_added_4_0">
     * Project classloader that able to load classes from project dependencies.
     * </p>
     */
    protected ClassLoader projectClassLoader;

    /**
     * <p class="changed_added_4_0">
     * Parsed generator configuration.
     * </p>
     */
    protected MockConfig mockConfig;
    
    protected int errorsCount = 0;

    public void execute() throws MojoExecutionException {
        File f = getOutputJavaDirectory();
        // Prepare output directory.
        if (!f.exists()) {
            f.mkdirs();
        }
        mockConfig = readConfig();
        projectClassLoader = createProjectClassLoader();
        MockControlSource mockControl = new MockControlSource(
                getOutputJavaDirectory(), mockConfig.getMockController());
        mockControl.printFileHeader();
        for (Mock mock : (List<Mock>) mockConfig.getMocks()) {
            generateClass(mock);
            mockControl.printMockClass(mock.getName());
        }
        if(errorsCount>0){
            throw new MojoExecutionException(
                    "Errors during mock classes grneration");
        }
        mockControl.printFileFooter();
        try {
            mockControl.writeClassFile();
        } catch (IOException e) {
            throw new MojoExecutionException(
                    "Error writing mock controller Java source", e);
        }
        addGeneratedSourcesToProject();
    }

    protected abstract void addGeneratedSourcesToProject();

    /**
     * <p class="changed_added_4_0"></p>
     * @return the outputJavaDirectory
     */
    protected abstract File getOutputJavaDirectory();

    /**
     * <p class="changed_added_4_0"></p>
     * @return the config
     */
    protected abstract File getConfig();

    /**
     * <p class="changed_added_4_0"></p>
     * @return the classpathElements
     */
    protected abstract Collection<String> getClasspathElements();

    private static final List<String> requiredSystemMethods = Arrays.asList("toString", "equals", "hashCode");

    private static final List<String> systemMethods = Arrays.asList("getClass", "wait", "notify", "notifyAll");

    /**
     * <p class="changed_added_4_0">
     * Generate single Mock class
     * </p>
     * 
     * @param mockConfig
     * @param mock
     */
    protected void generateClass(Mock mock) throws MojoExecutionException {
        try {
            String className = mock.getClassName();
            Class<?> baseClass = projectClassLoader.loadClass(className);
            String mockClassName = mock.getName();
            if (null == mockClassName || mockClassName.length() == 0) {
                // Calculate mock class name.
                int indexOfDot = className.lastIndexOf('.');
                mockClassName = mockConfig.getMockPackage() + "."
                        + mockConfig.getClassPrefix()
                        + className.substring(indexOfDot + 1);
                mock.setName(mockClassName);
            }
            MockJavaSource javaSource = new MockJavaSource(getOutputJavaDirectory(),
                    mockClassName, className, mockConfig.getMockController());
            javaSource.printFileHeader(mock.getPostConstruct());
            List<Method> declaredMethods = getPublicMethods(baseClass);
            for (Method method : declaredMethods) {
                String name = method.getName();
                if (!skipMethod(mock, name)) {
                    javaSource.printMethod(method);
                }
            }
            javaSource.printFileFooter(mock.getCode());
            javaSource.writeClassFile();
        } catch (ClassNotFoundException e) {
            getLog().error("Base class for Mock not found", e);
            errorsCount++;
        } catch (IOException e) {
            getLog().error("Error writing Mock class source",
                    e);
            errorsCount++;
        }

    }

    protected List<Method> getPublicMethods(Class<?> baseClass) {
        Method[] declaredMethods = baseClass.getMethods();
        List<Method> publicMethods = new LinkedList<Method>();
        for (Method method : declaredMethods) {
            List<Method> overridenPublicMethods = new ArrayList<Method>();

            boolean visible = true;
            int modifiers = method.getModifiers();
            // Do not generate non-abstract system , static, and final methods.
            if (requiredSystemMethods.contains(method.getName()) 
                || (systemMethods.contains(method.getName()) && 0 == (modifiers&Modifier.ABSTRACT))
            		|| 0 != (modifiers & (Modifier.STATIC|Modifier.FINAL))) {
                visible = false;
            } else {
            	for (Method otherMethod : publicMethods) {
                    if (method.getName().equals(otherMethod.getName())
                            && method != otherMethod) {
                        // two different methods with same name, check arguments
                        Class<?>[] methodParameters = method
                                .getParameterTypes();
                        Class<?>[] otherMethodParameters = otherMethod
                                .getParameterTypes();
                        if (Arrays.equals(methodParameters,
                                otherMethodParameters)) {
                            // these methods have same signatures, check their
                            // classes.
                            if (method.getDeclaringClass().isAssignableFrom(
                                    otherMethod.getDeclaringClass())) {
                            	
                            	if (!method.getDeclaringClass().equals(otherMethod.getDeclaringClass())) {
                                    // otherMetood overrides method, so we should
                                    // skip
                                    // first declaration.
                                    visible = false;
                                    break;
                            	} else {
                                    if (method.getReturnType().isAssignableFrom(
                                    		otherMethod.getReturnType())) {

                                    	//Java 5+ permits methods that return subtypes of parent's return type
                                        visible = false;
                                        break;
                                    } else {
                                        overridenPublicMethods.add(otherMethod);
                                    }
                            	}
                            } else {
                                overridenPublicMethods.add(otherMethod);
                            }
                        }
                    }
                }
            }

            publicMethods.removeAll(overridenPublicMethods);

            if (visible) {
                publicMethods.add(method);
            }
        }
        return publicMethods;
    }

    @SuppressWarnings("unchecked")
    protected boolean skipMethod(Mock mock, String name) {
        for (MockMethod mockMethod : (List<MockMethod>) mock.getMethods()) {
            if (name.equals(mockMethod.getName()) && mockMethod.isExclude()) {
                return true;
            }
        }
        return false;
    }

    @SuppressWarnings("unchecked")
    protected ClassLoader createProjectClassLoader() {
        ClassLoader classLoader = null;
        try {
            Collection<String> classpathElements = getClasspathElements();
            String outputDirectory = project.getBuild().getOutputDirectory();
            URL[] urls = new URL[classpathElements.size() + 1];
            int i = 0;
            urls[i++] = new File(outputDirectory).toURI().toURL();
            for (Iterator<String> iter = classpathElements.iterator(); iter
                    .hasNext();) {
                String element = iter.next();
                urls[i++] = new File(element).toURI().toURL();
            }
            classLoader = new URLClassLoader(urls);
        } catch (MalformedURLException e) {
            getLog().error("Bad URL in classpath", e);
        }

        return classLoader;
    }

    protected MockConfig readConfig() throws MojoExecutionException {
        // Read configuration
        FileInputStream configurationInput = null;
        try {
            MockConfigXpp3Reader reader = new MockConfigXpp3Reader();
            configurationInput = new FileInputStream(getConfig());
            MockConfig mockConfig = reader.read(configurationInput);
            return mockConfig;
        } catch (FileNotFoundException e) {
            throw new MojoExecutionException(
                    "Configuration file does not exists:"
                            + getConfig().getAbsolutePath());
        } catch (IOException e) {
            throw new MojoExecutionException(
                    "Error readind configuration file: "
                            + getConfig().getAbsolutePath(), e);
        } catch (XmlPullParserException e) {
            throw new MojoExecutionException(
                    "Error parsing configuration file: "
                            + getConfig().getAbsolutePath(), e);
        } finally {
            if (null != configurationInput) {
                try {
                    configurationInput.close();
                } catch (IOException e) {
                    // Ignore it.
                }
            }
        }
    }
}
