package industries.doublehelix.conditionalfilter.gui;

import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.SwingConstants;

import industries.doublehelix.conditionalfilter.filtertypes.Filter;
import industries.doublehelix.conditionalfilter.filtertypes.container.ContainerFilter;
import industries.doublehelix.conditionalfilter.filtertypes.flat.FlatFilter;
import industries.doublehelix.conditionalfilter.gui.filtersettings.FilterSettingsPanel;

/**
 * a panel representing a single filter (and its subfilters) containing filter field, filter options and add/delete buttons
 * @author klaue
 *
 */
public class FilterPanel extends JPanel implements ActionListener {
	private static final long serialVersionUID = -5068772983228465610L;
	
	// action commands
	public static final String ADD_BUTTON = "ADD";
	public static final String DELETE_BUTTON = "DELETE";
	public static final String FILTER_CHANGED = "FILTERCHANGED";
	
	private int filterLevel = 0;
	private Map<String, FlatFilter> possibleFilterMap = null;
	private Filter filter = null;
	private Filter prevFilter = null; // only used by listeners if this filter was changed
	private List<ActionListener> listeners = new ArrayList<ActionListener>();
	
	private JComboBox<String> filterBox = null;
	private JButton deleteButton = new JButton("-");
	private JButton addButton = new JButton("+");
	private JPanel contentContainerPanel = new JPanel();
	private ArrayList<FilterPanel> subFilterLines = new ArrayList<>();
	
	private int lineHeight = -1;
	
	/**
	 * constructs a new filter line based on an existing filter
	 * @param possibleFilterMap the values of possible filters (not including container filters)
	 * @param filterLevel the current level of container this filter is in, used to determine indent and availability of container filters
	 * @param filter the filter that is represented (and changed) by this gui element. Note that this filter may and probably will
	 * be deleted and replaced by a new one in the parent filter if the user choses a different filter from the dropdown.
	 */
	public FilterPanel(Map<String, FlatFilter> possibleFilterMap, int filterLevel, Filter filter) {
		this.possibleFilterMap = possibleFilterMap;
		this.filterLevel = filterLevel;
		this.filter = filter;
		
		if (filterLevel > 2) throw new IllegalArgumentException("filter level above 2 not allowed: " + filterLevel);
		if (filterLevel == 2 && filter instanceof ContainerFilter) throw new IllegalArgumentException("no container filter at level 2");
		
		ArrayList<String> filterNameList = new ArrayList<>(possibleFilterMap.keySet());
		if (filterLevel < 2) {
			filterNameList.add(ContainerFilter.ContainerType.ALL.toString());
			filterNameList.add(ContainerFilter.ContainerType.NONE.toString());
			filterNameList.add(ContainerFilter.ContainerType.ANY.toString());
		}
		String[] filterNameArray = filterNameList.toArray(new String[filterNameList.size()]);
		Arrays.sort(filterNameArray);

		filterBox = new JComboBox<>(filterNameArray);
		if (filter instanceof ContainerFilter) {
			filterBox.setSelectedItem(((ContainerFilter) filter).getType().toString());
		} else {
			filterBox.setSelectedItem(filter.getFieldName());
		}
		filterBox.addActionListener(this); // AFTER setting initial filter selection
		
		deleteButton.setToolTipText("Remove this filter");
		if (filterLevel == 0) {
			addButton.setToolTipText("Add a new filter");
		} else {
			addButton.setToolTipText("Add a new filter inside the parent category");
		}
		deleteButton.addActionListener(this);
		addButton.addActionListener(this);
		
		lineHeight = addButton.getPreferredSize().height;
		
		this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
		//this.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0)); // 5 px at top so that subfilters have some spacing
		
		addPanelForThisFilter();
		
		// add all subfilters
		if (this.filter instanceof ContainerFilter) {
			ContainerFilter cf = (ContainerFilter)filter;
			for (Filter subFilter : cf.getSubFilters()) {
				FilterPanel subFilterPanel = new FilterPanel(possibleFilterMap, filterLevel + 1, subFilter);
				subFilterPanel.addActionListener(this);
				this.add(subFilterPanel);
				subFilterLines.add(subFilterPanel);
			}
		}
		setSubPanelDeleteButtonState();
	}
	
