package industries.doublehelix.conditionalfilter.gui;

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;

import industries.doublehelix.conditionalfilter.filtertypes.Filter;
import industries.doublehelix.conditionalfilter.filtertypes.container.ContainerFilter;
import industries.doublehelix.conditionalfilter.filtertypes.container.ContainerFilter.ContainerType;
import industries.doublehelix.conditionalfilter.filtertypes.container.MasterFilter;
import industries.doublehelix.conditionalfilter.filtertypes.flat.DateFilter;
import industries.doublehelix.conditionalfilter.filtertypes.flat.FixedValuesStringFilter;
import industries.doublehelix.conditionalfilter.filtertypes.flat.FlatFilter;
import industries.doublehelix.conditionalfilter.filtertypes.flat.NumberFilter;
import industries.doublehelix.conditionalfilter.filtertypes.flat.StringFilter;
import industries.doublehelix.conditionalfilter.filtertypes.flat.StringFilter.StringFilterMode;

/**
 * The main panel for the conditional filtering list (the one you'll want to put in your GUI)
 * @author klaue
 *
 */
public class ConditionalFilter extends JPanel implements ActionListener {
	private static final long serialVersionUID = 4184939792579876106L;
	private final List<FlatFilter> givenPossibleFilters;
	private Map<String, FlatFilter> possibleFilterMap = null;
	private final MasterFilter masterFilter;
	
	// gui elements
	JCheckBox enabledCheckBox = new JCheckBox("Match");
	JComboBox<MasterFilter.ContainerType> masterType = new JComboBox<>();
	JLabel lblEnabled = new JLabel("of the following filters:"); // onclick will toggle match checkbox
	MasterFilterPanel filterPanel = null;
	
	/**
	 * Construct a new, empty conditional filtering panel
	 * @param isActive true if this filtering panel is set to be active. In the case of no given old filter values, this
	 * should probably be false
	 * @param possibleFilters a list of filter templates than can be used. The objects of this list are not changed, copies are made
	 * for the actual filters, so their filter data is disregarded, only type and field names are used.
	 */
	public ConditionalFilter(boolean isActive, List<FlatFilter> possibleFilters) {
		this.masterFilter = new MasterFilter(ContainerType.ANY);
		this.masterFilter.setActive(isActive);
		this.givenPossibleFilters = possibleFilters;
		buildPossibleFiltersMap();
		validateMasterFilter();
		buildGui();
	}
	
	/**
	 * Construct a new, empty conditional filtering panel
	 * @param isActive true if this filtering panel is set to be active. In the case of no given old filter values, this
	 * should probably be false
	 * @param possibleFilters a list of filter templates than can be used. The objects of this list are not changed, copies are made
	 * for the actual filters, so their filter data is disregarded, only type and field names are used.
	 */
	public ConditionalFilter(boolean isActive, FlatFilter... possibleFilters) {
		this(isActive, Arrays.asList(possibleFilters));
	}
	
	/**
	 * Construct a new, pre-filled conditional filtering panel
	 * @param previousFilters a set of filters already set to pre-populate this panel. Also determines if it's active or not
	 * @param possibleFilters a list of filter templates than can be used. The objects of this list are not changed, copies are made
	 * for the actual filters, so their filter data is disregarded, only type (not filtertype, class type) and field names are used.
	 */
	public ConditionalFilter(MasterFilter previousFilters, List<FlatFilter> possibleFilters) {
		this.masterFilter = previousFilters;
		this.givenPossibleFilters = possibleFilters;
		buildPossibleFiltersMap();
		validateMasterFilter();
		buildGui();
	}
	
	/**
	 * Construct a new, pre-filled conditional filtering panel
	 * @param previousFilters a set of filters already set to pre-populate this panel. Also determines if it's active or not
	 * @param possibleFilters a list of filter templates than can be used. The objects of this list are not changed, copies are made
	 * for the actual filters, so their filter data is disregarded, only type (not filtertype, class type) and field names are used.
	 */
	public ConditionalFilter(MasterFilter previousFilters, FlatFilter... possibleFilters) {
		this(previousFilters, Arrays.asList(possibleFilters));
	}
	
	private void buildPossibleFiltersMap() throws IllegalArgumentException {
		possibleFilterMap = new TreeMap<>();
		
		// add and check
		for (FlatFilter filter : givenPossibleFilters) {
			if (possibleFilterMap.containsKey(filter.getFieldName())) {
				throw new IllegalArgumentException("Filter names of the given filters not unique");
			}
			possibleFilterMap.put(filter.getFieldName(), filter);
		}
		
		if (possibleFilterMap.keySet().contains(ContainerType.ALL.toString())
				|| possibleFilterMap.keySet().contains(ContainerType.ANY.toString())
				|| possibleFilterMap.keySet().contains(ContainerType.NONE.toString())) {
			throw new IllegalArgumentException("Filter names of the given filters contain \"all\", \"any\" or \"none\" which are reserved");
		}
	}
	
