/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 *
 * $Header:$
 */

package org.apache.beehive.controls.system.jdbc.parser;

import org.apache.beehive.controls.api.ControlException;
import org.apache.beehive.controls.api.context.ControlBeanContext;
import org.apache.beehive.controls.system.jdbc.TypeMappingsFactory;
import org.apache.beehive.controls.system.jdbc.JdbcControl;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * Represents a method parameter substitution into the SQL annotation's statement member.  Delimited by '{' and '}'.
 * Method parameter names must exactly match the name used in the SQL statement in order for the substitution to occur.
 * <p/>
 * <pre>
 * SQL(statement="SELECT * FROM {tableName}")
 * public void getAll(String tableName) throws SQLException;
 * </pre>
 */
public final class ReflectionFragment extends SqlFragment {

    private static final String PREPARED_STATEMENT_SUB_MARK = "?";
    private static final Pattern s_parameterNamePattern = Pattern.compile("\\.");

    private final String _parameterName;
    private final String[] _nameQualifiers;
    private int _sqlDataType;

    /**
     * Create a new ReflectionFragment with the specifed method parameter name.
     *
     * @param parameterName The name of the parameter whose value should be substituted at this location.
     */
    protected ReflectionFragment(String parameterName) {
        _parameterName = parameterName;
        _sqlDataType = TypeMappingsFactory.TYPE_UNKNOWN;
        _nameQualifiers = s_parameterNamePattern.split(_parameterName);
    }

    /**
     * Create a new ReflectionFragment with the specified method parameter name and SQL type.
     *
     * @param parameterName The name of the parameter whose value should be substituted at this location.
     * @param sqlDataType   A String specifing the SQL data type for this parameter.
     */
    protected ReflectionFragment(String parameterName, String sqlDataType) {
        this(parameterName);
        if (sqlDataType != null) {
            _sqlDataType = TypeMappingsFactory.getInstance().convertStringToSQLType(sqlDataType);
        }
    }

    /**
     * Return text generated by this fragment for a PreparedStatement
     *
     * @param context A ControlBeanContext instance.
     * @param m       The annotated method.
     * @param args    The method's parameters
     * @return Always returns a PREPARED_STATEMENT_SUB_MARK
     */
    protected String getPreparedStatementText(ControlBeanContext context, Method m, Object[] args) {
        // this reflection fragment may resolve to a JdbcControl.ComplexSqlFragment
        // if so it changes the behavior a bit.
        Object val = getParameterValue(context, m, args);
        if (val instanceof JdbcControl.ComplexSqlFragment) {
            return ((JdbcControl.ComplexSqlFragment)val).getSQL();
        } else {
            return PREPARED_STATEMENT_SUB_MARK;
        }
    }

    /**
     * A reflection fragment may evaluate to an JdbcControl.ComplexSqlFragment type,
     * which requires additional steps to evaluate after reflection.
     *
     * @param context Control bean context.
     * @param m Method.
     * @param args Method args.
     * @return true or false.
     */
    protected boolean hasComplexValue(ControlBeanContext context, Method m, Object[] args) {
        Object val = getParameterValue(context, m, args);
        return val instanceof JdbcControl.ComplexSqlFragment;
    }

    /**
     * Always true for ReflectionFragment.
     *
     * @return true
     */
    protected boolean hasParamValue() { return true; }

    /**
     * Get the parameter name (as specified in the SQL statement).
     *
     * @return The parameter name.
     */
    protected String getParameterName() { return _parameterName; }

    /**
     * Get a copy of the array of parameter name qualifiers.
     *
     * @return An array of parameter name qualifiers.
     */
    protected String[] getParameterNameQualifiers() {
        String[] nameQualifiersCopy = new String[_nameQualifiers.length];
        System.arraycopy(_nameQualifiers, 0, nameQualifiersCopy, 0, _nameQualifiers.length);
        return nameQualifiersCopy;
    }

    /**
     * Get the SQL data type of this param.
     *
     * @return The SQL data type for this param.
     */
    protected int getParamSqlDataType() { return _sqlDataType; }

    /**
     * For JUnit testing.
     *
     * @return The String value of this fragment.
     */
    public String toString() { return PREPARED_STATEMENT_SUB_MARK; }

