/*
    MakeBuilder.m

    Implementation of the MakeBuilder class for the ProjectManager application.

    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 "MakeBuilder.h"

#import <Foundation/NSArray.h>
#import <Foundation/NSException.h>
#import <Foundation/NSFileHandle.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSString.h>
#import <Foundation/NSTask.h>
#import <Foundation/NSUserDefaults.h>

#import <AppKit/NSButton.h>
#import <AppKit/NSCell.h>
#import <AppKit/NSDocumentController.h>
#import <AppKit/NSImage.h>
#import <AppKit/NSMatrix.h>
#import <AppKit/NSMenuItem.h>
#import <AppKit/NSNibLoading.h>
#import <AppKit/NSPanel.h>
#import <AppKit/NSPopUpButton.h>
#import <AppKit/NSScroller.h>
#import <AppKit/NSScrollView.h>
#import <AppKit/NSTableColumn.h>
#import <AppKit/NSTableView.h>
#import <AppKit/NSToolbar.h>
#import <AppKit/NSToolbarItem.h>
#import <AppKit/NSView.h>
#import <AppKit/NSWindow+Toolbar.h>

#import "MakeBuilderDelegate.h"
#import "../../ProjectDocument.h"
#import "../../NSImageAdditions.h"
#import "OSType.h"

/**
 * Finds out whether a specified search string is contained inside another
 * string.
 *
 * @param string The string which to search.
 * @param search The string for which to look.
 *
 * @return YES if the searched string is inside `string', otherwise NO.
 */
static inline BOOL
ContainsString(NSString * string, NSString * search)
{
  return ([string rangeOfString: search].location != NSNotFound);
}

/**
 * Produces an build error/warning-message description dictionary from
 * the provided arguments.
 *
 * @param string The string which to parse and construct a description of.
 * @param dirStack The directory stack of the builder when it received
 *      the error/warning message.
 *
 * @return A dictionary describing the error/warning message. The dictionary
 *      will have the following format:
 *      {
 *        File = "/path/to/error/file";
 *        Line = (NSNumber *) error-line;
 *        Description = (NSString/NSAttributedString *) error-description;
 *      }
 *.
 *      Error messages have the 'Description' key's value colored red,
 *      warnings yellow and in case the string could not be parsed, both
 *      the 'File' and 'Line' keys will be left empty and the 'Description'
 *      key will contain the entire input line uncolored.
 */
static NSDictionary *
ErrorInfoFromString (NSString * string, NSArray * dirStack)
{
  NSArray * components;

  // try splitting the line at ':' boundaries. There should be at least 4
  // components, structured like this:
  //
  // <file>:<line-in-file>:<message-type>:<error-description>
  // or
  // <file>:<line-in-file>:<column>:<message-type>:<error-description>
  components = [string componentsSeparatedByString: @":"];
  if ([components count] >= 4 &&
    [[[components objectAtIndex: 1] stringByTrimmingCharactersInSet:
    [NSCharacterSet decimalDigitCharacterSet]] length] == 0)
    {
      NSString * filePath, * file, * lineString, * type, * description;
      NSNumber * line;
      unsigned int headerLength;
      NSColor * color;
      NSUserDefaults * df = [NSUserDefaults standardUserDefaults];

      // append the file name to the current build directory (top element
      // on the build directory stack)
      file = [components objectAtIndex: 0];
      filePath = [[dirStack lastObject] stringByAppendingPathComponent: file];

      lineString = [components objectAtIndex: 1];
      line = [NSNumber numberWithInt: [lineString intValue]];

      // first try the preprocessor format with "line:column:message-type"
      // indication, switching back to the compiler style (line:message-type)
      // if what we read was neither "warning" nor "error"
      type = [[components objectAtIndex: 3] stringByTrimmingSpaces];
      if (![type isEqualToString: _(@"warning")] &&
          ![type isEqualToString: _(@"error")])
        {
          type = [[components objectAtIndex: 2] stringByTrimmingSpaces];
        }

      // the rest is the error description
      headerLength = [file length] + [lineString length] + [[components
        objectAtIndex: 2] length] + 3;
      description = [[string substringFromIndex: headerLength]
        stringByTrimmingSpaces];

      // if the type is known (is one of "error" or "warning"),
      // then colorize the description. Otherwise leave it alone.
      if ([type caseInsensitiveCompare: @"error"] == NSOrderedSame)
        {
          color = [df objectForKey: @"ErrorColor"];
          if (color == nil)
            color = [NSColor redColor];

          description = [NSString stringWithFormat: _(@"Error: %@"),
            description];
        }
      else if ([type caseInsensitiveCompare: @"warning"] == NSOrderedSame)
        {
          color = [df objectForKey: @"WarningColor"];
          if (color == nil)
            color = [NSColor yellowColor];

          description = [NSString stringWithFormat: _(@"Warning: %@"),
            description];
        }
      else
        {
          color = nil;
        }

      if (color != nil)
        {
          NSDictionary * attributes = [NSDictionary
            dictionaryWithObject: color
                          forKey: NSForegroundColorAttributeName];

          return [NSDictionary dictionaryWithObjectsAndKeys:
            filePath, @"File",
            line, @"Line",
            [[[NSAttributedString alloc]
              initWithString: description attributes: attributes]
              autorelease], @"Description",
            nil];
        }
      else
        {
          return [NSDictionary dictionaryWithObjectsAndKeys:
            filePath, @"File",
            line, @"Line",
            description, @"Description",
            nil];
        }
    }
  else
    // otherwise mark this as an unknown error
    {
      return [NSDictionary dictionaryWithObject: string
                                         forKey: @"Description"];
    }
}

