/*
 * Copyright (c) 2004, 2005 TADA AB - Taby Sweden
 * Distributed under the terms shown in the file COPYRIGHT
 * found in the root folder of this project or at
 * http://eng.tada.se/osprojects/COPYRIGHT.html
 */
package org.postgresql.pljava.example;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Filip Hrbek
 */
public class MetaDataTest {
    public static Iterator<String> callMetaDataMethod(String methodCall)
                                                                        throws SQLException {
        return new MetaDataTest(methodCall).iterator();
    }

    private Object[]          m_methodArgs;
    private Class<?>[]        m_methodArgTypes;
    private String            m_methodName;
    private ArrayList<String> m_results;

    public MetaDataTest(String methodCall) throws SQLException {
        Connection conn = DriverManager.getConnection("jdbc:default:connection");
        DatabaseMetaData md = conn.getMetaData();
        ResultSet rs;
        m_results = new ArrayList<String>();
        StringBuffer result;

        parseMethodCall(methodCall);

        try {
            Method m = DatabaseMetaData.class.getMethod(m_methodName,
                                                        m_methodArgTypes);
            if (!m.getReturnType().equals(ResultSet.class)) {
                throw new NoSuchMethodException("Unexpected return type");
            }

            rs = (ResultSet) m.invoke(md, m_methodArgs);
            ResultSetMetaData rsmd = rs.getMetaData();

            int cnt = rsmd.getColumnCount();
            result = new StringBuffer();
            for (int i = 1; i <= cnt; i++) {
                result.append((rsmd.getColumnName(i) + "("
                               + rsmd.getColumnClassName(i) + ")").replaceAll("(\\\\|;)",
                                                                              "\\$1")
                              + ";");
            }
            m_results.add(result.toString());

            while (rs.next()) {
                result = new StringBuffer();
                Object rsObject = null;
                for (int i = 1; i <= cnt; i++) {
                    rsObject = rs.getObject(i);
                    if (rsObject == null) {
                        rsObject = "<NULL>";
                    }

                    result.append(rsObject.toString().replaceAll("(\\\\|;)",
                                                                 "\\$1")
                                  + ";");
                }
                m_results.add(result.toString());
            }
            rs.close();
        } catch (NoSuchMethodException nme) {
            StringBuffer sb = new StringBuffer();
            for (Class<?> m_methodArgType : m_methodArgTypes) {
                if (sb.length() > 0) {
                    sb.append(",");
                }
                sb.append(m_methodArgType.getName());
            }
            throw new SQLException(
                                   "No such method or non-resultset return type: "
                                           + m_methodName + "(" + sb.toString()
                                           + ")");
        } catch (InvocationTargetException ite) {
            throw new SQLException(ite.getTargetException().toString());
        } catch (Exception e) {
            throw new SQLException("Method error: " + e.toString());
        }
    }

    public void close() {
    }

    private Iterator<String> iterator() {
        return m_results.iterator();
    }

