package ca.pfv.spmf.gui.visuals.histograms;

/*
 * Copyright (c) 2024 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/>.
 */
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

import ca.pfv.spmf.gui.preferences.PreferencesManager;
import ca.pfv.spmf.test.MainTestApriori_saveToFile;

/**
 * A panel displaying a frequency histogram
 * 
 * @author Philippe Fournier-Viger, 2024
 */
public class HistogramDistributionPanel extends JPanel {

	/** The minimum bar width to display labels */
	private static final int MINIMUM_BAR_WIDTH__TO_DISPLAY_LABELS = 15;

	/** default serial UID */
	private static final long serialVersionUID = 1L;

	/** The x axis name */
	private String xAxisName = "Length";

	/** The y axis name */
	private String yAxisName = "Count";

	/** The values for the y axis */
	private int[] yValues;

	/** The values for the x axis */
	private int[] xLabels;

	/** The maximum value on the X axis */
	private int maxX;

	/** The maximum value on the Y axis */
	private int maxY;

	/** The scaling factor */
	private double scaleFactor;
	
	/** Selected order */
	private Order selectedOrder = null;

	/**
	 * A boolean that indicate if labels for each bar should be displayed on the X
	 * axis
	 */
	private boolean showBarLabels;

	/**
	 * A boolean that indicate if values for each bar should be displayed above them
	 */
	private boolean showBarValues;

	/** The default margin around the chart */
	private static final int MARGIN = 50;
	
	/** Additional margin for bottom to accommodate rotated labels */
	private static final int BOTTOM_MARGIN_EXTRA = 30;

	/** The title of the histogram */
	private String title;

	/** The bar width for this histogram **/
	private int barWidth = 2;
	
	/** Font constants for better performance */
	private static final Font FONT_NORMAL = new Font("Arial", Font.PLAIN, 10);
	private static final Font FONT_BOLD = new Font("Arial", Font.BOLD, 12);
	private static final Font FONT_TITLE = new Font("Arial", Font.BOLD, 16);
	
	/** Color constants for better performance and consistency */
	private static final Color GRID_COLOR = new Color(220, 220, 220);
	private static final Color AXIS_COLOR = new Color(60, 60, 60);
	private static final Color BAR_BORDER_COLOR = new Color(100, 100, 100);
	
	/** Number of grid lines to display on Y axis */
	private static final int Y_GRID_LINES = 5;
	
	/** The different possible orders for displaying values of the X axis*/
	public enum Order {ASCENDING_Y, DESCENDING_Y, ASCENDING_X, DESCENDING_X}

	/**
	 * A map that assign strings to values on the x axis. It can be set to null if
	 * no strings are provided
	 */
	private Map<Integer, String> mapXValuesToString;
	
	/** Cache for bar colors to avoid recalculating */
	private Color[] barColors;

	/**
	 * Constructor
	 * 
	 * @param yValues         the values for the Y axis
	 * @param xLabels         the corresponding values for the X axis
	 * @param title           the title of the chart
	 * @param showBarLabels   A boolean that indicate if labels for each bar should
	 *                        be displayed on the X axis
	 * @param showBarValues   A boolean that indicate if values for each bar should
	 *                        be displayed above them
	 * @param xAxisName       The x axis name
	 * @param yAxisName       The y axis name
	 * @param mapXValuesToString A map that associate String to values on the X axis
	 * @param order			The order to be used for sorting values of the X axis.
	 */
	public HistogramDistributionPanel(int[] yValues, int[] xLabels, String title, boolean showBarLabels,
			boolean showBarValues, String xAxisName, String yAxisName, Map<Integer, String> mapXValuesToString, Order order) {
		this.mapXValuesToString = mapXValuesToString;
		initializeHistogram(yValues, xLabels, title, showBarLabels, showBarValues, xAxisName, yAxisName, order);
	}

