/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.cmp.jdbc;

import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
import javax.ejb.EJBLocalObject;
import javax.ejb.EJBObject;
import org.jboss.as.cmp.ejbql.Catalog;
import org.jboss.as.cmp.jdbc.bridge.JDBCAbstractEntityBridge;
import org.jboss.as.cmp.jdbc.bridge.JDBCFieldBridge;
import org.jboss.logging.Logger;

/**
 * @author <a href="mailto:alex@jboss.org">Alex Loubyansky and others</a>
 */

public final class QueryParameter {
    public static List createParameters(int argNum, JDBCFieldBridge field) {
        List parameters;
        JDBCType type = field.getJDBCType();
        if (type instanceof JDBCTypeComplex) {
            JDBCTypeComplexProperty[] props = ((JDBCTypeComplex) type).getProperties();
            parameters = new ArrayList(props.length);
            for (int i = 0; i < props.length; i++) {
                QueryParameter param = new QueryParameter(
                        argNum,
                        false,
                        null,
                        props[i],
                        props[i].getJDBCType());
                parameters.add(param);
            }
        } else {
            QueryParameter param = new QueryParameter(argNum, type);
            parameters = Collections.singletonList(param);
        }
        return parameters;
    }

    public static List createParameters(int argNum, JDBCAbstractEntityBridge entity) {
        List parameters = new ArrayList();
        JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields();
        for (int i = 0; i < pkFields.length; ++i) {
            JDBCFieldBridge pkField = pkFields[i];

            JDBCType type = pkField.getJDBCType();
            if (type instanceof JDBCTypeComplex) {
                JDBCTypeComplexProperty[] props =
                        ((JDBCTypeComplex) type).getProperties();
                for (int j = 0; j < props.length; j++) {
                    QueryParameter param = new QueryParameter(
                            argNum,
                            false,
                            pkField,
                            props[j],
                            props[j].getJDBCType());
                    parameters.add(param);
                }
            } else {
                QueryParameter param = new QueryParameter(
                        argNum,
                        false,
                        pkField,
                        null,
                        type.getJDBCTypes()[0]);
                param.type = type;
                parameters.add(param);
            }
        }
        return parameters;
    }

    public static List createPrimaryKeyParameters(int argNum, JDBCAbstractEntityBridge entity) {
        List parameters = new ArrayList();
        JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields();
        for (int i = 0; i < pkFields.length; ++i) {
            JDBCFieldBridge pkField = pkFields[i];

            JDBCType type = pkField.getJDBCType();
            if (type instanceof JDBCTypeComplex) {
                JDBCTypeComplexProperty[] props = ((JDBCTypeComplex) type).getProperties();
                for (int j = 0; j < props.length; j++) {
                    QueryParameter param = new QueryParameter(
                            argNum,
                            true,
                            pkField,
                            props[j],
                            props[j].getJDBCType());
                    parameters.add(param);
                }
            } else {
                QueryParameter param = new QueryParameter(
                        argNum,
                        true,
                        pkField,
                        null,
                        type.getJDBCTypes()[0]);
                param.type = type;
                parameters.add(param);
            }
        }
        return parameters;
    }

    private int argNum;
    private final boolean isPrimaryKeyParameter;
    private JDBCFieldBridge field;
    private JDBCTypeComplexProperty property;
    private String parameterString;

    private int jdbcType;
    private JDBCType type;

