/*
 * ECatalog is a database front-end, with two main features:
 * 1. Use of preferences
 *  A preference-based approach, where the user is allowed to define the importance of each criterion.
 *  Then the items are ranked accordingly to his criteria.
 * 2. Trade-off analysis
 *  A cooperative database approach, where the system "argues" with the user about his criteria.
 *  When there are no matching items, the system explains the minimal conflicting set and
 *  give some possible strong and weak relaxations about his criteria.
 * This package also containts the software and the set-up details used for our User Study,
 * comparing the use or not of the two previous features mentioned above.
 *
 * Copyright (C) 2006 David Portabella Clotet, Artificial Intelligence Laboratory, EPFL
 * 
 * This file is part of ecatalog-1.0.zip
 * 
 * ECatalog is free software and a free user study set-up;
 * 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.
 * 
 * ECatalog 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 ECatalog; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * @version 1.0
 * @author David Portabella
 * To contact the author:
 * email: david@portabella.name and david.portabella@epfl.ch
 * 
 * More information about ECatalog:
 *  http://sourceforge.net/projects/ecatalog/
 *  http://icwww.epfl.ch/~portabel/ecatalogs/
 */

package ecatalog;

import java.io.*;
import java.sql.ResultSet;
import java.sql.SQLException;

import java.util.*;
import java.text.SimpleDateFormat;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.Unmarshaller;

import ecatalog.jaxb.ecatalogConfig.ECatalogConfigType;
import ecatalog.jaxb.ecatalogConfig.AttributeType;

import dpc.utils.DebugUtil;

import ecatalog.db.Database;
import ecatalog.gui.MainGui;
import ecatalog.gui.visualization.SelectedItem;

import ecatalog.db.*;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.SwingUtilities;

/* The ECatalog test application  */ 

public class ECatalog {
    //TODO: define getters for this variables, and explanations
    static final int minimalUnsolvableSubproblemMaxSize = 4;
    static final int relaxationListMaxSize = 6;
    static final int maxNbrOfCombinedRelaxations = 3;
    static Weight defaultWeight;
    //public boolean preferencesAllowed;

    public MainGui gui;

    ECatalogConfigType config;
    Database db;
    ECatalogLog log = null;

    DBAnalysis.PossibleRelaxations possibleRelaxations;
    DBAnalysis.MinimalUnsolvableSubproblem minimalUnsolvableSubproblem;

    static ECatalog instance; //TODO: how to get the log without a reference to ECatalog?