	/**
	 * Initialize the histogram panel
	 * 
	 * @param yValues       the values for the Y axis
	 * @param xLabels       the corresponding values for the X axis
	 * @param title         the title of the chart
	 * @param showBarLabels A boolean that indicate if labels for each bar should be
	 *                      displayed on the X axis
	 * @param showBarValues A boolean that indicate if values for each bar should be
	 *                      displayed above them
	 * @param xAxisName     The x axis name
	 * @param yAxisName     The y axis name
	 * @param order			The order to be used for sorting values of the X axis.
	 */
	private void initializeHistogram(int[] yValues, int[] xLabels, String title, boolean showBarLabels,
			boolean showBarValues, String xAxisName, String yAxisName, Order order) {
		// Initialize the values, labels, and title
		this.yValues = yValues;
		this.xLabels = xLabels;
		this.title = title;
		this.showBarLabels = showBarLabels;
		this.showBarValues = showBarValues;
		this.xAxisName = xAxisName;
		this.yAxisName = yAxisName;

		// Find the maximum value in the X and Y arrays
		maxY = findMax(yValues);
		if (order != null) {
			setSortOrder(order);
		}
		maxX = findMax(xLabels);

		setBackground(Color.WHITE);

		// Set the bar width automatically
		if (xLabels.length > 0 && xLabels.length < 600) {
			barWidth = Math.max(2, 600 / xLabels.length);
		}
		
		// Pre-generate bar colors for better performance
		generateBarColors();

		setToolTipText("");

		this.setPreferredSize(new Dimension(600, 50));
	}

	/**
	 * Find the maximum value in an array ot integers
	 * 
	 * @param array the array
	 * @return the maximum value
	 */
	private int findMax(int[] array) {
		if (array == null || array.length == 0) {
			return 0;
		}
		int max = array[0];
		for (int i = 1; i < array.length; i++) {
			if (array[i] > max) {
				max = array[i];
			}
		}
		return max;
	}
	
	/**
	 * Pre-generate colors for all bars to improve rendering performance
	 */
	private void generateBarColors() {
		if (yValues == null) {
			return;
		}
		
		barColors = new Color[yValues.length];
		for (int i = 0; i < yValues.length; i++) {
			barColors[i] = generateBarColor(i);
		}
	}

	/**
	 * Method that draw the histogram
	 * 
	 * @param g the graphic object for drawing.
	 */
	@Override
	public void paintComponent(Graphics g) {
		// Call the superclass method
		super.paintComponent(g);

		if (yValues == null || yValues.length == 0) {
			return;
		}
		
		// Enable anti-aliasing for smoother graphics
		Graphics2D g2d = (Graphics2D) g;
		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

		// Calculate the scale factor based on the maximum value and the panel height
		int availableHeight = getHeight() - (MARGIN * 2) - BOTTOM_MARGIN_EXTRA;
		scaleFactor = maxY > 0 ? (double) availableHeight / maxY : 0;

		// Set the font and color
		g2d.setFont(FONT_NORMAL);
		g2d.setColor(AXIS_COLOR);

		// Calculate the total width of the histogram based on the number of bars and
		// the minimum bar width
		int histogramWidth = yValues.length * getBarWidth();

		// Draw the x-axis and the y-axis
		int yAxisBottom = getHeight() - MARGIN - BOTTOM_MARGIN_EXTRA;
		int xAxisStart = MARGIN + histogramWidth;

		// Draw grid lines for better readability
		drawGridLines(g2d, yAxisBottom, xAxisStart, availableHeight);

		// Use the histogram width instead of the panel width when drawing the X axis line
		g2d.setColor(AXIS_COLOR);
		g2d.drawLine(MARGIN, yAxisBottom, xAxisStart, yAxisBottom);
		g2d.drawLine(MARGIN, MARGIN, MARGIN, yAxisBottom);

		// Pre-calculate effective display flags
		boolean showBarsLabelsEffective = showBarLabels && barWidth >= MINIMUM_BAR_WIDTH__TO_DISPLAY_LABELS;
		boolean showBarsValuesEffective = showBarValues && barWidth >= MINIMUM_BAR_WIDTH__TO_DISPLAY_LABELS;

		// Draw the bars and the labels
		drawBars(g2d, yAxisBottom, showBarsLabelsEffective, showBarsValuesEffective);

		// Draw axis labels and values
		drawAxisLabelsAndValues(g2d, yAxisBottom, xAxisStart);

		// Draw the title of the histogram
		drawCenteredTitle(g2d);
	}
	
