package org.codehaus.modello.plugin.converters;

/*
 * Copyright (c) 2006, Codehaus.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

import javax.inject.Named;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.codehaus.modello.ModelloException;
import org.codehaus.modello.ModelloParameterConstants;
import org.codehaus.modello.ModelloRuntimeException;
import org.codehaus.modello.model.Model;
import org.codehaus.modello.model.ModelAssociation;
import org.codehaus.modello.model.ModelClass;
import org.codehaus.modello.model.ModelDefault;
import org.codehaus.modello.model.ModelField;
import org.codehaus.modello.model.Version;
import org.codehaus.modello.model.VersionDefinition;
import org.codehaus.modello.plugin.java.AbstractJavaModelloGenerator;
import org.codehaus.modello.plugin.java.javasource.JClass;
import org.codehaus.modello.plugin.java.javasource.JInterface;
import org.codehaus.modello.plugin.java.javasource.JMethod;
import org.codehaus.modello.plugin.java.javasource.JMethodSignature;
import org.codehaus.modello.plugin.java.javasource.JParameter;
import org.codehaus.modello.plugin.java.javasource.JSourceCode;
import org.codehaus.modello.plugin.java.javasource.JSourceWriter;
import org.codehaus.modello.plugin.java.javasource.JType;
import org.codehaus.modello.plugin.java.metadata.JavaClassMetadata;
import org.codehaus.modello.plugin.java.metadata.JavaFieldMetadata;

/**
 * Generate a basic conversion class between two versions of a model.
 */
@Named("converters")
public class ConverterGenerator extends AbstractJavaModelloGenerator {
    public void generate(Model model, Map<String, Object> parameters) throws ModelloException {
        initialize(model, parameters);

        String[] versions = ((String) parameters.get(ModelloParameterConstants.ALL_VERSIONS)).split(",");

        List<Version> allVersions = new ArrayList<Version>(versions.length);
        for (String version : versions) {
            allVersions.add(new Version(version));
        }
        Collections.sort(allVersions);

        Version nextVersion = null;
        for (Version v : allVersions) {
            if (v.greaterThan(getGeneratedVersion())) {
                nextVersion = v;
                break;
            }
        }

        try {
            // if nextVersion remains null, there is none greater so we are converting back to the unpackaged version

            generateConverters(nextVersion);

            if (nextVersion == null) {
                generateConverterTool(allVersions);
            }
        } catch (IOException ex) {
            throw new ModelloException("Exception while generating model converters.", ex);
        }
    }