@interface MakeBuilder (Private)

- (BOOL) validateControl: control;

- (void) setBuilderState: (MakeBuilderState) aState;

- (void) flushBuildDirectoryStack;

@end

@implementation MakeBuilder (Private)

/**
 * Validates the provided control.
 *
 * @param control The control to be validated. The object must respond
 *      to an -action message.
 *
 * @return YES if the control is valid, NO otherwise.
 */
- (BOOL) validateControl: control
{
  SEL action = [control action];

  if (sel_eq (action, @selector (build:)) ||
      sel_eq (action, @selector (clean:)))
    {
      return (state == MakeBuilderReady);
    }
  else if (sel_eq (action, @selector (stopOperation:)))
    {
      return (state == MakeBuilderBuilding || state == MakeBuilderCleaning);
    }
  else
    {
      return YES;
    }
}

/**
 * Sets up the receiver to a certain state and updates it's GUI.
 *
 * @param aState The state to which to set the receiver up.
 */
- (void) setBuilderState: (MakeBuilderState) aState
{
  state = aState;

  switch (state)
    {
    case MakeBuilderReady:
      [buildTarget setEnabled: YES];
      break;

    case MakeBuilderBuilding:
      [buildTarget setEnabled: NO];
      break;

    case MakeBuilderCleaning:
      [buildTarget setEnabled: NO];
      break;
    }

  [self flushBuildDirectoryStack];
  [[[view window] toolbar] validateVisibleItems];
}

- (void) flushBuildDirectoryStack
{
  int n = [buildDirectoryStack count];

  while (n > 1)
    {
      [buildDirectoryStack removeLastObject];
    }
}

@end

@implementation MakeBuilder

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

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

- (NSArray *) moduleMenuItems
{
  return [NSArray arrayWithObjects:
    PMMakeMenuItem (_(@"Build"), @selector(build:), @"B", self),
    PMMakeMenuItem (_(@"Clean"), @selector(clean:), @"C", self),
    PMMakeMenuItem (_(@"Stop"), @selector(stopOperation:), nil, self),
    nil];
}

