/*
 * Smart GWT (GWT for SmartClient)
 * Copyright 2008 and beyond, Isomorphic Software, Inc.
 *
 * Smart GWT is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3
 * is published by the Free Software Foundation.  Smart GWT is also
 * available under typical commercial license terms - see
 * http://smartclient.com/license
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 */

package com.smartgwt.rebind;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JConstructor;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.smartgwt.rebind.BeanProperty;
import com.smartgwt.rebind.BeanMethod;
import com.smartgwt.rebind.BeanValueType;
import com.smartgwt.client.widgets.BaseWidget;
import com.smartgwt.client.core.BaseClass;
import com.smartgwt.client.core.DataClass;
import com.smartgwt.client.bean.BeanFactory;
import com.smartgwt.client.bean.BeanFactoryForBaseClass;
import com.smartgwt.client.bean.BeanFactoryForBaseWidget;
import com.smartgwt.client.bean.BeanFactoryForDataClass;

import java.util.Set;
import java.util.List;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.HashSet;
import java.util.HashMap;
import java.util.TreeMap;
import java.util.Map;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Collection;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.io.PrintWriter;

// Class which represents a BeanClass for which a factory is being generated.
// This is separated from the actual generator because we sometimes want to
// generate more than one of these (i.e. for superclasses).
public class BeanClass {
    private JClassType beanClassType;
    private TypeOracle typeOracle;
    private BeanClass superclass;
    private JClassType factoryClass;
    private SourceWriter source;
    private boolean hasStaticInitMethod;

    private Map<String, BeanProperty> properties;
    
    // A global list of our methods ... we write them out as JSNI functions,
    // so that properties can refer to them (and call them) by index.
    private LinkedList<BeanMethod> methods;

    // A set of property names which we don't want to include in the factory
    private static final Set<String> excludedPropertyNames = new HashSet<String> (
        Arrays.asList(
            "logicalStructure",
            "orCreateJsObj",
            // containerWidget causes problems in FormItem because there is a
            // getter but no setter, and the process for creating FormItems
            // sometimes calls the setter. By excluding it here, we fall
            // through to setting a JavaScript attribute, which is what we
            // want. The alternative would be to *generally* fall through
            // to setting a JavaScript attribute in situations where there
            // is a getter but no setter. Or, doing so in particular cases
            // that we would identify somehow.
            "containerWidget"
        )
    );

