/*
    UtilityFunctions.h

    Utility static functions for the FileManager class of ProjectManager.

    Copyright (C) 2005  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 <Foundation/NSError.h>
#import <Foundation/NSString.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSBundle.h>
#import <Foundation/NSFileManager.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSUserDefaults.h>
#import <Foundation/NSException.h>
#import <Foundation/NSValue.h>

#import <AppKit/NSPanel.h>
#import <AppKit/NSTextField.h>

#import "FileManager.h"

/**
 * This function pops up an alert panel describing an NSError object. The
 * description of an error is taken from it's NSLocalizedDescriptionKey
 * userInfo entry.
 *
 * @param error The error which to describe.
 * @param aTitle The title of the alert panel.
 * @param aDescription An additional description of the error. If specified,
 *      this argument is printed out in the alert panel's main text area,
 *      followed by a colon and a description of the NSError object. If it
 *      is `nil', only the error's description is printed in the panel.
 * @param firstButton The title of the first button. If nothing is specified,
 *      this defaults to "OK".
 * @param secondButton The title of the second button. If nothing is
 *      specified, this button is ommited.
 * @param thirdButton The title of the third button. If nothing is specified,
 *      this button is ommited.
 *
 * @return The code of the clicked button to dismiss the panel, as returned
 *      by NSRunAlertPanel.
 */
static int
DescribeError (NSError * error,
               NSString * aTitle,
               NSString * aDescription,
               NSString * firstButton,
               NSString * secondButton,
               NSString * thirdButton,
               ...)
{
  NSString * description;
  NSString * prefix, * errorDescription;

  if (aDescription == nil)
    {
      prefix = @"";
    }
  else
    {
      va_list arglist;

      va_start (arglist, thirdButton);
      prefix = [NSString stringWithFormat: aDescription arguments: arglist];
      va_end (arglist);

      prefix = [prefix stringByAppendingString: _(@": ")];
    }

  errorDescription = [[error userInfo] objectForKey:
    NSLocalizedDescriptionKey];

  description = [NSString stringWithFormat: @"%@%@", prefix, errorDescription];

  return NSRunAlertPanel (aTitle,
                          description,
                          firstButton,
                          secondButton,
                          thirdButton);
}

/**
 * This function is a shorthand for creating NSError objects and setting
 * error object pointers in the FileManager class code. The NSError
 * object's error domain is always set ProjectFilesErrorDomain.
 *
 * @param ptr A pointer to an area which is to be filled with the NSError
 *      object. In case the passed pointer is NULL, this function immediately
 *      returns and doesn't create any NSError object.
 * @param code The code of the error.
 * @param reasonFormat A format string specifying the reason of the error.
 *      This will be put into the error's userInfo dictionary under the
 *      NSLocalizedDescriptionKey. The remaining variable arguments are
 *      arguments for the format string.
 */
static void
SetFileError (NSError ** ptr, int code, NSString * reasonFormat, ...)
{
  if (ptr != NULL)
    {
      NSString * reason;
      NSDictionary * userInfo;
      va_list arglist;

      va_start (arglist, reasonFormat);
      reason = [[[NSString alloc]
        initWithFormat: reasonFormat arguments: arglist]
        autorelease];
      va_end (arglist);

      userInfo = [NSDictionary
        dictionaryWithObject: reason forKey: NSLocalizedDescriptionKey];
      *ptr = [NSError errorWithDomain: ProjectFilesErrorDomain
                                 code: code
                             userInfo: userInfo];
    }
}

/**
 * Posts a ProjectFilesDidChangeNotification to the default notification
 * center. The notification's user info will be structured like this:
 * {
 *   Project = "<path-to-project-file>";
 *   Category = "<category-who's-contents-changed>";
 * }
 *
 * @param sender The sender FileManager of the notification.
 * @param category The category which will be declared in the user info
 *      under the `Category' key. Passing `nil' will result in the
 *      `Category' key not being present in the user info dictionary.
 */
static void
PostFilesChangedNotification (FileManager * sender, NSString * category)
{
  NSDictionary * userInfo;
  NSString * projectPath;
  static NSNotificationCenter * nc = nil;

  if (nc == nil)
    {
      nc = [NSNotificationCenter defaultCenter];
    }

  projectPath = [[sender document] fileName];
  if (category != nil)
    {
      userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
        projectPath, @"Project",
        category, @"Category",
        nil];
    }
  else
    {
      userInfo = [NSDictionary dictionaryWithObject: projectPath
                                             forKey: @"Project"];
    }

  [nc postNotificationName: ProjectFilesDidChangeNotification
                    object: sender
                  userInfo: userInfo];
}


/**
 * Sets the text field `tf' to editable or non-editable, based on `flag'.
 * If the text field is set to editable, it's also made to draw it's
 * background and it's target is set to `target'. Otherwise, it's made
 * to not display it's background and it's target is reset.
 */
static inline void
SetTextFieldEnabled (NSTextField * tf, BOOL flag)
{
  if ([tf isEnabled] != flag)
    {
      [tf setEnabled: flag];
      [tf setEditable: flag];
      [tf setSelectable: flag];
      [tf setDrawsBackground: flag];
    }
}

/**
 * Constructs a human-readable description of the data size `size'.
 * E.g. if the value is greater than 1024, then the prefix 'kB' is
 * appended to indicate kilobytes and the number is represented as
 * a fraction of this unit.
 *
 * @return The resulting string.
 */
static inline NSString *
MakeSizeStringFromValue (unsigned long long size)
{
  if (size < 1024)
    {
      return [NSString stringWithFormat: _(@"%i bytes"), size];
    }
  else if (size < 1024 * 1024)
    {
      return [NSString stringWithFormat: _(@"%.2f kB"), (double) size / 1024];
    }
  else if (size < 1024 * 1024 * 1024)
    {
      return [NSString stringWithFormat: _(@"%.2f MB"), (double) size /
      (1024 * 1024)];
    }
  else
    {
      return [NSString stringWithFormat: _(@"%.2f GB"),
        (double) size / (1024 * 1024 * 1024)];
    }
}

/**
 * This function translates `linkTarget', which was the target of a
 * link originally located at `oldLocation', to correctly point to
 * it's target from `newLocation'.
 *
 * @return The translocated link target.
 */
static inline NSString *
TranslocateLinkTarget (NSString * linkTarget, NSString * oldLocation,
                       NSString * newLocation)
{
  // absolute paths do not need to be translocated
  if ([linkTarget isAbsolutePath])
    {
      return linkTarget;
    }
  else
    {
      return [newLocation stringByConstructingRelativePathTo:
        [oldLocation stringByConcatenatingWithPath: linkTarget]];
    }
}

/**
 * Removes the directory (and any superdirectories of it) at `aPath'
 * which are empty.
 */
static BOOL
PurgeUnneededDirectories (NSString * aPath, NSError ** error)
{
  NSFileManager * fm = [NSFileManager defaultManager];

  for (;
       [[fm directoryContentsAtPath: aPath] count] == 0;
       aPath = [aPath stringByDeletingLastPathComponent])
    {
      if (![fm removeFileAtPath: aPath handler: nil])
        {
          SetFileError (error, ProjectFilesDeletionError,
            _(@"Couldn't delete directory at path %@."), aPath);

          return NO;
        }
    }

  return YES;
}