    private void generateConverters(Version toVersion) throws ModelloException, IOException {
        Model objectModel = getModel();

        Version fromVersion = getGeneratedVersion();
        String packageName = objectModel.getDefaultPackageName(true, fromVersion) + ".convert";

        Version effectiveToVersion = (toVersion == null) ? fromVersion : toVersion;
        String jDoc = "Converts from version " + fromVersion + " (with version in package name) to version "
                + effectiveToVersion + " (with" + (toVersion != null ? "" : "out")
                + " version in package name) of the model.";

        JInterface conversionInterface = new JInterface(packageName + ".VersionConverter");
        initHeader(conversionInterface);
        suppressAllWarnings(objectModel, conversionInterface);
        conversionInterface.getJDocComment().setComment(jDoc);

        JClass basicConverterClass = new JClass(packageName + ".BasicVersionConverter");
        initHeader(basicConverterClass);
        suppressAllWarnings(objectModel, basicConverterClass);
        basicConverterClass.getJDocComment().setComment(jDoc);
        basicConverterClass.addInterface(conversionInterface);

        VersionDefinition versionDefinition = objectModel.getVersionDefinition();

        for (ModelClass modelClass : objectModel.getClasses(fromVersion)) {
            JavaClassMetadata javaClassMetadata = (JavaClassMetadata) modelClass.getMetadata(JavaClassMetadata.ID);

            if (!javaClassMetadata.isEnabled()) {
                // Skip generation of those classes that are not enabled for the java plugin.
                continue;
            }

            // check if it's present in the next version
            if (toVersion != null && !toVersion.inside(modelClass.getVersionRange())) {
                // Don't convert - it's not there in the next one
                continue;
            }

            String methodName = "convert" + modelClass.getName();
            String parameterName = uncapitalise(modelClass.getName());
            String sourceClass = getSourceClassName(modelClass, fromVersion);
            String targetClass = modelClass.getPackageName(toVersion != null, toVersion) + "." + modelClass.getName();

            if (!javaClassMetadata.isAbstract()) {
                // Don't generate converter for abstract classes.

                JMethodSignature methodSig = new JMethodSignature(methodName, new JType(targetClass));
                methodSig.addParameter(new JParameter(new JType(sourceClass), parameterName));
                conversionInterface.addMethod(methodSig);

                // Method from interface, delegates to converter with the given implementation of the target class
                JMethod jMethod = new JMethod(methodName, new JType(targetClass), null);
                jMethod.addParameter(new JParameter(new JType(sourceClass), parameterName));
                basicConverterClass.addMethod(jMethod);

                JSourceCode sc = jMethod.getSourceCode();

                sc.add("return " + methodName + "( " + parameterName + ", new " + targetClass + "() );");
            }

            // Actual conversion method, takes implementation as a parameter to facilitate being called as a superclass
            JMethod jMethod = new JMethod(methodName, new JType(targetClass), null);
            jMethod.addParameter(new JParameter(new JType(sourceClass), parameterName));
            jMethod.addParameter(new JParameter(new JType(targetClass), "value"));
            basicConverterClass.addMethod(jMethod);

            JSourceCode sc = jMethod.getSourceCode();

            sc.add("if ( " + parameterName + " == null )");

            sc.add("{");
            sc.indent();

            sc.add("return null;");

            sc.unindent();
            sc.add("}");

            if (modelClass.getSuperClass() != null) {
                sc.add("// Convert super class");

                sc.add("value = (" + targetClass + ") convert" + modelClass.getSuperClass() + "( " + parameterName
                        + ", value );");

                sc.add("");
            }

            for (ModelField modelField : modelClass.getFields(fromVersion)) {
                String name = capitalise(modelField.getName());

                if (toVersion != null) {
                    if (versionDefinition != null && versionDefinition.isFieldType()) {
                        if (versionDefinition.getValue().equals(modelField.getName())
                                || versionDefinition.getValue().equals(modelField.getAlias())) {
                            sc.add("value.set" + name + "( \"" + toVersion + "\" );");
                            continue;
                        }
                    }
                }

                // check if it's present in the next version
                if (toVersion != null && !toVersion.inside(modelField.getVersionRange())) {
                    // check if it is present in a new definition instead
                    ModelField newField = null;
                    try {
                        newField = modelClass.getField(modelField.getName(), toVersion);
                    } catch (ModelloRuntimeException e) {
                        // Don't convert - it's not there in the next one
                        continue;
                    }

                    if (!newField.getType().equals(modelField.getType())) {
                        // Don't convert - it's a different type in the next one
                        continue;
                    }
                }

                if (modelField instanceof ModelAssociation) {
                    ModelAssociation assoc = (ModelAssociation) modelField;

                    if (assoc.isManyMultiplicity()) {
                        String type = assoc.getType();
                        if (ModelDefault.LIST.equals(type) || ModelDefault.SET.equals(type)) {
                            sc.add("{");

                            sc.indent();

                            sc.add(assoc.getType() + " list = " + assoc.getDefaultValue() + ";");

                            sc.add("for ( java.util.Iterator i = " + parameterName + ".get" + name
                                    + "().iterator(); i.hasNext(); )");

                            sc.add("{");

                            sc.indent();

                            if (isClassInModel(assoc.getTo(), modelClass.getModel())) {
                                String className = getSourceClassName(assoc.getToClass(), fromVersion);
                                sc.add(className + " v = (" + className + ") i.next();");
                            } else {
                                sc.add(assoc.getTo() + " v = (" + assoc.getTo() + ") i.next();");
                            }

                            if (isClassInModel(assoc.getTo(), objectModel)) {
                                sc.add("list.add( convert" + assoc.getTo() + "( v ) );");
                            } else {
                                sc.add("list.add( v );");
                            }

                            sc.unindent();

                            sc.add("}");

                            sc.add("value.set" + name + "( list );");

                            sc.unindent();

                            sc.add("}");
                        } else {
                            sc.add("{");

                            sc.indent();

                            // Map or Properties
                            sc.add(assoc.getType() + " map = " + assoc.getDefaultValue() + ";");

                            sc.add("for ( java.util.Iterator i = " + parameterName + ".get" + name
                                    + "().entrySet().iterator(); i.hasNext(); )");

                            sc.add("{");

                            sc.indent();

                            sc.add("java.util.Map.Entry entry = (java.util.Map.Entry) i.next();");

                            if (isClassInModel(assoc.getTo(), modelClass.getModel())) {
                                String className = getSourceClassName(assoc.getToClass(), fromVersion);
                                sc.add(className + " v = (" + className + ") entry.getValue();");
                            } else {
                                sc.add(assoc.getTo() + " v = (" + assoc.getTo() + ") entry.getValue();");
                            }

                            if (isClassInModel(assoc.getTo(), objectModel)) {
                                sc.add("map.put( entry.getKey(), convert" + assoc.getTo() + "( v ) );");
                            } else {
                                sc.add("map.put( entry.getKey(), v );");
                            }

                            sc.unindent();

                            sc.add("}");

                            sc.add("value.set" + name + "( map );");

                            sc.unindent();

                            sc.add("}");
                        }
                    } else {
                        sc.add("value.set" + name + "( convert" + assoc.getTo() + "( " + parameterName + ".get" + name
                                + "() ) );");
                    }
                } else {
                    sc.add("// Convert field " + modelField.getName());

                    JavaFieldMetadata javaFieldMetadata =
                            (JavaFieldMetadata) modelField.getMetadata(JavaFieldMetadata.ID);
                    String value = parameterName + "." + getPrefix(javaFieldMetadata) + name + "()";
                    sc.add("value.set" + name + "( " + value + " );");
                }
            }

            sc.add("");

            sc.add("return value;");
        }

        try (JSourceWriter interfaceWriter = newJSourceWriter(packageName, conversionInterface.getName(true));
                JSourceWriter classWriter = newJSourceWriter(packageName, basicConverterClass.getName(true))) {
            conversionInterface.print(interfaceWriter);
            basicConverterClass.print(classWriter);
        }
    }

