/*
    SubprojectsManager.m

    Implementation of the SubprojectsManager class for the
    ProjectManager application.

    Copyright (C) 2005, 2006  Saso Kiselkov

    This program 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 2 of the License, or
    (at your option) any later version.

    This program 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 this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#import "SubprojectsManager.h"

#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSError.h>
#import <Foundation/NSFileManager.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSSortDescriptor.h>

#import <AppKit/NSDocumentController.h>
#import <AppKit/NSNibLoading.h>
#import <AppKit/NSOpenPanel.h>
#import <AppKit/NSPanel.h>
#import <AppKit/NSTableColumn.h>
#import <AppKit/NSOutlineView.h>
#import <AppKit/NSToolbarItem.h>
#import <AppKit/NSView.h>
#import <AppKit/NSWindow.h>

#import "../../NSImageAdditions.h"
#import "../../ProjectDocument.h"
#import "../../ProjectCreator.h"
#import "../../ProjectType.h"
#import "../../NSArrayAdditions.h"

#import "SubprojectsManagerDelegate.h"

static void
PrintStructure(NSArray * contents, int level)
{
  NSString * prefix = @"";
  int i;
  NSEnumerator * e;
  NSDictionary * entry;

  for (i = 0; i < level; i++)
    {
      prefix = [prefix stringByAppendingString: @" "];
    }

  e = [contents objectEnumerator];
  while ((entry = [e nextObject]) != nil)
    {
      NSLog(@"%@%@", prefix, [entry objectForKey: @"Name"]);
      if ([[entry objectForKey: @"Type"] isEqualToString: @"Category"])
        {
          PrintStructure([entry objectForKey: @"Contents"], level + 1);
        }
    }
}

static void
SortContentsArrayByName(NSMutableArray * contentsArray)
{
  NSSortDescriptor * sortDescriptor;

  sortDescriptor = [[[NSSortDescriptor alloc]
    initWithKey: @"Name"
      ascending: YES
       selector: @selector(caseInsensitiveCompare:)]
    autorelease];

  [contentsArray sortUsingDescriptors: [NSArray arrayWithObject:
    sortDescriptor]];
}

@interface SubprojectsManager (Private)

- (BOOL) validateControl: control;

- (NSMutableArray *) subprojectNamesInArray: (NSArray *) array;

- (NSString *) pathToSubprojectFile: (NSDictionary *) subprojectDescription;

- (NSMutableArray *) currentCategoryContentsArray;
- (NSMutableArray *) parentCategoryContentsArray;

- (NSMutableArray *) internalSubprojectNames;

- (BOOL) wipeSubprojectsFromCategory: (NSMutableArray *) contentsArray;

@end

@implementation SubprojectsManager (Private)

- (BOOL) validateControl: control
{
  SEL action;

  // don't allow our controls when we're not visible
  if ([document currentProjectModule] != self)
    {
      return NO;
    }

  action = [control action];

  if (sel_eq(action, @selector(removeSubprojectAction:)) ||
      sel_eq(action, @selector(openSubprojectAction:)))
    {
      int row = [outline selectedRow];

      // we have to make sure a real row is selected (by doing [outline
      // numberOfRows] > [outline selectedRow]) because of a bug in GNUstep
      // which incorrectly handles selection in outline views
      if (row >= 0 && [outline numberOfRows] > row)
        {
          return [[[outline itemAtRow: row]
            objectForKey: @"Type"] isEqualToString: @"Subproject"];
        }
      else
        {
          return NO;
        }
    }
  else if (sel_eq(action, @selector(removeSubprojectCategoryAction:)))
    {
      int row = [outline selectedRow];

      if (row >= 0 && [outline numberOfRows] > row)
        {
          return [[[outline itemAtRow: row]
            objectForKey: @"Type"] isEqualToString: @"Category"];
        }
      else
        {
          return NO;
        }
    }

  return YES;
}

/** 
 * Recursively lists all project names under the specified
 * category contents array.
 *
 * @param array An array of subproject and subcategory descriptions,
 *      as contained in the `subprojects' ivar.
 *
 * @return An array of subproject names under the given array.
 */