	/**
	 * Constructs and adds a panel representing this filter to this FilterPanels component
	 */
	public void addPanelForThisFilter() {
		// determine spacer/checkbox width
		int spacerWidth = filterLevel * 10; // 0-20
		int checkboxWidth = filterBox.getPreferredSize().width + (20 - spacerWidth);
		// for some reason, box layout needs both. It's a crap layout anyway but the simplest one for what is essentially a line
		filterBox.setPreferredSize(new Dimension(checkboxWidth, lineHeight));//filterBox.getPreferredSize().height));
		filterBox.setMaximumSize(new Dimension(checkboxWidth, filterBox.getMaximumSize().height));
		
		JPanel filterLinePanel = new JPanel(new GridBagLayout());
		
		GridBagConstraints c = new GridBagConstraints();
		c.insets = new Insets(0, spacerWidth, 0, 5);
		filterLinePanel.add(filterBox, c);
		
		// determine content
		c = new GridBagConstraints();
		c.weightx = 1.0;
		c.fill = GridBagConstraints.HORIZONTAL;
		c.anchor = GridBagConstraints.WEST;
		contentContainerPanel.setLayout(new GridBagLayout());
		contentContainerPanel.add(FilterSettingsPanel.getFilterSettingsPanelForFilter(filter, lineHeight), c);
		//contentContainerPanel.setBackground(Color.green);
		filterLinePanel.add(contentContainerPanel, c);
		
		//filterLinePanel.add(filterContentPanel);
		enableDeleteButton(false); // set to true by parent if this filter is not the last one
		filterLinePanel.add(deleteButton);
		filterLinePanel.add(addButton);
		
		filterLinePanel.setMaximumSize(new Dimension(filterLinePanel.getMaximumSize().width, deleteButton.getPreferredSize().height));
		
		// the separator has to be added to this panel instead of the parent that contains this filter panel for ease of use:
		// if the parent (which is another FilterPanel) added the separators, you could not simply add or delete filters
		// but would have to mind the separators every time
		// Boxlayout is used here because using gridbag would require you to add a spacer to the bottom to not have it
		// vertically centered, but due to this panel being just one of many that may be added, any such spacer would 
		// result in any other panel being paced weirdly.
		// adding such a spacer to the scrollpane this all ends up in doesn't seem to work either. so.. boxlayout
		JPanel panelWithSeparator = new JPanel();
		panelWithSeparator.setLayout(new BoxLayout(panelWithSeparator, BoxLayout.Y_AXIS));
		panelWithSeparator.add(filterLinePanel);
		
		JSeparator separator = new JSeparator(SwingConstants.HORIZONTAL);
		separator.setMaximumSize(new Dimension(separator.getMaximumSize().width, 1));
		panelWithSeparator.add(Box.createVerticalStrut(2));
		panelWithSeparator.add(separator);
		panelWithSeparator.add(Box.createVerticalStrut(2));
		this.add(panelWithSeparator);
	}
	
	public void addActionListener(ActionListener l) {
		for (ActionListener al : listeners) {
			if (al == l) return;
		}
		listeners.add(l);
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		Object source = e.getSource();
		String actionCommand = "";
		if (source == this.deleteButton) {
			// removal of this filter is handled in parent, so in the action listener
			actionCommand = DELETE_BUTTON;
		} else if (source == this.addButton) {
			// adding new filters of the same level is handled in parent, so in the action listener
			actionCommand = ADD_BUTTON;
		} else if(source == filterBox) {
			if (filterBox.getSelectedItem().equals(this.filter.getFieldName())) return;
			
			// filter changed
			String filterName = (String)filterBox.getSelectedItem();
			Filter newFilter = null;
			boolean oldFilterWasContainer = filter instanceof ContainerFilter;
			
			FlatFilter template = possibleFilterMap.get(filterName);
			if (template == null) {
				// new filter is container
				ContainerFilter.ContainerType type = ContainerFilter.ContainerType.fromString(filterName);
				newFilter = new ContainerFilter(type);
				// if old filter was container
				if (oldFilterWasContainer) {
					((ContainerFilter)newFilter).setSubFilters(((ContainerFilter)this.filter).getSubFilters());
					// no need to make any new filter panels, just use the existing ones
				} else {
					// add an empty filter of the first type in the flat filters
					FlatFilter newSubFilter = (FlatFilter)possibleFilterMap.values().iterator().next().clone();
					((ContainerFilter)newFilter).addFilter(newSubFilter);
					FilterPanel subFilterPanel = new FilterPanel(possibleFilterMap, filterLevel + 1, newSubFilter);
					subFilterPanel.addActionListener(this);
					this.add(subFilterPanel);
					subFilterLines.add(subFilterPanel);
				}
			} else {
				newFilter = (FlatFilter)template.clone();
				
				// warn if the old filter was a containerfilter because the new one isn't anymore
				if (oldFilterWasContainer) {
					ContainerFilter oldFilter = (ContainerFilter)filter;
					// check if we had sub filters set
					if (!confirmRemovalOfContainer(oldFilter)) {
						filterBox.setSelectedItem(oldFilter.getType().toString());
						return; // no notifying listeners
					}
					
					// confirmed, remove sub filter panels
					for (int i = subFilterLines.size() - 1; i >= 0; --i) {
						this.remove(subFilterLines.get(i));
						subFilterLines.remove(i);
					}
				}
			}
			this.prevFilter = this.filter;
			this.filter = newFilter;
			
			// TODO rebuild filterContentPanel
			contentContainerPanel.removeAll();
			FilterSettingsPanel contentPanel = FilterSettingsPanel.getFilterSettingsPanelForFilter(newFilter, lineHeight);
			if (contentPanel != null) {
				GridBagConstraints c = new GridBagConstraints();
				c.weightx = 1.0;
				c.anchor = GridBagConstraints.WEST;
				c.fill = GridBagConstraints.HORIZONTAL;
				contentContainerPanel.add(contentPanel, c);
			}
			
			this.revalidate();
			
			actionCommand = FILTER_CHANGED;
			
		}
		
		if (!actionCommand.isEmpty()) {
			ActionEvent actionEvent = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, actionCommand);
			for (ActionListener al : listeners) {
				al.actionPerformed(actionEvent);
			}
			return;
		}
		