    /**
     * Get the value of this parameter.
     *
     * @param context ControlBeanContext instance to evaluate the parameter's value against.
     * @param method  Method instance to evaluate against.
     * @param args    Method argument values
     * @return All parameter object values contained within this fragment
     */
    protected Object[] getParameterValues(ControlBeanContext context, Method method, Object[] args) {

        Object value = getParameterValue(context, method, args);
        if (value instanceof JdbcControl.ComplexSqlFragment) {
            JdbcControl.SQLParameter[] params = ((JdbcControl.ComplexSqlFragment)value).getParameters();
            Object[] values = new Object[params.length];
            for (int i = 0; i < params.length; i++) {
                values[i] = params[i].value;
            }
            return values;
        }
        return new Object[]{value};
    }

    //
    // /////////////////////////////////////////////// PRIVATE METHODS /////////////////////////////////////////////
    //

    /**
     * Get the value from the method param.
     * @param method
     * @param args
     * @return Value of reflected method param.
     */
    private Object getParameterValue(ControlBeanContext context, Method method, Object[] args) {
        Object value;
        try {
            value = context.getParameterValue(method, _nameQualifiers[0], args);
        } catch (IllegalArgumentException iae) {
            throw new ControlException("Invalid argument name in SQL statement: " + _nameQualifiers[0], iae);
        }

        for (int i = 1; i < _nameQualifiers.length; i++) {
            // handle maps, properties, and fields...
            value = extractValue(value, _nameQualifiers[i - 1], _nameQualifiers[i]);
        }
        return value;
    }

    /**
     * Get the value from the referenced method parameter using java reflection
     *
     * @param aValue
     * @param aName
     * @param bName
     * @return The value
     */
    private Object extractValue(Object aValue, String aName, String bName) {

        Class aClass = aValue.getClass();
        Object value;

        //
        //  a.isB() or a.getB()
        //
        String bNameCapped = Character.toUpperCase(bName.charAt(0)) + bName.substring(1);
        Method getMethod = null;
        Class retType;

        //
        // try a.isB() first, if found verify that a.isB() returns a boolean value,
        // and that there is not also a a.getB() method - if there is except
        //
        try {

            getMethod = aClass.getMethod("is" + bNameCapped, (Class[]) null);
            retType = getMethod.getReturnType();
            if (!(retType.equals(Boolean.class) ||
                    retType.equals(Boolean.TYPE))) {
                // only boolean returns can be isB()
                getMethod = null;
            } else {
                /**
                 *  make sure ("get" + bNameCapped) does not exist as well
                 *  see CR216159
                 */
                boolean getMethodFound = true;
                try {
                    aClass.getMethod("get" + bNameCapped, (Class[])null);
                } catch (NoSuchMethodException e) {
                    getMethodFound = false;
                }

                if (getMethodFound) {
                    throw new ControlException("Colliding field accsessors in user defined class '"
                                               + aClass.getName() + "' for field '" + bName
                                               + "'. Please use is<FieldName> for boolean fields and get<FieldName> name for other datatypes.");
                }
            }
        } catch (NoSuchMethodException e) {
            // ignore
        }

        //
        // try a.getB() if a.isB() was not found.
        //
        if (getMethod == null) {
            try {
                getMethod = aClass.getMethod("get" + bNameCapped, (Class[])null);
            } catch (NoSuchMethodException e) {
                // ignore
            }
        }

        if (getMethod != null) {
            // OK- a.getB()
            try {
                value = getMethod.invoke(aValue, (Object[]) null);
            } catch (IllegalAccessException e) {
                throw new ControlException("Unable to access public method: " + e.toString());
            } catch (java.lang.reflect.InvocationTargetException e) {
                throw new ControlException("Exception thrown when executing : " + getMethod.getName() + "() to use as parameter");
            }
            return value;
        }

        //
        // try a.b
        //

        try {
            value = aClass.getField(bName).get(aValue);
            return value;
        } catch (NoSuchFieldException e) {
            // ignore
        } catch (IllegalAccessException e) {
            // ignore
        }

        //
        // try a.get(b)
        //

        if (aValue instanceof Map) {
            try {
                value = TypeMappingsFactory.getInstance().lookupType(aValue, new Object[]{bName});
                return value;
            } catch (Exception mapex) {
                throw new ControlException("Exception thrown when executing Map.get() to resolve parameter" + mapex.toString());
            }
        }

        // no other options...
        throw new ControlException("Illegal argument in SQL statement: " + _parameterName
                                    + "; unable to find suitable method of retrieving property " + bName
                                    + " out of object " + aName + ".");
    }
}