- (NSMutableArray *) subprojectNamesInArray: (NSArray *) array
{
  NSMutableArray * names = [NSMutableArray array];
  NSEnumerator * e = [array objectEnumerator];
  NSDictionary * entry;

  while ((entry = [e nextObject]) != nil)
    {
      if ([[entry objectForKey: @"Type"] isEqualToString: @"Subproject"])
        {
          [names addObject: [entry objectForKey: @"Name"]];
        }
      else
        {
          [names addObjectsFromArray: [self subprojectNamesInArray:
            [entry objectForKey: @"Contents"]]];
        }
    }

  return names;
}

/**
 * Returns a path to the subproject file of the subproject described
 * by the argument dictionary.
 *
 * @param subprojectDescription A dictionary describing the subproject,
 *      formatted like the contents of the `subprojects' ivar.
 *
 * @return A path to the subproject's project file.
 */
- (NSString *) pathToSubprojectFile: (NSDictionary *) subprojectDescription
{
  return [[[delegate pathToSubprojectsDirectory]
    stringByAppendingPathComponent: [subprojectDescription
    objectForKey: @"Name"]] stringByAppendingPathComponent:
    [subprojectDescription objectForKey: @"ProjectFile"]];
}

/**
 * Returns the category contents array of the category which contains
 * the currently selected item, or the item's contents array if the
 * item selected is a category itself.
 */
- (NSMutableArray *) currentCategoryContentsArray
{
  int row = [outline selectedRow];

  if (row >= 0 && [outline numberOfRows] > row)
    {
      NSDictionary * selectedEntry = [outline itemAtRow: row];

      // if the selected entry is a category itself, use that
      if ([[selectedEntry objectForKey: @"Type"] isEqualToString: @"Category"])
        {
          return [selectedEntry objectForKey: @"Contents"];
        }

      // otherwise look for the superentry
      for (row -= 1; row >= 0; row--)
        {
          NSDictionary * entry = [outline itemAtRow: row];

          if ([[entry objectForKey: @"Contents"] containsObject:
            selectedEntry])
            {
              return [entry objectForKey: @"Contents"];
            }
        }
    }

  return subprojects;
}

/**
 * Returns the category contents array of the category which contains
 * the currently selected item, but never the current item, even if it
 * is a category.
 */
- (NSMutableArray *) parentCategoryContentsArray
{
  int row = [outline selectedRow];

  if (row >= 0 && [outline numberOfRows] > row)
    {
      NSDictionary * selectedEntry = [outline itemAtRow: row];

      for (row -= 1; row >= 0; row--)
        {
          NSDictionary * entry = [outline itemAtRow: row];

          if ([[entry objectForKey: @"Contents"] containsObject:
            selectedEntry])
            {
              return [entry objectForKey: @"Contents"];
            }
        }
    }

  return subprojects;
}

/**
 * Returns a mutable array of subproject names. This method is
 * here for internal purposes, when we need a mutable version of
 * the array, instead of an immutable one.
 */
- (NSMutableArray *) internalSubprojectNames
{
  return [self subprojectNamesInArray: subprojects];
}

/**
 * Deletes the subprojects contained in a category's contents array
 * (and subcategories recursively as well) from disk.
 *
 * @param contentsArray The contents array of the top-level category
 *      which to wipe of subprojects.
 *
 * @return YES if the operation succeeds, NO if it doesn't.
 */
- (BOOL) wipeSubprojectsFromCategory: (NSMutableArray *) contentsArray
{
  NSFileManager * fm = [NSFileManager defaultManager];
  int i, n;

  for (i = 0, n = [contentsArray count]; i < n; i++)
    {
      NSDictionary * entry = [contentsArray objectAtIndex: 0];
      NSMutableArray * subcontentsArray = [entry objectForKey: @"Contents"];

      if (subcontentsArray != nil)
        {
          if (![self wipeSubprojectsFromCategory: subcontentsArray])
            {
              return NO;
            }
        }
      else
        {
          NSString * subprojectDirectoryPath = [[self pathToSubprojectFile:
            entry] stringByDeletingLastPathComponent];

          if (![fm removeFileAtPath: subprojectDirectoryPath
                            handler: self])
            {
              NSRunAlertPanel(_(@"Error deleting subproject"),
                _(@"Couldn't delete path %@: %@"),
                nil, nil, nil, [fileOpErrorDict objectForKey: @"Path"],
                [fileOpErrorDict objectForKey: @"Error"]);

              return NO;
            }
        }

      [contentsArray removeObjectAtIndex: 0];
      [document updateChangeCount: NSChangeDone];
    }

  return YES;
}