	/**
	 * Draw grid lines on the histogram for better readability
	 * @param g2d the graphics context
	 * @param yAxisBottom the bottom of the Y axis
	 * @param xAxisStart the end of the X axis
	 * @param availableHeight the available height for the chart
	 */
	private void drawGridLines(Graphics2D g2d, int yAxisBottom, int xAxisStart, int availableHeight) {
		g2d.setColor(GRID_COLOR);
		
		// Draw horizontal grid lines
		for (int i = 1; i <= Y_GRID_LINES; i++) {
			int gridY = yAxisBottom - (i * availableHeight / Y_GRID_LINES);
			g2d.drawLine(MARGIN, gridY, xAxisStart, gridY);
			
			// Draw grid value labels
			int gridValue = (i * maxY) / Y_GRID_LINES;
			g2d.setColor(AXIS_COLOR);
			g2d.setFont(FONT_NORMAL);
			String valueStr = String.valueOf(gridValue);
			FontMetrics fm = g2d.getFontMetrics();
			int labelWidth = fm.stringWidth(valueStr);
			g2d.drawString(valueStr, MARGIN - labelWidth - 5, gridY + 4);
			g2d.setColor(GRID_COLOR);
		}
	}
	
	/**
	 * Draw all bars in the histogram
	 * @param g2d the graphics context
	 * @param yAxisBottom the bottom of the Y axis
	 * @param showBarsLabelsEffective whether to show bar labels
	 * @param showBarsValuesEffective whether to show bar values
	 */
	private void drawBars(Graphics2D g2d, int yAxisBottom, boolean showBarsLabelsEffective, 
			boolean showBarsValuesEffective) {
		
		FontMetrics fm = g2d.getFontMetrics(FONT_NORMAL);
		
		for (int i = 0; i < yValues.length; i++) {
			// Calculate the bar height
			int barHeight = (int) (yValues[i] * scaleFactor);

			int barX = MARGIN + i * getBarWidth();
			int barY = yAxisBottom - barHeight;

			// Draw the bar with a different color for each value
			g2d.setColor(barColors[i]);
			g2d.fillRect(barX, barY, getBarWidth(), barHeight);
			
			// Draw border around bar for better definition
			g2d.setColor(BAR_BORDER_COLOR);
			g2d.drawRect(barX, barY, getBarWidth(), barHeight);

			g2d.setColor(AXIS_COLOR);
			g2d.setFont(FONT_NORMAL);
			
			// Draw the label below the bar
			if (showBarsLabelsEffective) {
				String text = getXValueAsString(i);
				int textWidth = fm.stringWidth(text);
				int textX = barX + (getBarWidth() - textWidth) / 2;
				g2d.drawString(text, textX, yAxisBottom + 15);
			}

			// Draw the value above the bar
			if (showBarsValuesEffective && barHeight > 15) {
				String valueText = String.valueOf(yValues[i]);
				int textWidth = fm.stringWidth(valueText);
				int textX = barX + (getBarWidth() - textWidth) / 2;
				g2d.drawString(valueText, textX, barY - 3);
			}
		}
	}
	
	/**
	 * Draw axis labels and values
	 * @param g2d the graphics context
	 * @param yAxisBottom the bottom of the Y axis
	 * @param xAxisStart the end of the X axis
	 */
	private void drawAxisLabelsAndValues(Graphics2D g2d, int yAxisBottom, int xAxisStart) {
		g2d.setFont(FONT_BOLD);
		g2d.setColor(AXIS_COLOR);
		
		// Draw the maximum value at the end of the Y axis
//		String maxYStr = String.valueOf(maxY);
		FontMetrics fm = g2d.getFontMetrics();
//		int maxYWidth = fm.stringWidth(maxYStr);
//		g2d.drawString(maxYStr, MARGIN - maxYWidth - 5, MARGIN + 4);
//		
		// Draw Y axis label
		g2d.drawString(yAxisName, MARGIN - 40, MARGIN - 12);
		
		// Draw X axis label and max value if sorted by X
		if (Order.DESCENDING_X.equals(selectedOrder) || Order.ASCENDING_X.equals(selectedOrder)) {
//			String maxXStr = String.valueOf(maxX);
//			int maxXWidth = fm.stringWidth(maxXStr);
//			g2d.drawString(maxXStr, xAxisStart - maxXWidth / 2, yAxisBottom + 15);
			
			int xAxisLabelWidth = fm.stringWidth(xAxisName);
			g2d.drawString(xAxisName, xAxisStart - xAxisLabelWidth - 5, yAxisBottom + 30);
		} else {
			// Draw X axis label centered
			int xAxisLabelWidth = fm.stringWidth(xAxisName);
			int centerX = MARGIN + (xAxisStart - MARGIN) / 2;
			g2d.drawString(xAxisName, centerX - xAxisLabelWidth / 2, yAxisBottom + 30);
		}

		// Draw the value 0 at the intersection of both axes
//		g2d.drawString("0", MARGIN - 15, yAxisBottom + 15);
	}