- (NSArray *) toolbarItemIdentifiers
{
  return [NSArray arrayWithObjects:
    @"MakeBuilderBuildItemIdentifier",
    @"MakeBuilderCleanItemIdentifier",
    @"MakeBuilderStopItemIdentifier",
    nil];
}

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

  [item setTarget: self];

  if ([identifier isEqualToString: @"MakeBuilderBuildItemIdentifier"])
    {
      [item setImage: [NSImage imageNamed: @"Build" owner: self]];
      [item setAction: @selector(build:)];
      [item setLabel: _(@"Build")];
      [item setToolTip: _(@"Builds the selected target of the project")];
    }
  else if ([identifier isEqualToString: @"MakeBuilderCleanItemIdentifier"])
    {
      [item setImage: [NSImage imageNamed: @"Clean" owner: self]];
      [item setAction: @selector(clean:)];
      [item setLabel: _(@"Clean")];
      [item setToolTip: _(@"Cleans the project")];
    }
  else if ([identifier isEqualToString: @"MakeBuilderStopItemIdentifier"])
    {
      [item setImage: [NSImage imageNamed: @"Stop" owner: self]];
      [item setAction: @selector(stopOperation:)];
      [item setLabel: _(@"Stop")];
      [item setToolTip: _(@"Stops a build or clean operation")];
    }

  return item;
}

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

  TEST_RELEASE(view);

  TEST_RELEASE(targets);
  TEST_RELEASE(buildArguments);
  TEST_RELEASE(buildErrorList);
  TEST_RELEASE(lastIncompleteOutputLine);
  TEST_RELEASE(lastIncompleteErrorLine);
  TEST_RELEASE(buildDirectoryStack);

  if (task != nil && [task isRunning])
    {
      [task interrupt];
    }
  TEST_RELEASE(task);

  TEST_RELEASE(info);

  [super dealloc];
}

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

      buildArguments = [NSMutableArray new];
      [buildArguments addObjectsFromArray: [infoDict objectForKey:
        @"BuildArguments"]];

      buildErrorList = [NSMutableArray new];
      [buildErrorList addObjectsFromArray: [infoDict objectForKey:
        @"BuildErrors"]];

      buildDirectoryStack = [NSMutableArray new];
      [buildDirectoryStack addObject: [document projectDirectory]];

      lastIncompleteOutputLine = [NSString new];
      lastIncompleteErrorLine = [NSString new];

      ASSIGNCOPY(info, infoDict);
    }

  return self;
}

- (ProjectDocument *) document
{
  return document;
}

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

  return view;
}

- (NSDictionary *) infoDictionary
{
  return [NSDictionary dictionaryWithObjectsAndKeys:
    buildArguments, @"BuildArguments",
    [NSNumber numberWithBool: [verboseBuild state]], @"VerboseBuild",
    [NSNumber numberWithBool: [warnings state]], @"Warnings",
    [NSNumber numberWithBool: [allWarnings state]], @"AllWarnings",
    nil];
}

- (BOOL) regenerateDerivedFiles
{
  return YES;
}

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

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

- (void) finishInit
{
  delegate = (id <MakeBuilderDelegate>) [document projectType];

  ASSIGN(targets, [delegate buildTargetsForMakeBuilder: self]);
  [buildTarget removeAllItems];
  [buildTarget addItemsWithTitles: targets];
  if ([buildTarget numberOfItems] > 0)
    {
      [buildTarget selectItemAtIndex: 0];
    }
}

- (void) awakeFromNib
{
  NSButtonCell * cell;
  NSRect r;

  [view retain];
  [view removeFromSuperview];
  DESTROY(bogusWindow);

  [buildErrors setTarget: self];
  [buildErrors setDoubleAction: @selector(openErrorFile:)];

  [[[buildArgs enclosingScrollView] verticalScroller]
    setArrowsPosition: NSScrollerArrowsNone];

  [self setBuilderState: MakeBuilderReady];

  cell = [buildArgsMovementMatrix cellWithTag: 0];
  [cell setTarget: self];
  [cell setAction: @selector(moveBuildArgumentUp:)];
  cell = [buildArgsMovementMatrix cellWithTag: 1];
  [cell setTarget: self];
  [cell setAction: @selector(moveBuildArgumentDown:)];

  cell = [buildArgsManipulationMatrix cellWithTag: 0];
  [cell setTarget: self];
  [cell setAction: @selector(addBuildArgument:)];
  cell = [buildArgsManipulationMatrix cellWithTag: 1];
  [cell setTarget: self];
  [cell setAction: @selector(removeBuildArgument:)];

  [buildTarget removeAllItems];
  [buildTarget addItemsWithTitles: targets];
  if ([buildTarget numberOfItems] > 0)
    {
      [buildTarget selectItemAtIndex: 0];
    }

  [verboseBuild setState: [[info objectForKey: @"VerboseBuild"] boolValue]];
  [warnings setState: [[info objectForKey: @"Warnings"] boolValue]];
  [allWarnings setState: [[info objectForKey: @"AllWarnings"] boolValue]];
}