    private void generateConverterTool(List<Version> allVersions) throws ModelloException, IOException {
        Model objectModel = getModel();
        String root = objectModel.getRoot(getGeneratedVersion());

        ModelClass rootClass = objectModel.getClass(root, getGeneratedVersion());

        String basePackage = objectModel.getDefaultPackageName(false, null);
        String packageName = basePackage + ".convert";

        String jDoc = "Converts between the available versions of the model.";

        JClass converterClass = new JClass(packageName + ".ConverterTool");
        initHeader(converterClass);
        suppressAllWarnings(objectModel, converterClass);
        converterClass.getJDocComment().setComment(jDoc);

        converterClass.addImport("java.io.File");
        converterClass.addImport("java.io.IOException");

        converterClass.addImport("javax.xml.stream.*");

        for (Version v : allVersions) {
            writeConvertMethod(converterClass, objectModel, basePackage, allVersions, v, rootClass);
        }
        writeConvertMethod(converterClass, objectModel, basePackage, allVersions, null, rootClass);

        try (JSourceWriter classWriter = newJSourceWriter(packageName, converterClass.getName(true))) {
            converterClass.print(new JSourceWriter(classWriter));
        }
    }

    private static void writeConvertMethod(
            JClass converterClass,
            Model objectModel,
            String basePackage,
            List<Version> allVersions,
            Version v,
            ModelClass rootClass) {
        String modelName = objectModel.getName();
        String rootClassName = rootClass.getName();

        String targetPackage = objectModel.getDefaultPackageName(v != null, v);
        String targetClass = targetPackage + "." + rootClassName;

        String methodName = "convertFromFile";
        if (v != null) {
            methodName += "_" + v.toString("v", "_");
        }
        JMethod method = new JMethod(methodName, new JType(targetClass), null);
        method.addParameter(new JParameter(new JType("File"), "f"));
        method.addException(new JClass("IOException"));
        method.addException(new JClass("XMLStreamException"));
        converterClass.addMethod(method);

        JSourceCode sc = method.getSourceCode();

        sc.add(basePackage + ".io.stax." + modelName + "StaxReaderDelegate reader = new " + basePackage + ".io.stax."
                + modelName + "StaxReaderDelegate();");

        sc.add("Object value = reader.read( f );");

        String prefix = "";
        for (Version sourceVersion : allVersions) {
            String sourcePackage = objectModel.getDefaultPackageName(true, sourceVersion);
            String sourceClass = sourcePackage + "." + rootClassName;
            sc.add(prefix + "if ( value instanceof " + sourceClass + " )");
            sc.add("{");
            sc.indent();

            boolean foundFirst = false;
            for (Version targetVersion : allVersions) {
                if (!foundFirst) {
                    if (targetVersion.equals(sourceVersion)) {
                        foundFirst = true;
                    } else {
                        continue;
                    }
                }

                if (targetVersion.equals(v)) {
                    break;
                }

                // TODO: need to be able to specify converter class implementation
                String p = objectModel.getDefaultPackageName(true, targetVersion);
                String c = p + "." + rootClassName;
                sc.add("value = new " + p + ".convert.BasicVersionConverter().convert" + rootClassName + "( (" + c
                        + ") value );");
            }

            sc.unindent();
            sc.add("}");

            prefix = "else ";

            if (sourceVersion.equals(v)) {
                break;
            }
        }
        sc.add("else");
        sc.add("{");
        sc.indent();

        sc.add("throw new IllegalStateException( \"Can't find converter for class '\" + value.getClass() + \"'\" );");

        sc.unindent();
        sc.add("}");

        sc.add("return (" + targetClass + ") value;");
    }

    private static String getSourceClassName(ModelClass modelClass, Version generatedVersion) {
        return modelClass.getPackageName(true, generatedVersion) + "." + modelClass.getName();
    }
}