@end

/**
 * This class is a manager of subprojects for a project.
 *
 * It simply identifies subprojects by name and path where they
 * live and handles adding, removing and opening them. It also
 * provides the user with the possibility to organize subprojects
 * into "subproject categories".
 *
 * It's delegate must conform to the SubprojectsManagerDelegate
 * protocol, in order to tell it where to put the subprojects
 * it manages.
 */
@implementation SubprojectsManager

+ (NSString *) moduleName
{
  return @"SubprojectsManager";
}

+ (NSString *) humanReadableModuleName
{
  return _(@"Subprojects");
}

- (NSArray *) moduleMenuItems
{
  return [NSArray arrayWithObjects:
    PMMakeMenuItem (_(@"Open Subproject"), @selector(openSubprojectAction:),
      nil, self),
    PMMakeMenuItem (_(@"New Subproject..."), @selector(addSubprojectAction:),
      nil, self),
    PMMakeMenuItem (_(@"Add Subproject..."), @selector(addSubprojectAction:),
      nil, self),
    PMMakeMenuItem (_(@"Remove Subproject..."),
      @selector(removeSubprojectAction:), nil, self),
    PMMakeMenuItem (_(@"New Subproject Category..."),
      @selector(newSubprojectCategoryAction:), nil, self),
    PMMakeMenuItem (_(@"Remove Subproject Category..."),
      @selector(removeSubprojectCategoryAction:), nil, self),
    nil];
}

- (NSArray *) toolbarItemIdentifiers
{
  return [NSArray arrayWithObjects:
    @"SubprojectsManagerNewSubprojectItemIdentifier",
    @"SubprojectsManagerAddSubprojectItemIdentifier",
    @"SubprojectsManagerRemoveSubprojectItemIdentifier",
    @"SubprojectsManagerNewCategoryItemIdentifier",
    @"SubprojectsManagerRemoveCategoryItemIdentifier",
    nil];
}

- (NSToolbarItem *) toolbarItemForItemIdentifier: (NSString *) itemIdentifier
{
  NSToolbarItem * item = [[[NSToolbarItem alloc]
    initWithItemIdentifier: itemIdentifier]
    autorelease];

  [item setTarget: self];

  if ([itemIdentifier isEqualToString:
    @"SubprojectsManagerNewSubprojectItemIdentifier"])
    {
      [item setLabel: _(@"New")];
      [item setImage: [NSImage imageNamed: @"NewSubproject" owner: self]];
      [item setAction: @selector(newSubprojectAction:)];
      [item setToolTip: _(@"Creates a new subproject")];
    }
  else if ([itemIdentifier isEqualToString:
    @"SubprojectsManagerAddSubprojectItemIdentifier"])
    {
      [item setLabel: _(@"Add")];
      [item setImage: [NSImage imageNamed: @"AddSubproject" owner: self]];
      [item setAction: @selector(addSubprojectAction:)];
      [item setToolTip: _(@"Adds a subproject to the project")];
    }
  else if ([itemIdentifier isEqualToString:
    @"SubprojectsManagerRemoveSubprojectItemIdentifier"])
    {
      [item setLabel: _(@"Remove")];
      [item setImage: [NSImage imageNamed: @"RemoveSubproject" owner: self]];
      [item setAction: @selector(removeSubprojectAction:)];
      [item setToolTip: _(@"Removes a subproject from the project")];
    }
  else if ([itemIdentifier isEqualToString:
    @"SubprojectsManagerNewCategoryItemIdentifier"])
    {
      [item setLabel: _(@"New Category")];
      [item setImage: [NSImage imageNamed: @"NewSubprojectCategory"
                                    owner: self]];
      [item setAction: @selector(newSubprojectCategoryAction:)];
      [item setToolTip: _(@"Adds a subprojects category")];
    }
  else if ([itemIdentifier isEqualToString:
    @"SubprojectsManagerRemoveCategoryItemIdentifier"])
    {
      [item setLabel: _(@"Remove Category")];
      [item setImage: [NSImage imageNamed: @"RemoveSubprojectCategory"
                                    owner: self]];
      [item setAction: @selector(removeSubprojectCategoryAction:)];
      [item setToolTip: _(@"Removes a subprojects category")];
    }

  return item;
}