		// action events by sub filter panels
		for (int i = 0; i < subFilterLines.size(); ++i) {
			FilterPanel subPanel = subFilterLines.get(i);
			if (source == subPanel) {
				// if we have any subpanels, that means our filter is a containerFilter
				ContainerFilter thisFilter = (ContainerFilter)this.filter;
				if (e.getActionCommand().equals(DELETE_BUTTON)) {
					// remove that subfilter
					if (subPanel.getFilter() instanceof ContainerFilter) {
						if (!confirmRemovalOfContainer(((ContainerFilter)subPanel.getFilter()))) {
							// no delete
							return;
						}
					}
					thisFilter.removeFilter(subPanel.getFilter());
					this.remove(subPanel);
					this.subFilterLines.remove(subPanel);
					setSubPanelDeleteButtonState();
					this.revalidate();
				} else if (e.getActionCommand().equals(ADD_BUTTON)) {
					// add a new filter of the first entry in the possible filters
					boolean addToEnd = (i == subFilterLines.size() - 1);
					// TODO: add at the position i + 1
					FlatFilter newSubFilter = (FlatFilter)possibleFilterMap.values().iterator().next().clone();
					FilterPanel subFilterPanel = new FilterPanel(possibleFilterMap, filterLevel + 1, newSubFilter);
					subFilterPanel.addActionListener(this);
					
					if (addToEnd) {
						thisFilter.addFilter(newSubFilter);
						subFilterLines.add(subFilterPanel);
						this.add(subFilterPanel);
					} else {
						thisFilter.getSubFilters().add(i+1, newSubFilter);
						
						// rebuild subfilters
						for (FilterPanel fp : subFilterLines) this.remove(fp);
						subFilterLines.add(i+1, subFilterPanel);
						for (FilterPanel fp : subFilterLines) this.add(fp);
					}
					setSubPanelDeleteButtonState();
					this.revalidate();
				} else if(e.getActionCommand().equals(FILTER_CHANGED)) {
					// replace old filter with changed one
					if (subPanel.getPreviousFilter() != null) {
						thisFilter.replaceFilter(subPanel.getFilter(), subPanel.getPreviousFilter());
					}
				}
				break;
			}
		}
	}
	
	public boolean confirmRemovalOfContainer(ContainerFilter cf) {
		boolean warn = false;
		if (cf.getSubFilters().size() == 0) {
			// should never happen, but just in case
			warn = false;
		} else if (cf.getSubFilters().size() > 1) {
			// always warn when more than one subfilter
			warn = true;
		} else {
			Filter firstSubFilter = cf.getSubFilters().get(0);
			warn = (firstSubFilter instanceof ContainerFilter) || !firstSubFilter.isEmpty();
		}
		
		if (warn) {
			int reply = JOptionPane.showConfirmDialog(this, "This will remove all filters in this group, are you sure?", "Are you sure?", JOptionPane.YES_NO_OPTION);
			if (reply != JOptionPane.YES_OPTION) {
				return false;
			}
		}
		return true;
	}

	public Filter getFilter() {
		return filter;
	}

	protected Filter getPreviousFilter() {
		return prevFilter;
	}
	
	public void enableDeleteButton(boolean enable) {
		this.deleteButton.setEnabled(enable);
	}
	
	private void setSubPanelDeleteButtonState() {
		if (this.subFilterLines.size() == 1) {
			this.subFilterLines.get(0).enableDeleteButton(false);
		} else if (this.subFilterLines.size() > 1) {
			for (FilterPanel panel : this.subFilterLines) {
				panel.enableDeleteButton(true);
			}
		}
	}
}
