package beast.app.beauti;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.DefaultCellEditor;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.event.CellEditorListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;

import beast.app.draw.ListInputEditor;
import beast.app.draw.SmallButton;
import beast.app.util.FileDrop;
import beast.core.BEASTInterface;
import beast.core.Input;
import beast.core.MCMC;
import beast.core.State;
import beast.core.StateNode;
import beast.core.util.CompoundDistribution;
import beast.core.util.Log;
import beast.evolution.alignment.Alignment;
import beast.evolution.alignment.FilteredAlignment;
import beast.evolution.alignment.Taxon;
import beast.evolution.branchratemodel.BranchRateModel;
import beast.evolution.likelihood.GenericTreeLikelihood;
import beast.evolution.sitemodel.SiteModel;
import beast.evolution.sitemodel.SiteModelInterface;
import beast.evolution.tree.TreeInterface;

// TODO: add useAmbiguities flag 
// TODO: add warning if useAmbiguities=false and nr of patterns=1 (happens when all data is ambiguous)

public class AlignmentListInputEditor extends ListInputEditor {
	private static final long serialVersionUID = 1L;

	final static int NAME_COLUMN = 0;
	final static int FILE_COLUMN = 1;
	final static int TAXA_COLUMN = 2;
	final static int SITES_COLUMN = 3;
	final static int TYPE_COLUMN = 4;
	final static int SITEMODEL_COLUMN = 5;
	final static int CLOCKMODEL_COLUMN = 6;
	final static int TREE_COLUMN = 7;
	final static int USE_AMBIGUITIES_COLUMN = 8;
	
	final static int NR_OF_COLUMNS = 9;

    final static int STRUT_SIZE = 5;

	/**
	 * alignments that form a partition. These can be FilteredAlignments *
	 */
	List<Alignment> alignments;
	int partitionCount;
	GenericTreeLikelihood[] likelihoods;
	Object[][] tableData;
	JTable table;
	JTextField nameEditor;
	List<JButton> linkButtons;
	List<JButton> unlinkButtons;
	JButton splitButton;

    /**
     * The button for deleting an alignment in the alignment list.
     */
    JButton delButton;
    protected SmallButton replaceButton;

	private JScrollPane scrollPane;

	public AlignmentListInputEditor(BeautiDoc doc) {
		super(doc);
	}

	@Override
	public Class<?> type() {
		return List.class;
	}

	@Override
	public Class<?> baseType() {
		return Alignment.class;
	}

	@Override
	public Class<?>[] types() {
		Class<?>[] types = new Class[2];
		types[0] = List.class;
		types[1] = Alignment.class;
		return types;
	}

	@Override
	@SuppressWarnings("unchecked")
	public void init(Input<?> input, BEASTInterface beastObject, int itemNr, ExpandOption isExpandOption, boolean addButtons) {
		this.itemNr = itemNr;
		if (input.get() instanceof List) {
			alignments = (List<Alignment>) input.get();
		} else {
			// we just have a single Alignment
			alignments = new ArrayList<>();
			alignments.add((Alignment) input.get());
		}
		linkButtons = new ArrayList<>();
		unlinkButtons = new ArrayList<>();
		partitionCount = alignments.size();

        // override BoxLayout in superclass
        setLayout(new BorderLayout());

        add(createLinkButtons(), BorderLayout.NORTH);
        add(createListBox(), BorderLayout.CENTER);

        //Box box = Box.createVerticalBox();
		//box.add(Box.createVerticalStrut(STRUT_SIZE));
		//box.add(createLinkButtons());
		//box.add(Box.createVerticalStrut(STRUT_SIZE));
		//box.add(createListBox());
        //box.add(Box.createVerticalStrut(STRUT_SIZE));
        //box.add(Box.createVerticalGlue());
		//add(box, BorderLayout.CENTER);

        Color focusColor = UIManager.getColor("Focus.color");
        Border focusBorder = BorderFactory.createMatteBorder(2, 2, 2, 2, focusColor);
        new FileDrop(null, scrollPane, focusBorder, new FileDrop.Listener() {
            @Override
			public void filesDropped(java.io.File[] files) {
            	SwingUtilities.invokeLater(new Runnable() {
					@Override
					public void run() {
						addItem(files);
					}
				});
            }   // end filesDropped
        }); // end FileDrop.Listener

        // this should place the add/remove/split buttons at the bottom of the window.
        add(createAddRemoveSplitButtons(), BorderLayout.SOUTH);

        updateStatus();
	}

    /**
     * Creates the link/unlink button component
     * @return a box containing three link/unlink button pairs.
     */
	private JComponent createLinkButtons() {

        Box box = Box.createHorizontalBox();
		addLinkUnlinkPair(box, "Site Models");
        box.add(Box.createHorizontalStrut(STRUT_SIZE));
        addLinkUnlinkPair(box, "Clock Models");
        box.add(Box.createHorizontalStrut(STRUT_SIZE));
        addLinkUnlinkPair(box, "Trees");
		box.add(Box.createHorizontalGlue());
		return box;
	}

    private JComponent createAddRemoveSplitButtons() {
        Box buttonBox = Box.createHorizontalBox();

        addButton = new SmallButton("+", true, SmallButton.ButtonType.square);
        addButton.setName("+");
        addButton.setToolTipText("Add item to the list");
        addButton.addActionListener(e -> addItem());
        buttonBox.add(Box.createHorizontalStrut(STRUT_SIZE));
        buttonBox.add(addButton);
        buttonBox.add(Box.createHorizontalStrut(STRUT_SIZE));

        delButton = new SmallButton("-", true, SmallButton.ButtonType.square);
        delButton.setName("-");
        delButton.setToolTipText("Delete selected items from the list");
        delButton.addActionListener(e -> {
            if (doc.hasLinkedAtLeastOnce) {
                JOptionPane.showMessageDialog(null, "Cannot delete partition while parameters are linked");
                return;
            }
            delItem();
        });
        buttonBox.add(delButton);
        buttonBox.add(Box.createHorizontalStrut(STRUT_SIZE));

        replaceButton = new SmallButton("r", true, SmallButton.ButtonType.square);
        replaceButton.setName("r");
        replaceButton.setToolTipText("Replace alignment by one loaded from file");
        replaceButton.addActionListener(e -> replaceItem());
        buttonBox.add(Box.createHorizontalStrut(STRUT_SIZE));
        buttonBox.add(replaceButton);
        buttonBox.add(Box.createHorizontalStrut(STRUT_SIZE));

        
        splitButton = new JButton("Split");
        splitButton.setName("Split");
        splitButton.setToolTipText("Split alignment into partitions, for example, codon positions");
        splitButton.addActionListener(e -> splitItem());
        buttonBox.add(splitButton);

        buttonBox.add(Box.createHorizontalGlue());

        return buttonBox;
    }