- (void) dealloc
{
  [[NSNotificationCenter defaultCenter] removeObserver: self];

  TEST_RELEASE(view);
  TEST_RELEASE(subprojects);
  TEST_RELEASE(subprojectNames);

  TEST_RELEASE(fileOpErrorDict);

  [super dealloc];
}

- initWithDocument: (ProjectDocument *) aDocument
    infoDictionary: (NSDictionary *) infoDict
{
  if ((self = [self init]) != nil)
    {
      document = aDocument;

      // the values in this dictionary aren't full paths to the subprojects
      // yet - we'll need to change these in -setDelegate: where we can
      // ask the delegate about the subproject directory path
      ASSIGN(subprojects, [[infoDict objectForKey: @"Subprojects"]
        makeDeeplyMutableEquivalent]);
      if (subprojects == nil)
        {
          subprojects = [NSMutableArray new];
        }

      // regenerate the subproject names
      ASSIGN(subprojectNames, [self internalSubprojectNames]);
    }

  return self;
}

- (ProjectDocument *) document
{
  return document;
}

- (NSView *) view
{
  if (view == nil)
    {
      [NSBundle loadNibNamed: @"SubprojectsManager" owner: self];
    }

  return view;
}

- (NSDictionary *) infoDictionary
{
  return [NSDictionary dictionaryWithObject: subprojects
                                     forKey: @"Subprojects"];
}

- (BOOL) regenerateDerivedFiles
{
  return YES;
}

- (BOOL) validateMenuItem: (id <NSMenuItem>) item
{
  return [self validateControl: item];
}