    public QueryParameter(JDBCEntityPersistenceStore manager,
                          Method method,
                          String parameterString) {

        // Method parameter will never be a primary key object, but always
        // a complete entity.
        this.isPrimaryKeyParameter = false;

        this.parameterString = parameterString;

        if (parameterString == null || parameterString.length() == 0) {
            throw new IllegalArgumentException("Parameter string is empty");
        }

        StringTokenizer tok = new StringTokenizer(parameterString, ".");

        // get the argument number
        try {
            argNum = Integer.parseInt(tok.nextToken());
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("The parameter must begin with a number");
        }

        // get the argument type
        if (argNum > method.getParameterTypes().length) {
            throw new IllegalArgumentException("The parameter index is " + argNum +
                    " but the query method only has " +
                    method.getParameterTypes().length + "parameter(s)");
        }
        Class argType = method.getParameterTypes()[argNum];

        // get the jdbc type object
        JDBCType type;

        // if this is an entity parameter
        if (EJBObject.class.isAssignableFrom(argType) ||
                EJBLocalObject.class.isAssignableFrom(argType)) {
            // get the field name
            // check more tokens
            if (!tok.hasMoreTokens()) {
                throw new IllegalArgumentException("When the parameter is an ejb a field name must be supplied.");
            }
            String fieldName = tok.nextToken();

            // get the field from the entity
            field = getCMPField(manager, argType, fieldName);
            if (!field.isPrimaryKeyMember()) {
                throw new IllegalArgumentException("The specified field must be a primay key field");
            }

            // get the jdbc type object
            type = field.getJDBCType();
        } else {
            // get jdbc type from type manager
            type = manager.getJDBCTypeFactory().getJDBCType(argType);
        }

        if (type instanceof JDBCTypeSimple) {
            if (tok.hasMoreTokens()) {
                throw new IllegalArgumentException("Parameter is NOT a known " +
                        "dependent value class, so a properties cannot supplied.");
            }
            jdbcType = type.getJDBCTypes()[0];
            this.type = type;
        } else {
            if (!tok.hasMoreTokens()) {
                throw new IllegalArgumentException("Parmeter is a known " +
                        "dependent value class, so a property must be supplied");
            }

            // build the propertyName
            StringBuffer propertyName = new StringBuffer(parameterString.length());
            propertyName.append(tok.nextToken());
            while (tok.hasMoreTokens()) {
                propertyName.append('.').append(tok.nextToken());
            }
            property = ((JDBCTypeComplex) type).getProperty(propertyName.toString());
            jdbcType = property.getJDBCType();
        }
    }

    public QueryParameter(int argNum, JDBCType type) {
        this.argNum = argNum;
        this.type = type;
        this.jdbcType = type.getJDBCTypes()[0];
        this.isPrimaryKeyParameter = false;
        initToString();
    }

    public QueryParameter(
            int argNum,
            boolean isPrimaryKeyParameter,
            JDBCFieldBridge field,
            JDBCTypeComplexProperty property,
            int jdbcType) {

        this.argNum = argNum;
        this.isPrimaryKeyParameter = isPrimaryKeyParameter;
        this.field = field;
        this.property = property;
        this.jdbcType = jdbcType;

        initToString();
    }

    private void initToString() {
        StringBuffer parameterBuf = new StringBuffer();
        parameterBuf.append(argNum);
        if (field != null) {
            parameterBuf.append('.').append(field.getFieldName());
        }
        if (property != null) {
            parameterBuf.append('.').append(property.getPropertyName());
        }
        parameterString = parameterBuf.toString();
    }

    public void set(Logger log, PreparedStatement ps, int index, Object[] args)
            throws Exception {
        Object arg = args[argNum];
        JDBCParameterSetter param;
        if (field != null) {
            if (!isPrimaryKeyParameter) {
                if (arg instanceof EJBObject) {
                    arg = ((EJBObject) arg).getPrimaryKey();
                } else if (arg instanceof EJBLocalObject) {
                    arg = ((EJBLocalObject) arg).getPrimaryKey();
                } else {
                    throw new IllegalArgumentException("Expected an instanc of " +
                            "EJBObject or EJBLocalObject, but got an instance of " +
                            arg.getClass().getName());
                }
            }
            arg = field.getPrimaryKeyValue(arg);

            // use mapper
            final JDBCType jdbcType = field.getJDBCType();
            arg = jdbcType.getColumnValue(0, arg);
            param = jdbcType.getParameterSetter()[0];
        } else if (property != null) {
            arg = property.getColumnValue(arg);
            param = property.getParameterSetter();
        } else {
            if (type != null) {
                arg = type.getColumnValue(0, arg);
                param = type.getParameterSetter()[0];
            } else {
                param = JDBCUtil.getParameterSetter(jdbcType, arg == null ? null : arg.getClass());
            }
        }
        param.set(ps, index, jdbcType, arg, log);
        //JDBCUtil.setParameter(log, ps, index, jdbcType, arg);
    }

    private static JDBCFieldBridge getCMPField(
            JDBCEntityPersistenceStore manager,
            Class intf,
            String fieldName) {
        Catalog catalog = manager.getCatalog();
        JDBCAbstractEntityBridge entityBridge = (JDBCAbstractEntityBridge) catalog.getEntityByInterface(intf);
        if (entityBridge == null) {
            throw new IllegalArgumentException("Entity not found in application " +
                    "catalog with interface=" + intf.getName());
        }

        JDBCFieldBridge cmpField = (JDBCFieldBridge) entityBridge.getFieldByName(fieldName);
        if (cmpField == null) {
            throw new IllegalArgumentException("cmpField not found:" +
                    " cmpFieldName=" + fieldName +
                    " entityName=" + entityBridge.getEntityName());
        }
        return cmpField;
    }

    public String toString() {
        return parameterString;
    }
}