	/**
     * This method just adds the two buttons (with add()) and does not add any glue or struts before or after.
     * @param box
     * @param label
     */
	private void addLinkUnlinkPair(Box box, String label) {

        //JLabel label = new JLabel(label+":");
        //box.add(label);
        JButton linkSModelButton = new JButton("Link " + label);
		linkSModelButton.setName("Link " + label);
		linkSModelButton.addActionListener(e -> {
            JButton button = (JButton) e.getSource();
            link(columnLabelToNr(button.getText()));
            table.repaint();
        });
		box.add(linkSModelButton);
		linkSModelButton.setEnabled(!getDoc().hasLinkedAtLeastOnce);
		JButton unlinkSModelButton = new JButton("Unlink " + label);
		unlinkSModelButton.setName("Unlink " + label);
		unlinkSModelButton.addActionListener(e -> {
            JButton button = (JButton) e.getSource();
            unlink(columnLabelToNr(button.getText()));
            table.repaint();
        });
		box.add(unlinkSModelButton);
		unlinkSModelButton.setEnabled(!getDoc().hasLinkedAtLeastOnce);

		linkButtons.add(linkSModelButton);
		unlinkButtons.add(unlinkSModelButton);
	}

	private int columnLabelToNr(String column) {
		int columnNr;
		if (column.contains("Tree")) {
			columnNr = TREE_COLUMN;
		} else if (column.contains("Clock")) {
			columnNr = CLOCKMODEL_COLUMN;
		} else {
			columnNr = SITEMODEL_COLUMN;
		}
		return columnNr;
	}

	private void link(int columnNr) {
		int[] selected = getTableRowSelection();
		// do the actual linking
		for (int i = 1; i < selected.length; i++) {
			int rowNr = selected[i];
			link(columnNr, rowNr, selected[0]);
		}
	}
	
	/** links partition in row "rowToLink" with partition in "rowToLinkWith" so that
	 * after linking there is only one partition for context "columnNr", namely that
	 * of "rowToLinkWith"
	 */
	private void link(int columnNr, int rowToLink, int rowToLinkWith) {
		Object old = tableData[rowToLink][columnNr];
		tableData[rowToLink][columnNr] = tableData[rowToLinkWith][columnNr];
		try {
			updateModel(columnNr, rowToLink);
		} catch (Exception ex) {
			Log.warning.println(ex.getMessage());
			// unlink if we could not link
			tableData[rowToLink][columnNr] = old;
			try {
				updateModel(columnNr, rowToLink);
			} catch (Exception ex2) {
				// ignore
			}
		}
		MRCAPriorInputEditor.customConnector(doc);
	}

	
	private void unlink(int columnNr) {
		int[] selected = getTableRowSelection();
		for (int i = 1; i < selected.length; i++) {
			int rowNr = selected[i];
			tableData[rowNr][columnNr] = getDoc().partitionNames.get(rowNr).partition;
			try {
				updateModel(columnNr, rowNr);
			} catch (Exception ex) {
				Log.err.println(ex.getMessage());
				ex.printStackTrace();
			}
		}
	}


    int[] getTableRowSelection() {
        return table.getSelectedRows();
	}