- (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
{
  return [self validateControl: toolbarItem];
}

- (void) awakeFromNib
{
  view = [[bogusWindow contentView] retain];
  DESTROY(bogusWindow);

  [outline setDoubleAction: @selector(openSubprojectAction:)];
}

/**
 * Sets the delegate of the receiver. This method may be invoked
 * only once!
 *
 * @param aDelegate An object which must conform to the
 *      SubprojectsManagerDelegate protocol.
 */
- (void) finishInit
{
  delegate = (id <SubprojectsManagerDelegate>) [document projectType];
}

/**
 * Returns absolute paths to all subprojects.
 */
- (NSArray *) subprojectNames
{
  return [self internalSubprojectNames];
}

- (BOOL) addSubproject: (NSString *) aProject
            toCategory: (NSMutableArray *) contentsArray
{
  NSString * projectFilePath;
  NSString * sourceDirectory = [aProject stringByDeletingLastPathComponent];
  NSString * destinationDirectory;
  NSString * subprojectName = [[aProject lastPathComponent]
    stringByDeletingPathExtension];
  ProjectDocument * doc;

  if ([subprojectNames containsObject: subprojectName])
    {
      NSRunAlertPanel(_(@"Subproject already exists"),
        _(@"The already is a subproject named %@."),
        nil, nil, nil, subprojectName);

      return NO;
    }

  destinationDirectory = [[delegate pathToSubprojectsDirectory]
    stringByAppendingPathComponent: [sourceDirectory lastPathComponent]];

  // do we have to copy the subproject into the project?
  if (![destinationDirectory isEqualToString: sourceDirectory])
    {
      NSFileManager * fm = [NSFileManager defaultManager];
      NSError * error;

      // make sure we won't overwrite something
      if ([fm fileExistsAtPath: destinationDirectory])
        {
          NSRunAlertPanel(_(@"Failed to add subproject"),
            _(@"A file at %@ is in the way - remove it first."),
            nil, nil, nil, destinationDirectory);

          return NO;
        }

      // create the intermediate directories
      if (!CreateDirectoryAndIntermediateDirectories([destinationDirectory
        stringByDeletingLastPathComponent], &error))
        {
          NSRunAlertPanel(_(@"Failed to add subproject"),
            _(@"Couldn't copy %@ to the project's subproject directory.\n%@."),
            nil, nil, nil, sourceDirectory, [[error userInfo]
            objectForKey: NSLocalizedDescriptionKey]);

          return NO;
        }

      // and copy the thing in
      if (![fm copyPath: sourceDirectory
                 toPath: destinationDirectory
                handler: self])
        {
          NSRunAlertPanel(_(@"Failed to add subproject"),
            _(@"Couldn't copy %@ to the project's subproject directory\n"
              @"in %@. Error while processing path %@: %@"),
            nil, nil, nil, [fileOpErrorDict objectForKey: @"FromPath"],
            [fileOpErrorDict objectForKey: @"ToPath"], [fileOpErrorDict
            objectForKey: @"Path"], [fileOpErrorDict objectForKey:
            @"Error"]);

          return NO;
        }
    }

  // open the subproject's document invisibly, save it and close it again,
  // in order to make the project regenerate it's derived files
  projectFilePath = [destinationDirectory stringByAppendingPathComponent:
    [aProject lastPathComponent]];
  doc = [[NSDocumentController sharedDocumentController]
    makeDocumentWithContentsOfFile: projectFilePath ofType: @"pmproj"];
  if (doc == nil)
    {
      NSRunAlertPanel(_(@"Failed to add subproject"),
        _(@"Couldn't open the subproject - perhaps it is corrupt?"),
        nil, nil, nil);

      return NO;
    }
  [doc saveDocument: nil];
  [doc close];

  // add the entry
  [contentsArray addObject: [NSMutableDictionary dictionaryWithObjectsAndKeys:
    @"Subproject", @"Type",
    subprojectName, @"Name",
    [aProject lastPathComponent], @"ProjectFile",
    nil]];

  // and sort the array properly
  SortContentsArrayByName(contentsArray);

  // these don't need to be sorted
  [subprojectNames addObject: subprojectName];

  [document updateChangeCount: NSChangeDone];

  PrintStructure(subprojects, 0);
  NSLog(@"");

  [outline reloadData];

  return YES;
}

- (BOOL) removeSubproject: (NSString *) subprojectName
             fromCategory: (NSMutableArray *) contentsArray
                   delete: (BOOL) deleteFlag
{
  NSEnumerator * e;
  NSDictionary * entry;
  NSString * subprojectDirectory,
           * subprojectFileName;

  // first locate the subproject's entry
  e = [contentsArray objectEnumerator];
  while ((entry = [e nextObject]) != nil)
    {
      if ([[entry objectForKey: @"Name"] isEqualToString: subprojectName] &&
        [[entry objectForKey: @"Type"] isEqualToString: @"Subproject"])
        {
          break;
        }
    }
  NSAssert(entry != nil, @"Couldn't find associated subproject entry.");

  subprojectFileName = [self pathToSubprojectFile: entry];
  subprojectDirectory = [subprojectFileName stringByDeletingLastPathComponent];

  if (deleteFlag)
    {
      NSFileManager * fm = [NSFileManager defaultManager];
      ProjectDocument * doc;

      // close an open document
      doc = [[NSDocumentController sharedDocumentController]
        documentForFileName: subprojectFileName];
      if (doc != nil)
        {
          [doc close];
        }

      if (![fm removeFileAtPath: subprojectDirectory
                        handler: self])
        {
          NSRunAlertPanel(_(@"Cannot remove subproject"),
            _(@"Couldn't delete the subproject's files from disk\n"
              @"at %@. Error while processing path: %@: %@"),
            nil, nil, nil, [fileOpErrorDict objectForKey: @"FromPath"],
            [fileOpErrorDict objectForKey: @"Path"], [fileOpErrorDict
            objectForKey: @"Error"]);

          return NO;
        }
    }

  [subprojectNames removeObject: subprojectName];
  [contentsArray removeObject: entry];
  [document updateChangeCount: NSChangeDone];
  [outline reloadItem: nil reloadChildren: YES];

  return YES;
}

- (void) newSubprojectAction: sender
{
  NSDictionary * projectSetup;

  projectSetup = [[ProjectCreator shared] getNewProjectSetupWithLocation: NO];
  if (projectSetup != nil)
    {
      NSString * projectName = [projectSetup objectForKey: @"ProjectName"];
      NSString * template = [projectSetup objectForKey: @"ProjectTemplate"];
      NSString * projectPath = [[delegate pathToSubprojectsDirectory]
        stringByAppendingPathComponent: projectName];
      NSError * error;

      if (![ProjectCreator createNewProjectAtPath: projectPath
                                      projectName: projectName
                                     fromTemplate: template
                                            error: &error])
        {
          NSRunAlertPanel(_(@"Error creating subproject"),
            _(@"Unable to create subproject at %@: %@"),
            nil, nil, nil, projectPath, [[error userInfo]
            objectForKey: NSLocalizedDescriptionKey]);
        }
      else
        {
          [self addSubproject: [projectPath stringByAppendingPathComponent:
            [projectName stringByAppendingPathExtension: @"pmproj"]]
                   toCategory: [self currentCategoryContentsArray]];
        }
    }
}

- (void) addSubprojectAction: sender
{
  NSOpenPanel * op = [NSOpenPanel openPanel];

  [op setCanChooseDirectories: NO];
  [op setCanChooseFiles: YES];
  [op setAllowsMultipleSelection: NO];
  if ([op runModalForTypes: [NSArray arrayWithObject: @"pmproj"]] ==
    NSOKButton)
    {
      [self addSubproject: [op filename]
               toCategory: [self currentCategoryContentsArray]];
    }
}

- (void) removeSubprojectAction: sender
{
  int row = [outline selectedRow];

  if (row >= 0 && [outline numberOfRows] > row)
    {
      NSString * subprojectName = [[outline itemAtRow: row]
        objectForKey: @"Name"];
      BOOL delete = NO;

      switch (NSRunAlertPanel(_(@"Really remove subproject?"),
        _(@"Are you really sure you want to remove %@ from the subprojects?"),
        _(@"Yes, but keep files on disk"),
        _(@"Yes and DELETE it from disk"),
        _(@"Cancel"), subprojectName))
        {
        case NSAlertAlternateReturn:
          delete = YES;
          break;
        case NSAlertOtherReturn:
          return;
        }

      [self removeSubproject: subprojectName
                fromCategory: [self currentCategoryContentsArray]
                      delete: delete];
    }
}

- (void) openSubprojectAction: sender
{
  int row = [outline selectedRow];

  if (row >= 0 && [outline numberOfRows] > row)
    {
      NSDictionary * selectedSubproject = [outline itemAtRow: row];
      NSString * subprojectName = [selectedSubproject objectForKey: @"Name"];
      NSString * path = [self pathToSubprojectFile: selectedSubproject];
      ProjectDocument * doc;
      NSDocumentController * dc = [NSDocumentController
        sharedDocumentController];

      doc = [dc openDocumentWithContentsOfFile: path display: YES];

      if (doc != nil)
        {
          [doc showWindows];
        }
      else
        {
          NSRunAlertPanel(_(@"Failed to open subproject"),
            _(@"Couldn't open subproject %@"),
            nil, nil, nil, subprojectName);
        }
    }
}

- (void) newSubprojectCategoryAction: sender
{
  NSMutableArray * parentContentsArray = [self
    currentCategoryContentsArray];
  NSArray * names = [parentContentsArray valueForKey: @"Name"];
  NSString * newName = _(@"New Category");
  int i;

  // try to locate a unique name
  for (i = 1; [names containsObject: newName]; i++)
    {
      newName = [NSString stringWithFormat: _(@"New Category %i"), i];
    }

  [parentContentsArray addObject: [NSMutableDictionary
    dictionaryWithObjectsAndKeys:
    @"Category", @"Type",
    newName, @"Name",
    [NSMutableArray array], @"Contents",
    nil]];

  SortContentsArrayByName(parentContentsArray);

  PrintStructure(subprojects, 0);
  NSLog(@"");

  [outline reloadData];
  [document updateChangeCount: NSChangeDone];
}

- (void) removeSubprojectCategoryAction: sender
{
  NSDictionary * selectedCategory = [outline itemAtRow: [outline
    selectedRow]];
  NSMutableArray * contentsArray = [selectedCategory objectForKey:
    @"Contents"];
  NSMutableArray * parentContentsArray = [self
    parentCategoryContentsArray];

  if ([contentsArray count] > 0)
    {
      switch (NSRunAlertPanel(_(@"Really remove category?"),
        _(@"Are you sure that you want to remove category %@?\n"
          @"Also, do you want me to keep any of it's subprojects\n"
          @"on disk, or DELETE them?"),
        _(@"Remove, but keep subprojects on disk"),
        _(@"Remove, and DELETE subprojects from disk"),
        _(@"Cancel"), [selectedCategory objectForKey: @"Name"]))
        {
        case NSAlertDefaultReturn:
          break;
        case NSAlertAlternateReturn:
          if (![self wipeSubprojectsFromCategory: contentsArray])
            {
              [outline reloadData];

              return;
            }
          break;
        case NSAlertOtherReturn:
          return;
        }
    }
  else
    {
      if (NSRunAlertPanel(_(@"Really remove category?"),
        _(@"Are you sure that you want to remove category %@?"),
        _(@"Remove"), _(@"Cancel"), nil, [selectedCategory
        objectForKey: @"Name"]) == NSAlertAlternateReturn)
        {
          return;
        }
    }

  [parentContentsArray removeObject: selectedCategory];

  PrintStructure(subprojects, 0);
  NSLog(@"");

  [outline reloadData];

  [document updateChangeCount: NSChangeDone];
}

- (id)outlineView: (NSOutlineView *)outlineView 
            child: (int)index 
           ofItem: (id)item
{
  if (item != nil)
    {
      return [[item objectForKey: @"Contents"] objectAtIndex: index];
    }
  else
    {
      return [subprojects objectAtIndex: index];
    }
}

- (BOOL)outlineView: (NSOutlineView *)outlineView
   isItemExpandable: (id)item
{
  return [[item objectForKey: @"Type"] isEqualToString: @"Category"];
}

- (int)outlineView: (NSOutlineView *)outlineView
numberOfChildrenOfItem: (id)item
{
  if (item != nil)
    {
      return [[item objectForKey: @"Contents"] count];
    }
  else
    {
      return [subprojects count];
    }
}

- (id)outlineView: (NSOutlineView *)outlineView
objectValueForTableColumn:(NSTableColumn *)tableColumn
           byItem:(id)item
{
  if ([[tableColumn identifier] isEqualToString: @"Name"])
    {
      return [item objectForKey: @"Name"];
    }
  else
    {
      NSString * projectFile = [item objectForKey: @"ProjectFile"];

      if (projectFile != nil)
        {
          projectFile = [self pathToSubprojectFile: item];
        }

      return projectFile;
    }
}

- (void)outlineView: (NSOutlineView *)outlineView
     setObjectValue: (id)object
     forTableColumn: (NSTableColumn *)tableColumn
             byItem: (id)item
{
  [item setObject: object forKey: @"Name"];

  SortContentsArrayByName([self parentCategoryContentsArray]);

  PrintStructure(subprojects, 0);
  NSLog(@"");

  [outline reloadData];
}

- (BOOL) fileManager: (NSFileManager*)fileManager
  shouldProceedAfterError: (NSDictionary*)errorDictionary
{
  ASSIGN(fileOpErrorDict, errorDictionary);

  return NO;
}

- (void) fileManager: (NSFileManager*)fileManager
     willProcessPath: (NSString *) aPath
{}

@end