    /**
     * Method call parser. Let's say that only String, String[], int, int[] and
     * boolean types are accepted. Examples: "foo" => String {"foo","bar"} =>
     * String[] {} => String[] (zero length array, not null!) 10 => int -7 =>
     * int [10,3] => int[] [] => int[] (zero length array, not null!) TRUE =>
     * boolean (String)null => String (value null) (String[])null => String[]
     * (value null) (int[])null => int[] (value null)
     * 
     * Complete example: select * from
     * callMetaDataMethod('getTables((String)null,"sqlj","jar%",{"TABLE"})');
     * 
     * It makes the implementation simplier, and it is sufficient for
     * DatabaseMetaData testing.
     */
    @SuppressWarnings("unchecked")
    private void parseMethodCall(String methodCall) throws SQLException {
        try {
            Pattern p = Pattern.compile("^\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\((.*)\\)\\s*$");
            Matcher m = p.matcher(methodCall);
            String paramString;
            String auxParamString;
            String param;
            @SuppressWarnings("rawtypes")
            ArrayList objects = new ArrayList();
            ArrayList<Class<?>> types = new ArrayList<Class<?>>();

            if (m.matches()) {
                m_methodName = m.group(1);
                paramString = m.group(2).trim();
                p = Pattern.compile("^\\s*("
                                    + "\\(\\s*(?:String|int)\\s*(?:\\[\\s*\\])?\\s*\\)\\s*null|"
                                    + //String, String[] or int[] null
                                    "TRUE|"
                                    + //boolean TRUE
                                    "FALSE|"
                                    + //boolean FALSE
                                    "(?:\\-|\\+)?[0-9]+|"
                                    + //int
                                    "\\[((?:[^\\[\\]])*)\\]|"
                                    + //int[]
                                    "\"((?:[^\\\\\"]|\\\\.)*)\"|"
                                    + //String
                                    "\\{((?:[^\\{\\}]|\"(?:[^\\\\\"]|\\\\.)*\")*)\\}"
                                    + //String[]
                                    ")\\s*" + "(?:,|$)" + //comma separator
                                    "(.*)$"); //rest of the string
                auxParamString = paramString;
                while (!auxParamString.equals("")) {
                    m = p.matcher(auxParamString);
                    if (!m.matches()) {
                        throw new SQLException("Invalid parameter list: "
                                               + paramString);
                    }

                    param = m.group(1);
                    if (param.startsWith("\"")) //it is a string
                    {
                        param = m.group(3); //string without the quotes
                        objects.add(param);
                        types.add(String.class);
                    } else if (param.startsWith("{")) //it is a string array
                    {
                        param = m.group(4); //string array without the curly brackets
                        Pattern parr = Pattern.compile("^\\s*\"((?:[^\\\\\"]|\\\\.)*)\"\\s*(?:,|$)(.*)$");
                        Matcher marr;
                        String auxParamArr = param.trim();
                        ArrayList<String> strList = new ArrayList<String>();

                        while (!auxParamArr.equals("")) {
                            marr = parr.matcher(auxParamArr);
                            if (!marr.matches()) {
                                throw new SQLException("Invalid string array: "
                                                       + param);
                            }

                            strList.add(marr.group(1));
                            auxParamArr = marr.group(2).trim();
                        }
                        objects.add(strList.toArray(new String[0]));
                        types.add(String[].class);
                    } else if (param.equals("TRUE") || param.equals("FALSE")) //it is a boolean
                    {
                        objects.add(new Boolean(param));
                        types.add(Boolean.TYPE);
                    } else if (param.startsWith("(")) //it is String, String[] or int[] null
                    {
                        Pattern pnull = Pattern.compile("^\\(\\s*(String|int)\\s*(\\[\\s*\\])?\\s*\\)\\s*null\\s*$");
                        Matcher mnull = pnull.matcher(param);

                        if (mnull.matches()) {
                            objects.add(null);
                            if (mnull.group(2) == null) {
                                if (mnull.group(1).equals("String")) {
                                    types.add(String.class);
                                } else {
                                    throw new SQLException(
                                                           "Primitive 'int' cannot be null");
                                }
                            } else {
                                if (mnull.group(1).equals("String")) {
                                    types.add(String[].class);
                                } else {
                                    types.add(int[].class);
                                }
                            }
                        } else {
                            throw new SQLException("Invalid null value: "
                                                   + param);
                        }
                    } else if (param.startsWith("[")) //it is a int array
                    {
                        param = m.group(2); //int array without the square brackets
                        Pattern parr = Pattern.compile("^\\s*(\\d+)\\s*(?:,|$)(.*)$");
                        Matcher marr;
                        String auxParamArr = param.trim();
                        ArrayList<Integer> intList = new ArrayList<Integer>();

                        while (!auxParamArr.equals("")) {
                            marr = parr.matcher(auxParamArr);
                            if (!marr.matches()) {
                                throw new SQLException("Invalid int array: "
                                                       + param);
                            }

                            intList.add(new Integer(marr.group(1)));
                            auxParamArr = marr.group(2).trim();
                        }
                        objects.add(intList.toArray(new Integer[0]));
                        types.add(int[].class);
                    } else //it is an int
                    {
                        objects.add(new Integer(param));
                        types.add(Integer.TYPE);
                    }

                    auxParamString = m.group(5).trim();
                }
            } else {
                throw new SQLException("Syntax error");
            }

            m_methodArgs = objects.toArray(new Object[0]);
            m_methodArgTypes = types.toArray(new Class[0]);
        } catch (Exception e) {
            throw new SQLException("Invalid method call: " + methodCall
                                   + ". Cause: " + e.toString());
        }
    }
}