	/** set partition of type columnNr to partition model nr rowNr **/
	void updateModel(int columnNr, int rowNr) {
		Log.warning.println("updateModel: " + rowNr + " " + columnNr + " " + table.getSelectedRow() + " "
				+ table.getSelectedColumn());
		for (int i = 0; i < partitionCount; i++) {
			Log.warning.println(i + " " + tableData[i][0] + " " + tableData[i][SITEMODEL_COLUMN] + " "
					+ tableData[i][CLOCKMODEL_COLUMN] + " " + tableData[i][TREE_COLUMN]);
		}

		getDoc();
		String partition = (String) tableData[rowNr][columnNr];

		// check if partition needs renaming
		String oldName = null;
		boolean isRenaming = false;
		try {
			switch (columnNr) {
			case SITEMODEL_COLUMN:
				if (!doc.pluginmap.containsKey("SiteModel.s:" + partition)) {
					String id = ((BEASTInterface)likelihoods[rowNr].siteModelInput.get()).getID();
					oldName = BeautiDoc.parsePartition(id);
					doc.renamePartition(BeautiDoc.SITEMODEL_PARTITION, oldName, partition);
					isRenaming = true;
				}
				break;
			case CLOCKMODEL_COLUMN: {
				String id = likelihoods[rowNr].branchRateModelInput.get().getID();
				String clockModelName = id.substring(0, id.indexOf('.')) + ".c:" + partition;
				if (!doc.pluginmap.containsKey(clockModelName)) {
					oldName = BeautiDoc.parsePartition(id);
					doc.renamePartition(BeautiDoc.CLOCKMODEL_PARTITION, oldName, partition);
					isRenaming = true;
				}
			}
				break;
			case TREE_COLUMN:
				if (!doc.pluginmap.containsKey("Tree.t:" + partition)) {
					String id = likelihoods[rowNr].treeInput.get().getID();
					oldName = BeautiDoc.parsePartition(id);
					doc.renamePartition(BeautiDoc.TREEMODEL_PARTITION, oldName, partition);
					isRenaming = true;
				}
				break;
			}
		} catch (Exception e) {
			JOptionPane.showMessageDialog(this, "Cannot rename item: " + e.getMessage());
			tableData[rowNr][columnNr] = oldName;
			return;
		}
		if (isRenaming) {
			doc.determinePartitions();
			initTableData();
			setUpComboBoxes();
			table.repaint();
			return;
		}
		
		int partitionID = BeautiDoc.ALIGNMENT_PARTITION;
		switch (columnNr) {
		case SITEMODEL_COLUMN:
			partitionID = BeautiDoc.SITEMODEL_PARTITION;
			break;
		case CLOCKMODEL_COLUMN:
			partitionID = BeautiDoc.CLOCKMODEL_PARTITION;
			break;
		case TREE_COLUMN:
			partitionID = BeautiDoc.TREEMODEL_PARTITION;
			break;
		}
		int partitionNr = doc.getPartitionNr(partition, partitionID);
		GenericTreeLikelihood treeLikelihood = null;
		if (partitionNr >= 0) {
			// we ar linking
			treeLikelihood = likelihoods[partitionNr];
		}
		// (TreeLikelihood) doc.pluginmap.get("treeLikelihood." +
		// tableData[rowNr][NAME_COLUMN]);

		boolean needsRePartition = false;
		
		PartitionContext oldContext = new PartitionContext(this.likelihoods[rowNr]);

		switch (columnNr) {
		case SITEMODEL_COLUMN: {
			SiteModelInterface siteModel = null;
			if (treeLikelihood != null) { // getDoc().getPartitionNr(partition,
											// BeautiDoc.SITEMODEL_PARTITION) !=
											// rowNr) {
				siteModel = treeLikelihood.siteModelInput.get();
			} else {
				siteModel = (SiteModel) doc.pluginmap.get("SiteModel.s:" + partition);
				if (siteModel != likelihoods[rowNr].siteModelInput.get()) {
					PartitionContext context = getPartitionContext(rowNr);
					try {
					siteModel = (SiteModel.Base) BeautiDoc.deepCopyPlugin((BEASTInterface) likelihoods[rowNr].siteModelInput.get(),
							likelihoods[rowNr], (MCMC) doc.mcmc.get(), oldContext, context, doc, null);
					} catch (RuntimeException e) {
						JOptionPane.showMessageDialog(this, "Could not clone site model: " + e.getMessage());
						return;
					}
				}
			}
			SiteModelInterface target = this.likelihoods[rowNr].siteModelInput.get();
			if (target instanceof SiteModel.Base && siteModel instanceof SiteModel.Base) {
				if (!((SiteModel.Base)target).substModelInput.canSetValue(((SiteModel.Base)siteModel).substModelInput.get(), (SiteModel.Base) target)) {
					throw new IllegalArgumentException("Cannot link site model: substitution models (" + 
							((SiteModel.Base)target).substModelInput.get().getClass().toString() + " and " +
							((SiteModel.Base)siteModel).substModelInput.get().getClass().toString() +
							") are incompatible");
				}
			} else {
				throw new IllegalArgumentException("Don't know how to link this site model");
			}
			needsRePartition = (this.likelihoods[rowNr].siteModelInput.get() != siteModel);
			this.likelihoods[rowNr].siteModelInput.setValue(siteModel, this.likelihoods[rowNr]);

			partition = ((BEASTInterface)likelihoods[rowNr].siteModelInput.get()).getID();
			partition = BeautiDoc.parsePartition(partition);
			getDoc().setCurrentPartition(BeautiDoc.SITEMODEL_PARTITION, rowNr, partition);
		}
			break;
		case CLOCKMODEL_COLUMN: {
			BranchRateModel clockModel = null;
			if (treeLikelihood != null) { // getDoc().getPartitionNr(partition,
											// BeautiDoc.CLOCKMODEL_PARTITION)
											// != rowNr) {
				clockModel = treeLikelihood.branchRateModelInput.get();
			} else {
				clockModel = getDoc().getClockModel(partition);
				if (clockModel != likelihoods[rowNr].branchRateModelInput.get()) {
					PartitionContext context = getPartitionContext(rowNr);
					try {
						clockModel = (BranchRateModel) BeautiDoc.deepCopyPlugin(likelihoods[rowNr].branchRateModelInput.get(),
							likelihoods[rowNr], (MCMC) doc.mcmc.get(), oldContext, context, doc, null);
					} catch (RuntimeException e) {
						JOptionPane.showMessageDialog(this, "Could not clone clock model: " + e.getMessage());
						return;
					}
				}
			}
			// make sure that *if* the clock model has a tree as input, it is
			// the same as
			// for the likelihood
			TreeInterface tree = null;
			for (Input<?> input : ((BEASTInterface) clockModel).listInputs()) {
				if (input.getName().equals("tree")) {
					tree = (TreeInterface) input.get();
				}

			}
			if (tree != null && tree != this.likelihoods[rowNr].treeInput.get()) {
				JOptionPane.showMessageDialog(this, "Cannot link clock model with different trees");
				throw new IllegalArgumentException("Cannot link clock model with different trees");
			}

			needsRePartition = (this.likelihoods[rowNr].branchRateModelInput.get() != clockModel);
			this.likelihoods[rowNr].branchRateModelInput.setValue(clockModel, this.likelihoods[rowNr]);
			partition = likelihoods[rowNr].branchRateModelInput.get().getID();
			partition = BeautiDoc.parsePartition(partition);
			getDoc().setCurrentPartition(BeautiDoc.CLOCKMODEL_PARTITION, rowNr, partition);
		}
			break;
		case TREE_COLUMN: {
			TreeInterface tree = null;
			if (treeLikelihood != null) { // getDoc().getPartitionNr(partition,
											// BeautiDoc.TREEMODEL_PARTITION) !=
											// rowNr) {
				tree = treeLikelihood.treeInput.get();
			} else {
				tree = (TreeInterface) doc.pluginmap.get("Tree.t:" + partition);
				if (tree != likelihoods[rowNr].treeInput.get()) {
					PartitionContext context = getPartitionContext(rowNr);
					try {
						tree = (TreeInterface) BeautiDoc.deepCopyPlugin((BEASTInterface) likelihoods[rowNr].treeInput.get(), likelihoods[rowNr],
							(MCMC) doc.mcmc.get(), oldContext, context, doc, null);
					} catch (RuntimeException e) {
						JOptionPane.showMessageDialog(this, "Could not clone tree model: " + e.getMessage());
						return;
					}
					
					State state = ((MCMC) doc.mcmc.get()).startStateInput.get();
					List<StateNode> stateNodes = new ArrayList<>();
					stateNodes.addAll(state.stateNodeInput.get());
					for (StateNode s : stateNodes) {
						if (s.getID().endsWith(".t:" + oldContext.tree) && !(s instanceof TreeInterface)) {
							try {
								@SuppressWarnings("unused")
								StateNode copy = (StateNode) BeautiDoc.deepCopyPlugin(s, likelihoods[rowNr], (MCMC) doc.mcmc.get(), oldContext, context, doc, null);
							} catch (RuntimeException e) {
								JOptionPane.showMessageDialog(this, "Could not clone tree model: " + e.getMessage());
								return;
							}

						}
					}
				}
			}
			// sanity check: make sure taxon sets are compatible
			Taxon.assertSameTaxa(tree.getID(), tree.getTaxonset().getTaxaNames(),
					likelihoods[rowNr].dataInput.get().getID(), likelihoods[rowNr].dataInput.get().getTaxaNames());

			needsRePartition = (this.likelihoods[rowNr].treeInput.get() != tree);
			Log.warning.println("needsRePartition = " + needsRePartition);			
			if (needsRePartition) {
				TreeInterface oldTree = this.likelihoods[rowNr].treeInput.get();
				List<TreeInterface> tModels = new ArrayList<>();
				for (GenericTreeLikelihood likelihood : likelihoods) {
					if (likelihood.treeInput.get() == oldTree) {
						tModels.add(likelihood.treeInput.get());
					}
				}
				if (tModels.size() == 1) {
					// remove old tree from model
					((BEASTInterface)oldTree).setInputValue("estimate", false);
                	// use toArray to prevent ConcurrentModificationException
					for (Object beastObject : BEASTInterface.getOutputs(oldTree).toArray()) { //.toArray(new BEASTInterface[0])) {
						for (Input<?> input : ((BEASTInterface)beastObject).listInputs()) {
							try {
							if (input.get() == oldTree) {
								if (input.getRule() != Input.Validate.REQUIRED) {
									input.setValue(tree/*null*/, (BEASTInterface) beastObject);
								//} else {
									//input.setValue(tree, (BEASTInterface) beastObject);
								}
							} else if (input.get() instanceof List) {
								@SuppressWarnings("unchecked")
								List<TreeInterface> list = (List<TreeInterface>) input.get();
								if (list.contains(oldTree)) { // && input.getRule() != Validate.REQUIRED) {
									list.remove(oldTree);
									if (!list.contains(tree)) {
										list.add(tree);
									}
								}
							}
							} catch (Exception e) {
								e.printStackTrace();
							}
						}
					}
				}
			}
			likelihoods[rowNr].treeInput.setValue(tree, likelihoods[rowNr]);
			// TreeDistribution d = getDoc().getTreePrior(partition);
			// CompoundDistribution prior = (CompoundDistribution)
			// doc.pluginmap.get("prior");
			// if (!getDoc().posteriorPredecessors.contains(d)) {
			// prior.pDistributions.setValue(d, prior);
			// }
			partition = likelihoods[rowNr].treeInput.get().getID();
			partition = BeautiDoc.parsePartition(partition);
			getDoc().setCurrentPartition(BeautiDoc.TREEMODEL_PARTITION, rowNr, partition);
		}
		}
		tableData[rowNr][columnNr] = partition;
		if (needsRePartition) {
			List<BeautiSubTemplate> templates = new ArrayList<>();
			templates.add(doc.beautiConfig.partitionTemplate.get());
			templates.addAll(doc.beautiConfig.subTemplates);
			// keep applying rules till model does not change
			doc.setUpActivePlugins();
			int n;
			do {
				n = doc.posteriorPredecessors.size();
				doc.applyBeautiRules(templates, false, oldContext);
				doc.setUpActivePlugins();
			} while (n != doc.posteriorPredecessors.size());
			doc.determinePartitions();
		}
		if (treeLikelihood == null) {
			initTableData();
			setUpComboBoxes();
		}
		
		updateStatus();
	}