/**
 * Action to initiate the build process with the current target.
 */
- (void) build: sender
{
  [self buildTarget: [buildTarget titleOfSelectedItem]];
}

/**
 * Builds the specified target of the project.
 *
 * @param target The target which to build.
 */
- (void) buildTarget: (NSString *) target
{
  NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];
  NSString * makeCommand;
  NSMutableArray * arguments = [NSMutableArray array];

  NSAssert(state == MakeBuilderReady, _(@"Attempted to start build when "
    @"builder wasn't ready."));

  [self setBuilderState: MakeBuilderBuilding];

  [document logMessage: [NSString stringWithFormat: _(@"Building target %@"),
    target]];

  if ([delegate prepareForBuildByBuilder: self target: target] == NO)
    {
      return;
    }

  // construct the build task
  ASSIGN(task, [NSTask new]);

  // set the build task's launch path
  makeCommand = [[NSUserDefaults standardUserDefaults]
    objectForKey: @"MakeCommand"];
  if (makeCommand == nil)
    {
#ifdef LINUX
      // GNU/Linux systems use GNU Make by default
      makeCommand = @"make";
#elif defined (FREEBSD)
      // FreeBSD has it named "gmake"
      makeCommand = @"gmake";
#else
#warning "Your system type is not supported, so I don't know which make"
#warning "command to use by default - assuming 'make' for now. You can"
#warning "change that at run-time by setting the MakeCommand default"
      makeCommand = @"make";
#endif
    }
  [task setLaunchPath: makeCommand];
  [task setLaunchPath: [task validatedLaunchPath]];

   // build in the project path
  [task setCurrentDirectoryPath: [document projectDirectory]];

   // construct the build task's arguments
  [arguments addObjectsFromArray: [delegate buildArgumentsForBuilder: self
                                                              target: target]];

  if ([verboseBuild state])
    {
      [arguments addObject: @"messages=yes"];
    }

  if ([warnings state])
    {
      [arguments addObject: @"warnings=yes"];
    }
  if ([allWarnings state])
    {
      [arguments addObject: @"allwarnings=yes"];
    }

   // append additional user-defined arguments
  [arguments addObjectsFromArray: buildArguments];

  [task setArguments: arguments];

  [nc addObserver: self
         selector: @selector(buildCompleted:)
             name: NSTaskDidTerminateNotification
           object: task];

   // set up the stdout and stderr pipes
  {
    NSPipe * outputPipe = [NSPipe pipe],
           * errorPipe = [NSPipe pipe];

    [task setStandardOutput: outputPipe];
    [task setStandardError: errorPipe];

    ASSIGN(outputFileHandle, [outputPipe fileHandleForReading]);
    ASSIGN(errorFileHandle, [errorPipe fileHandleForReading]);

    [nc addObserver: self
           selector: @selector(collectOutput:)
               name: NSFileHandleDataAvailableNotification
             object: outputFileHandle];
    [nc addObserver: self
           selector: @selector(collectErrorOutput:)
               name: NSFileHandleDataAvailableNotification
             object: errorFileHandle];
  }

   // clean the output lists
  [buildErrorList removeAllObjects];
  [buildErrors reloadData];
  [buildOutput setString: @""];

   // and start the build
  NS_DURING
    [task launch];
  NS_HANDLER
    {
      NSDictionary * userInfo;

      [self setBuilderState: MakeBuilderReady];

      userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
        document, @"Project",
        target, @"Target",
        [NSNumber numberWithBool: NO], @"Success",
        nil];

      [nc postNotificationName: MakeBuilderBuildDidEndNotification
                        object: self
                      userInfo: userInfo];

      NSRunAlertPanel(_(@"Failed to build"),
        _(@"Failed to run the build command \"%@\""),
        nil, nil, nil, makeCommand);

      return;
    }
  NS_ENDHANDLER

  [outputFileHandle waitForDataInBackgroundAndNotify];
  [errorFileHandle waitForDataInBackgroundAndNotify];

  {
    NSDictionary * userInfo;

    userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
      document, @"Project",
      target, @"Target",
      nil];
    [nc postNotificationName: MakeBuilderBuildDidBeginNotification
                      object: self
                    userInfo: userInfo];
  }
}