    public static void main(String[] argv) throws Exception {
	final ECatalog ecatalog = new ECatalog();
	ecatalog.configure(argv[0]);
	ecatalog.setLog(new ECatalogLog(ecatalog, getLogFileName()));

	ecatalog.init();

	UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
	JFrame frame = new JFrame("Application");

	frame.getContentPane().add(ecatalog.getComponent());
	frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { ecatalog.exitRequested(); } });
	frame.pack();
	fixWindowSize(frame);
	frame.setSize(1200, 900);
	frame.setVisible(true);
    }


    public void configure(String configFileName) throws Exception {
	JAXBContext jc = JAXBContext.newInstance("ecatalog.jaxb.ecatalogConfig");
	Unmarshaller u = jc.createUnmarshaller();

        config = (ECatalogConfigType) ((JAXBElement<?>)u.unmarshal(new FileInputStream(configFileName))).getValue();
	configure(config);
    }


    /* Initializes the database and the GUI */
    public void configure(ECatalogConfigType config) throws Exception {
	instance = this;
	this.config = config;
	lowlevelMsg("Config: " + config.getTitle());

	if (config.isPreferencesAllowed()) {
	    defaultWeight = new Weight(0.6);
	    //preferencesAllowed = true;
	} else {
	    defaultWeight = new Weight(-1);
	    //preferencesAllowed = false;
	}

	if (!config.isConstraintStringFunctionsEnabled()) {
	    //remove all the constraint string functions...
	    for (Iterator it = config.getAttribute().iterator(); it.hasNext(); ) {
		AttributeType attributeConfig = (AttributeType) it.next();
		attributeConfig.setWithConstraintStringFunction(null);
		attributeConfig.setRelaxConstraintStringFunction(null);
	    }
	}
    }

    public void init() throws Exception {
	if (log != null)
	    log.start();

	db = new Database();
	db.init(config, log);

	db.updateContext();

	gui = new MainGui();

	try { gui.init(this); }
	catch (Exception e) { throw new Error(e); }
	update();

	if (log != null)
	    log.genericInfo("ready");

	//try { Thread.currentThread().sleep(10000); } catch (InterruptedException e) {}
	//test();
    }

    public JComponent getComponent() {
	return gui.getComponent();
    }


    public ECatalogConfigType getConfig() { return config; }
    public Database getDatabase() { return db; }

    /* The user asks to restart */
    public void restart() {
	if (log != null)
	    log.genericIc("restart", null);

	gui.visualizationGui.resetSorting();
	gui.visualizationGui.resetConsiderationWindow();
	gui.criteriaSelectionGui.reset();
	update();
    }

    /* Returns the current selected item, or null otherwise. */
    public SelectedItem getSelectedItem() {
	return gui.visualizationGui.getSelectedItem();
    }


    /* After each user selection, call this update function. It updates the database context, and the GUI. */
    public void update() {
	if (log != null)
	    log.nextIc();

	Runnable updateRunnable = new Runnable() {
		public void run() {
		    lowlevelMsg("UPDATE START");
		    long start = System.currentTimeMillis();

		    if (config.isConflictAnalysisDisplayed()) {
			/* If there is an analysis running, cancel it (outdated) */
			cancelAnalysis();  

			//gui.setMainMessage("Searching...");
			//gui.analysisGui.setText("");
		    }

		    try {
			//if (restart)
			//    gui.restart();
			lowlevelMsg("UPDATE. UPDATE_DB_CONTEXT");
			db.updateContext();
			possibleRelaxations = null;
			minimalUnsolvableSubproblem = null;

			lowlevelMsg("UPDATE. UPDATE_GUI");
			gui.setMainMessage(getNbrSolutionsMessage());
			gui.update();

			gui.analysisGui.setText("");
			gui.setEnabled(true);
	
			if (config.isConflictAnalysisDisplayed() && db.getCurrentNbrSolutions() == 0) {
			    lowlevelMsg("UPDATE. ANALYZING ");
			    gui.analysisGui.setText("Analyzing tradeoffs...");
			    performAnalysis();
			}
			lowlevelMsg("UPDATE END  ");
		    } catch (Exception e) {
			if (log != null)
			    log.error(e);
			throw new Error(e);
		    }

		    if (log != null) {
			long duration = System.currentTimeMillis() - start;
			log.genericInfo("update", new String[][] {{"duration", String.valueOf(duration)}});
			log.writeState();
		    }
		}
	    };
	gui.analysisGui.setText("Analysing...");
	gui.setEnabled(false);
	SwingUtilities.invokeLater(updateRunnable);
    }

    /* Sets the example from the current row in the provided ResultSet */
    public void itemSelected(SelectedItem selectedItem) {
	try {
	    if (selectedItem != null) {
		boolean compatibleItem = db.isItemCompatible(selectedItem.item); 

		if (compatibleItem) {
		    try { 
			db.updateExample(selectedItem.item);
			gui.criteriaSelectionGui.update();
		    } catch (SQLException e) {
			if (log != null)
			    log.error(e);
			throw new Error(e); 
		    }
		}
	    }

	    for ( ECatalogListener l : dl)
		l.itemSelected(selectedItem);
	} catch (SQLException e) {
	    error(e);
	}
    }

    /* To inform that a weight has changed */
    public void weightChanged() {
	gui.visualizationGui.update();
    }

    /* Analysis is finished. Update the GUI */
    public void analysisFinished() {
	lowlevelMsg("UPDATE. ANALYSIS FINISHED");
	gui.analysisGui.showAnalysis(getPossibleRelaxationsText(), getMinimalUnsolvableSubproblemText());
    }

    /* The user accepted a relaxation. Execute it */
    public void executeRelaxation(String relaxationCode) {
	DBAnalysis.executeRelaxation(relaxationCode, possibleRelaxations, minimalUnsolvableSubproblem, log);
	update();
    }

    /* The main message at the top of the window */
    String getNbrSolutionsMessage() {   //TODO minor: put this info in the config file
    	return "There are " + db.getCurrentNbrSolutions() + " items (out of " + db.getTotalNbrSolutions() + ") matching your criteria.";
    }

    /* The default weight for all the constraints */
    public Weight getDefaultWeight() {
	return defaultWeight;
    }


    public void performAnalysis() throws SQLException {
	cancelAnalysis();

	if (db.getCurrentNbrSolutions() > 0) {
	    possibleRelaxations = null;
	    minimalUnsolvableSubproblem = null;
	    return;
	}

	currentAnalysisRunnable = new AnalysisRunnable();
	new Thread(currentAnalysisRunnable).start();
    }

    public void cancelAnalysis() {
	if (currentAnalysisRunnable != null)
	    currentAnalysisRunnable.cancel();
	currentAnalysisRunnable = null;
    }

    public String getPossibleRelaxationsText() {
	if (possibleRelaxations == null || possibleRelaxations.text == null)
	    return "Analyzing possible relaxations...<br>";
	return possibleRelaxations.text;
    }

    public String getMinimalUnsolvableSubproblemText() {
	if (minimalUnsolvableSubproblem == null || minimalUnsolvableSubproblem.text == null)
	    return "";
	return minimalUnsolvableSubproblem.text;
    }

    AnalysisRunnable currentAnalysisRunnable = null;

    class AnalysisRunnable implements Runnable {
	boolean cancelled = false;
	DBAnalysis analysis;
	
	void cancel() { 
	    lowlevelMsg("AnalysisRunnable.cancel()");
	    cancelled = true;
	    if (analysis != null)
		analysis.cancel();
	}
	public void run() {
	    try {
		long start, duration;
		if (cancelled) return;
		
		analysis = new DBAnalysis(db, db.getConstraints());
		
		computeMinimalUnsolvableSubproblem();
		if (cancelled) return;

		computePossibleRelaxations();
	    } catch (SQLException e) {
		error(new Error(e));
	    }
	}
	
	void computeMinimalUnsolvableSubproblem() throws SQLException {
	    long start = System.currentTimeMillis();
	    minimalUnsolvableSubproblem = analysis.getMinimalUnsolvableSubproblem(minimalUnsolvableSubproblemMaxSize);
	    if (cancelled) return;
	    
	    long duration = System.currentTimeMillis() - start;
	    if (log != null)
		log.minimalUnsolvableSubproblemAnalysis(minimalUnsolvableSubproblem, duration);

	    analysisFinished();
	}

	void computePossibleRelaxations() throws SQLException {
	    long start = System.currentTimeMillis();

	    possibleRelaxations = analysis.getPossibleRelaxations(relaxationListMaxSize, maxNbrOfCombinedRelaxations, "There are items <b>if you</b> ", "-1 ");
		
	    if (cancelled) return;

	    long duration = System.currentTimeMillis() - start;
	    if (log != null)
		log.possibleRelaxationsAnalysis(possibleRelaxations, duration);

	    analysisFinished();
	}
    }



    public void exitRequested() {
	if (log != null) {
	    log.genericInfo("exit");
	    log.close();
	}
	System.exit(0);
    }

    public static void error(Throwable e) throws Error {
	ECatalogLog log = getInstance().log;
	if (log != null)
	    log.error(e);
	throw new Error(e);
	//TODO: stop the system if specified in the config file...
    }

    /* The user has finally selected an item */
    /*
      public void itemActionSelected(Item item) {
      System.out.println("selected: " + item);
      for (ECatalogListener l : dl)
      l.itemSelected(item);
      }
    */

    public static void lowlevelMsg(String text) {
	//DebugUtil.printTrace(text);
    }

    LinkedList<ECatalogListener> dl = new LinkedList<ECatalogListener>();
    public void addECatalogListener(ECatalogListener l) { dl.add(l); }
    public void removeECatalogListener(ECatalogListener l) { dl.remove(l); }

    public void setLog(ECatalogLog log) {
	if (this.log != null)
	    this.log.close();
	this.log = log;
    }

    public ECatalogLog getLog() {
	return log;
    }

    public static ECatalog getInstance() {
	return instance;
    }


    static void fixWindowSize(JFrame frame)
    {
	Dimension dim = frame.getSize();

	Dimension maxDim = Toolkit.getDefaultToolkit().getScreenSize();
	//Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(...);
	Insets insets = new Insets(20,20,20,20);
	maxDim.width = (maxDim.width - (insets.left + insets.right))*3/4;
	maxDim.height = (maxDim.height - (insets.top + insets.bottom))*35/40;
		
	//dim.width = Math.min(dim.width, maxDim.width);
	//dim.height = Math.min(dim.height, maxDim.height);
	dim = maxDim;
	frame.setSize(dim);
    }


    static String getLogFileName() {
	Calendar cal = Calendar.getInstance(TimeZone.getDefault());
	String DATE_FORMAT = "yyyyMMddHHmmssSSS";
	SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
	sdf.setTimeZone(TimeZone.getDefault());          
	String timeString = sdf.format(cal.getTime());
	return "./logs/ECatalog-log-" + timeString + ".xml";
    }

    /*
      void test() throws Exception {
      for (Constraint c : db.constraints) {
      if (c instanceof BooleanConstraint) {
      ((BooleanConstraint)c).setValueId(true);
      } else if (c instanceof SimpleNumberAttributeConstraint) {
      ((SimpleNumberAttributeConstraint)c).setValueId(((SimpleNumberAttributeConstraint)c).attribute.currentExampleValueId);
      }
      }
      db.updateContext();

      System.out.println("TESTTTTTTTT: START");
      db.performAnalysis(minimalUnsolvableSubproblemMaxSize, relaxationListMaxSize);
      try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) {}

      System.out.println("TESTTTTTTTT: CANCEL");
      for (Constraint c : db.constraints)
      c.removeDetails();
      db.updateContext();
      db.performAnalysis(minimalUnsolvableSubproblemMaxSize, relaxationListMaxSize);
      System.out.println("TESTTTTTTTT: FINISHED");
      }
    */
}


