/*
 * 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.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.JEnumType;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JArrayType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.SourceWriter;

import com.google.gwt.core.client.JavaScriptObject;

import com.smartgwt.rebind.BeanProperty;
import com.smartgwt.client.types.ValueEnum;
import com.smartgwt.client.widgets.BaseWidget;
import com.smartgwt.client.bean.types.*;

import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.HashSet;
import java.util.Set;
import java.util.Arrays;
import java.util.Date;

// We import the run-time value types to fail fast if there are typos
import com.smartgwt.client.bean.types.*;

public class BeanMethod {
    // The method
    private JMethod method;

    // The type we return, or take
    private JType valueType;

    // The class literal for the valueType
    private String classLiteral;

    // The generic type equivalent to the valueType, if the valueType is
    // primitive
    private JType genericType;

    // Are we a setter or a getter?
    private boolean setter = false;
    private boolean getter = false;
 
    // Our name, with get, set or is cut off, and recapitalized
    // But, we keep any As... suffix for now
    private String name;

    // Our prefix ... i.e. "get", "set" or "is"
    private String prefix;

    // A fragment which deboxes for setting primitive values, since
    // we always box the signature.
    private String deboxer = "";

    public JType getValueType () {
        return valueType;
    }

    public JType getGenericType () {
        return genericType;
    }
    
    public boolean isSetter () {
        return setter;
    }
    
    public boolean isGetter () {
        return getter;
    }
    
    public String getName () {
        return name;
    }
    
    public String getPrefix () {
        return prefix;
    }
    
    public JMethod getMethod () {
        return method;
    }
    
    public boolean isPrimitive () {
        return valueType.isPrimitive() != null;
    }

    // Determines whether the method might be a custom getter, considering
    // everything but name
    private boolean couldBeCustomGetter () {
        // We may be able to optimize later in various ways to avoid calling
        // setters or getters that ultimately just set or get a value on the
        // underlying JSO (which is what we will do by default anyway).
        return method.getReturnType() != JPrimitiveType.VOID &&
               method.isPublic() &&
               !method.isAbstract() &&
               !method.isStatic() &&
               method.getParameters().length == 0;
    }

    // Determines whether the method might be a custom setter, considering
    // everything but name
    private boolean couldBeCustomSetter () {
        // We may be able to optimize later in various ways to avoid calling
        // setters or getters that ultimately just set or get a value on the
        // underlying JSO (which is what we will do by default anyway).
        return method.getReturnType() == JPrimitiveType.VOID &&
               method.isPublic() &&
               !method.isAbstract() &&
               !method.isStatic() &&
               method.getParameters().length == 1;
    }

    public boolean isStaticInitMethod () {
        return method.isPublic() &&
               method.isStatic() &&
               method.getParameters().length == 0 &&
               method.getName().equals("beanFactoryInit");
    }

    // We lowercase the first character, unless the second character is
    // uppercase.  This catches cases like ID, or HAlign and VAlign
    private String recapitalize(String propertyName) {
        if (propertyName.length() < 2) {
            return propertyName.toLowerCase();
        } else if (Character.isUpperCase(propertyName.charAt(1))) {
            return propertyName;
        } else {
            return propertyName.substring(0,1).toLowerCase() + propertyName.substring(1);
        }
    }
    
    // Some regexes to use in recognizing getters and setters
    final static Pattern getterPattern = Pattern.compile("^(get|is)([A-Z].*)");
    final static Pattern setterPattern = Pattern.compile("^set([A-Z].*)");

    public BeanMethod (JMethod method, TypeOracle typeOracle) {
        this.method = method;

        if (couldBeCustomSetter()) {
            Matcher matcher = setterPattern.matcher(method.getName());
            if (matcher.matches()) {
                valueType = method.getParameters()[0].getType();
                setter = true;
                name = recapitalize(matcher.group(1));
                prefix = "set";
            }
        } else if (couldBeCustomGetter()) {
            Matcher matcher = getterPattern.matcher(method.getName());
            if (matcher.matches()) {
                valueType = method.getReturnType();
                getter = true;
                prefix = matcher.group(1);
                name = recapitalize(matcher.group(2));
            }
        }

        if (valueType != null) {
            JPrimitiveType primitiveType = valueType.isPrimitive();
            if (primitiveType == null) {
                genericType = valueType;
                classLiteral = valueType.getQualifiedSourceName() + ".class";
            } else {
                if (primitiveType == JPrimitiveType.BOOLEAN) {
                    genericType = typeOracle.findType(Boolean.class.getCanonicalName());
                    classLiteral = boolean.class.getCanonicalName() + ".class";
                    deboxer = ".@" + genericType.getQualifiedSourceName() + "::booleanValue()()";
                } else if (primitiveType == JPrimitiveType.FLOAT) {
                    genericType = typeOracle.findType(Float.class.getCanonicalName());
                    classLiteral = float.class.getCanonicalName() + ".class"; 
                    deboxer = ".@" + genericType.getQualifiedSourceName() + "::floatValue()()";
                } else if (primitiveType == JPrimitiveType.DOUBLE) {
                    genericType = typeOracle.findType(Double.class.getCanonicalName());
                    classLiteral = double.class.getCanonicalName() + ".class"; 
                    deboxer = ".@" + genericType.getQualifiedSourceName() + "::doubleValue()()";
                } else if (primitiveType == JPrimitiveType.INT) {
                    genericType = typeOracle.findType(Integer.class.getCanonicalName());
                    classLiteral = int.class.getCanonicalName() + ".class"; 
                    deboxer = ".@" + genericType.getQualifiedSourceName() + "::intValue()()";
                } else if (primitiveType == JPrimitiveType.LONG) {
                    genericType = typeOracle.findType(Long.class.getCanonicalName());
                    classLiteral = long.class.getCanonicalName() + ".class"; 
                    deboxer = ".@" + genericType.getQualifiedSourceName() + "::longValue()()";
                }
            }
        }
    }

    public String jsniGetter () {
       return "b.@" + method.getEnclosingType().getQualifiedSourceName() +
              "::" + method.getName() + "()()";
    }

    public String box (String basicGetter) {
        if (isPrimitive()) {
            return "@" + genericType.getQualifiedSourceName() + "::new(" +
                   valueType.getJNISignature() + ")(" + basicGetter + ")";
        } else {
            return basicGetter;
        }
    }

    public void writeJSNIFunction (SourceWriter source, boolean addComma) {
        if (isGetter()) {
            source.println("function (b) {return " + box(jsniGetter()) + "}" + (addComma ? "," : ""));
        } else {
            source.println(
                "function (b,v) {b.@" +
                method.getEnclosingType().getQualifiedSourceName() +
                "::" +
                method.getName() +
                "(" + valueType.getJNISignature() + ")" +
                "(v" + deboxer + ")}" + 
                (addComma ? "," : "")
            );
        }
    }

    public void writeNew (SourceWriter source, String beanClassName, Integer getterMethodNumber, Integer setterMethodNumber, boolean addComma) {
        // This should look like this:
        // new BeanMethod<Canvas, Integer>(Integer.class, 27, 35),  
        StringBuilder s = new StringBuilder();

        s.append("new BeanMethod<");
        s.append(beanClassName);
        s.append(", ");
        s.append(genericType.getQualifiedSourceName());
        s.append(">(");
        s.append(classLiteral);
        s.append(", ");
        s.append(getterMethodNumber == null ? "null" : ("methods.get(" + getterMethodNumber.toString() + ")"));
        s.append(", ");
        s.append(setterMethodNumber == null ? "null" : ("methods.get(" + setterMethodNumber.toString() + ")"));
        s.append(")");
        if (addComma) s.append(",");

        source.println(s.toString());
    }
}