/**
 * Action to initiate the clean process with the current target.
 */
- (void) clean: (id)sender
{
  [self cleanTarget: [buildTarget titleOfSelectedItem]];
}

/**
 * Cleans the specified target of the project.
 *
 * @param target The target which to clean.
 */
- (void) cleanTarget: (NSString *) target
{
  NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];
  NSString * makeCommand;
  NSMutableArray * arguments = [NSMutableArray array];

  NSAssert(state == MakeBuilderReady, _(@"Attempted to start clean when "
    @"builder wasn't ready."));

  [self setBuilderState: MakeBuilderCleaning];

  if ([delegate prepareForCleanByBuilder: self target: target] == NO)
    {
      return;
    }


  [document logMessage: [NSString stringWithFormat: _(@"Cleaning target %@"),
    target]];

   // construct the clean task
  ASSIGN(task, [NSTask new]);

   // set the clean task's launch path
  makeCommand = [[NSUserDefaults standardUserDefaults]
    objectForKey: @"MakeCommand"];
  if (makeCommand == nil)
    {
      makeCommand = @"make";
    }
  [task setLaunchPath: makeCommand];
  [task setLaunchPath: [task validatedLaunchPath]];

   // build in the project path
  [task setCurrentDirectoryPath: [document projectDirectory]];

   // construct the task's arguments
  [arguments addObjectsFromArray: [delegate cleanArgumentsForBuilder: self
                                                              target: target]];
  if ([verboseBuild state])
    {
      [arguments addObject: @"messages=yes"];
    }
  if ([warnings state])
    {
      [arguments addObject: @"warnings=yes"];
    }
  if ([allWarnings state])
    {
      [arguments addObject: @"allwarnings=yes"];
    }

  [task setArguments: arguments];

  [nc addObserver: self
         selector: @selector(cleanCompleted:)
             name: NSTaskDidTerminateNotification
           object: task];

   // set up the stdout and stderr pipes
  {
    NSPipe * outputPipe = [NSPipe pipe],
           * errorPipe = [NSPipe pipe];

    [task setStandardOutput: outputPipe];
    [task setStandardError: errorPipe];

    ASSIGN(outputFileHandle, [outputPipe fileHandleForReading]);
    ASSIGN(errorFileHandle, [errorPipe fileHandleForReading]);

    [nc addObserver: self
           selector: @selector(collectOutput:)
               name: NSFileHandleDataAvailableNotification
             object: outputFileHandle];
    [nc addObserver: self
           selector: @selector(collectErrorOutput:)
               name: NSFileHandleDataAvailableNotification
             object: errorFileHandle];
  }

  // clean the output lists
  [buildErrorList removeAllObjects];
  [buildErrors reloadData];
  [buildOutput setString: @""];

  // and start the clean operation
  NS_DURING
    [task launch];
  NS_HANDLER
    {
      NSDictionary * userInfo;

      [self setBuilderState: MakeBuilderReady];

      userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
        document, @"Project",
        target, @"Target",
        [NSNumber numberWithBool: NO], @"Success",
        nil];

      [nc postNotificationName: MakeBuilderCleanDidEndNotification
                        object: self
                      userInfo: userInfo];
      NSRunAlertPanel(_(@"Failed to clean"),
        _(@"Failed to run the clean command \"%@\""),
        nil, nil, nil, makeCommand);

      return;
    }
  NS_ENDHANDLER

  [outputFileHandle waitForDataInBackgroundAndNotify];
  [errorFileHandle waitForDataInBackgroundAndNotify];

  {
    NSDictionary * userInfo;

    userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
      document, @"Project",
      target, @"Target",
      nil];
    [nc postNotificationName: MakeBuilderCleanDidBeginNotification
                      object: self
                    userInfo: userInfo];
  }
}