	private PartitionContext getPartitionContext(int rowNr) {
		PartitionContext context = new PartitionContext(
				tableData[rowNr][NAME_COLUMN].toString(),
				tableData[rowNr][SITEMODEL_COLUMN].toString(),
				tableData[rowNr][CLOCKMODEL_COLUMN].toString(),
				tableData[rowNr][TREE_COLUMN].toString());
		return context;
	}

	@Override
	protected void addInputLabel() {
	}

	void initTableData() {
		this.likelihoods = new GenericTreeLikelihood[partitionCount];
		if (tableData == null) {
			tableData = new Object[partitionCount][NR_OF_COLUMNS];
		}
		CompoundDistribution likelihoods = (CompoundDistribution) doc.pluginmap.get("likelihood");

		for (int i = 0; i < partitionCount; i++) {
			Alignment data = alignments.get(i);
			// partition name
			tableData[i][NAME_COLUMN] = data;

			// alignment name
			if (data instanceof FilteredAlignment) {
				tableData[i][FILE_COLUMN] = ((FilteredAlignment) data).alignmentInput.get();
			} else {
				tableData[i][FILE_COLUMN] = data;
			}
			// # taxa
			tableData[i][TAXA_COLUMN] = data.getTaxonCount();
			// # sites
			tableData[i][SITES_COLUMN] = data.getSiteCount();
			// Data type
			tableData[i][TYPE_COLUMN] = data.getDataType();
			// site model
			GenericTreeLikelihood likelihood = (GenericTreeLikelihood) likelihoods.pDistributions.get().get(i);
			assert (likelihood != null);
			this.likelihoods[i] = likelihood;
			tableData[i][SITEMODEL_COLUMN] = getPartition(likelihood.siteModelInput);
			// clock model
			tableData[i][CLOCKMODEL_COLUMN] = getPartition(likelihood.branchRateModelInput);
			// tree
			tableData[i][TREE_COLUMN] = getPartition(likelihood.treeInput);
			// useAmbiguities
			tableData[i][USE_AMBIGUITIES_COLUMN] = null;
			try {
				if (hasUseAmbiguitiesInput(i)) {
					tableData[i][USE_AMBIGUITIES_COLUMN] = likelihood.getInputValue("useAmbiguities");
				}
			} catch (Exception e) {
				// ignore
			}
		}
	}

