package ca.pfv.spmf.gui.visual_pattern_viewer;

import java.awt.BorderLayout;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JTextField;

import ca.pfv.spmf.gui.Main;

/*
 * Copyright (c) 2008-2025 Philippe Fournier-Viger
 *
 * This file is part of the SPMF DATA MINING SOFTWARE
 * (http://www.philippe-fournier-viger.com/spmf).
 *
 * SPMF is free software: 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 3 of the License, or (at your option) any later
 * version.
 *
 * SPMF 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 SPMF. If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * This class implements a GUI Window (a viewer) for viewing rules found by data
 * mining algorithms.
 * <p>
 * It provides sorting controls for different rule measures and an export button
 * to save the visualization as a PNG file.
 * <p>
 * Additionally, a summary panel is shown at the top of the window with
 * statistical information (minimum and maximum) for each available measure.
 * 
 * @author Philippe Fournier-Viger 2025
 */
public class VisualPatternViewer extends JFrame {
	/** Serial UIID */
	private static final long serialVersionUID = 3545413603350311234L;

	/** Window title */
	private static final String WINDOW_TITLE = "SPMF Visual Pattern Viewer ";

	/** Label for exporting to PNG */
	private static final String DEFAULT_EXPORT_FILENAME = "rules_visualization.png";

	/** Store the path to the original pattern file, so we can re‐open it **/
	private final String filePath;

	/** Selected layout mode **/
	LayoutMode mode = LayoutMode.GRID;

	/** Panel reference for toggling summary statistics **/
	private JPanel statsPanel;

	/** Panel for showing patterns **/
	private PatternsPanel patternsPanel;

	/** Panel for search/filtering **/
	private JPanel searchPanel;

	private JLabel countLabel;