	/**
	 * Draw the title centered at the top of the panel
	 * @param g the graphics context
	 */
	private void drawCenteredTitle(Graphics g) {
		g.setFont(FONT_TITLE);
		g.setColor(AXIS_COLOR);
		// Get the font metrics of the current font
		FontMetrics fm = g.getFontMetrics();
		// Get the width of the title string
		int titleWidth = fm.stringWidth(title);
		// Calculate the x coordinate of the title string
		int titleX = (getWidth() - titleWidth) / 2;
		g.drawString(title, titleX, 25);
	}

	/**
	 * Generate a color for a bar based on its index using HSB color space
	 * for better color distribution
	 * @param index the bar index
	 * @return the color
	 */
	private Color generateBarColor(int index) {
		// Use HSB color space for more visually distinct colors
		float hue = (index * 0.618033988749895f) % 1.0f; // Golden ratio conjugate for good distribution
		float saturation = 0.7f; // Vibrant but not oversaturated
		float brightness = 0.85f; // Bright but not washed out
		return Color.getHSBColor(hue, saturation, brightness);
	}

	/**
	 * Get a string representation of a given X value
	 * @param i the position of the X values in the array of X values
	 * @return the string representation
	 */
	private String getXValueAsString(int i) {
		int value = xLabels[i];
		if (mapXValuesToString != null) {
			String name = mapXValuesToString.get(value);
			if (name != null) {
				return name;
			}
		}
		return Integer.toString(value);
	}

	/**
	 * The method that sets the preferred size of the histogram JPanel
	 * 
	 * @param preferredSize a Dimension object indicating the preferred size
	 */
	@Override
	public void setPreferredSize(Dimension preferredSize) {
		// Calculate the total width of the histogram based on the number of bars and
		// the minimum bar width
		int histogramWidth = yValues.length * getBarWidth();

		// Set the preferred size of the histogram JPanel to the calculated width and
		// the same height as before
		super.setPreferredSize(new Dimension(histogramWidth + MARGIN * 2, getHeight()));
	}