	private void validateMasterFilter() {
		Set<String> filterNamesThatNeedToBeSet = getFilterNamesFromContainerFilter(this.masterFilter);
		for (String filterName : possibleFilterMap.keySet()) {
			filterNamesThatNeedToBeSet.remove(filterName);
		}
		if (!filterNamesThatNeedToBeSet.isEmpty()) {
			throw new IllegalArgumentException("Given previous filters contain fields not in the possible filter list: "
					+ filterNamesThatNeedToBeSet);
		}
		
		// check if it doesn't go too deep and correct it by adding flatfilters if needed
		int level = insertFlatFiltersIfNeededAndGetDeepestLevel(0, this.masterFilter);
		if (level > 2) {
			throw new IllegalArgumentException("Given previous filters are more than 2 levels deep which is not allowed. Max level at "
					+ level);
		}
	}
	
	/**
	 * I hate long function names but oh well, better than than doing the same work twice.
	 * this will ensure that every innermost container filter has at least one flatfilter and count
	 * the deepest level at the same time
	 * @param levelAt
	 * @param filter
	 * @return the deepest level
	 */
	private int insertFlatFiltersIfNeededAndGetDeepestLevel(int levelAt, Filter filter) {
		if (!(filter instanceof ContainerFilter)) return levelAt;
		ContainerFilter containerFilter = (ContainerFilter)filter;
		if (containerFilter.getSubFilters().isEmpty()) {
			// a containerFilter which does not contain any subfilters, gonna fix that
			FlatFilter newSubFilter = (FlatFilter)possibleFilterMap.values().iterator().next().clone();
			containerFilter.addFilter(newSubFilter);
			return levelAt + 1;
		} else {
			int deepestLevel = levelAt;
			for (Filter subfilter : containerFilter.getSubFilters()) {
				int levelOfFilter = insertFlatFiltersIfNeededAndGetDeepestLevel(levelAt + 1, subfilter);
				if (levelOfFilter > deepestLevel) deepestLevel = levelOfFilter;
			}
			return deepestLevel;
		}
	}
	
	private Set<String> getFilterNamesFromContainerFilter(ContainerFilter cfilter) {
		if (cfilter == null) return new TreeSet<String>();
		Set<String> names = new TreeSet<>();
		for (Filter filter : cfilter.getSubFilters()) {
			if (filter instanceof ContainerFilter) {
				names.addAll(getFilterNamesFromContainerFilter((ContainerFilter)filter));
			} else {
				names.add(filter.getFieldName());
			}
		}
		return names;
	}
	