/**
 * Queries whether the receiver is currently executing either a build or
 * clean operation (is busy).
 *
 * @return YES if the receiver is busy, NO otherwise.
 */
- (BOOL) isBusy
{
  return (state != MakeBuilderReady);
}

/**
 * Action to initiate the stop a running build or clean process.
 */
- (void) stopOperation: sender
{
  NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];
  NSDictionary * userInfo;
  NSString * notification;

  NSAssert(state == MakeBuilderBuilding || state == MakeBuilderCleaning,
    _(@"Attempted to stop an operation when builder was neither building "
      @"nor cleaning."));

  [nc removeObserver: self];
  [task interrupt];
  DESTROY(task);
  DESTROY(outputFileHandle);
  DESTROY(errorFileHandle);

  userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
    document, @"Project",
    [buildTarget titleOfSelectedItem], @"Target",
    [NSNumber numberWithBool: NO], @"Success",
    nil];

  if (state == MakeBuilderBuilding)
    {
      notification = MakeBuilderBuildDidEndNotification;
    }
  else
    {
      notification = MakeBuilderCleanDidEndNotification;
    }

  [nc postNotificationName: notification
                    object: self
                  userInfo: userInfo];

  [self setBuilderState: MakeBuilderReady];
}

/**
 * Action to add a build argument.
 */
- (void) addBuildArgument: (id)sender
{
  [buildArguments addObject: _(@"<new-argument>")];
  [buildArgs reloadData];
}

/**
 * Action to remove a build argument.
 */
- (void) removeBuildArgument: (id)sender
{
  int row = [buildArgs selectedRow];

  if (row >= 0)
    {
      [buildArguments removeObjectAtIndex: row];
      [buildArgs reloadData];
    }
}

/**
 * Action to move a build argument upwards in the list.
 */
- (void) moveBuildArgumentUp: sender
{
  int row = [buildArgs selectedRow];

  if (row > 0 && [buildArguments count] > 1)
    {
      [buildArguments insertObject: [buildArguments objectAtIndex: row]
                           atIndex: row - 1];
      [buildArguments removeObjectAtIndex: row + 1];
      [buildArgs reloadData];
      [buildArgs selectRow: row - 1 byExtendingSelection: NO];
    }
}

/**
 * Action to move a build argument downwards in the list.
 */
- (void) moveBuildArgumentDown: sender
{
  int row = [buildArgs selectedRow];

  if (row >= 0 &&
      row + 1 < (int) [buildArguments count] &&
      [buildArguments count] > 1)
    {
      [buildArguments insertObject: [buildArguments objectAtIndex: row]
                           atIndex: row + 2];
      [buildArguments removeObjectAtIndex: row];
      [buildArgs reloadData];
      [buildArgs selectRow: row + 1 byExtendingSelection: NO];
    }
}

/**
 * Action to open the error file at the specified line when the user
 * double-clicks an error in the build error list.
 */
- (void) openErrorFile: sender
{
  id filename;
  int clickedRow = [buildErrors clickedRow];
  unsigned int line;
  NSDictionary * description;

  if (clickedRow < 0)
    return;

  // extract the filename and line
  description = [buildErrorList objectAtIndex: clickedRow];
  filename = [description objectForKey: @"File"];

  if ([filename length] == 0)
    {
      NSRunAlertPanel(_(@"No file to open"),
        _(@"This error/warning message doesn't have a file "
          @"associated with it."),
        nil, nil, nil);
      return;
    }

  line = [[description objectForKey: @"Line"] intValue];

  if (![document openFile: filename inCodeEditorOnLine: line])
    {
      NSRunAlertPanel (_(@"Unable to open file"),
        _(@"Unable to open the file %@ in an editor"),
        nil, nil, nil, filename);
    }
}