	private boolean hasUseAmbiguitiesInput(int i) {
		try {
			for (Input<?> input : likelihoods[i].listInputs()) {
				if (input.getName().equals("useAmbiguities")) {
					return true;
				}
			}
		} catch (Exception e) {
			// ignore
		}
		return false;
	}

	private String getPartition(Input<?> input) {
		BEASTInterface beastObject = (BEASTInterface) input.get();
		String id = beastObject.getID();
		String partition = BeautiDoc.parsePartition(id);
		return partition;
	}

	protected Component createListBox() {
		String[] columnData = new String[] { "Name", "File", "Taxa", "Sites", "Data Type", "Site Model", "Clock Model",
				"Tree", "Ambiguities" };
		initTableData();

		// set up table.
		// special features: background shading of rows
		// custom editor allowing only Date column to be edited.
		table = new JTable(tableData, columnData) {
			private static final long serialVersionUID = 1L;

			// method that induces table row shading
			@Override
			public Component prepareRenderer(TableCellRenderer renderer, int Index_row, int Index_col) {
				Component comp = super.prepareRenderer(renderer, Index_row, Index_col);
				// even index, selected or not selected
				if (isCellSelected(Index_row, Index_col)) {
					comp.setBackground(Color.gray);
				} else if (Index_row % 2 == 0 && !isCellSelected(Index_row, Index_col)) {
					comp.setBackground(new Color(237, 243, 255));
				} else {
					comp.setBackground(Color.white);
				}
			    JComponent jcomp = (JComponent) comp;
		    	switch (Index_col) {
		    	case NAME_COLUMN:			    		
	    		case CLOCKMODEL_COLUMN: 
	    		case TREE_COLUMN: 
	    		case SITEMODEL_COLUMN: 
			        jcomp.setToolTipText("Set " + table.getColumnName(Index_col).toLowerCase() + " for this partition");
					break;
	    		case FILE_COLUMN:
	    		case TAXA_COLUMN:
	    		case SITES_COLUMN:
	    		case TYPE_COLUMN:
			        jcomp.setToolTipText("Report " + table.getColumnName(Index_col).toLowerCase() + " for this partition");
					break;
	    		case USE_AMBIGUITIES_COLUMN: 
					jcomp.setToolTipText("<html>Flag whether to use ambiguities.<br>" +
							"If not set, the treelikelihood will treat ambiguities in the<br>" +
							"data as unknowns<br>" +
							"If set, the treelikelihood will use ambiguities as equally<br>" +
							"likely values for the tips.<br>" +
							"This will make the computation twice as slow.</html>");
					break;
				default:
			        jcomp.setToolTipText(null);
		    	}
				updateStatus();
				return comp;
			}
		};
		int size = table.getFont().getSize();
		table.setRowHeight(25 * size/13);
		table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
		table.setColumnSelectionAllowed(false);
		table.setRowSelectionAllowed(true);
		table.setName("alignmenttable");

		setUpComboBoxes();

		TableColumn col = table.getColumnModel().getColumn(NAME_COLUMN);
		nameEditor = new JTextField();
		nameEditor.getDocument().addDocumentListener(new DocumentListener() {
			@Override
			public void removeUpdate(DocumentEvent e) {
				processPartitionName();
			}

			@Override
			public void insertUpdate(DocumentEvent e) {
				processPartitionName();
			}

			@Override
			public void changedUpdate(DocumentEvent e) {
				processPartitionName();
			}
		});

		col.setCellEditor(new DefaultCellEditor(nameEditor));

		// // set up editor that makes sure only doubles are accepted as entry
		// // and only the Date column is editable.
		table.setDefaultEditor(Object.class, new TableCellEditor() {
			JTextField m_textField = new JTextField();
			int m_iRow, m_iCol;

			@Override
			public boolean stopCellEditing() {
				//Log.warning.println("stopCellEditing()");
				table.removeEditor();
				String text = m_textField.getText();
				try {
					Double.parseDouble(text);
				} catch (Exception e) {
					return false;
				}
				tableData[m_iRow][m_iCol] = text;
				return true;
			}

			@Override
			public boolean isCellEditable(EventObject anEvent) {
				//Log.warning.println("isCellEditable()");
				return table.getSelectedColumn() == 0;
			}

			@Override
			public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int rowNr,
					int colNr) {
				return null;
			}

			@Override
			public boolean shouldSelectCell(EventObject anEvent) {
				return false;
			}

			@Override
			public void removeCellEditorListener(CellEditorListener l) {
			}

			@Override
			public Object getCellEditorValue() {
				return null;
			}

			@Override
			public void cancelCellEditing() {
			}

			@Override
			public void addCellEditorListener(CellEditorListener l) {
			}

		});

		// show alignment viewer when double clicking a row
		table.addMouseListener(new MouseListener() {

			@Override
			public void mouseReleased(MouseEvent e) {
			}

			@Override
			public void mousePressed(MouseEvent e) {
			}

			@Override
			public void mouseExited(MouseEvent e) {
			}

			@Override
			public void mouseEntered(MouseEvent e) {
			}

			@Override
			public void mouseClicked(MouseEvent e) {
				if (e.getClickCount() > 1) {
					try {
						int alignmemt = table.rowAtPoint(e.getPoint());
						Alignment alignment = alignments.get(alignmemt);
						int best = 0;
						BeautiAlignmentProvider provider = null;
						for (BeautiAlignmentProvider provider2 : doc.beautiConfig.alignmentProvider) {
							int match = provider2.matches(alignment);
							if (match > best) {
								best = match;
								provider = provider2;
							}
						}
						provider.editAlignment(alignment, doc);
					} catch (Exception e1) {
						e1.printStackTrace();
					}
					updateStatus();
				} else if (e.getButton() == e.BUTTON3) {
					int alignmemt = table.rowAtPoint(e.getPoint());
					Alignment alignment = alignments.get(alignmemt);
					int result = JOptionPane.showConfirmDialog(null, "Do you want to replace alignment " + alignment.getID());
					if (result == JOptionPane.YES_OPTION) {
						replaceItem(alignment);
					}
				}
			}
		});