	private void buildGui() {
		masterType.addActionListener(this);
		enabledCheckBox.addActionListener(this);
		
		// let a click on the label toggle checkbox
		lblEnabled.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				super.mouseClicked(e);
				enabledCheckBox.setSelected(!enabledCheckBox.isSelected());
			}
		});
		
		// fill the main combobox with values
		masterType.setModel(new DefaultComboBoxModel<>(MasterFilter.ContainerType.values()));
		
		// build the gui
		JPanel enabledPanel = new JPanel();
		enabledPanel.setLayout(new BoxLayout(enabledPanel, BoxLayout.X_AXIS));
		enabledPanel.add(enabledCheckBox);
		enabledPanel.add(Box.createHorizontalStrut(5));
		masterType.setMaximumSize(masterType.getPreferredSize()); // box layout is kinda weird
		enabledPanel.add(masterType);
		enabledPanel.add(Box.createHorizontalStrut(5));
		enabledPanel.add(lblEnabled);
		enabledPanel.add(Box.createHorizontalGlue());
		enabledPanel.setMaximumSize(new Dimension(enabledPanel.getMaximumSize().width, enabledCheckBox.getPreferredSize().height));
		
		filterPanel = new MasterFilterPanel(possibleFilterMap, masterFilter);
		
		
		this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
		this.add(enabledPanel);
		this.add(Box.createVerticalStrut(5));
		this.add(new JScrollPane(filterPanel));
		
		populateElementsByMasterFilter();
		

		enabledCheckBox.setSelected(this.masterFilter.isActive()); // at the end so all gui elements are added already
	}
	
	private void populateElementsByMasterFilter() {
		enabledCheckBox.setSelected(masterFilter.isActive());
		masterType.setSelectedItem(masterFilter.getType());
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		// change master filter type and enabled value depending on gui elements
		if (e.getSource() == masterType) {
			masterFilter.setType(masterType.getItemAt(masterType.getSelectedIndex())); // no cast when going over index
		} else if (e.getSource() == enabledCheckBox) {
			masterFilter.setActive(enabledCheckBox.isSelected());
		}
	}

	/**
	 * returns a list of filter templates set as possible filters to this conditional filtering panel.
	 * Those are not the filters chosen by the user, use getMasterFilter() for this,
	 * @return the filter templates
	 */
	public final List<FlatFilter> getPossibleFilters() {
		return givenPossibleFilters;
	}

	/**
	 * Gets the master (top level) filter, encompassing all filters set by the user
	 * @return the master filter
	 */
	public final MasterFilter getMasterFilter() {
		return masterFilter;
	}

	/**
	 * Gets the master (top level) filter, encompassing all filters set by the user
	 * @return the master filter
	 */
	public final MasterFilter getFilter() {
		return getMasterFilter();
	}
	
	
	
	public static void main(String[] args) {
		JFrame frame = new JFrame ("Filter Test");
		frame.setSize(800, 600);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		
		
		// lets make some possible filters that the user can use
		FlatFilter firstNameFilter = new StringFilter("First name", "firstname");
		FlatFilter lastNameFilter = new StringFilter("Last name", "lastname");
		NumberFilter favNumberFilter = new NumberFilter("Favorite number", "favno");
		StringFilter favColorFilter = new FixedValuesStringFilter("Favorite color", "favco",
				"red", "blue", "green", "black", "white", "purple");
		DateFilter birthdayFilter = new DateFilter("Birthday"); // example that a raw name isn't needed
		
		// premake existing filtering (which would usually originate from ConditionalFiler itself)
		// to make them now, we can use our possible filters
		MasterFilter masterFilter = new MasterFilter(ContainerType.ANY);
		ContainerFilter lincNameContainer = new ContainerFilter(ContainerType.ALL);
		StringFilter sf1 = (StringFilter)firstNameFilter.clone();
		sf1.setFilterConditions(StringFilterMode.CONTAINS, "ab"); // abraham, abe
		StringFilter sf2 = (StringFilter)lastNameFilter.clone();
		sf2.setFilterConditions(StringFilterMode.IS, "lincoln");
		lincNameContainer.addFilter(sf1);
		lincNameContainer.addFilter(sf2);
		
		ContainerFilter johnNameContainer = new ContainerFilter(ContainerType.ALL);
		sf1 = (StringFilter)firstNameFilter.clone();
		// note, default filter mode is IS, so sf1.setString("andrew") would be enough, this is just to be safe
		sf1.setFilterConditions(StringFilterMode.IS, "andrew");
		sf2 = (StringFilter)lastNameFilter.clone();
		sf2.setFilterConditions(StringFilterMode.IS, "johnson");
		johnNameContainer.addFilter(sf1);
		johnNameContainer.addFilter(sf2);
		
		FixedValuesStringFilter colorFilter = (FixedValuesStringFilter)favColorFilter.clone();
		colorFilter.setFilterMode(StringFilterMode.ISNOT);
		colorFilter.setString(colorFilter.getPossibleValues().iterator().next()); // just the first one
		
		masterFilter.addFilter(lincNameContainer);
		masterFilter.addFilter(johnNameContainer);
		masterFilter.addFilter(colorFilter);
		
		ConditionalFilter filterPanel = new ConditionalFilter(masterFilter, firstNameFilter, lastNameFilter,
				favNumberFilter, favColorFilter, birthdayFilter);
		
		// a filterpanel with no pre-existing filters would be:
		//ConditionalFilter filterPanel = new ConditionalFilter(false, firstNameFilter, lastNameFilter,
		//		favNumberFilter, favColorFilter, birthdayFilter);
		
		JPanel frameContent = new JPanel();
		JPanel buttonPanel = new JPanel();
		
		JButton report = new JButton("Report");
		JButton reportSql = new JButton("Report SQL");
		report.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				MasterFilter mf = filterPanel.getMasterFilter();
				System.out.println(mf.toString());
				JOptionPane.showMessageDialog(frame, mf.toString());
			}
		});
		reportSql.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				MasterFilter mf = filterPanel.getMasterFilter();
				System.out.println(mf.getSqlWhere());
				// the sql is one long string, to add line wrap to optionpane, we wrap it in html
				String escapedSql = mf.getSqlWhere().replace(">", "&gt;").replace("<", "&lt;");
				JOptionPane.showMessageDialog(frame, "<html><body><p style='width: 400px;'>" + escapedSql + "</p></body></html>");
			}
		});
		
		
		buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
		buttonPanel.add(Box.createHorizontalGlue());
		buttonPanel.add(report);
		buttonPanel.add(Box.createHorizontalStrut(5));
		buttonPanel.add(reportSql);
		buttonPanel.setMaximumSize(new Dimension(buttonPanel.getMaximumSize().width, report.getPreferredSize().height));
		
		frameContent.setLayout(new BoxLayout(frameContent, BoxLayout.Y_AXIS));
		frameContent.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
		frameContent.add(filterPanel);
		frameContent.add(Box.createVerticalStrut(5));
		frameContent.add(buttonPanel);
		
		
		frame.add(frameContent);
		frame.setVisible(true);
	}
}