- (int) numberOfRowsInTableView: (NSTableView *)aTableView
{
  if (aTableView == buildArgs)
    {
      return [buildArguments count];
    }
  else
    {
      return [buildErrorList count];
    }
}

- (id) tableView: (NSTableView *)aTableView 
objectValueForTableColumn: (NSTableColumn *)aTableColumn 
             row: (int)rowIndex
{
  if (aTableView == buildArgs)
    {
      NSString * identifier = [aTableColumn identifier];

      if ([identifier isEqualToString: @"Number"])
        {
          return [NSNumber numberWithInt: rowIndex];
        }
      else
        {
          return [buildArguments objectAtIndex: rowIndex];
        }
    }
  else
    {
      NSString * identifier = [aTableColumn identifier];
      NSString * value = [[buildErrorList objectAtIndex: rowIndex]
        objectForKey: identifier];

      if ([identifier isEqualToString: @"File"])
        {
          NS_DURING
            // strip the project directory path from it, leaving intact
            // any subproject directories, so the user can follow them
            value = [value stringByDeletingPrefix: [document projectDirectory]];
          NS_HANDLER
            value = nil;
          NS_ENDHANDLER
        }

      return value;
    }
}

- (void) tableView: (NSTableView *)aTableView 
    setObjectValue: (id)anObject 
    forTableColumn: (NSTableColumn *)aTableColumn
               row: (int)rowIndex
{
  [buildArguments replaceObjectAtIndex: rowIndex withObject: anObject];
}

- (void) collectOutput: (NSNotification *) notif
{
  NSString * string;
  NSMutableArray * incommingLines;

  string = [[[NSString alloc]
    initWithData: [outputFileHandle availableData]
        encoding: NSUTF8StringEncoding]
    autorelease];
  incommingLines = [[[string componentsSeparatedByString: @"\n"]
    mutableCopy] autorelease];

  [buildOutput replaceCharactersInRange:
    NSMakeRange([[buildOutput string] length], 0)
                             withString: string];
  [buildOutput scrollRangeToVisible: 
    NSMakeRange([[buildOutput string] length], 0)];

  // complete the lastIncompleteOutputLine from the first line of output
  if ([incommingLines count] > 0)
    {
      ASSIGN(lastIncompleteOutputLine, [lastIncompleteOutputLine
        stringByAppendingString: [incommingLines objectAtIndex: 0]]);

      [incommingLines removeObjectAtIndex: 0];
    }

  // now if there are still more lines to process this means that
  // the lastIncompleteLine has been completed and start processing
  if ([incommingLines count] > 0)
    {
      NSEnumerator * e;
      NSString * line;
      NSMutableArray * linesToProcess = [NSMutableArray
        arrayWithCapacity: [incommingLines count]];

      [linesToProcess addObject: lastIncompleteOutputLine];
      // the new last incomplete output line will the last line of output
      ASSIGN(lastIncompleteOutputLine, [incommingLines lastObject]);
      [incommingLines removeLastObject];

      [linesToProcess addObjectsFromArray: incommingLines];

      // now process the completed lines
      e = [linesToProcess objectEnumerator];
      while ((line = [e nextObject]) != nil)
        {
          if ([line hasPrefix: @"make["])
            {
              unsigned int offset;

              // watch for directory changes
              if (ContainsString(line, _(@"Entering directory")) &&
               (offset = [line rangeOfString: @"`"].location) != NSNotFound)
                {
                  NSString * newDirectory = [line substringWithRange:
                    NSMakeRange(offset, [line length] - offset - 1)];

                  [buildDirectoryStack addObject: newDirectory];
                }
              else if (ContainsString(line, _(@"Leaving directory")))
                {
                  NSAssert([buildDirectoryStack count] > 1, @"Build "
                    @"directory stack underflown.");

                  [buildDirectoryStack removeLastObject];
                }
            }
        }
    }

  [outputFileHandle waitForDataInBackgroundAndNotify];
}