	/**
	 * Constructor
	 * 
	 * @param runAsStandaloneProgram if true, this tool will be run in standalone
	 *                               mode (close the window will close the program).
	 *                               Otherwise not.
	 * @param filePath               path to the pattern file to visualize
	 * @param patternType            type of pattern to display (e.g., rules vs.
	 *                               itemsets)
	 * @throws IOException if error reading the input file
	 */
	public VisualPatternViewer(boolean runAsStandaloneProgram, String filePath, PatternType patternType)
			throws IOException {
		super(WINDOW_TITLE + Main.SPMF_VERSION);
		if (runAsStandaloneProgram) {
			// Set the default close operation
			setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		}

		// Assign to our new field
		this.filePath = filePath;

		patternsPanel = PatternPanelFactory.getPatternPanel(filePath, patternType, mode);

		// === Panel for summary statistics ===
		// Create a panel at the top to show min and max for each measure
		JPanel statsPanelLocal = new JPanel();
		statsPanelLocal.setLayout(new BoxLayout(statsPanelLocal, BoxLayout.Y_AXIS));
		statsPanelLocal.setBorder(javax.swing.BorderFactory.createTitledBorder("Statistics"));

		countLabel = new JLabel(" Pattern count: " + patternsPanel.getTotalPatternCount());
		statsPanelLocal.add(countLabel);

		// Retrieve all measures and display their min and max values
		Set<String> measures = patternsPanel.getAllMeasures();
		Map<String, Double> minMap = new LinkedHashMap<>();
		Map<String, Double> maxMap = new LinkedHashMap<>();

		// For each measure, get the minimum and maximum value
		for (String measure : measures) {
			Double minVal = patternsPanel.getMinForMeasureOriginal(measure);
			Double maxVal = patternsPanel.getMaxForMeasureOriginal(measure);

			if (minVal != null && maxVal != null) {
				minMap.put(measure, minVal);
				maxMap.put(measure, maxVal);
			}
		}

		// Field for toggling visibility
		this.statsPanel = statsPanelLocal;

		// Statistics panel
		getContentPane().add(statsPanelLocal, BorderLayout.SOUTH);

		// Add the main pattern panel inside a scroll pane to the CENTER region
		getContentPane().add(new JScrollPane((JPanel) patternsPanel), BorderLayout.CENTER);

		// Create menu bars
		this.setJMenuBar(createMenuBar(this, patternsPanel, this.filePath));

		// Get the file name
		String fileName = new java.io.File(filePath).getName();
		this.setTitle(WINDOW_TITLE + Main.SPMF_VERSION + " - " + fileName);

		// === Search panel ===
		searchPanel = new JPanel(new GridBagLayout());
		searchPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Search & Filter"));

		GridBagConstraints gbc = new GridBagConstraints();
		gbc.insets = new Insets(4, 4, 4, 4);
		gbc.gridx = 0;
		gbc.gridy = 0;
		gbc.anchor = GridBagConstraints.WEST;

		// Text search field
		JLabel searchLabel = new JLabel("Search:");
		searchPanel.add(searchLabel, gbc);
		gbc.gridwidth = 3;
		gbc.gridx = 1;
		JTextField searchField = new JTextField(12);
		JButton searchButton = new JButton("Search");
		JPanel searchInputPanel = new JPanel(new BorderLayout());
		searchInputPanel.add(searchField, BorderLayout.CENTER);
		searchInputPanel.add(searchButton, BorderLayout.EAST);
		searchPanel.add(searchInputPanel, gbc);

		// Sliders and operator combo boxes for numeric measures
		Map<String, JSlider> sliders = new LinkedHashMap<>();
		Map<String, JComboBox<String>> operatorCombos = new LinkedHashMap<>();
		int row = 1; // Row 0 was the search field
		
		//For each measure, create a slider and other interface objects
		for (String measure : minMap.keySet()) {
			gbc.gridy = row;
			gbc.gridx = 0;
			gbc.gridwidth = 1;

			JLabel label = new JLabel(measure);
			searchPanel.add(label, gbc);

			gbc.gridx = 1;
			gbc.fill = GridBagConstraints.NONE;
			gbc.weightx = 0;
			String[] ops = { "≥", "≤" };
			JComboBox<String> combo = new JComboBox<>(ops);
			searchPanel.add(combo, gbc);

			gbc.gridx = 2;
			gbc.fill = GridBagConstraints.HORIZONTAL;
			JPanel sliderPanel = new JPanel(new BorderLayout());

			double min = minMap.get(measure);
			double max = maxMap.get(measure);
			JSlider slider = new JSlider(0, 1000);
			slider.setPaintTicks(true);
			slider.setPaintLabels(true);

			// Set tooltip dynamically on mouse movement
			slider.addMouseMotionListener(new MouseMotionAdapter() {
				@Override
				public void mouseMoved(MouseEvent e) {
					int sliderValue = slider.getValue();
					double realValue = min + ((sliderValue / 1000.0) * (max - min));
					slider.setToolTipText(String.format("%.3f", realValue));
				}
			});

			// Add min/max labels around slider
			JLabel minLabel = new JLabel(String.format("%.2f", min));
			JLabel maxLabel = new JLabel(String.format("%.2f", max));
			sliderPanel.add(minLabel, BorderLayout.WEST);
			sliderPanel.add(slider, BorderLayout.CENTER);
			sliderPanel.add(maxLabel, BorderLayout.EAST);

			searchPanel.add(sliderPanel, gbc);

			sliders.put(measure, slider);
			operatorCombos.put(measure, combo);
			row++;
		}

		// Button to clear filters
		gbc.gridy = row;
		gbc.gridx = 0;
		gbc.gridwidth = 3;
		JButton clearButton = new JButton("Clear Filters");
		clearButton.setEnabled(false);
		searchPanel.add(clearButton, gbc);

		// Action listeners
		Runnable enableClearButton = () -> clearButton.setEnabled(true);

		searchField.addActionListener(e -> {
			applySearchAndFilter(searchField.getText().trim(), sliders, operatorCombos, minMap, maxMap);
			enableClearButton.run();
		});

		searchButton.addActionListener(e -> {
			applySearchAndFilter(searchField.getText().trim(), sliders, operatorCombos, minMap, maxMap);
			enableClearButton.run();
		});

		clearButton.addActionListener(e -> {
			searchField.setText("");
			for (Map.Entry<String, JSlider> entry : sliders.entrySet()) {
				entry.getValue().setValue(0);
			}
			for (Map.Entry<String, JComboBox<String>> entry : operatorCombos.entrySet()) {
				entry.getValue().setSelectedItem("≥");
			}
			patternsPanel.clearSearchAndFilters();
			clearButton.setEnabled(false); // Disable after clearing
		});

		// Listen for slider changes
		for (Map.Entry<String, JSlider> entry : sliders.entrySet()) {
			JSlider slider = entry.getValue();
			slider.addChangeListener(e -> {
				if (!slider.getValueIsAdjusting()) {
					applySearchAndFilter(searchField.getText().trim(), sliders, operatorCombos, minMap, maxMap);
					enableClearButton.run();
				}
			});
		}

		// Listen for operator combo changes to update filtering
		for (Map.Entry<String, JComboBox<String>> entry : operatorCombos.entrySet()) {
			JComboBox<String> combo = entry.getValue();
			combo.addActionListener(e -> {
				applySearchAndFilter(searchField.getText().trim(), sliders, operatorCombos, minMap, maxMap);
				enableClearButton.run();
			});
		}

		getContentPane().add(searchPanel, BorderLayout.EAST);
		// === End: Search panel ===

		configureFrameSize(this, patternsPanel);
	}