    public BeanClass (JClassType beanClassType) {
        this.beanClassType = beanClassType;
        this.typeOracle = beanClassType.getOracle();

        final JClassType baseWidgetType = typeOracle.findType(BaseWidget.class.getCanonicalName());
        final JClassType dataClassType = typeOracle.findType(DataClass.class.getCanonicalName());
        final JClassType baseClassType = typeOracle.findType(BaseClass.class.getCanonicalName());
        
        if (beanClassType.isAssignableTo(baseWidgetType)) {
            this.factoryClass = typeOracle.findType(BeanFactoryForBaseWidget.class.getCanonicalName());
        } else if (beanClassType.isAssignableTo(dataClassType)) {
            this.factoryClass = typeOracle.findType(BeanFactoryForDataClass.class.getCanonicalName());
        } else if (beanClassType.isAssignableTo(baseClassType)) {
            this.factoryClass = typeOracle.findType(BeanFactoryForBaseClass.class.getCanonicalName());
        }

        // We'll look for superclasses up to BaseWidget or DataClass, and make sure that we
        // generate factories for them as well. That way, we can keep track of
        // just our own properties.
        if (beanClassType != baseWidgetType && beanClassType != dataClassType && beanClassType != baseClassType) {
            JClassType beanSuperclassType = this.beanClassType.getSuperclass();
            if (beanSuperclassType != null) {
                this.superclass = new BeanClass(beanSuperclassType);
            }
        }

        // We use a TreeMap to sort the properties ... not really necessary,
        // but it's nice for debugging!
        properties = new TreeMap<String, BeanProperty>();
        methods = new LinkedList<BeanMethod>();

        // Iterate through our methods, and create properties from them
        for (JMethod method : beanClassType.getMethods()) {
            BeanMethod beanMethod = new BeanMethod(method, typeOracle);
            String propertyName = beanMethod.getName();

            if (beanMethod.isStaticInitMethod()) hasStaticInitMethod = true;
            
            if (!excludedPropertyNames.contains(propertyName)) {
                if (beanMethod.isGetter() || beanMethod.isSetter()) {
                    BeanProperty property = properties.get(propertyName);
                    if (property == null) {
                        property = new BeanProperty(propertyName, this);
                        properties.put(propertyName, property);
                    }
                    // This will call back to add the method to our global list as well
                    property.addMethod(beanMethod);
                }
            }
        }

        // Now that we have our properties, iterate through them and make a few
        // adjustments We create a list for new properties, since we can't
        // modify the properties HashMap while iterating through it.
        LinkedList<BeanProperty> additionalProperties = new LinkedList<BeanProperty>();
        for (BeanProperty property : properties.values()) {
            // Check if a superclass has the same property.  If so, copy its
            // methods here if they are not overridden.  This simplifies the
            // run-time logic -- if the property is here, we can just use it,
            // and if it's not, we check the superclasses. But we don't have to
            // consolidate methods at run-time.
            //
            // Essentially, this deals with the case where a subclass overrides
            // one overloaded method but not others.
            BeanProperty superProperty = findPropertyInSuperclass(property.getName());
            if (superProperty != null) property.mergeProperty(superProperty);

            // If the name might be overloaded (e.g. getWidthAsString), then
            // check whether we have the unoverloaded property, or a superclass
            // does. If so, merge the overloaded property into the main
            // property. Note that we don't delete the overloaded property, so
            // it still can be used separately (though with limited types -- we
            // don't merge the main property into the overloaded property).
            if (property.getMaybeOverloadedGetter()) {
                BeanProperty mainProperty = properties.get(property.getNameWithoutOverload());

                if (mainProperty == null) {
                    // If no main property, check superclasses as well.  
                    superProperty = findPropertyInSuperclass(property.getNameWithoutOverload());
                    if (superProperty != null) {
                        // If there is a superclass property, then create a new
                        // property here for the overload and then merge the
                        // superProperty.  If there is no superclass property
                        // either, then we just leave things alone.
                        mainProperty = new BeanProperty(property.getNameWithoutOverload(), this);
                        
                        // We need to add the first getter of the super property first,
                        // because that will be the default getter.
                        mainProperty.addMethod(superProperty.firstGetter());

                        // For the rest, merge the overloaded property first,
                        // so it has priority over the superProperty
                        mainProperty.mergeProperty(property);
                        mainProperty.mergeProperty(superProperty);
                        
                        // Add it to the list of additional properties, since we can't modify the
                        // properties HashMap while we're iterating over it.
                        additionalProperties.add(mainProperty);
                    }
                } else {
                    // If there already was a mainProperty, then just merge the
                    // overloaded methods
                    mainProperty.mergeProperty(property);
                }
            }
        }

        // If we created any additionalProperties, add them now that iteration is done
        for (BeanProperty additionalProperty : additionalProperties) {
            properties.put(additionalProperty.getName(), additionalProperty);
        }
    }

    // This finds the first superclass with the specified property. We only
    // need one, because we're consolidating at each level -- it will already
    // consolidate further superclasses.
    public BeanProperty findPropertyInSuperclass (String propertyName) {
        BeanClass iterator = superclass;
        while (iterator != null) {
            BeanProperty superProperty = iterator.getProperty(propertyName); 
            if (superProperty != null) return superProperty;
            iterator = iterator.superclass;
        }
        return null;
    }

    public BeanProperty getProperty (String propertyName) {
        return properties.get(propertyName);
    }

    public void addMethod (BeanMethod method) {
        // This would be more efficient if methods were a set, but we need
        // the indexOf method, which the Set variants don't seem to have.
        if (methods.contains(method)) return;
        methods.add(method);
    }

    public void removeMethod (BeanMethod method) {
        methods.remove(method);
    }

    public int indexOfMethod (BeanMethod method) {
        return methods.indexOf(method);
    }