- (void) collectErrorOutput: (NSNotification *) notif
{
  NSMutableArray * incommingLines;

  incommingLines = [[[[[[NSString alloc]
    initWithData: [errorFileHandle availableData]
        encoding: NSUTF8StringEncoding]
    autorelease]
    componentsSeparatedByString: @"\n"]
    mutableCopy]
    autorelease];

  // complete the lastIncompleteErrorLine from the first line of error output
  if ([incommingLines count] > 0)
    {
      ASSIGN(lastIncompleteErrorLine, [lastIncompleteErrorLine
        stringByAppendingString: [incommingLines objectAtIndex: 0]]);

      [incommingLines removeObjectAtIndex: 0];
    }
  // now if there are still more lines to process this means that
  // the lastIncompleteLine has been completed and start processing
  if ([incommingLines count] > 0)
    {
      NSEnumerator * e;
      NSString * line;
      NSMutableArray * linesToProcess = [NSMutableArray
        arrayWithCapacity: [incommingLines count]];

      [linesToProcess addObject: lastIncompleteErrorLine];
      // the new last incomplete error line will the last line of error output
      ASSIGN(lastIncompleteErrorLine, [incommingLines lastObject]);
      [incommingLines removeLastObject];

      [linesToProcess addObjectsFromArray: incommingLines];

      // now process the completed lines
      e = [linesToProcess objectEnumerator];
      while ((line = [e nextObject]) != nil)
        {
          [buildErrorList addObject: ErrorInfoFromString(line,
            buildDirectoryStack)];
        }

      // and finally tell the error table to update itself
      [buildErrors reloadData];
      [buildErrors scrollPoint: NSMakePoint(0,
        [buildErrors frame].size.height)];
    }

  [errorFileHandle waitForDataInBackgroundAndNotify];
}

- (void) buildCompleted: (NSNotification *) notif
{
  NSString * target = [buildTarget titleOfSelectedItem];
  NSString * result;
  NSDictionary * userInfo;
  BOOL successFlag;
  NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];

  [nc removeObserver: self];

  [self setBuilderState: MakeBuilderReady];

  if ([task terminationStatus] == 0)
    {
      result = [NSString stringWithFormat: _(@"Build of target %@ SUCCEEDED"),
        target];
      successFlag = YES;
    }
  else
    {
      result = [NSString stringWithFormat: _(@"Build of target %@ FAILED"),
        target];
      successFlag = NO;
    }

  userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
    document, @"Project",
    target, @"Target",
    [NSNumber numberWithBool: successFlag], @"Success",
    nil];
  [nc postNotificationName: MakeBuilderBuildDidEndNotification
                    object: self
                  userInfo: userInfo];

  [document logMessage: result];

  DESTROY(task);
  DESTROY(outputFileHandle);
  DESTROY(errorFileHandle);
}

- (void) cleanCompleted: (NSNotification *) notif
{
  NSString * target = [buildTarget titleOfSelectedItem];
  NSString * result;
  NSDictionary * userInfo;
  BOOL successFlag;
  NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];

  [nc removeObserver: self];

  [self setBuilderState: MakeBuilderReady];

  if ([task terminationStatus] == 0)
    {
      result = [NSString stringWithFormat: _(@"Clean of target %@ SUCCEEDED"),
        target];
      successFlag = YES;
    }
  else
    {
      result = [NSString stringWithFormat: _(@"Clean of target %@ FAILED"),
        target];
      successFlag = NO;
    }

  userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
    document, @"Project",
    target, @"Target",
    [NSNumber numberWithBool: successFlag], @"Success",
    nil];
  [nc postNotificationName: MakeBuilderCleanDidEndNotification
                    object: self
                  userInfo: userInfo];

  [document logMessage: result];

  DESTROY(task);
  DESTROY(outputFileHandle);
  DESTROY(errorFileHandle);
}

/**
 * Action invoked when a build-option (like "Verbose Build" or "Warnings")
 * is changed. It simply marks the document as dirty.
 */
- (void) buildOptionChanged: sender
{
  [document updateChangeCount: NSChangeDone];
}

@end
