package usda.weru.weps.reports.query;

import de.schlichtherle.truezip.file.TFile;
import de.schlichtherle.truezip.file.TFileReader;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.BufferedReader;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.log4j.Logger;
import org.h2.engine.Constants;
import org.h2.engine.Session;
import org.h2.jdbc.JdbcConnection;
import org.h2.value.ValueInt;
import org.jfree.data.category.DefaultCategoryDataset;
import org.openide.util.Exceptions;
import usda.weru.util.ConfigData;
import usda.weru.util.ReportContext;
import usda.weru.util.Util;
import usda.weru.weps.RunFileData;
import usda.weru.weps.reports.Locks;
import usda.weru.weps.reports.query.parse.HarvestParser;
import usda.weru.weps.reports.query.parse.HydrobalParser;
import usda.weru.weps.reports.query.parse.Interval;
import usda.weru.weps.reports.query.parse.ParsedIntervalResultSet;
import usda.weru.weps.reports.query.parse.ParsedRotationResultSet;
import usda.weru.weps.reports.query.parse.SeasonParser;

/**
 * Workhorse class of the weps reporting. This connection class extends the H2
 * databases's JdbcConnection and connects to the H2 engine as a memory only
 * database. This database is specific to this connection. A single connection
 * may be used for more than one report. This is often desirable because
 * WepsConnection caches loaded data until closed.
 *
 * @author joelevin
 */
public class WepsConnection extends JdbcConnection {

    private static final Logger LOGGER = Logger.getLogger(WepsConnection.class);

    /**
     *
     */
    public static final String PROP_UNITS = "units";
    private static final String WEPS_CONNECTION_VARIABLE = "WEPS_CONNECTION";
    private static final Map<Integer, WepsConnection> c_connections = new HashMap<Integer, WepsConnection>();

    /**
     *
     */
    protected String c_wepsUrl;
    
    private QuickPlotResultSet tempData; //This needs to be resolved in a better way when the map
                                         //is reenabled, but for now we need this for a quick fix.

    /**
     *
     */
    protected String c_rootString;

    private String name;

    /**
     *
     */
    protected TFile[] c_rootFiles;
    private Map<String, WepsResultSet> c_data;
    private SeasonParser  season;
    private HarvestParser harvest;
    private HydrobalParser hydro;
//    private static Map<String, WepsResultSet> c_data = new HashMap<String, WepsResultSet>();
    private static int scrub = 0;
    private List<TFile> c_runFiles;
    private String c_units = Util.SIUnits;
    private PropertyChangeSupport c_changes;
    
    public SeasonParser  getSeasonParser() { return season; }
    public HarvestParser getHarvestParser() { return harvest; }
    public HydrobalParser getHydroParser() { return hydro; }

    /**
     *
     * @param url
     * @param info
     * @throws SQLException
     */
    public WepsConnection(String url, Properties info, String name) throws SQLException {
        super("jdbc:h2:mem:", info);
        c_wepsUrl = url;
        parseUrl(url, info);
        init();
        checkParams(info);
        throw new NullPointerException("Function Call that is unsupported, thought to be unused.");
    }

    /**
     *
     * @param files
     * @throws SQLException
     */
    public WepsConnection(String inputName, TFile... files) throws SQLException {
        super("jdbc:h2:mem:", new Properties());
        //make a copy of the file array
        TFile[] copy = Arrays.copyOf(files, files.length);
        name = inputName;
        c_rootFiles = copy;
        init();
    }

    /**
     * Sets up the WEPS alias in the H2 database. Registeres this connection so
     * sqlFunctionWeps can lookup the actual WepsConnection being used.
     *
     * @throws java.sql.SQLException
     */
    private void init() throws SQLException {
        //create an alias for accessing the weps data
        RFD = new RunFileData[0];
        PreparedStatement aliasStatement = null;
        try {
            aliasStatement = prepareStatement(
                    "CREATE ALIAS WEPS FOR \"usda.weru.weps.reports.query.WepsConnection.sqlFunctionWeps\";");
            aliasStatement.execute();
        } finally {
            if (aliasStatement != null) {
                try {
                    aliasStatement.close();
                } catch (SQLException e) {
                    LOGGER.warn("Unable to close the WEPS sql alias statement.", e);
                }
            }
        }
        Session session = (Session) getSession();
        session.setVariable(WEPS_CONNECTION_VARIABLE, ValueInt.get(hashCode()));
        register(this);
        TFile seasonFile = new TFile(this.getRunFiles()[0], "season.out");
        if (seasonFile.exists()) 
        {
            season = new SeasonParser();
            season.parse(seasonFile.getAbsolutePath());
        }
        TFile hydrobalFile = new TFile(this.getRunFiles()[0], "hydrobal.out");
        if (hydrobalFile.exists()) 
        {
            hydro = new HydrobalParser();
            hydro.parse(hydrobalFile.getAbsolutePath());
        }
    }