		scrollPane = new JScrollPane(table);

        int rowsToDisplay = 3;
        Dimension d = table.getPreferredSize();
        scrollPane.setPreferredSize(
                new Dimension(d.width,table.getRowHeight()*rowsToDisplay+table.getTableHeader().getHeight()));

		return scrollPane;
	} // createListBox
	
	void setUpComboBoxes() {
		// set up comboboxes
		@SuppressWarnings("unchecked")
		Set<String>[] partitionNames = new HashSet[3];
		for (int i = 0; i < 3; i++) {
			partitionNames[i] = new HashSet<>();
		}
		for (int i = 0; i < partitionCount; i++) {
			partitionNames[0].add(((BEASTInterface) likelihoods[i].siteModelInput.get()).getID());
			partitionNames[1].add(likelihoods[i].branchRateModelInput.get().getID());
			partitionNames[2].add(likelihoods[i].treeInput.get().getID());
		}
		String[][] partitionNameStrings = new String[3][];
		for (int i = 0; i < 3; i++) {
			partitionNameStrings[i] = partitionNames[i].toArray(new String[0]);
		}
		for (int j = 0; j < 3; j++) {
			for (int i = 0; i < partitionNameStrings[j].length; i++) {
				partitionNameStrings[j][i] = BeautiDoc.parsePartition(partitionNameStrings[j][i]);
			}
		}
		TableColumn col = table.getColumnModel().getColumn(SITEMODEL_COLUMN);
		JComboBox<String> siteModelComboBox = new JComboBox<>(partitionNameStrings[0]);
		siteModelComboBox.setEditable(true);
		siteModelComboBox.addActionListener(new ComboActionListener(SITEMODEL_COLUMN));

		col.setCellEditor(new DefaultCellEditor(siteModelComboBox));
		// If the cell should appear like a combobox in its
		// non-editing state, also set the combobox renderer
		col.setCellRenderer(new MyComboBoxRenderer(partitionNameStrings[0]));
		col = table.getColumnModel().getColumn(CLOCKMODEL_COLUMN);

		JComboBox<String> clockModelComboBox = new JComboBox<>(partitionNameStrings[1]);
		clockModelComboBox.setEditable(true);
		clockModelComboBox.addActionListener(new ComboActionListener(CLOCKMODEL_COLUMN));

		col.setCellEditor(new DefaultCellEditor(clockModelComboBox));
		col.setCellRenderer(new MyComboBoxRenderer(partitionNameStrings[1]));
		col = table.getColumnModel().getColumn(TREE_COLUMN);

		JComboBox<String> treeComboBox = new JComboBox<>(partitionNameStrings[2]);
		treeComboBox.setEditable(true);
		treeComboBox.addActionListener(new ComboActionListener(TREE_COLUMN));
		col.setCellEditor(new DefaultCellEditor(treeComboBox));
		col.setCellRenderer(new MyComboBoxRenderer(partitionNameStrings[2]));
		col = table.getColumnModel().getColumn(TAXA_COLUMN);
		col.setPreferredWidth(30);
		col = table.getColumnModel().getColumn(SITES_COLUMN);
		col.setPreferredWidth(30);
		
		col = table.getColumnModel().getColumn(USE_AMBIGUITIES_COLUMN);
		JCheckBox checkBox = new JCheckBox();
		checkBox.addActionListener(e -> {
				if (table.getSelectedRow() >= 0 && table.getSelectedColumn() >= 0) {
					Log.warning.println(" " + table.getValueAt(table.getSelectedRow(), table.getSelectedColumn()));
				}
				try {
					int row = table.getSelectedRow();
					if (hasUseAmbiguitiesInput(row)) {
						likelihoods[row].setInputValue("useAmbiguities", checkBox.isSelected());
						tableData[row][USE_AMBIGUITIES_COLUMN] = checkBox.isSelected();
					} else {
						if (checkBox.isSelected()) {
							checkBox.setSelected(false);
						}
					}
				} catch (Exception ex) {
					// TODO: handle exception
				}
		
			});
		col.setCellEditor(new DefaultCellEditor(checkBox));
		col.setCellRenderer(new MyCheckBoxRenderer());
		col.setPreferredWidth(20);
		col.setMaxWidth(20);
	}

	void processPartitionName() {
		Log.warning.println("processPartitionName");
		Log.warning.println(table.getSelectedColumn() + " " + table.getSelectedRow());
		String oldName = tableData[table.getSelectedRow()][table.getSelectedColumn()].toString();
		String newName = nameEditor.getText();
		if (!oldName.equals(newName) && newName.indexOf(".") >= 0) {
			// prevent full stops to be used in IDs
			newName = newName.replaceAll("\\.", "");
			table.setValueAt(newName, table.getSelectedRow(), table.getSelectedColumn());
			table.repaint();
		}
		if (!oldName.equals(newName)) {
			try {
				int partitionID = -2;
				switch (table.getSelectedColumn()) {
				case NAME_COLUMN:
					partitionID = BeautiDoc.ALIGNMENT_PARTITION;
					break;
				case SITEMODEL_COLUMN:
					partitionID = BeautiDoc.SITEMODEL_PARTITION;
					break;
				case CLOCKMODEL_COLUMN:
					partitionID = BeautiDoc.CLOCKMODEL_PARTITION;
					break;
				case TREE_COLUMN:
					partitionID = BeautiDoc.TREEMODEL_PARTITION;
					break;
				default:
					throw new IllegalArgumentException("Cannot rename item in column");
				}
				getDoc().renamePartition(partitionID, oldName, newName);
				table.setValueAt(newName, table.getSelectedRow(), table.getSelectedColumn());
				setUpComboBoxes();
			} catch (Exception e) {
				JOptionPane.showMessageDialog(null, "Renaming failed: " + e.getMessage());
			}
		}
		// debugging code:
		//for (int i = 0; i < partitionCount; i++) {
		//	Log.warning.println(i + " " + tableData[i][0]);
		//}
	}

	class ComboActionListener implements ActionListener {
		int m_nColumn;

		public ComboActionListener(int columnNr) {
			m_nColumn = columnNr;
		}

		@Override
		public void actionPerformed(ActionEvent e) {
//			 SwingUtilities.invokeLater(new Runnable() {
//			 @Override
//			 public void run() {
			Log.warning.println("actionPerformed ");
			Log.warning.println(table.getSelectedRow() + " " + table.getSelectedColumn());
			if (table.getSelectedRow() >= 0 && table.getSelectedColumn() >= 0) {
				Log.warning.println(" " + table.getValueAt(table.getSelectedRow(), table.getSelectedColumn()));
			}
			for (int i = 0; i < partitionCount; i++) {
				try {
					updateModel(m_nColumn, i);
				} catch (Exception ex) {
					Log.warning.println(ex.getMessage());
				}
			}
//		    }});
		}
	}

	public class MyComboBoxRenderer extends JComboBox<String> implements TableCellRenderer {
		private static final long serialVersionUID = 1L;

		public MyComboBoxRenderer(String[] items) {
			super(items);
			setOpaque(true);
		}

		@Override
		public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
				boolean hasFocus, int row, int column) {
			if (isSelected) {
				// setForeground(table.getSelectionForeground());
				super.setBackground(table.getSelectionBackground());
			} else {
				setForeground(table.getForeground());
				setBackground(table.getBackground());
			}

			// Select the current value
			setSelectedItem(value);
			return this;
		}
	}

	public class MyCheckBoxRenderer extends JCheckBox implements TableCellRenderer {
		private static final long serialVersionUID = 1L;

		public MyCheckBoxRenderer() {
			super();
			setOpaque(true);
		}

		@Override
		public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
				boolean hasFocus, int row, int column) {
			if (hasUseAmbiguitiesInput(row)) {
				if (isSelected) {
					// setForeground(table.getSelectionForeground());
					super.setBackground(table.getSelectionBackground());
				} else {
					setForeground(table.getForeground());
					setBackground(table.getBackground());
				}
				setEnabled(true);
				setSelected((Boolean) value);
			} else {
				setEnabled(false);
			}
			return this;
		}
	}

	@Override
	protected void addSingleItem(BEASTInterface beastObject) {
		initTableData();
		repaint();
	}

	@Override
	protected void addItem() {
		addItem(null);
	} // addItem

	private void addItem(File[] fileArray) {
		List<BEASTInterface> beastObjects = doc.beautiConfig.selectAlignments(doc, this, fileArray);

		// Component c = this;
		if (beastObjects != null) {
			refreshPanel();
		}
	}

	void delItem() {
		int[] selected = getTableRowSelection();
		if (selected.length == 0) {
			JOptionPane.showMessageDialog(this, "Select partitions to delete, before hitting the delete button");
		}
		// do the actual deleting
		for (int i = selected.length - 1; i >= 0; i--) {
			int rowNr = selected[i];
			
			// before deleting, unlink site model, clock model and tree
			
			// check whether any of the models are linked
			BranchRateModel.Base clockModel = likelihoods[rowNr].branchRateModelInput.get();
			SiteModelInterface siteModel = likelihoods[rowNr].siteModelInput.get();
			TreeInterface tree = likelihoods[rowNr].treeInput.get();
			List<GenericTreeLikelihood> cModels = new ArrayList<>();
			List<GenericTreeLikelihood> models = new ArrayList<>();
			List<GenericTreeLikelihood> tModels = new ArrayList<>();
			for (GenericTreeLikelihood likelihood : likelihoods) {
				if (likelihood != likelihoods[rowNr]) {
				if (likelihood.branchRateModelInput.get() == clockModel) {
					cModels.add(likelihood);
				}
				if (likelihood.siteModelInput.get() == siteModel) {
					models.add(likelihood);
				}
				if (likelihood.treeInput.get() == tree) {
					tModels.add(likelihood);
				}
				}
			}
			
			try {
				if (cModels.size() > 0) {
					// clock model is linked, so we need to unlink
					if (doc.getPartitionNr(clockModel) != rowNr) {
						tableData[rowNr][CLOCKMODEL_COLUMN] = getDoc().partitionNames.get(rowNr).partition;
					} else {
						int freePartition = doc.getPartitionNr(cModels.get(0));
						tableData[rowNr][CLOCKMODEL_COLUMN] = getDoc().partitionNames.get(freePartition).partition;
					}
					updateModel(CLOCKMODEL_COLUMN, rowNr);
				}
				
				if (models.size() > 0) {
					// site model is linked, so we need to unlink
					if (doc.getPartitionNr((BEASTInterface) siteModel) != rowNr) {
						tableData[rowNr][SITEMODEL_COLUMN] = getDoc().partitionNames.get(rowNr).partition;
					} else {
						int freePartition = doc.getPartitionNr(models.get(0));
						tableData[rowNr][SITEMODEL_COLUMN] = getDoc().partitionNames.get(freePartition).partition;
					}
					updateModel(SITEMODEL_COLUMN, rowNr);
				}
				
				if (tModels.size() > 0) {
					// tree is linked, so we need to unlink
					if (doc.getPartitionNr((BEASTInterface) tree) != rowNr) {
						tableData[rowNr][TREE_COLUMN] = getDoc().partitionNames.get(rowNr).partition;
					} else {
						int freePartition = doc.getPartitionNr(tModels.get(0));
						tableData[rowNr][TREE_COLUMN] = getDoc().partitionNames.get(freePartition).partition;
					}
					updateModel(TREE_COLUMN, rowNr);
				}
				getDoc().delAlignmentWithSubnet(alignments.get(rowNr));
				alignments.remove(rowNr);
			    // remove deleted likelihood from likelihoods array
				GenericTreeLikelihood[] tmp = new GenericTreeLikelihood[likelihoods.length - 1];
				int k = 0;
				for (int j = 0; j < likelihoods.length; j++) {
					if (j != rowNr) {
						tmp[k] = likelihoods[j];
						k++;
					}
				}
				likelihoods = tmp;
				partitionCount--;
			} catch (Exception e) {
				JOptionPane.showMessageDialog(null, "Deletion failed: " + e.getMessage());
				e.printStackTrace();
			}
		}
		MRCAPriorInputEditor.customConnector(doc);
		refreshPanel();
	} // delItem

	
	void replaceItem() {
		int [] selected = getTableRowSelection();
		if (selected.length != 1) {
			// don't know how to replace multiple alignments at the same time
			// should never get here (button is disabled)
			return;
		}
		Alignment alignment = alignments.get(selected[0]);
		replaceItem(alignment);
	}
	
	private void replaceItem(Alignment alignment) {
		BeautiAlignmentProvider provider = new BeautiAlignmentProvider();
		List<BEASTInterface> list = provider.getAlignments(doc);
		List<Alignment> alignments = new ArrayList<>();
		for (BEASTInterface o : list) {
			if (o instanceof Alignment) {
				alignments.add((Alignment) o);
			}
		}
		Alignment replacement = null;
		if (alignments.size() > 1) {
			JComboBox<Alignment> jcb = new JComboBox<Alignment>(alignments.toArray(new Alignment[]{}));
			JOptionPane.showMessageDialog( null, jcb, "Select a replacement alignment", JOptionPane.QUESTION_MESSAGE);
			replacement = (Alignment) jcb.getSelectedItem();
		} else if (alignments.size() == 1) {
			replacement = alignments.get(0);
		}
		if (replacement != null) {
			if (!replacement.getDataType().getClass().getName().equals(alignment.getDataType().getClass().getName())) {
				JOptionPane.showMessageDialog(null, "Data types do not match, so alignment cannot be replaced: " + 
						replacement.getID() + " " + replacement.getDataType().getClass().getName() + " != " + 
						alignment.getID() + " " + alignment.getDataType().getClass().getName());
				return;
			}
			// replace alignment
			Set<BEASTInterface> outputs = new LinkedHashSet<>();
			outputs.addAll(alignment.getOutputs());
			for (BEASTInterface o : outputs) {
				for (Input<?> input : o.listInputs()) {
					if (input.get() == alignment) {
						input.setValue(replacement, o);
						replacement.getOutputs().add(o);
					} else if (input.get() instanceof List) {
						@SuppressWarnings("rawtypes")
						List inputlist = (List) input.get();
						int i = inputlist.indexOf(alignment);
						if (i >= 0) {
							inputlist.set(i, replacement);
							replacement.getOutputs().add(o);
						}
					}
				}
			}
			int i = doc.alignments.indexOf(alignment);
			doc.alignments.set(i, replacement);
			refreshPanel();
		}
	} // replaceItem
	
	void splitItem() {
		int[] selected = getTableRowSelection();
		if (selected.length == 0) {
			JOptionPane.showMessageDialog(this, "Select partitions to split, before hitting the split button");
			return;
		}
		String[] options = { "{1,2} + 3", "{1,2} + 3 frame 2", "{1,2} + 3 frame 3", "1 + 2 + 3", "1 + 2 + 3 frame 2", "1 + 2 + 3 frame 3"};

		String option = (String)JOptionPane.showInputDialog(null, "Split selected alignments into partitions", "Option",
		                    JOptionPane.WARNING_MESSAGE, null, options, "1 + 2 + 3");
		if (option == null) {
			return;
		}
		
		String[] filters = null;
		String[] ids = null;
		if (option.equals(options[0])) {
			filters = new String[] { "1::3,2::3", "3::3" };
			ids = new String[] { "_1,2", "_3" };
		} else if (option.equals(options[1])) {
			filters = new String[] { "1::3,3::3", "2::3" };
			ids = new String[] { "_1,2", "_3" };
		} else if (option.equals(options[2])) {
			filters = new String[] { "2::3,3::3", "1::3" };
			ids = new String[] { "_1,2", "_3" };
		} else if (option.equals(options[3])) {
			filters = new String[] { "1::3", "2::3", "3::3" };
			ids = new String[] { "_1", "_2", "_3" };
		} else if (option.equals(options[4])) {
			filters = new String[] { "2::3", "3::3", "1::3" };
			ids = new String[] { "_1", "_2", "_3" };
		} else if (option.equals(options[5])) {
			filters = new String[] { "3::3", "1::3", "2::3" };
			ids = new String[] { "_1", "_2", "_3" };
		} else {
			return;
		}

		for (int i = selected.length - 1; i >= 0; i--) {
			int rowNr = selected[i];
			Alignment alignment = alignments.remove(rowNr);
			getDoc().delAlignmentWithSubnet(alignment);
			try {
				List<Alignment> newAlignments = new ArrayList<>();
				for (int j = 0; j < filters.length; j++) {
					FilteredAlignment f = new FilteredAlignment();
					f.initByName("data", alignment, "filter", filters[j], "dataType", alignment.dataTypeInput.get());
					f.setID(alignment.getID() + ids[j]);
					getDoc().addAlignmentWithSubnet(f, getDoc().beautiConfig.partitionTemplate.get());
					newAlignments.add(f);
				}
				alignments.addAll(newAlignments);
				partitionCount = alignments.size();
				tableData = null; 
				initTableData();			
				if (newAlignments.size() == 2) {
					link(TREE_COLUMN, alignments.size() - 1, alignments.size() - 2);
				} else {
					link(TREE_COLUMN, alignments.size() - 2, alignments.size() - 3);
					tableData = null; 
					initTableData();			
					link(TREE_COLUMN, alignments.size() - 1, alignments.size() - 2);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		refreshPanel();
	} // splitItem

	/** enable/disable buttons, etc **/
	void updateStatus() {
		boolean status = (alignments.size() > 1);
		if (alignments.size() >= 2 && getTableRowSelection().length == 0) {
			status = false;
		}
		for (JButton button : linkButtons) {
			button.setEnabled(status);
		}
		for (JButton button : unlinkButtons) {
			button.setEnabled(status);
		}
		status = (getTableRowSelection().length > 0);
		splitButton.setEnabled(status);
		delButton.setEnabled(status);
		replaceButton.setEnabled(getTableRowSelection().length == 1);
	}
	
} // class AlignmentListInputEditor

