/*
 * 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.gui.visualization;

import ecatalog.ECatalog;
import ecatalog.ECatalogLog;
import ecatalog.db.Database;
import ecatalog.db.Attribute;
import ecatalog.db.Constraint;
import ecatalog.db.Weight;
import ecatalog.db.Item;
import ecatalog.db.ResultSetItem;
import ecatalog.db.DefaultItem;
import ecatalog.db.DBRank;

import ecatalog.gui.MainGui;

import utils.LinkedHashMap2;
import utils.IconButton;
import utils.itemTable.*;
import javax.swing.table.TableCellRenderer;
//import ecatalog.jaxb.ecatalogConfig.ECatalogConfigType;

import info.clearthought.layout.TableLayout;


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

import java.sql.*;

import dpc.utils.DebugUtil;
import dpc.utils.SqlUtil;

public class VisualizationGui {
    ECatalogLog log;
    String itemTableRendererClassName;
    boolean preferencesAllowed;
    MainGui mainGui;
    //ECatalogConfigType config;
    Database db;
    VisualizationGuiListener listener;

    //ECatalogItemTableRenderer renderer;

    JComponent component;
    JSplitPane splitPane;

    enum Window {ITEMS_TABLE, CONSIDERATION_TABLE};

    ItemTable itemsTable;
    ItemsModel itemsModel;

    ItemTable considerationTable;
    ConsiderationModel considerationModel;

    public void update() {
	itemsModel.update();
	itemsTable.selectItem(-1);
	considerationTable.selectItem(-1);
	listener.itemSelected(null);
    }

    public void restart() {
	itemsTable.sortBy(null, null);
	considerationModel.removeAll();
    }

    public void resetSorting() {
	itemsTable.sortBy(null, null);
    }

    public void resetConsiderationWindow() {
	considerationModel.removeAll();
    }

    public void setEnabled(boolean enabled) {
        itemsTable.setEnabled(enabled);
	considerationTable.setEnabled(enabled);
    }

    public void blockSplitPane(boolean block) {
	splitPane.setEnabled(!block);
    }

    public void consider(SelectedItem selectedItem) {
	try {
	    if (log != null)
		log.considerItem(selectedItem, considerationModel.containsItem(selectedItem.item));

	    considerationModel.addItem(selectedItem.item);
	} catch (Throwable e) {
	    ECatalog.error(e); 
	}
    }

    public void consider(Item item) {
	try {
	    considerationModel.addItem(item);
	} catch (Throwable e) {
	    ECatalog.error(e); 
	}
    }


    public void removeConsider(SelectedItem selectedItem) {
	try {
	    if (log != null)
		log.removeConsiderItem(selectedItem);

	    considerationModel.removeItem(selectedItem.item);
	} catch (Throwable e) {
	    ECatalog.error(e); 
	}
    }


    /* Returns the selected item, or null if none is selected */
    public SelectedItem getSelectedItem() {
	int index = itemsTable.getSelectedItemIndex();
	if (index != -1)
	    return new SelectedItem((Item) itemsModel.getItem(index), Window.ITEMS_TABLE, index);

	index = considerationTable.getSelectedItemIndex();
	if (index != -1)
	    return new SelectedItem((Item) considerationModel.getItem(index), Window.CONSIDERATION_TABLE, index);

	return null;
    }

    public int getCurrentNbrItemsInConsideration() {
	return considerationModel.getNbrItems();
    }

    /* ItemTable informs that this item has been selected */
    public void itemSelected(Window window, int itemIndex) {
	//DebugUtil.printTrace(1, "Window:" + window + ", itemIndex:" + itemIndex, false);

	//First unselect the item (if selected) on the other window
	ItemTable table = (window != Window.ITEMS_TABLE) ? itemsTable : considerationTable;
	table.selectItem(-1);

	//Inform about the selection
	ItemTableModel model = (window == Window.ITEMS_TABLE) ? itemsModel : considerationModel;
	Item item = (Item) model.getItem(itemIndex);
	SelectedItem selectedItem = new SelectedItem(item, window, itemIndex);

	if (log != null)
	    log.itemSelected(selectedItem);

	listener.itemSelected(selectedItem);
    }


    public JComponent getComponent() {
	return component;
    }

    public void init(MainGui mainGui, final ECatalog ecatalog) throws SQLException {
	VisualizationGuiListener listener = 
	    new VisualizationGuiListener() {public void itemSelected(SelectedItem selectedItem) { ecatalog.itemSelected(selectedItem); }};

	String title = ecatalog.getConfig().isPreferencesAllowed() ? "Ranked Items" : "Matching Items";

	init(mainGui, 
	     ecatalog.getDatabase(),
	     listener, 
	     title,
	     ecatalog.getConfig().isPreferencesAllowed(), 
	     ecatalog.getConfig().getItemTableRendererClassName(),
	     ecatalog.getLog());
    }

    public void init(MainGui mainGui, Database db, VisualizationGuiListener listener, String tableTitle, 
		     boolean preferencesAllowed, String itemTableRendererClassName, ECatalogLog log) throws SQLException {
	this.mainGui = mainGui;
	this.db = db;
	this.listener = listener;
	this.preferencesAllowed = preferencesAllowed;
	this.itemTableRendererClassName = itemTableRendererClassName;
	this.log = log;

	ECatalogItemTableRenderer renderer = createRenderer();

	itemsModel = new ItemsModel();
	ConsiderProxyItemTableRenderer crenderer = new ConsiderProxyItemTableRenderer(renderer);
	itemsTable = new MyItemTable(itemsModel, crenderer);
	itemsTable.addItemTableListener(new ItemTableListener() { 
		public void itemSelected(int itemIndex) { VisualizationGui.this.itemSelected(Window.ITEMS_TABLE, itemIndex); }});

	considerationModel = new ConsiderationModel();
	RemoveConsiderProxyItemTableRenderer rrenderer = new RemoveConsiderProxyItemTableRenderer(renderer);
	considerationTable = new MyItemTable(considerationModel, rrenderer);
	considerationTable.addItemTableListener(new ItemTableListener() { 
		public void itemSelected(int itemIndex) { VisualizationGui.this.itemSelected(Window.CONSIDERATION_TABLE, itemIndex); }});

	initGui(tableTitle);
    } 

    ECatalogItemTableRenderer createRenderer() {
	ECatalogItemTableRenderer renderer;
	try {
	    renderer = (itemTableRendererClassName != null) 
		? (ECatalogItemTableRenderer) Class.forName(itemTableRendererClassName).newInstance()
		: new DefaultItemTableRenderer();
	} catch (Exception e) { 
	    ECatalog.error(e); 
	    throw new Error(); //it is already done, but to avoid the java compiling error
	}
	return renderer;
    }
    
    void initGui(String tableTitle) {
	//PANEL 1
	JPanel panel1 = new JPanel();
	TableLayout layout = new TableLayout(new double[][] {{TableLayout.FILL},{TableLayout.PREFERRED, 10, TableLayout.FILL}});
	panel1.setLayout(layout);
	
	panel1.add(mainGui.createPanelLabel(tableTitle), "0, 0");
	JScrollPane scrollPane = new JScrollPane(itemsTable, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
	panel1.add(scrollPane, "0, 2");

	//PANEL 2
	JPanel panel2 = new JPanel();
	layout = new TableLayout(new double[][] {{TableLayout.FILL},{TableLayout.PREFERRED, 10, TableLayout.FILL}});
	panel2.setLayout(layout);
	panel2.add(createConsiderationTopPanel(), "0, 0");
	considerationTable.setTableHeader(null);
	scrollPane = new JScrollPane(considerationTable, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
	panel2.add(scrollPane, "0, 2");

	splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, panel1, panel2);
        splitPane.setResizeWeight(0.66);

	component = splitPane;
    }

    JPanel createConsiderationTopPanel() {
	JPanel topPanel = new JPanel();
	topPanel.setLayout(new TableLayout(new double[][] {{TableLayout.PREFERRED, TableLayout.FILL, TableLayout.PREFERRED},
							   {TableLayout.PREFERRED}}));
	topPanel.setBackground(Color.BLUE);

	JLabel aLabel = new JLabel("Considered items");
	aLabel.setFont(new Font(mainGui.fontName, Font.BOLD, 18));
	aLabel.setForeground(Color.WHITE);
	topPanel.add(aLabel, "0,0");

	JButton resetButton = new JButton("Reset");
	resetButton.setFont(new Font(mainGui.fontName, Font.BOLD, 10));
	//resetButton.setPreferredSize(resetButton.getMinimumSize());
	resetButton.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { resetConsiderationWindow(); } });
	topPanel.add(resetButton, "2,0");
	return topPanel;
    }


    class MyItemTable extends ItemTable {
	BackgroundItemCellRenderer backgroundItemCellRenderer = new BackgroundItemCellRenderer();
    
	MyItemTable(ItemTableModel itemModel, ItemTableRenderer itemRenderer) {
	    super(itemModel, itemRenderer);
	    setDefaultItemTableCellRenderer(new SatisfyItemTableCellRenderer());
	}

	public ItemTableCellRenderer getItemTableCellRenderer(int itemIndex, int itemRow, int tableCol) {
	    return backgroundItemCellRenderer;
	}

	ItemTableCellRenderer getSuperItemTableCellRenderer(int itemIndex, int itemRow, int tableCol) {
	    return super.getItemTableCellRenderer(itemIndex, itemRow, tableCol);
	}

	class BackgroundItemCellRenderer extends DefaultItemTableCellRenderer {
	    public Component getItemTableCellRendererComponent(ItemTable table, Object value, boolean isSelected, boolean hasFocus, 
							       int itemIndex, int itemRow, int tableCol) {
		try {
		    ItemTableCellRenderer r = getSuperItemTableCellRenderer(itemIndex, itemRow, tableCol);
		    Component c = r.getItemTableCellRendererComponent(table, value, isSelected, hasFocus, itemIndex, itemRow, tableCol);
		    boolean isItemCompatible = (table == itemsTable)
			? itemsModel.isItemCompatible(itemIndex)           // table == itemsTable
			: considerationModel.isItemCompatible(itemIndex);  // table == considerationTable
		    
		    Color bgColor = (( itemIndex % 2) == 0)
			? (isItemCompatible ? MainGui.colorBgOddItems : MainGui.colorBgOddUnmatchingItems)
			: (isItemCompatible ? MainGui.colorBgEvenItems : MainGui.colorBgEvenUnmatchingItems);
		    
		    c.setBackground(bgColor);
		    return c;
		} catch (SQLException e) {
		    ECatalog.error(e); 
		    throw new Error(); //it is already done, but to avoid the java compiling error
		}
	    }
	}

	class SatisfyItemTableCellRenderer extends DefaultItemTableCellRenderer {
	    public Component getItemTableCellRendererComponent(ItemTable table, Object value, boolean isSelected, boolean hasFocus, 
							       int itemIndex, int itemRow, int tableCol) {
		Component c = super.getItemTableCellRendererComponent(table, value, isSelected, hasFocus, itemIndex, itemRow, tableCol);
		ItemTableModel itemTableModel = table.getItemTableModel();
		Item item = (Item) itemTableModel.getItem(itemIndex);
		ECatalogItemTableRenderer renderer = (ECatalogItemTableRenderer) table.getItemTableRenderer();
		boolean attributeSatisfied = renderer.isCellSatisfied(item, itemRow, tableCol);
		c.setForeground(attributeSatisfied ? MainGui.colorExample : MainGui.colorOverconstrained);
		return c;
	    }
	}
    }


    public class ItemsModel implements ItemTableModel {
	int nbrSatisfyingSolutions;
	int nbrPartiallySatisfyingSolutions;

	/* In case preferences are not allowed */
	int sortAttributeIndex = -1;
	ItemTableModel.SortDirection sortDirection;
	
	/* In case preferences are allowed */
	double[] satisfyingRanks = null;
	double[] partiallySatisfyingRanks = null;

	/* Sort based on the ranks. In case preferences are allowed */
	int[] satisfyingSortedIndex = null;
	int[] partiallySatisfyingSortedIndex = null;

	ResultSet satisfyingRs = null;
	ResultSet partiallySatisfyingRs = null;

	void update() {
	    updateResultSet();
	    itemsTable.update();
	}


	/* get the number of items */
	public int getNbrItems() {
	    return nbrSatisfyingSolutions + nbrPartiallySatisfyingSolutions;
	}

	/* get the item (according to the order selected using sortBy) */
	public Object getItem(int itemIndex) {
	    try {
		if (itemIndex < nbrSatisfyingSolutions) {
		    if (preferencesAllowed)
			itemIndex = satisfyingSortedIndex[itemIndex];
		    satisfyingRs.absolute(itemIndex + 1);

		    // TODO: maybe I should use DefaultItem.createFromResultSet() ...
		    return new ResultSetItem(db, satisfyingRs);
		} else {
		    itemIndex -= nbrSatisfyingSolutions;
		    if (preferencesAllowed)
			itemIndex = partiallySatisfyingSortedIndex[itemIndex];
		    partiallySatisfyingRs.absolute(itemIndex + 1);

		    // TODO: maybe I should use DefaultItem.createFromResultSet() ...
		    return new ResultSetItem(db, partiallySatisfyingRs);
		}
	    } catch (SQLException e) { 
		ECatalog.error(e); 
		throw new Error(); //it is already done, but to avoid the java compiling error
	    }
	}

	//A different implementation of this function can be found at Database.isItemCompatible(Item item)
	public boolean isItemCompatible(int itemIndex) { 
	    return (itemIndex < nbrSatisfyingSolutions);
	}

	/* Returns true if the items can be sorted using this attribute.
	 * If so, and the user ask to sort, sortBy will be called
	 */
	public boolean isSortableByAttribute(int attributeIndex) {
	    return (preferencesAllowed == false);
	}

	/* if attributeIndex == -1, cancel sorting.
	   if direction == TableSorter2.ASCENDING, sort column ascending
	   if direction == TableSorter2.DESCENDING, sort column descending
	*/
	public void sortBy(int attributeIndex, ItemTableModel.SortDirection direction) {
	    if (preferencesAllowed)
		return;

	    this.sortAttributeIndex = attributeIndex;
	    this.sortDirection = direction;

	    //System.out.println("sortBy " + sortAttributeIndex + ", " + sortDirection);
	    updateResultSet();
	    itemsTable.update();
	}


	void updateResultSet() {
	    if (satisfyingRs != null) try { satisfyingRs.close(); } catch (Exception e) {}
	    if (partiallySatisfyingRs != null) try { partiallySatisfyingRs.close(); } catch (Exception e) {}

	    try {

		String preferenceClause = "1=1";
		String mandatoryClause = "1=1";
		for (Constraint c : db.getConstraints()) {
		    if (c.areDetailsDefined() == true) {
			if (c.getWeight().getWeightValue() == -1)
			    mandatoryClause += " AND (" + c.getSqlClauseTerm() + ")";
			else
			    preferenceClause += " AND (" + c.getSqlClauseTerm() + ")";
		    }
		}

		String satisfyingClause = "(" + db.getWhereClause() + ") AND (" + mandatoryClause + ") AND (" + preferenceClause + ")";
		String partiallySatisfyingClause = ("1=1".equals(preferenceClause))
		    ? "1=0"
		    : "(" + db.getWhereClause() + ") AND (" + mandatoryClause + ") AND (NOT(" + preferenceClause + "))";

		nbrSatisfyingSolutions = SqlUtil.getUniqueInt(db.executeQuery("SELECT COUNT(*) FROM " + db.getFromClause() + " WHERE " + satisfyingClause));
		nbrPartiallySatisfyingSolutions = SqlUtil.getUniqueInt(db.executeQuery("SELECT COUNT(*) FROM " + db.getFromClause() + " WHERE " + partiallySatisfyingClause));

		
		String sortClause = "";
		if (!preferencesAllowed && sortAttributeIndex != -1)
		    sortClause = " ORDER BY " + db.getAttribute(sortAttributeIndex).getId() 
			+ ((sortDirection == ItemTableModel.SortDirection.ASCENDING) ? " ASC" : " DESC");

		String satisfyingQuery = "SELECT " + db.getAttributesClause() + " FROM " + db.getFromClause() 
		    + " WHERE " + satisfyingClause + sortClause;

		String partiallySatisfyingQuery = "SELECT " + db.getAttributesClause() + " FROM " + db.getFromClause() 
		    + " WHERE " + partiallySatisfyingClause + sortClause;

		//System.out.println("nbrSatisfyingSolutions: " + satisfyingQuery);
		//System.out.println("nbrPartiallySatisfyingSolutions: " + nbrPartiallySatisfyingSolutions);
		//System.out.println("satisfyingQuery: " + satisfyingQuery);
		//System.out.println("partiallySatisfyingQuery: " + partiallySatisfyingQuery);

		satisfyingRs = db.executeQuery(satisfyingQuery, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
		partiallySatisfyingRs = db.executeQuery(partiallySatisfyingQuery, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
		
		if (preferencesAllowed) {
		    satisfyingRanks = DBRank.computeRanks(db, satisfyingRs);
		    satisfyingSortedIndex = DBRank.sortRanks(satisfyingRanks);

		    partiallySatisfyingRanks = DBRank.computeRanks(db, partiallySatisfyingRs);
		    partiallySatisfyingSortedIndex = DBRank.sortRanks(partiallySatisfyingRanks);
		}
	    } catch (SQLException e) {
		satisfyingRs = null;
		partiallySatisfyingRs = null;
		e.printStackTrace(System.out);
	    }
	}
    }



    public class ConsiderProxyItemTableRenderer extends ItemActionProxyItemTableRenderer {
	public ConsiderProxyItemTableRenderer(ECatalogItemTableRenderer proxy) {
	    super(proxy, db, itemsModel, Window.ITEMS_TABLE);
	    addItemActionProxyItemTableRendererListener(new ItemActionProxyItemTableRendererListener() {
		    public void itemActionSelected(SelectedItem selectedItem) {
			consider(selectedItem);
		    }
		});
	}
	public JButton createNewItemActionButton() { return new ConsiderationButton();}

	public class ConsiderationButton extends IconButton {
	    public ConsiderationButton() {
		super(new ImageIcon("images/cun.png"), new ImageIcon("images/csel.png"),
		      new ImageIcon("images/cpre.png"), new ImageIcon("images/chid.png"));
	    }
	}
    }


    public class RemoveConsiderProxyItemTableRenderer extends ItemActionProxyItemTableRenderer {
	public RemoveConsiderProxyItemTableRenderer(ECatalogItemTableRenderer proxy) {
	    super(proxy, db, considerationModel, Window.CONSIDERATION_TABLE);
	    addItemActionProxyItemTableRendererListener(new ItemActionProxyItemTableRendererListener() {
		    public void itemActionSelected(SelectedItem selectedItem) {
			removeConsider(selectedItem);
		    }
		});
	}
	public JButton createNewItemActionButton() { return new RemoveConsiderationButton();}

	public class RemoveConsiderationButton extends IconButton {
	    public RemoveConsiderationButton() {
		super(new ImageIcon("images/supun.png"), new ImageIcon("images/supsel.png"),
		      new ImageIcon("images/suppre.png"), new ImageIcon("images/suphid.png"));
	    }
	}
    }


    public class ConsiderationModel implements ItemTableModel {
	LinkedHashMap2<String, DefaultItem> items = new LinkedHashMap2<String, DefaultItem>();
	Attribute idAttribute;

	ConsiderationModel() {
	    idAttribute = db.getAttributeById("id");
	}

	/* get the number of items */
	public int getNbrItems() {
	    return items.size();
	}

	/* get the item (according to the order selected using sortBy) */
	public Object getItem(int itemIndex) {
	    return items.get(itemIndex);
	}

	public boolean isItemCompatible(int itemIndex) throws SQLException { 
	    return db.isItemCompatible(items.get(itemIndex));
	}

	public boolean isSortableByAttribute(int attributeIndex) {
	    return false;
	}

	public void sortBy(int attributeIndex, ItemTableModel.SortDirection direction) {
	}

	void addItem(Item item) throws Exception {
	    DefaultItem itemCopy = DefaultItem.createFromItem(db, item);
	    String itemId = idAttribute.getDbValueString(itemCopy, false);
	    items.put(itemId, itemCopy);
	    considerationTable.update();
	}

	void removeItem(Item item) throws Exception {
	    String itemId = idAttribute.getDbValueString(item, false);
	    items.remove(itemId);
	    considerationTable.update();

	    //If the user removes an item that it was selected, it needs to inform that there is currently not a selection
	    itemsTable.selectItem(-1);
	    considerationTable.selectItem(-1);
	    listener.itemSelected(null);
	}

	boolean containsItem(Item item) throws Exception {
	    String itemId = idAttribute.getDbValueString(item, false);
	    return items.containsKey(itemId);
	}

	void removeAll() {
	    items.clear();
	    considerationTable.update();
	}
    }
}