    private void checkParams(Properties info) throws SQLException {
        //check the units value
        if (info.containsKey(PROP_UNITS)) {
            String newUnits = info.getProperty(PROP_UNITS);
            if (Util.SIUnits.equals(newUnits) || Util.USUnits.equals(newUnits)) {
                c_units = newUnits;
            } else {
                throw new SQLException("Unrecognized units value: " + newUnits);
            }
        }
    }

    private void parseUrl(String url, Properties info) {
        url = url.replaceFirst(WepsDriver.URL_PREFIX, "");
        int index = url.indexOf(";");
        if (index < 0) {
            //no parameters in the url
            c_rootString = url;
        } else {
            c_rootString = url.substring(0, index);
            String paramString = url.substring(index);
            String[] params = paramString.split("\\;", -1);
            for (String param : params) {
                if (param.length() == 0) {
                    continue;
                }
                String[] pair = param.split("=", 2);
                info.setProperty(pair[0].toLowerCase(), pair[1]);

            }
        }
    }

    /**
     *
     * @param units
     */
    public void setUnits(String units) {
        String old = c_units;
        c_units = units;
        changeSupport().firePropertyChange(PROP_UNITS, old, c_units);
    }

    /**
     *
     * @return
     */
    public String getUnits() {
        return c_units;
    }

    /**
     *
     */
    public void clearWepsData() {
        synchronized (this) {
            if (c_data != null) {
                c_data.clear();
            }
        }
    }

    /**
     *
     * @param dataname
     */
    public void clearWepsData(String dataname) {
        synchronized (this) {
            if (c_data != null) {
                String identity = name + ConfigData.getDefault().getData(ConfigData.Units) + dataname;
                c_data.remove(identity);
            }
        }
    }

    /**
     *
     * @return
     */
    @Override
    public String toString() {
        StringBuilder buffer = new StringBuilder();
        buffer.append(this.getClass().getName() + "\n");
        buffer.append("\t" + super.toString() + "\n");
        for (TFile file : getRunFiles()) {
            buffer.append("\t" + file.getAbsolutePath() + "\n");
        }
        return buffer.toString();
    }

    /**
     *
     * @param dataname
     * @param fill
     * @return
     * @throws SQLException
     */
    protected ResultSet getWepsData(String dataname, boolean fill) throws SQLException {
        ReportContext.enter();
        try {
            synchronized (this) {

                String identity = name + ConfigData.getDefault().getData(ConfigData.Units) + dataname;
                //System.out.println(name);
                //System.out.println(identity);
                if(c_data == null) c_data = new HashMap<String, WepsResultSet>();
                WepsResultSet data = c_data.get(dataname);
                if (data == null) {
                    //No cached data so try to create a new resultset.
                    switch (dataname) {
                        case RunsResultSet.NAME:
                            data = new RunsResultSet(this);
                            break;
                        case StirEnergyResultSet.NAME:
                            data = new StirEnergyResultSet(this);
                            break;
                        case CropRotationResultSet.NAME:
//                            data = new CropRotationResultSet(this, name);
                            data = new ParsedRotationResultSet(this);
                            break;
                        case SciEnergyResultSet.NAME:
                            data = new SciEnergyResultSet(this);
                            break;
                        case NotesResultSet.NAME:
                            data = new NotesResultSet(this);
                            break;
                        case SoilsResultSet.NAME:
                            data = new SoilsResultSet(this);
                            break;
                        case BarriersResultSet.NAME:
                            data = new BarriersResultSet(this);
                            break;
                        case OutputResultSet.NAME:
                            data = new OutputResultSet(this);
                            break;
                        case HarvestsResultSet.NAME:
                            data = new HarvestsResultSet(this, name);
                            break;
                        case CoverCropResultSet.NAME:
                            data = new CoverCropResultSet(this, name);
                            break;
                        case CropIntervalResultSet.NAME:
//                            data = new CropIntervalResultSet(this, name);
                            data = new ParsedIntervalResultSet(this);
                            break;
                        case ManagementResultSet.NAME:
                            data = new ManagementResultSet(this);
                            break;
                        case OutputOpCropResultSet.NAME:
                            data = new OutputOpCropResultSet(this);
                            break;
                        case ManagementNotesResultSet.NAME:
                            data = new ManagementNotesResultSet(this);
                            break;
                        case FuelsResultSet.NAME:
                            data = new FuelsResultSet(this);
                            break;
                        case DatabaseCropsResultSet.NAME:
                            data = new DatabaseCropsResultSet(this);
                            break;
                        case DatabaseOperationsResultSet.NAME:
                            data = new DatabaseOperationsResultSet(this);
                            break;
                        case DatabaseManagementsResultSet.NAME:
                            data = new DatabaseManagementsResultSet(this);
                            break;
                        case ConfidenceIntervalResultSet.NAME:
                            data = new ConfidenceIntervalResultSet(this);
                            break;
                        case QuickPlotResultSet.NAME:
                            if(tempData != null) return tempData;  //Also needs to be resolved in a better way when the map
                                                                   //is reenabled, but for now we need this for a quick fix.
                        default:
                            //this isn't a known resultset
                            throw new SQLException("Unknown WEPS ResultSet: " + dataname);
                    }
                    data.fill();
                    if (data.getOlderRunCheck()) {
                        System.out.println("running HarvestResultSetArchine.java...");
                        data = new HarvestsResultSetArchive(this);
                        data.fill();
                    }
                    c_data.put(data.getName(), data);
                    //c_data.put(identity, data);
                }
                return data;
            }
        } finally {
            ReportContext.exit();
        }
    }