	/**
	 * Applies search text and numeric filters to the patternsPanel.
	 * 
	 * @param text           the search text
	 * @param sliders        map of measure names to their JSlider components
	 * @param operatorCombos map of measure names to JComboBox components holding
	 *                       operators
	 * @param minMap         map of measure names to their minimum observed values
	 * @param maxMap         map of measure names to their maximum observed values
	 */
	private void applySearchAndFilter(String text, Map<String, JSlider> sliders,
			Map<String, JComboBox<String>> operatorCombos, Map<String, Double> minMap, Map<String, Double> maxMap) {
		Map<String, Double> thresholds = new LinkedHashMap<>();
		Map<String, String> operators = new LinkedHashMap<>();
		for (Map.Entry<String, JSlider> entry : sliders.entrySet()) {
			String measure = entry.getKey();
			JSlider slider = entry.getValue();
			double min = minMap.get(measure);
			double max = maxMap.get(measure);
			double val = slider.getValue() / 1000.0 * (max - min) + min;
			thresholds.put(measure, val);
			operators.put(measure, (String) operatorCombos.get(measure).getSelectedItem());
		}
		patternsPanel.applySearchAndFilters(text, thresholds, operators);
		
		updateStatusBar();
		
	}

	/**
	 * Update the text of the status bar (e.g. after the user does a search or apply a filter)
	 */
	private void updateStatusBar() {
		// Update the status
		int totalPatterns = patternsPanel.getTotalPatternCount();
		int visiblePatterns = patternsPanel.getNumberOfVisiblePatterns();
		// if a filter is applied
		if(totalPatterns != visiblePatterns) {
			countLabel.setText(" Pattern count: " + visiblePatterns + " (with filter)");
		}else {
			//If no filter is applied
			countLabel.setText(" Pattern count: " + totalPatterns);
		}
	}

