/**
 * GUI Commands
 * Copyright 2004 Andrew Pietsch
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * $Id: AbstractRecentFileGroup.java,v 1.13 2007/01/11 08:28:40 pietschy Exp $
 */
package org.pietschy.command.file;

import org.pietschy.command.ActionCommand;
import org.pietschy.command.CommandGroup;
import org.pietschy.command.CommandManager;
import org.pietschy.command.Face;

import javax.swing.*;
import java.awt.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;

/**
 * Provides a custom group implementation that manages a list of recently used files.  Subclasses
 * must override {@link #openFile} and implement the required behaviour.
 * <p/>
 * The group also includes a command for clearing the list.  The text for the clear command is
 * configured from the <tt>clear-text</tt> property of the group.  If the property isn't defined, the
 * value <tt>Clear</tt> will be used.  The clear command is hidden by default, to make it visible
 * simply call {@link #getClearCommand getClearCommand()}.setVisible(true);
 * <p>
 * The methods {@link #store(java.util.prefs.Preferences)} and {@link #load(java.util.prefs.Preferences)} are
 * also provided to allow easy persistence of the groups file list.
 *
 * @author andrewp
 * @version $Revision: 1.13 $
 */
public abstract class
AbstractRecentFileGroup
extends CommandGroup
{
   private static final String _ID_ = "$Id: AbstractRecentFileGroup.java,v 1.13 2007/01/11 08:28:40 pietschy Exp $";

   private static final String FILE_NAME = "file";
   private static final String PREFS_KEY_SIZE = "size";

   private ClearCommand clearCommand;

   private ArrayList commands = new ArrayList();
   private int displaySize = 9;

   private boolean includeNonExistentFiles = false;

   private ArrayList excludedFiles = new ArrayList();
   private boolean accelerated = true;
   private int acceleratorModifiers = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();


   /**
    * Creates a new anonymous group.
    *
    * @param commandManager the {@link CommandManager} to which the group will belong.
    */
   public AbstractRecentFileGroup(CommandManager commandManager)
   {
      super(commandManager);
      init();
   }

   /**
    * Constructs a new group with the specified Id that is bound to the default command manager.
    *
    * @param id the id of the group.
    * @see CommandManager#defaultInstance()
    */
   protected AbstractRecentFileGroup(String id)
   {
      super(id);
      init();
   }

   /**
    * Creates a new group with the specified id.
    *
    * @param id             the commands id.
    * @param commandManager the {@link CommandManager} to which the group will belong.
    */
   public AbstractRecentFileGroup(CommandManager commandManager, String id)
   {
      super(commandManager, id);
      init();
   }

   private void
   init()
   {
      setEnabled(false);
      clearCommand = new ClearCommand();
      clearCommand.getDefaultFace(true).setText(getProperty("clear-text", "Clear"));
      clearCommand.setVisible(false);
      clearCommand.addPropertyChangeListener("visible", new PropertyChangeListener()
      {
         public void propertyChange(PropertyChangeEvent evt)
         {
            rebuildGroupContent();
         }
      });
   }

   /**
    * Gets the ActionCommand that clears the files from the group.
    *
    * @return the ActionCommand that clears the files from the group.
    */
   public ActionCommand
   getClearCommand()
   {
      return clearCommand;
   }

   /**
    * Gets the number of files the group will display at any one time.
    *
    * @return the number of files the group will display at any one time
    */
   public int
   getDisplaySize()
   {
      return displaySize;
   }

   /**
    * Configures the number of files to display in the group.
    *
    * @param displaySize the number of files to display in the group.
    */
   public void
   setDisplaySize(int displaySize)
   {
      this.displaySize = displaySize;
   }

   /**
    * Checks if the group should include files for which {@link java.io.File#exists()} returns
    * <tt>false</tt>.  The default value is <tt>false</tt>.
    *
    * @return <tt>true</tt> if the group is including non existant files, <tt>false</tt> otherwise.
    */
   public boolean
   isIncludeNonExistentFiles()
   {
      return includeNonExistentFiles;
   }

   /**
    * Configures if the group should include files for which {@link java.io.File#exists()} returns
    * <tt>false</tt>.  The default value is <tt>false</tt>.
    *
    * @param includeNonExistentFiles <tt>true</tt> if the group is to include non existant files,
    *                                <tt>false</tt> otherwise.
    */
   public void
   setIncludeNonExistentFiles(boolean includeNonExistentFiles)
   {
      this.includeNonExistentFiles = includeNonExistentFiles;
      rebuildGroupContent();
   }


   /**
    * Gets the file that is currently excluded from the list or <tt>null</tt> if all the
    * files are included.
    *
    * @return the file that is currently excluded from the list or <tt>null</tt> if all the
    *         files are included.
    */
   public File[]
   getExcludedFile()
   {
      return (File[]) excludedFiles.toArray(new File[excludedFiles.size()]);
   }

   /**
    * Sets the file to exclude from the list.  This typically used to exclude the file currently
    * being edited by the application.  If set to <tt>null</tt>, all files will be included in the
    * list.
    *
    * @param excludedFile the file to exclude from the list, or <tt>null</tt>.
    */
   public void
   setExcludedFile(File excludedFile)
   {
      excludedFiles.clear();
      excludedFiles.add(excludedFile);
      rebuildGroupContent();
   }

   /**
    * Sets the file to exclude from the list.  This typically used to exclude the file currently
    * being edited by the application.  If set to <tt>null</tt>, all files will be included in the
    * list.
    *
    * @param excludedFiles the file to exclude from the list, or <tt>null</tt>.
    */
   public void
   setExcludedFiles(File[] excludedFiles)
   {
      this.excludedFiles.clear();
      this.excludedFiles.addAll(Arrays.asList(excludedFiles));
      rebuildGroupContent();
   }

   /**
    * Checks if the list will have accelerator for each entry.
    *
    * @return <tt>true</tt> if the list will have accelerator for each entry, <tt>false</tt>
    *         otherwise.
    */
   public boolean
   isAccelerated()
   {
      return accelerated;
   }

   /**
    * Configures the group to installFace accelerators for the list.
    *
    * @param accelerated <tt>true</tt> to display accelerators, <tt>false</tt> to ommit them.
    * @see #setAcceleratorModifiers(int)
    */
   public void
   setAccelerated(boolean accelerated)
   {
      this.accelerated = accelerated;
      rebuildGroupContent();
   }

   /**
    * Gets the modifiers that are used to create the accelerators.  This defaults to
    * {@link java.awt.Toolkit#getMenuShortcutKeyMask()}.
    *
    * @return the modifiers that are used to create the accelerators.
    */
   public int
   getAcceleratorModifiers()
   {
      return acceleratorModifiers;
   }

    /**
    * Sets the modifiers that are used to create the accelerators.  This defaults to
    * {@link java.awt.Toolkit#getMenuShortcutKeyMask()}.
    *
    * @param acceleratorModifiers the modifiers that are used to create the accelerators.  
    */
   public void
   setAcceleratorModifiers(int acceleratorModifiers)
   {
      this.acceleratorModifiers = acceleratorModifiers;
      rebuildGroupContent();
   }


   /**
    * Adds a file to the list.  The file will be added to the start of the list (in the most recent
    * position).  If the file is already in the list it will be moved to the start.
    *
    * @param file the file to add to the list.
    */
   public void
   add(File file)
   {
      if (file == null)
      {
         throw new NullPointerException("file is null");
      }

      addImpl(file, true);
   }

   /**
    * Adds all the specified files to the list.  The files are added to the start of the list
    * such that file[0] will be the first entry and files[files.length - 1] will proceed any
    * existing files.  If any file is already in the list it will be moved to the new position.
    *
    * @param files the files to add to the list.
    */
   public void
   addAll(File[] files)
   {
      if (files == null)
      {
         throw new NullPointerException("files is null");
      }

      for (int i = files.length - 1; i >= 0; i--)
      {
         addImpl(files[i], i == 0);
      }
   }

   private void
   addImpl(File file, boolean rebuild)
   {
      if (file == null)
      {
         throw new NullPointerException("file is null");
      }

      setEnabled(true);
      OpenCommand command = getCommandFor(file);

      if (command != null)
         commands.remove(command);
      else
         command = new OpenCommand(file);

      commands.add(command);


      if (rebuild)
         rebuildGroupContent();
   }

   /**
    * Gets all files in the group.  The number of files may exceed the number displayed by
    * the group.
    *
    * @return all the files in the group.
    * @see #setDisplaySize
    */
   public File[]
   getFiles()
   {
      File[] files = new File[commands.size()];
      int size = commands.size();
      for (int i = 0; i < size; i++)
      {
         files[i] = ((OpenCommand) commands.get(i)).getFile();
      }

      return files;
   }

   /**
    * Gets the command that opens the specified file.
    *
    * @param file the file of interest.
    * @return the command that will open the specified file, or <tt>null</tt> if the file isn't
    *         in the list.
    */
   protected OpenCommand
   getCommandFor(File file)
   {
      for (Iterator iter = commands.iterator(); iter.hasNext();)
      {
         OpenCommand cmd = (OpenCommand) iter.next();
         if (file.equals(cmd.getFile()))
            return cmd;
      }

      return null;
   }

   /**
    * Called to open the selected file.  Subclasses must implement this method to provide
    * the file open behaviour.
    *
    * @param file the file to be opened.
    */
   public abstract void
   openFile(File file);


   /**
    * Stores the groups file list to the specified preference node.
    *
    * @param prefs the preference node in which to save the groups file list.
    * @throws BackingStoreException if the store operation fails.
    * @see #load(java.util.prefs.Preferences)
    */
   public void
   store(Preferences prefs)
   throws BackingStoreException
   {
      File[] files = getFiles();
      prefs.clear();
      prefs.putInt(PREFS_KEY_SIZE, files.length);
      for (int i = 0; i < files.length; i++)
         putFile(prefs, i, files[i]);

      prefs.flush();
   }

   /**
    * Loads the groups file list from the specified preference node.
    *
    * @param prefs the preference node in which to retrieve the groups file list.
    * @see #store(java.util.prefs.Preferences)
    */
   public void
   load(Preferences prefs)
   {
      int size = prefs.getInt(PREFS_KEY_SIZE, 0);
      ArrayList files = new ArrayList();
      for (int i = size - 1; i >= 0; i--)
      {
         File f = getFile(prefs, i);
         if (f != null)
            files.add(f);
      }

      addAll((File[]) files.toArray(new File[files.size()]));
   }

   private void
   putFile(Preferences prefs, int index, File file)
   {
      prefs.put(FILE_NAME + "." + index, file.getAbsolutePath());
   }

   private File
   getFile(Preferences prefs, int index)
   {
      String name = prefs.get(FILE_NAME + "." + index, null);
      return name != null ? new File(name) : null;
   }

   public void refresh()
   {
      rebuildGroupContent();
   }

   private void
   rebuildGroupContent()
   {
      reset();

      OpenCommand[] openCommands = (OpenCommand[]) commands.toArray(new OpenCommand[commands.size()]);
      int added = 0;

      for (int i = openCommands.length - 1; i >= 0; i--)
      {
         if (added < displaySize && (openCommands[i].getFile().exists() || includeNonExistentFiles))
         {
            OpenCommand command = (OpenCommand) openCommands[i];
            if (!excludedFiles.contains(command.getFile()))
            {
               installAccelerator(command.getDefaultFace(), added);
               add(command, false);
               added++;
            }
         }
         else
         {
            openCommands[i].getDefaultFace().setAccelerator(null);
         }
      }

      if (clearCommand.isVisible())
      {
         addSeparator(false);
         add(clearCommand, false);
      }

      rebuildAllPopups();

      setEnabled(added > 0);
   }

   void
   installAccelerator(Face face, int index)
   {
      face.setAccelerator(accelerated ? getAcceleratorForIndex(index) : null);
   }

   protected KeyStroke
   getAcceleratorForIndex(int index)
   {
      int keyStrokeNumber = index + 1;

      if (keyStrokeNumber > 9)
      {
         return null;
      }

      int keycode = KeyStroke.getKeyStroke(Integer.toString(keyStrokeNumber)).getKeyCode();
      return KeyStroke.getKeyStroke(keycode, acceleratorModifiers);
   }

   private class OpenCommand
   extends ActionCommand
   {
      private File file;

      public OpenCommand(File file)
      {
         this.file = file;
         Face face = getDefaultFace(true);
         face.setText(file.getAbsolutePath());
      }

      protected void
      handleExecute()
      {
         openFile(file);
      }

      public File
      getFile()
      {
         return file;
      }
   }

   private class ClearCommand
   extends ActionCommand
   {
      public ClearCommand()
      {
      }

      protected void handleExecute()
      {
         commands.clear();
         rebuildGroupContent();
         AbstractRecentFileGroup.this.setEnabled(false);
      }
   }

}