    public void connectQuickplot(ArrayList<DefaultCategoryDataset> col0, ArrayList<DefaultCategoryDataset> col1) throws SQLException {
        QuickPlotResultSet data = new QuickPlotResultSet(this);
        data.add(col0, col1);
        if (c_data == null) {
            c_data = new HashMap<String, WepsResultSet>();
        }
        String identifier = name + ConfigData.getDefault().getData(ConfigData.Units) + QuickPlotResultSet.NAME;
        c_data.put(identifier, data);
        tempData = data;
    }

    private RunFileData[] RFD;

    public RunFileData[] getRFD() {
        if (RFD.length == c_runFiles.size()) {
            return RFD;
        } else {
            RFD = new RunFileData[c_runFiles.size()];
        }
        for (int index = 0; index < c_runFiles.size(); index++) {
            RFD[index] = new RunFileData(c_runFiles.get(index).getAbsolutePath(), false);
        }
        return RFD;
    }

    /**
     *
     * @return
     */
    public synchronized TFile[] getRunFiles() {
        if (c_runFiles == null) {
            //create the list of runs            
            c_runFiles = new LinkedList<TFile>();

            if (c_rootFiles == null) {
                String[] roots = c_rootString.split(",", -1);
                System.out.println(Arrays.toString(roots));
                TFile[] rootFiles = new TFile[roots.length];
                for (int i = 0; i < roots.length; i++) {
                    rootFiles[i] = new TFile(roots[i]);
                }
                buildFileList(c_runFiles, rootFiles);
            } else {
                buildFileList(c_runFiles, c_rootFiles);
            }

        }
        return c_runFiles.toArray(new TFile[c_runFiles.size()]);
    }

    private void buildFileList(List<TFile> toKeep, TFile[] roots) {
        if (roots == null) {
            return;
        }
        for (TFile root : roots) {
            if (accept(root) && !toKeep.contains(root)) {
                toKeep.add(root.getCanOrAbsFile());
            } else if (root.isDirectory()) {
                buildFileList(toKeep, root.listFiles());
            }
        }
    }

    private boolean accept(TFile file) {
        return file.isDirectory() && file.getName().toLowerCase().endsWith(RunFileData.RunSuffix);
    }

    private static void register(WepsConnection connection) {
        c_connections.put(connection.hashCode(), connection);
    }

    private static WepsConnection get(int hashcode) {
        return c_connections.get(hashcode);
    }

    private static void remove(WepsConnection connection) {
        c_connections.remove(connection.hashCode());
    }

    /**
     *
     * @throws SQLException
     */
    @Override
    public void close() throws SQLException {
        super.close();
        //remove from the registry
        remove(this);

        synchronized (this) {
            //dispose of the data
            if (c_data != null) {
                for (WepsResultSet data : c_data.values()) {
                    data.dispose();
                }
            }
            //clear the data to prevent memory leaks
            //c_data = null;
        }
    }

    private synchronized PropertyChangeSupport changeSupport() {
        if (c_changes == null) {
            c_changes = new PropertyChangeSupport(this);
        }
        return c_changes;
    }

    /**
     *
     * @param listener
     */
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        changeSupport().addPropertyChangeListener(listener);
    }

    /**
     *
     * @param propertyName
     * @param listener
     */
    public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        changeSupport().addPropertyChangeListener(propertyName, listener);
    }

    /**
     *
     * @param listener
     */
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        changeSupport().removePropertyChangeListener(listener);
    }

    /**
     *
     * @param propertyName
     * @param listener
     */
    public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        changeSupport().removePropertyChangeListener(propertyName, listener);
    }

    /**
     *
     * @param con
     * @param data
     * @return
     * @throws SQLException
     */
    public static ResultSet sqlFunctionWeps(Connection con, String data) throws SQLException {
        try {
            if (con instanceof JdbcConnection) {
                JdbcConnection jdbcCon = (JdbcConnection) con;
                Session session = (Session) jdbcCon.getSession();
                WepsConnection wepCon = get(session.getVariable(WEPS_CONNECTION_VARIABLE).getInt());
                return wepCon.getWepsData(data, !con.toString().contains(Constants.CONN_URL_COLUMNLIST));
            } else {
                throw new SQLException("WEPS(String) calls only work for on " + WepsConnection.class.getName());
            }
        } catch (SQLException se) {
            LOGGER.error("Unexpected Error", se);
            throw se;
        }
    }
}