	/**
	 * Builds the menu bar.
	 *
	 * @param parentFrame      the frame to anchor dialogs
	 * @param rulesPanel       the panel displaying the patterns
	 * @param originalFilePath path to the original file for opening or exporting
	 * @return a JMenuBar with View, Sort, and Tools menus
	 */
	private static JMenuBar createMenuBar(JFrame parentFrame, PatternsPanel rulesPanel, String originalFilePath) {
		JMenuBar menuBar = new JMenuBar();

		// === View Menu ===
		JMenu menuView = new JMenu("View");

		// --- Submenu: Sort ---
		JMenu menuSort = new JMenu("Sort by");
		ButtonGroup sortGroup = new ButtonGroup();
		for (String sortOption : rulesPanel.getListOfSortingOrders()) {
			JRadioButtonMenuItem item = new JRadioButtonMenuItem(sortOption);
			sortGroup.add(item);
			item.addActionListener(e -> {
				try {
					rulesPanel.sortPatterns(sortOption);
					rulesPanel.revalidate();
					rulesPanel.repaint();
				} catch (Exception ex) {
					showErrorDialog(parentFrame, "Sort failed: " + ex.getMessage());
				}
			});
			menuSort.add(item);
		}

		JRadioButtonMenuItem itemShowStats = new JRadioButtonMenuItem("Statistics");
		itemShowStats.setSelected(true);
		itemShowStats.addActionListener(e -> {
			if (parentFrame instanceof VisualPatternViewer viewer) {
				viewer.statsPanel.setVisible(itemShowStats.isSelected());
				viewer.pack();
				viewer.setLocationRelativeTo(null);
			}
		});
		menuView.add(itemShowStats);

		// *** Menu Item to Show/Hide Search Panel ***
		JRadioButtonMenuItem itemShowSearchPanel = new JRadioButtonMenuItem("Search & Filter Panel");
		itemShowSearchPanel.setSelected(true); // Initially visible
		itemShowSearchPanel.addActionListener(e -> {
			if (parentFrame instanceof VisualPatternViewer viewer) {
				viewer.searchPanel.setVisible(itemShowSearchPanel.isSelected());
				viewer.pack();
				viewer.setLocationRelativeTo(null);
			}
		});
		menuView.add(itemShowSearchPanel);

		menuView.addSeparator();

		// --- Submenu: Layout modes ---
		JMenu menuLayout = new JMenu("Layout");
		ButtonGroup group = new ButtonGroup();
		LayoutMode currentMode = LayoutMode.GRID;
		if (parentFrame instanceof VisualPatternViewer viewerFrame) {
			currentMode = viewerFrame.mode;
		}
		for (LayoutMode mode : LayoutMode.values()) {
			JRadioButtonMenuItem item = new JRadioButtonMenuItem(mode.toString());
			group.add(item);
			if (mode == currentMode) {
				item.setSelected(true);
			}
			item.addActionListener(e -> {
				if (parentFrame instanceof VisualPatternViewer viewer) {
					viewer.mode = mode;
				}
				if (rulesPanel instanceof PatternsPanel patternsPanel) {
					patternsPanel.setLayoutModeWithCallback(mode, () -> configureFrameSize(parentFrame, patternsPanel));
				} else {
					rulesPanel.setLayoutMode(mode);
				}
			});
			menuLayout.add(item);
		}
		menuView.add(menuLayout);

		// === Tools Menu ===
		JMenu menuTools = new JMenu("Tools");

		JMenuItem itemOpenOriginal = new JMenuItem("Open file with system text editor");
		itemOpenOriginal.addActionListener(e -> {
			try {
				if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
					File fileToOpen = new File(originalFilePath);
					if (fileToOpen.exists() && fileToOpen.canRead()) {
						Desktop.getDesktop().open(fileToOpen);
					} else {
						JOptionPane.showMessageDialog(parentFrame, "Cannot open file:\n" + originalFilePath,
								"File Not Found / Unreadable", JOptionPane.ERROR_MESSAGE);
					}
				} else {
					JOptionPane.showMessageDialog(parentFrame,
							"Desktop API is not supported on this platform.\nCannot open the file.",
							"Operation Not Supported", JOptionPane.ERROR_MESSAGE);
				}
			} catch (IOException ex) {
				JOptionPane.showMessageDialog(parentFrame, "Failed to open file:\n" + ex.getMessage(), "Error",
						JOptionPane.ERROR_MESSAGE);
			}
		});
		menuTools.add(itemOpenOriginal);

		JMenuItem itemExport = new JMenuItem("Export as PNG");
		itemExport.addActionListener(e -> {
			try {
				JFileChooser fileChooser = new JFileChooser();
				fileChooser.setSelectedFile(new File(DEFAULT_EXPORT_FILENAME));
				int result = fileChooser.showSaveDialog(parentFrame);
				if (result == JFileChooser.APPROVE_OPTION) {
					File file = fileChooser.getSelectedFile();
					rulesPanel.exportAsPNG(originalFilePath);
				}
			} catch (Exception ex) {
				showErrorDialog(parentFrame, "Export failed: " + ex.getMessage());
			}
		});
		menuTools.add(itemExport);

		menuBar.add(menuView);
		menuBar.add(menuSort);
		menuBar.add(menuTools);

		return menuBar;
	}

	/**
	 * Configure the frame size to fit within 90% of the screen and set minimum
	 * size.
	 * 
	 * @param frame         the JFrame to configure
	 * @param patternsPanel the panel containing the visualized patterns
	 */
	private static void configureFrameSize(JFrame frame, PatternsPanel patternsPanel) {
		frame.pack();
		Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
		int maxWidth = (int) (screenSize.width * 0.9);
		int maxHeight = (int) (screenSize.height * 0.9);
		Dimension preferred = frame.getSize();
		if (preferred.width > maxWidth || preferred.height > maxHeight) {
			frame.setSize(Math.min(preferred.width, maxWidth), Math.min(preferred.height, maxHeight));
		}
		frame.setMinimumSize(new Dimension(500, 300)); // <- sets the minimum size
		frame.setLocationRelativeTo(null);
	}

	/**
	 * Show an error dialog with the given message.
	 * 
	 * @param parentFrame the parent frame to center the dialog
	 * @param message     the message to display
	 */
	private static void showErrorDialog(JFrame parentFrame, String message) {
		JOptionPane.showMessageDialog(parentFrame, message, "Error", JOptionPane.ERROR_MESSAGE);
	}
}