	/**
	 * Exports the histogram as an image file (PNG)
	 * 
	 * @param fileName the file path for the image file
	 */
	public void exportAsImage(String fileName) {
		String outputFilePath = promptForFilePath("png");
		if (outputFilePath == null) {
			return;
		}

		try {
			// add the .png extension
			if (!outputFilePath.endsWith(".png")) {
				outputFilePath = outputFilePath + ".png";
			}
			// Create a BufferedImage object with the same size as the panel
			BufferedImage image = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);

			// Get the Graphics object from the image
			Graphics2D g2d = image.createGraphics();
			
			// Enable anti-aliasing for the exported image
			g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
			g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
			
			// Paint the histogram on the image
			paint(g2d);
			g2d.dispose();

			// Create a File object with the given file name
			File file = new File(outputFilePath);

			// Write the image to the file as PNG format
			ImageIO.write(image, "png", file);

		} catch (IOException e) {
			showErrorDialog("An error occured while attempting to save the plot. ERROR MESSAGE = " + e.toString());
		}
	}

	/**
	 * The method that exports the histogram as a CSV file
	 * 
	 * @param fileName the file path for the CSV file
	 */
	public void exportAsCSV(String fileName) {
		String outputFilePath = promptForFilePath("csv");
		if (outputFilePath == null) {
			return;
		}

		try {
			// add the .csv extension
			if (!outputFilePath.endsWith(".csv")) {
				outputFilePath = outputFilePath + ".csv";
			}
			// Create a File object with the given file name
			File file = new File(outputFilePath);

			// Create a PrintWriter object to write to the file
			try (PrintWriter pw = new PrintWriter(file)) {
				// Write the title of the histogram as the first line of the file, followed by a
				// newline character
				pw.println(title + "\n");
				
				// Write header
				pw.println(xAxisName + "," + yAxisName);

				// Write the labels and values of the histogram as comma-separated values, one
				// pair per line, followed by a newline character
				for (int i = 0; i < yValues.length; i++) {
					pw.println(xLabels[i] + "," + yValues[i]);
				}
			}

		} catch (IOException e) {
			showErrorDialog("An error occured while attempting to save the plot. ERROR MESSAGE = " + e.toString());
		}
	}

	/**
	 * Prompt the user for a file path to save to
	 * @param extension the file extension (without dot)
	 * @return the selected file path or null if cancelled
	 */
	private String promptForFilePath(String extension) {
		try {
			File path = getInitialDirectory();

			// ASK THE USER TO CHOOSE A FILE
			final JFileChooser fc = (path != null) ? new JFileChooser(path.getAbsolutePath()) : new JFileChooser();
			int returnVal = fc.showSaveDialog(this);

			// If the user chose a file
			if (returnVal == JFileChooser.APPROVE_OPTION) {
				File file = fc.getSelectedFile();
				// save the path of this folder for next time.
				if (fc.getSelectedFile() != null) {
					PreferencesManager.getInstance().setOutputFilePath(fc.getSelectedFile().getParent());
				}
				return file.getPath();
			}
		} catch (Exception e) {
			showErrorDialog("An error occured while opening the save plot dialog. ERROR MESSAGE = " + e.toString());
		}
		return null;
	}

	/**
	 * Get the initial directory for the file chooser
	 * @return the initial directory or null
	 */
	private File getInitialDirectory() {
		// Get the last path used by the user, if there is one
		String previousPath = PreferencesManager.getInstance().getOutputFilePath();
		// If there is no previous path (first time user),
		// show the files in the "examples" package of
		// the spmf distribution.
		if (previousPath == null) {
			URL main = MainTestApriori_saveToFile.class.getResource("MainTestApriori_saveToFile.class");
			if (main != null && "file".equalsIgnoreCase(main.getProtocol())) {
				return new File(main.getPath());
			}
			return null;
		} else {
			// Otherwise, use the last path used by the user.
			return new File(previousPath);
		}
	}

	/**
	 * Show an error dialog to the user
	 * @param message the error message
	 */
	private void showErrorDialog(String message) {
		JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
	}

	/**
	 * A constructor that takes data records and an index i
	 * 
	 * @param data          a list of data records
	 * @param width         the width of the panel
	 * @param height        the height of the panel
	 * @param index         the attribute of interest in the list of data records
	 * @param title         the title of the histogram * @param showBarLabels A
	 *                      boolean that indicate if labels for each bar should be
	 *                      displayed on the X axis
	 * @param showBarValues A boolean that indicate if values for each bar should be
	 *                      displayed above them
	 * @param xAxisName     The x axis name
	 * @param yAxisName     The y axis name
	 * @param order			The order to be used for sorting values of the X axis.
	 */
	public HistogramDistributionPanel(Vector<List<Object>> data, int width, int height, int index, String title,
			boolean showBarLabels, boolean showBarValues, String xAxisName, String yAxisName, Order order) {
		super();

		// The maximum count of patterns for any support value
		int maxLabel = findMaxLabelFromData(data, index);

		int[] valuesArray = new int[maxLabel + 1];
		int[] labelsArray = new int[maxLabel + 1];

		for (int i = 0; i <= maxLabel; i++) {
			labelsArray[i] = i;
		}

		populateValuesFromData(data, index, valuesArray);
		
		initializeHistogram(valuesArray, labelsArray, title, showBarLabels, showBarValues, xAxisName, yAxisName, order);
	}

	/**
	 * Find the maximum label value from the data
	 * @param data the data records
	 * @param index the attribute index
	 * @return the maximum label value
	 */
	private int findMaxLabelFromData(Vector<List<Object>> data, int index) {
		int maxLabel = 0;
		for (List<Object> record : data) {
			if (index < record.size()) {
				Object value = record.get(index);
				double valueAsDouble = convertToDouble(value);
				if (valueAsDouble > maxLabel) {
					maxLabel = (int) valueAsDouble;
				}
			}
		}
		return maxLabel;
	}

	/**
	 * Populate the values array from the data
	 * @param data the data records
	 * @param index the attribute index
	 * @param valuesArray the array to populate
	 */
	private void populateValuesFromData(Vector<List<Object>> data, int index, int[] valuesArray) {
		for (List<Object> record : data) {
			if (index < record.size()) {
				Object value = record.get(index);
				double valueAsDouble = convertToDouble(value);
				int position = (int) valueAsDouble;
				if (position >= 0 && position < valuesArray.length) {
					valuesArray[position]++;
				}
			}
		}
	}

	/**
	 * Convert an object to a double value
	 * @param value the object to convert
	 * @return the double value or -1 if conversion fails
	 */
	private double convertToDouble(Object value) {
		if (value instanceof Integer) {
			return ((Integer) value).doubleValue();
		} else if (value instanceof Double) {
			return (Double) value;
		} else if (value instanceof Float) {
			return ((Float) value).doubleValue();
		} else if (value instanceof Boolean) {
			return ((Boolean) value) ? 1.0 : 0.0;
		} else if (value instanceof Short) {
			return ((Short) value).doubleValue();
		}
		return -1;
	}

	/**
	 * Get the width of bars
	 * 
	 * @return the width (integer)
	 */
	int getBarWidth() {
		return barWidth;
	}

	/**
	 * Set the width to be used for bars
	 * 
	 * @param width the width
	 */
	void setBarWidth(int width) {
		barWidth = Math.max(1, width);
		int histogramWidth = yValues.length * getBarWidth();
		setPreferredSize(new Dimension(histogramWidth + MARGIN * 2, getHeight()));
		
		// Regenerate colors when bar width changes
		generateBarColors();
		repaint();
	}
	
	/**
	 * Set the sort order for displaying the x axis values.
	 * @param order the order
	 */
	public void setSortOrder(Order order) {
		if (order == null) {
			return;
		}

		switch (order) {
			case ASCENDING_Y:
				// Sort the xLabels and yValues arrays in ascending order of yValues
				sortArrays(yValues, xLabels, true);
				break;
			case DESCENDING_Y:
				// Sort the xLabels and yValues arrays in descending order of yValues
				sortArrays(yValues, xLabels, false);
				break;
			case ASCENDING_X:
				// Sort the xLabels and yValues arrays in ascending order of xLabels
				sortArrays(xLabels, yValues, true);
				break;
			case DESCENDING_X:
				// Sort the xLabels and yValues arrays in descending order of xLabels
				sortArrays(xLabels, yValues, false);
				break;
		}

		selectedOrder = order;
		if (xLabels.length > 0) {
			maxX = xLabels[xLabels.length - 1];
		}
		
		// Regenerate colors after sorting
		generateBarColors();
	}

	/**
	 * A helper method that will sort two arrays based on the values of one array
	 * using a more efficient quicksort-based algorithm
	 * The boolean parameter ascending determines the order of sorting
	 * @param array1 an array
	 * @param array2 another array
	 * @param ascending if true, sort by ascending order. otherwise, descending order
	 */
	private void sortArrays(int[] array1, int[] array2, boolean ascending) {
		// Create indices array for indirect sorting
		Integer[] indices = new Integer[array1.length];
		for (int i = 0; i < indices.length; i++) {
			indices[i] = i;
		}
		
		// Sort indices based on array1 values
		Arrays.sort(indices, (i1, i2) -> {
			int cmp = Integer.compare(array1[i1], array1[i2]);
			return ascending ? cmp : -cmp;
		});
		
		// Create temporary arrays to hold sorted values
		int[] temp1 = new int[array1.length];
		int[] temp2 = new int[array2.length];
		
		// Rearrange both arrays based on sorted indices
		for (int i = 0; i < indices.length; i++) {
			temp1[i] = array1[indices[i]];
			temp2[i] = array2[indices[i]];
		}
		
		// Copy back to original arrays
		System.arraycopy(temp1, 0, array1, 0, array1.length);
		System.arraycopy(temp2, 0, array2, 0, array2.length);
	}

}