    public String generateFactory (TreeLogger logger, GeneratorContext context) throws UnableToCompleteException {
        // Must extend BaseWidget or DataCLass or BaseClass
        if (factoryClass == null) {
            logger.log(TreeLogger.ERROR, 
                "Cannot generate a BeanFactory for " + 
                beanClassType.getQualifiedSourceName() + 
                " because it does not extend BaseWidget or DataClass or BaseClass"
            );
            throw new UnableToCompleteException();
        }
        
        // We only generate factories for classes that have a no-arg constructor
        JConstructor constructor = beanClassType.findConstructor(new JType[]{});
        if (constructor == null) {
            logger.log(TreeLogger.ERROR, 
                "Cannot generate a BeanFactory for " + 
                beanClassType.getQualifiedSourceName() + 
                " because it does not have a no-arg constructor"
            );
            throw new UnableToCompleteException();
        }

        final String packageName = beanClassType.getPackage().getName();
        final String factoryName = getSimpleFactoryName();
        ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(packageName, factoryName);

        // We use class literals so that we fail early if the class isn't found
        composer.addImport("com.smartgwt.client.bean.types.*");
        composer.addImport(com.smartgwt.client.bean.BeanFactory.class.getCanonicalName());
        composer.addImport(com.smartgwt.client.bean.BeanFactoryForBaseWidget.class.getCanonicalName());
        composer.addImport(com.smartgwt.client.bean.BeanFactoryForDataClass.class.getCanonicalName());
        composer.addImport(com.smartgwt.client.bean.BeanFactoryForBaseClass.class.getCanonicalName());
        composer.addImport(com.google.gwt.core.client.JavaScriptObject.class.getCanonicalName());
        composer.addImport(com.google.gwt.core.client.JsArray.class.getCanonicalName());
        composer.addImport(com.google.gwt.core.client.UnsafeNativeLong.class.getCanonicalName());
        composer.addImport(com.smartgwt.client.bean.BeanProperty1Getter1Setter.class.getCanonicalName());
        composer.addImport(com.smartgwt.client.bean.BeanProperty1Getter.class.getCanonicalName());
        composer.addImport(com.smartgwt.client.bean.BeanProperty1Setter.class.getCanonicalName());
        composer.addImport(com.smartgwt.client.bean.BeanPropertyMultiple.class.getCanonicalName());
        composer.addImport(com.smartgwt.client.bean.BeanProperty.class.getCanonicalName());
        composer.addImport(com.smartgwt.client.bean.BeanMethod.class.getCanonicalName());

        // Import our beanClass ... makes the generated code prettier ...
        composer.addImport(getQualifiedBeanClassName());
        
        // We inherit from just one class -- we will have a reference to an object for the
        // superclass, rather than it being our superclass.
        composer.setSuperclass(factoryClass.getSimpleSourceName() +  "<" + beanClassType.getSimpleSourceName() + ">");

        PrintWriter printWriter = context.tryCreate(logger, packageName, factoryName);
        if (printWriter != null) {        
            source = composer.createSourceWriter(context, printWriter);
             
            source.println("// This class lovingly generated by com.smartgwt.rebind.BeanFactoryGenerator");
            writeBlankLine();

            writeStaticCreate();
            writeBlankLine();

            writeGetBeanClass();
            writeBlankLine();

            if (superclass == null) {
                source.println("// No relevant superclass, so not generating getSuperclassName or createSuperclassFactory");
                writeBlankLine();
            } else {
                writeGetSuperclass();
                writeBlankLine();

                writeCreateSuperclassFactory();
                writeBlankLine();
            }

            if (hasStaticInitMethod) {
                writeTriggerStaticInitializers();
                writeBlankLine();
            }

            writeDoNewInstance();
            writeBlankLine();

            writeMetadata();

            writeRegisterValueTypes(logger, context);
            writeBlankLine();

            writeGetPropertiesAndGetMethods();

            source.commit(logger);
            
            // Note that this won't do much if the factory has already been
            // generated, which will quite often be the case. Also, note that
            // we only need to generate our immediate superclass, since it will
            // in turn generate its superclass.
            if (superclass != null) {
                superclass.generateFactory(logger, context);
            }
        }

        return composer.getCreatedClassName();
    }
    
    public String getSimpleBeanClassName () {
        return beanClassType.getSimpleSourceName();
    }

    public String getQualifiedBeanClassName () {
        return beanClassType.getParameterizedQualifiedSourceName(); 
    }

    public String getSimpleFactoryName () { 
        StringBuilder builder = new StringBuilder();
        JClassType iterator = beanClassType;
        while (iterator != null) {
            if (iterator != beanClassType) builder.insert(0, "_");
            builder.insert(0, iterator.getSimpleSourceName());
            iterator = iterator.getEnclosingType();
        }
        builder.append("BeanFactory");
        return builder.toString();
    }

    public String getQualifiedFactoryName () {
        return beanClassType.getPackage().getName() + "." + getSimpleFactoryName();
    }

    private void writeBlankLine () {
        source.println("");
    }
    
    private void writeStaticCreate () {
        // We check whether a BeanFactory has already been registered for our
        // beanClass.  This can happen if GWT.create is called first on a
        // subclass of our beanClass and then later on our beanClass as well
        // (since the subclass will have generated factories for its
        // superclasses).
        //
        // If a factory has already been registered, we can leave this one
        // uninitialized.  We don't do anything with the result of the
        // GWT.create, so there's no need to actually initialize it.
        source.println("public static BeanFactory create (boolean forSuperclass) {");
        source.indent();
        source.println("BeanFactory factory = BeanFactory.getFactory(" + getSimpleBeanClassName() + ".class);");

        source.println("if (factory == null) factory = new " + getSimpleFactoryName() + "();");
        
        // We register our class name with SGWTFactory only if we're not for a superclass
        source.println("if (!forSuperclass) factory.registerClassNameWithSGWTFactory();");
        source.println("return factory;");
        source.outdent();
        source.println("}");
    }

