/*
 *  Copyright (C) 2005, 2006 Mikhail A. Kryshen
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 *  $Id: Tema.java,v 1.17 2006/12/14 19:44:31 mikhail Exp $
 */

package kryshen.tema;

import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;

import java.util.Map;
import java.util.HashMap;
import java.util.Properties;
import java.util.List;
import java.util.Collections;

import java.io.*;

import kryshen.tema.demo.DemoFrame;

/**
 * Tema main class.
 *
 * @author Mikhail A. Kryshen
 */
public class Tema {
    public static final String TITLE = "TEMA";
    public static final String VERSION = "0.1";
    public static final String AUTHOR = "Mikhail A. Kryshen";

    private static final String CONFIG_FILE = "tema.properties";
    private static final String ENV_PREFIX = "kryshen.tema.";

    private static Properties config = new Properties();
    private static Connection connection = null;

    private static String inputEncoding;
    private static String outputEncoding;

    private static Map<File, String> fileCache = null;

    public static void main(String[] args) 
	throws IOException, SQLException,
	       ClassNotFoundException, InstantiationException, 
	       IllegalAccessException {
	
        boolean demo = false;
        boolean version = false;
        boolean help = false;

        // Parse arguments.
        for (int i = 0; i < args.length; i++) {
            if ("-demo".equals(args[i]) || 
                "-d".equals(args[i])) {
                demo = true;
            } else if ("-version".equals(args[i]) ||
                       "-v".equals(args[i])) {
                version = true;
            } else if ("-help".equals(args[i]) ||
                       "-h".equals(args[i]) ||
                       "-usage".equals(args[i]) ||
                       "-u".equals(args[i])) {
                help = true;
            } else {
                System.err.println("Invalid option: " + args[i]);
                return;
            }
        }

        if (version || help) {
            System.err.println(TITLE + " " + VERSION + " by " + AUTHOR);        

            if (help) {
                PrintStream err = System.err;
                
                err.println();
                err.println("Usage: tema [OPTIONS]");
                err.println();
                err.println("Options:");
                err.println("  -d[emo]             Demo mode");
                err.println("  -v[ersion]          Print version");
                err.println("  -h[help] -u[sage]   Print this help message");
            }
            return;
        }

        if (demo) {
            // Run in demo mode.
            DemoFrame df = new DemoFrame();
            df.pack();
            df.setVisible(true);
            return;
        }

        process();
    }

    /**
     * Standard execution.
     */
    private static void process()
        throws IOException, SQLException,
               ClassNotFoundException, InstantiationException, 
               IllegalAccessException {

        InputStream configIn = new FileInputStream(CONFIG_FILE);
        config.load(configIn);
        configIn.close();
        
        configure();
	
	String main = getProperty("main_template");
	String output = getProperty("output");
	
	Writer out;

	if ("stderr".equals(output))
	    out = new OutputStreamWriter(System.err);
	else
	    out = new OutputStreamWriter(System.out);

        TemplateReader in = createTemplateReader(new File(main));
        TemplateParser p = new TemplateParser();
        TemplateException te = null;

	try {
	    p.parse(in, out);
        } catch (TemplateException e) {
            te = e;
	} finally {
	    in.close();
	    out.flush();
	}  

        if (te != null) {
            System.err.println(te.getMessage());
            if (te.getCause() != null) {
                System.err.println("Caused by " + te.getCause());
            }
        } else {
            System.err.println("Done");
        }
    }

    private static void configure() 
        throws IOException, SQLException, IllegalAccessException,
               ClassNotFoundException, InstantiationException {

        String logFile = getProperty("log");
        if (logFile != null && logFile.length() > 0)
            System.setErr(new PrintStream(new FileOutputStream(logFile)));

	if (Boolean.parseBoolean(getProperty("cache_read")) == true)
	    fileCache = new HashMap<File, String>();

	inputEncoding = getProperty("input_encoding", "iso-8859-1");
	outputEncoding = getProperty("output_encoding", "iso-8859-1");

	String resourceProperty = getProperty("resource");

	if (resourceProperty != null) {
	    String driverProperty = getProperty("driver");
	    if (driverProperty != null) {
		// Register driver.  
		Driver d = (Driver)Class.forName(driverProperty).newInstance();
	    }
	
	    // Open connection.
	    connection = DriverManager.getConnection(resourceProperty);
	}
    }

    

    /**
     * Get configuration property.
     */
    public static String getProperty(String name) {
        String value = System.getProperty(ENV_PREFIX + name);
        if (value != null) return value;
        return config.getProperty(name);
    }

    /**
     * Get configuration property.
     */
    public static String getProperty(String name, String fallback) {
	String value = getProperty(name);
	if (value != null) return value;
	return fallback;
    }

    /**
     * Process database query.
     *
     * @param templateReader reader for the template 
     *                       to fill with data.
     * @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.
     */
    public static int query(TemplateReader templateReader,
                            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 r = ps.executeQuery();
	ResultSetMetaData rm = r.getMetaData();
	int columnCount = rm.getColumnCount();

	String[] names = new String[columnCount];
	
	for (int j = 0; j < columnCount; j++) {
	    names[j] = rm.getColumnName(j + 1);
	}

	final TemplateParser p = new TemplateParser(superParser);
	final HashMap<String, Object> fields = new HashMap<String, Object>();

        p.registerFunction("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);
                }
            });

	for (i = 1; r.next(); i++) {

	    for (int j = 0; j < columnCount; j++) {
		Object value = r.getObject(j + 1);
		if (r.wasNull()) value = null;
                
		fields.put(names[j], value);
	    }

	    p.setValue("number", i);
            p.parse(templateReader, out);
	    templateReader.reset();
            fields.clear();
	}

	r.close();
	ps.clearParameters();
	return i - 1;
    }

    public static BufferedWriter createFileWriter(String filename) 
        throws IOException {

	OutputStream os = new FileOutputStream(filename);
	return new BufferedWriter
            (new OutputStreamWriter(os, Tema.outputEncoding));
    }

    public static BufferedReader createFileReader(File file)
        throws IOException {

	InputStream is = new FileInputStream(file);
	return new BufferedReader
            (new InputStreamReader(is, Tema.inputEncoding));
    }

    public static TemplateReader createTemplateReader(File file)
        throws IOException {

        return new TemplateReader
            (new LineNumberReader(createCachedFileReader(file)), file.getPath());
    }

    /**
     * Creates StringReader for a cached file if caching is enabled or
     * a BufferedReader.
     *
     * @param file File to read.
     * @return Reader for a file.
     */
    public static Reader createCachedFileReader(File file) 
        throws IOException {
        
	if (fileCache != null) return new StringReader(readFile(file));
	else return createFileReader(file);
    }

    /**
     * Read text file.
     *
     * @return file contents.
     */
    public static String readFile(File file) throws IOException {
	String data;
        
	if (fileCache != null) {
	    data = fileCache.get(file);	    
	    if (data != null) return data;
	}
        
        Reader r = new BufferedReader(createFileReader(file));

        // FIXME: possible overflow (long -> int).
	StringBuffer sb = new StringBuffer((int)file.length());

	try {
 	    for (int c = r.read(); c >= 0; c = r.read()) {
 		sb.append((char)c);
 	    }
	} finally {
	    r.close();
	}

	data = sb.toString();

	if (fileCache != null) {
	    fileCache.put(file, data);
	}

	return data;
    }

    /**
     * Get database connection.
     *
     * @return database connection if any was configured.
     */
    public static Connection getDbConnection() {
        return connection;
    }
}