content: package tim.prune.function.cache;
  
  import java.awt.BorderLayout;
  import java.awt.CardLayout;
  import java.awt.Dimension;
  import java.awt.FlowLayout;
  import java.awt.GridBagConstraints;
  import java.awt.GridBagLayout;
  import java.awt.Insets;
  import java.io.File;
  
  import javax.swing.BorderFactory;
  import javax.swing.Box;
  import javax.swing.BoxLayout;
  import javax.swing.ButtonGroup;
  import javax.swing.JButton;
  import javax.swing.JDialog;
  import javax.swing.JLabel;
  import javax.swing.JOptionPane;
  import javax.swing.JPanel;
  import javax.swing.JProgressBar;
  import javax.swing.JRadioButton;
  import javax.swing.JScrollPane;
  import javax.swing.JTable;
  import javax.swing.ListSelectionModel;
  
  import tim.prune.App;
  import tim.prune.GenericFunction;
  import tim.prune.I18nManager;
  import tim.prune.config.Config;
  import tim.prune.fileutils.FileList;
  import tim.prune.gui.WholeNumberField;
  
  /**
   * Function class to manage the tile cache on local disk
   */
  public class ManageCacheFunction extends GenericFunction implements Runnable
  {
  	private JDialog _dialog = null;
  	private CardLayout _cards = null;
  	private JPanel _cardPanel = null;
  	private File _cacheDir = null;
  	private TileCacheModel _model = null;
  	private JTable _setsTable = null;
  	private JButton _deleteSetButton = null;
  	private JLabel _tileSetLabel = null, _zoomLabel = null;
  	private JLabel _ageLabel = null;
  	private JRadioButton _deleteAllRadio = null;
  	private WholeNumberField _daysField = null;
  
  	private static TileFilter _TILEFILTER = new TileFilter();
  
  
  	/**
  	 * Constructor
  	 * @param inApp App object
  	 */
  	public ManageCacheFunction(App inApp) {
  		super(inApp);
  	}
  
  	/**
  	 * @return name key
  	 */
  	public String getNameKey() {
  		return "function.managetilecache";
  	}
  
  	/**
  	 * Show the dialog to start
  	 */
  	public void begin()
  	{
  		// First check if directory even exists
  		_cacheDir = null;
  		String path = getConfig().getConfigString(Config.KEY_DISK_CACHE);
  		if (path != null && !path.equals("")) {
  			_cacheDir = new File(path);
  		}
  		if (_cacheDir == null || !_cacheDir.exists() || !_cacheDir.isDirectory())
  		{
  			_app.showErrorMessage(getNameKey(), "error.cache.notthere");
  			return;
  		}
  
  		// Build the dialog if it hasn't already been built
  		if (_dialog == null)
  		{
  			_dialog = new JDialog(_parentFrame, getName(), true); // modal
  			_dialog.setLocationRelativeTo(_parentFrame);
  			_cardPanel = makeContents();
  			_dialog.getContentPane().add(_cardPanel);
  			_dialog.pack();
  		}
  		// Start a new thread to build the model
  		new Thread(this).start();
  		// Show the first panel of the dialog including progress bar
  		_cards.first(_cardPanel);
  		_dialog.setVisible(true);
  	}
  
  
  	/**
  	 * Make the components for the dialog
  	 * @return contents inside a panel
  	 */
  	private JPanel makeContents()
  	{
  		JPanel dialogPanel = new JPanel();
  		_cards = new CardLayout();
  		dialogPanel.setLayout(_cards);
  
  		// Make first card including progress bar
  		JPanel firstCard = new JPanel();
  		firstCard.setLayout(new BorderLayout());
  		JPanel progPanel = new JPanel();
  		progPanel.setLayout(new BoxLayout(progPanel, BoxLayout.Y_AXIS));
  		progPanel.add(Box.createVerticalGlue());
  		progPanel.add(new JLabel(I18nManager.getText("confirm.running")));
  		JProgressBar progressBar = new JProgressBar(0, 10);
  		progressBar.setIndeterminate(true);
  		progPanel.add(progressBar);
  		progPanel.add(Box.createVerticalGlue());
  		firstCard.add(progPanel, BorderLayout.CENTER);
  		// Cancel button at bottom
  		JPanel buttonPanel = new JPanel();
  		buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
  		JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
  		cancelButton.addActionListener(e -> {
  			// Cancel model building and close dialog
  			if (_model != null) _model.cancel();
  			_dialog.dispose();
  		});
  		buttonPanel.add(cancelButton);
  		firstCard.add(buttonPanel, BorderLayout.SOUTH);
  		dialogPanel.add(firstCard, "card1");
  
  		// Make second card including tileset table
  		JPanel secondCard = new JPanel();
  		secondCard.setLayout(new BorderLayout());
  		// Table in the middle
  		JPanel midPanel = new JPanel();
  		midPanel.setLayout(new BorderLayout());
  		_setsTable = new JTable();
  		_setsTable.setPreferredScrollableViewportSize(new Dimension(500, 130));
  		_setsTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
  		midPanel.add(new JScrollPane(_setsTable), BorderLayout.CENTER);
  		midPanel.setBorder(BorderFactory.createEmptyBorder(8, 5, 8, 5));
  		secondCard.add(midPanel, BorderLayout.CENTER);
  		// Activate buttons if a tileset is selected
  		_setsTable.getSelectionModel().addListSelectionListener(e -> {
  			ListSelectionModel lsm = (ListSelectionModel) e.getSource();
  			_deleteSetButton.setEnabled(!lsm.isSelectionEmpty());
  		});
  
  		// button panel at bottom
  		buttonPanel = new JPanel();
  		// left group
  		JPanel leftPanel = new JPanel();
  		leftPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
  		_deleteSetButton = new JButton(I18nManager.getText("button.delete"));
  		_deleteSetButton.addActionListener(e -> showDeleteCard());
  		leftPanel.add(_deleteSetButton);
  		// right group
  		JPanel rightPanel = new JPanel();
  		rightPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
  		JButton closeButton = new JButton(I18nManager.getText("button.close"));
  		closeButton.addActionListener(e -> {
  			if (_dialog != null) _dialog.dispose();
  		});
  		rightPanel.add(closeButton);
  		buttonPanel.add(leftPanel, BorderLayout.WEST);
  		buttonPanel.add(rightPanel, BorderLayout.EAST);
  		secondCard.add(buttonPanel, BorderLayout.SOUTH);
  		dialogPanel.add(secondCard, "card2");
  
  		// Make third card including delete options
  		JPanel thirdCard = new JPanel();
  		thirdCard.setLayout(new BorderLayout());
  		// main panel
  		JPanel mainPanel = new JPanel();
  		GridBagLayout gridbag = new GridBagLayout();
  		GridBagConstraints c = new GridBagConstraints();
  		mainPanel.setLayout(gridbag);
  		c.gridx = 0; c.gridy = 0;
  		c.gridheight = 1; c.gridwidth = 2;
  		c.weightx = 0.0; c.weighty = 0.0;
  		c.anchor = GridBagConstraints.FIRST_LINE_START;
  		_tileSetLabel = new JLabel("dummy text to be replaced");
  		mainPanel.add(_tileSetLabel, c);
  		c.gridx = 0; c.gridy = 1;
  		c.ipady = 20;
  		_zoomLabel = new JLabel("dummy text to be replaced");
  		mainPanel.add(_zoomLabel, c);
  
  		JRadioButton deleteOldRadio = new JRadioButton(I18nManager.getText("dialog.diskcache.deleteold"));
  		_deleteAllRadio = new JRadioButton(I18nManager.getText("dialog.diskcache.deleteall"));
  		ButtonGroup bGroup = new ButtonGroup();
  		bGroup.add(_deleteAllRadio);
  		bGroup.add(deleteOldRadio);
  		_deleteAllRadio.addChangeListener(e -> enableAgeFields());
  		c.gridx = 0; c.gridy = 2;
  		c.anchor = GridBagConstraints.LINE_START;
  		c.ipady = 0;
  		mainPanel.add(deleteOldRadio, c);
  		c.gridwidth = 1;
  		c.gridx = 0; c.gridy = 3;
  		c.insets = new Insets(0, 40, 0, 0);
  		_ageLabel = new JLabel(I18nManager.getText("dialog.diskcache.maximumage"));
  		mainPanel.add(_ageLabel, c);
  		_daysField = new WholeNumberField(2);
  		_daysField.setMinimumSize(new Dimension(20, 1));
  		_daysField.setText("30"); // default is 30 days
  		c.gridx = 1; c.gridy = 3;
  		c.ipadx = 20;
  		c.insets = new Insets(0, 15, 0, 0);
  		mainPanel.add(_daysField, c);
  		c.gridx = 0; c.gridy = 4;
  		c.gridwidth = 2;
  		c.ipadx = 0;
  		c.insets = new Insets(0, 0, 0, 0);
  		mainPanel.add(_deleteAllRadio, c);
  		_deleteAllRadio.setSelected(true);
  		thirdCard.add(mainPanel, BorderLayout.CENTER);
  		// button panel
  		buttonPanel = new JPanel();
  		buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
  		JButton deleteButton = new JButton(I18nManager.getText("button.delete"));
  		deleteButton.addActionListener(e -> deleteCurrentSets());
  		buttonPanel.add(deleteButton);
  		cancelButton = new JButton(I18nManager.getText("button.cancel"));
  		cancelButton.addActionListener(e -> {
  			// Go back to second card
  			_cards.show(_cardPanel, "card2");
  		});
  		buttonPanel.add(cancelButton);
  		thirdCard.add(buttonPanel, BorderLayout.SOUTH);
  		dialogPanel.add(thirdCard, "card3");
  		return dialogPanel;
  	}
  
  
  	/**
  	 * Construct the model in a separate thread
  	 * and go on to the second panel of the dialog
  	 */
  	public void run()
  	{
  		// TODO: Maybe this can be speeded up so that it just finds the tilesets first and then gets the details later
  		// Check if directory has anything in it
  		_model = new TileCacheModel(_cacheDir);
  		_model.buildTileSets();
  		if (_model.isAborted()) return;
  
  		if (_model.getNumTileSets() <= 0)
  		{
  			_app.showErrorMessage(getNameKey(), "error.cache.empty");
  			_dialog.dispose();
  			return;
  		}
  
  		// Set controls according to current config
  		_setsTable.setModel(new TileSetTableModel(_model));
  		_deleteSetButton.setEnabled(false);
  		// Set column widths after model has been set
  		_setsTable.getColumnModel().getColumn(0).setPreferredWidth(220);
  		_setsTable.getColumnModel().getColumn(1).setPreferredWidth(200);
  		// Show second panel
  		_cards.next(_cardPanel);
  	}
  
  	/**
  	 * Prepare and show the delete panel
  	 */
  	private void showDeleteCard()
  	{
  		// set tileset label
  		int numSelected = 0;
  		String desc = null;
  		RowInfo totals = new RowInfo();
  		for (int i=0; i<_setsTable.getRowCount(); i++) {
  			if (_setsTable.isRowSelected(i))
  			{
  				if (desc == null) desc = _model.getTileSet(i).getPath();
  				totals.addRow(_model.getTileSet(i).getRowInfo());
  				numSelected++;
  			}
  		}
  		if (numSelected == 0) return;
  		String tileSetDesc = (numSelected == 1?desc:I18nManager.getText("dialog.diskcache.tileset.multiple"));
  		_tileSetLabel.setText(I18nManager.getText("dialog.diskcache.tileset") + " : "
  			+ tileSetDesc);
  		_zoomLabel.setText(I18nManager.getText("dialog.diskcache.table.zoom") + " : "
  			+ totals.getZoomRange());
  
  		// enable/disable edit fields
  		enableAgeFields();
  		// show the next card
  		_cards.next(_cardPanel);
  	}
  
  	/**
  	 * Enable or disable the fields for entering tile age
  	 */
  	private void enableAgeFields()
  	{
  		boolean showAgeBoxes = !_deleteAllRadio.isSelected();
  		_ageLabel.setEnabled(showAgeBoxes);
  		_daysField.setEnabled(showAgeBoxes);
  	}
  
  	/**
  	 * Try to delete all the files in the currently selected tilesets
  	 * (Maybe more than one tileset is selected in the table)
  	 */
  	private void deleteCurrentSets()
  	{
  		// Determine age limit if given
  		int ageLimit = -1;
  		if (!_deleteAllRadio.isSelected()) {
  			ageLimit = _daysField.getValue();
  		}
  		// Loop over selected tilesets and delete them
  		int totalDeleted = 0;
  		for (int i=0; i<_setsTable.getRowCount(); i++)
  		{
  			if (_setsTable.isRowSelected(i))
  			{
  				File dir = new File(_model.getCacheDir(), _model.getTileSet(i).getPath());
  				if (dir.exists())
  				{
  					int numFilesDeleted = deleteFilesFrom(dir, ageLimit);
  					if (numFilesDeleted > 0) {
  						totalDeleted += numFilesDeleted;
  					}
  				}
  			}
  		}
  		if (totalDeleted > 0)
  		{
  			// Show confirmation message
  			JOptionPane.showMessageDialog(_dialog, I18nManager.getTextWithNumber("dialog.diskcache.deleted", totalDeleted),
  				getName(), JOptionPane.INFORMATION_MESSAGE);
  			// reload model
  			_cards.first(_cardPanel);
  			new Thread(this).start();
  		}
  		else {
  			_app.showErrorMessage(getNameKey(), "error.cache.cannotdelete");
  		}
  	}
  
  
  	/**
  	 * Delete recursively all files which are older than the age limit
  	 * @param inDir directory to delete from
  	 * @param inMaxDays age limit in days
  	 * @return number of files deleted
  	 */
  	private static int deleteFilesFrom(File inDir, int inMaxDays)
  	{
  		int numDeleted = 0;
  		long now = System.currentTimeMillis();
  		if (inDir.exists() && inDir.isDirectory())
  		{
  			for (File subdir : FileList.filesIn(inDir))
  			{
  				if (subdir.isDirectory()) {
  					numDeleted += deleteFilesFrom(subdir, inMaxDays);
  				}
  				else if (subdir.isFile() && subdir.exists() && _TILEFILTER.accept(subdir))
  				{
  					long fileAge = (now - subdir.lastModified()) / 1000 / 60 / 60 / 24;
  					if (inMaxDays < 0 || fileAge > inMaxDays)
  					{
  						if (subdir.delete()) {
  							numDeleted++;
  						}
  					}
  				}
  			}
  			// Try to delete the directory (doesn't work if not empty)
  			inDir.delete();
  		}
  		return numDeleted;
  	}
  }
  