    private void writeGetBeanClass () {
        String beanClassName = getSimpleBeanClassName();
        source.println("@Override public Class<" + beanClassName + "> getBeanClass () {");
        source.indent();
        source.println("return " + beanClassName + ".class;");
        source.outdent();
        source.println("}");
    }

    private void writeMetadata () {
        if (beanClassType.isAnnotationPresent(BeanFactory.FrameworkClass.class)) {
            source.println("@Override public boolean isFrameworkClass () {");
            source.indent();
            source.println("return true;");
            source.outdent();
            source.println("}");
            writeBlankLine();
        }

        BeanFactory.ScClassName scClass = beanClassType.getAnnotation(BeanFactory.ScClassName.class);
        if (scClass != null) {
            String className = scClass.value();
            if (className != null && !className.equals("")) {
                source.println("@Override public String getDefaultScClassName () {");
                source.indent();
                source.println("return \"" + className + "\";");
                source.outdent();
                source.println("}");
                writeBlankLine();
            }
        }
    }

    private void writeGetPropertiesAndGetMethods () {
        String beanClassName = getSimpleBeanClassName();
        
        source.println("@Override protected BeanProperty<" + beanClassName + ">[] getProperties (JsArray<JavaScriptObject> methods) {");
        source.indent();

        source.println("// Note that we cast to the generic array type, since you can't create generic array types directly.");
        source.println("return (BeanProperty<" + beanClassName + ">[]) new BeanProperty[] {");
        source.indent();

        Iterator<BeanProperty> prop = properties.values().iterator();
        while (prop.hasNext()) {
            BeanProperty property = prop.next();
            property.writeConstructor(source, prop.hasNext());
        }

        source.outdent();
        source.println("};");

        source.outdent();
        source.println("}");

        writeBlankLine();
       
        source.println("// Any native longs are safe because we're not actually manipulating them in Javascript");
        source.println("// We will reference some deprecated methods ... we could exclude them if desired");
        source.println("@Override @UnsafeNativeLong @SuppressWarnings(\"deprecation\")");
        source.println("protected native JsArray<JavaScriptObject> getMethods () /*-{");
        source.indent();
        source.println("return [");
        source.indent();

        Iterator<BeanMethod> i = methods.iterator();
        while (i.hasNext()) {
            BeanMethod method = i.next();
            method.writeJSNIFunction(source, i.hasNext());
        }

        source.outdent();
        source.println("];");
        source.outdent();
        source.println("}-*/;");
    }

    public Collection<BeanValueType> getValueTypes () {
        HashMap<JType, BeanValueType> result = new HashMap<JType, BeanValueType>();
        for (BeanMethod method : methods) {
            JType type = method.getValueType();
            if (!result.containsKey(type)) {
                result.put(type, new BeanValueType(type, typeOracle));
            }
        }
        return result.values();
    }

    private void writeRegisterValueTypes (TreeLogger logger, GeneratorContext context) {
        source.println("@Override protected void registerValueTypes () {");
        source.indent();
        for (BeanValueType valueType : getValueTypes()) {
            // Create the types ... the constructor won't register more than one
            valueType.writeRegisterValueType(source, logger, context);
        }
        source.outdent();
        source.println("}");
    }

    private void writeGetSuperclass () {
        if (superclass == null) return;

        source.println("@Override protected Class getSuperclass () {");
        source.indent();
        source.println("return " + superclass.getQualifiedBeanClassName() + ".class" + ";");
        source.outdent();
        source.println("}");
    }

    private void writeCreateSuperclassFactory () {
        if (superclass == null) return;

        source.println("@Override protected BeanFactory<?> createSuperclassFactory () {");
        source.indent();
        source.println("return " + superclass.getQualifiedFactoryName() + ".create(true);");
        source.outdent();
        source.println("}");
    }

    private void writeTriggerStaticInitializers () {
        source.println("// Trigger static initialization before creating new instance, in order to avoid bug");
        source.println("@Override protected void triggerStaticInitializers () {");
        source.indent();
        source.println(getSimpleBeanClassName() + ".beanFactoryInit();");
        source.outdent();
        source.println("}");
    }

    private void writeDoNewInstance() {
        source.println("@Override protected " + getSimpleBeanClassName() + " doNewInstance() {");
        source.indent();
        
        if (beanClassType.isAbstract()) {
            source.println("// The base class is abstract, so we'll return null.");
            source.println("return null;");
        } else {
            source.println("return new " + getSimpleBeanClassName() + "();");
        }
        
        source.outdent();
        source.println("}");
    }
}
