/*
 *  Copyright 2006-2009 Mikhail Kryshen
 *
 *  This file is part of Tema.
 *
 *  Tema 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 3 of the
 *  License, or (at your option) any later version.
 *
 *  Tema 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 Tema.  
 *  If not, see <http://www.gnu.org/licenses/>.
 */

package kryshen.tema.functions;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import kryshen.tema.Context;
import kryshen.tema.Function;
import kryshen.tema.FunctionDataParser;
import kryshen.tema.TemplateException;
import kryshen.tema.TemplateParser;
import kryshen.tema.io.TemplateReader;

/**
 * Data access functions.
 *
 * @author Mikhail Kryshen
 */
public class Database {   
    /* db_connect:connection_name resource */
    public static final Function CONNECT = new Function() {
        public int invoke(FunctionDataParser fdp, Writer out)
        throws IOException, TemplateException {
            
            String arg0 = fdp.getNextArg();
            String data = fdp.getData();
            
            Connection connection;
            
            try {
                connection = DriverManager.getConnection(data);
            } catch (SQLException e) {
                throw new TemplateException
                        (e.getMessage(), e, fdp.getTemplateReader());
            }
            
            fdp.getContext().set(arg0, connection);
            
            out.write(arg0);
            return 1;
        }
    };
    
    /* db_prepare:qury_name connection sql_statement */
    public static final Function PREPARE = new Function() {
        public int invoke(FunctionDataParser fdp, Writer out)
        throws IOException, TemplateException {
            
            String arg0 = fdp.getNextArg(); // resulting variable
            String arg1 = fdp.getNextArg(); // connection
            String data = fdp.getData();    // statement
            
            Connection connection;
            PreparedStatement statement;
            
            try {
                connection = (Connection) fdp.getContext().get(arg1);
                statement = connection.prepareStatement(data);
            } catch (ClassCastException e) {
                throw new TemplateException
                        (e.getMessage(), e, fdp.getTemplateReader());
            } catch (SQLException e) {
                throw new TemplateException
                        (e.getMessage(), e, fdp.getTemplateReader());
            }
            
            fdp.getContext().set(arg0, statement);
            
            out.write(arg0);
            return 1;
        }
    };
    
    /* db_query:sql_query template arg1 arg2 ... argN */
    public static final Function QUERY = new Function() {
        public int invoke(FunctionDataParser fdp, Writer out)
        throws IOException, TemplateException {
            
            String query = fdp.getNextArg();
            String template = fdp.getNextArg();
            List<String> args = fdp.getArgs();
            
            TemplateParser tp = fdp.getTemplateParser();
            Context context = fdp.getContext();
            
            try {
                PreparedStatement statement =
                        (PreparedStatement) context.get(query);
                return query(template, fdp.getTemplateReader(), 
                        statement, args, tp, out);
            } catch (ClassCastException e) {
                throw new TemplateException
                        (e.getMessage(), e, fdp.getTemplateReader());
            } catch (SQLException e) {
                throw new TemplateException
                        (e.getMessage(), e, fdp.getTemplateReader());
            }
        }
    };
    
    /**
     * Process database query.
     *
     * @param template template to fill with data.
     * @param parentReader parent TemplateReader.
     * @param ps query statement to execute.
     * @param args list of query parameters.
     * @param superParser invoking object.
     * @param out Writer to output processed data.
     *
     * @return number of processed rows in query result.
     */
    private static int query(String template,
            TemplateReader parentReader,
            PreparedStatement ps,
            List<String> args,
            TemplateParser superParser,
            Writer out)
            throws IOException, SQLException, TemplateException {
        
        int i = 1;
        
        for (String arg : args) {
            ps.setString(i++, arg);
        }
        
        ResultSet resultSet = ps.executeQuery();
        ResultSetMetaData metaData = resultSet.getMetaData();
        int columnCount = metaData.getColumnCount();
        
        String[] names = new String[columnCount];
        
        for (int j = 0; j < columnCount; j++) {
            names[j] = metaData.getColumnName(j + 1);
        }
        
        final TemplateParser p = new TemplateParser(superParser);
        final HashMap<String, Object> fields = new HashMap<String, Object>();
        
        p.getContext().set("db", new Function() {
            public int invoke(FunctionDataParser fdp,
                    final Writer out)
                    throws IOException, TemplateException {
                
                String name = fdp.getData();
                Object value = fields.get(name);
                
                return p.parseValue(value, out);
            }
        });
        
        Reader input = new StringReader(template);
        
        for (i = 1; resultSet.next(); i++) {
            
            for (int j = 0; j < columnCount; j++) {
                Object value = resultSet.getObject(j + 1);
                if (resultSet.wasNull()) value = null;
                
                fields.put(names[j], value);
            }
            
            p.getContext().set("db_row", i);
            
            TemplateReader tr = new TemplateReader(input, parentReader);
            
            p.parse(tr, out);

            input.reset();            
            fields.clear();
        }
        
        resultSet.close();
        input.close();
        ps.clearParameters();
        return i - 1;
    }
}