/*
 * WKWizardPanel.m
 *
 * Implementation of the WKWizardPanel class for the WizardKit framework
 *
 * Copyright (c) 2006, by Saso Kiselkov
 *
 * For license details please see the file COPYING included with this
 * source distribution package.
 */

#import "WKWizardPanel.h"

#import <Foundation/NSString.h>
#import <Foundation/NSBundle.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSException.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSCoder.h>

#import <AppKit/NSApplication.h>

/**
 * Notification sent when the current stage of a WKWizardPanel changes.
 * The argument dictionary contains a key "Stage" set to the name
 * of the current stage.
 */
NSString * const WKWizardPanelDidChangeCurrentStageNotification
 = @"WKWizardPanelDidChangeCurrentStageNotification";

@interface WKWizardPanel (Private)

- (void) setupForStage: (NSString *) aStage;

@end

/**
 * A wizard panel class.
 *
 * A WKWizardPanel object is an NSPanel object which provides support for
 * wizard-style operation. General wizard support works like this:
 *
 * - when a wizard panel is created, you set it's stages list (using
 *   -[WKWizardPanel setStages:]). The list of stages is a list of
 *   string names of various stages through which the wizard may pass.
 *   Optionally, you may also set it's initial stage (using -[WKWizardPanel
 *   setInitialStage:]) if you want the initial stage to be some stage
 *   other than the first stage.
 *
 * - you set a delegate of the wizard panel (using -[NSWindow setDelegate:]),
 *   which must be an object which implements some methods necessary
 *   to supply the wizard panel with information about what it's supposed
 *   to display in each stage. Please consult the
 *   NSObject(WKWizardPanelDelegate) informal protocol for more information
 *   on which methods a wizard panel delegate must and may implement.
 *
 * - when events occur in the various stages in the user interface you can
 *   tell the wizard panel to move through the various stages of the wizard
 *   cycle with -[WKWizardPanel goToStage:], -[WKWizardPanel goToNextStage:]
 *   and -[WKWizardPanel goToPreviousStage:].
 */
@implementation WKWizardPanel

- (void) dealloc
{
  TEST_RELEASE(stages);
  TEST_RELEASE(initialStage);

  [super dealloc];
}

/**
 * Sets the stages of the receiver.
 *
 * Please note that invoking this method resets the receiver's initial
 * stage. An NSInternalInconsistencyException is raised in case it is
 * invoked when the receiver is active.
 *
 * @param someStages The new stages of the wizard.
 */
- (void) setStages: (NSArray *) someStages
{
  if (isActive == YES)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: _(@"-[WKWizardPanel setStages:]: panel already "
        @"active - stages must be set before activating the panel.")];
    }

  ASSIGNCOPY(stages, someStages);
  DESTROY(initialStage);
}

/**
 * Returns a list of stages of the receiver.
 *
 * @return The list of stages of the receiver.
 *
 * @see [WKWizardPanel setStages:]
 */
- (NSArray *) stages
{
  return stages;
}

/**
 * Sets whether the receiver activates itself in a modal session.
 * This method raises an NSInternalInconsistencyException in case
 * it is invoked while the receiver is active.
 *
 * @param flag The flag which should be YES if the receiver is to
 *      activate itself in a modal session, or NO if it shouldn't.
 */
- (void) setRunsInModalSession: (BOOL) flag
{
  if (isActive == YES)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: _(@"-[WKWizardPanel setRunsInModalSession:] "
        @"panel already active - modality must be set before activating "
        @"the panel.")];
    }

  runsInModalSession = flag;
}

/**
 * Returns whether the receiver activates itself in a modal session.
 *
 * @return YES if the receiver activates in a modal session, or NO if not.
 *
 * @see [WKWizardPanel setRunsInModalSession:]
 */
- (BOOL) runsInModalSession
{
  return runsInModalSession;
}

/**
 * Sets the receiver's initial stage. Raises an NSInvalidArgumentException
 * in case the provided stage name isn't in the receiver's list of stages.
 *
 * @param aStageName The new initial stage of the receiver.
 */
- (void) setInitialStage: (NSString *) aStageName
{
  if (aStageName != nil && [stages containsObject: aStageName] == NO)
    {
      [NSException raise: NSInvalidArgumentException
                  format: _(@"-[WKWizardPanel setInitialStage:]: "
        @"invalid stage name passed. Stage \"%@\" not in stages list: %@."),
        aStageName, stages];
    }

  ASSIGN(initialStage, aStageName);
}

/**
 * Returns the receiver's initial stage.
 *
 * @return The receiver's initial stage.
 *
 * @see [WKWizardPanel setInitialStage:]
 */
- (NSString *) initialStage
{
  return initialStage;
}

/**
 * Sets whether the receiver centers itself on the screen before activating.
 *
 * @param flag The flag which specifies whether the receiver is to perform
 *      center prior to activation.
 */
- (void) setCentersBeforeActivating: (BOOL) flag
{
  centersBeforeActivating = flag;
}

/**
 * Returns whether the receiver centers itself before activating.
 *
 * @return YES if the receiver centers itself before activating, NO otherwise.
 *
 * @see [WKWizardPanel setCentersBeforeActivating:]
 */
- (BOOL) centersBeforeActivating
{
  return centersBeforeActivating;
}

/**
 * Makes the receiver display a specified stage. Raises an
 * NSInvalidArgumentException in case the specified stage isn't contained
 * in the stages list of the receiver.
 *
 * @param aStageName The stage to set.
 *
 * @see [WKWizardPanel nextStage:]
 * @see [WKWizardPanel previousStage:]
 * @see [WKWizardPanel setStages:]
 */
- (void) setCurrentStage: (NSString *) aStageName
{
  currentStage = [stages indexOfObject: aStageName];
  if (currentStage == NSNotFound)
    {
      [NSException raise: NSInvalidArgumentException
                  format: _(@"-[WKWizardPanel setCurrentStage:]: "
        @"invalid stage name passed. Stage \"%@\" not in stages list: %@."),
        aStageName, stages];
    }

  [self setupForStage: aStageName];

  [[NSNotificationCenter defaultCenter]
    postNotificationName: WKWizardPanelDidChangeCurrentStageNotification
                  object: self
                userInfo: [NSDictionary dictionaryWithObject: aStageName
                                                      forKey: @"Stage"]];
}

/**
 * Returns the current stage which the receiver displays.
 *
 * @return The name of the current stage of the receiver.
 *
 * @see [WKWizardPanel setCurrentStage:]
 */
- (NSArray *) currentStage
{
  return [stages objectAtIndex: currentStage];
}

/**
 * Makes the receiver display the next stage.
 *
 * @see [WKWizardPanel previousStage:]
 * @see [WKWizardPanel setCurrentStage:]
 */
- (void) nextStage: (id) sender
{
  if (currentStage + 1 < [stages count])
    {
      [self setCurrentStage: [stages objectAtIndex: currentStage + 1]];
    }
}

/**
 * Makes the receiver display the previous stage.
 *
 * @see [WKWizardPanel nextStage:]
 * @see [WKWizardPanel setCurrentStage:]
 */
- (void) previousStage: (id) sender
{
  if (currentStage > 0)
    {
      [self setCurrentStage: [stages objectAtIndex: currentStage - 1]];
    }
}

/**
 * Makes the panel activate itself. This means that the panel will
 * become visible and possibly run itself in a modal session (if that
 * was requested). An NSInternalInconsistencyException is raised in
 * case the panel is already active.
 *
 * @return If activation in a modal session has been requested, this
 *      method returns the code with which the modal session ended.
 *      The code can be controlled by -[WKWizardPanel deactivateWithCode:].
 *      In case the receiver isn't modal, or the modal session has been
 *      terminated by -[WKWizardPanel deactivate:], this method always
 *      returns NSRunStoppedResponse.
 *
 * @see [WKWizardPanel deactivate:]
 * @see [WKWizardPanel deactivateWithCode:]
 */
- (int) activate: (id) sender
{
  if (isActive)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: _(@"-[WKWizardPanel activate:]: panel "
        @"already active.")];
    }

  if ([stages count] == 0)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: _(@"-[WKWizardPanel activate:]: no stages "
        @"set. You must set the panel's stages before activating it.")];
    }

  isActive = YES;

  if (initialStage != nil)
    {
      [self setCurrentStage: initialStage];
    }
  else
    {
      [self setCurrentStage: [stages objectAtIndex: 0]];
    }

  if (centersBeforeActivating)
    {
      [self center];
    }

  if (runsInModalSession)
    {
      int code;

      code = [NSApp runModalForWindow: self];

      [self close];

      return code;
    }
  else
    {
      [self makeKeyAndOrderFront: nil];

      return NSRunStoppedResponse;
    }
}

/**
 * Makes the receiver deactivate itself. This means that it will close itself
 * and possibly leave it's modal session (if it was in one). An
 * NSInternalInconsistencyException is raised in case the panel was not
 * active.
 *
 * In case the panel was run in a modal session, the return code of the
 * modal session will be NSRunStoppedResponse.
 *
 * @see [WKWizardPanel activate]
 */
- (void) deactivate: (id) sender
{
  if (isActive == NO)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: _(@"-[WKWizardPanel deactivate:]: panel "
        @"not active.")];
    }

  isActive = NO;

  if (runsInModalSession)
    {
      [NSApp stopModal];
    }
  else
    {
      [self close];
    }
}

/**
 * Deactivates the receiver and closes it. This method is suitable only
 * for use if the receiver was run in a modal session - otherwise use
 * -[WKWizardPanel deactivate:]. It is different from the previous method
 * in that it allows you to specify the return code of the modal session
 * explicitly.
 *
 * @param code The code with which the modal session will exit.
 */
- (void) deactivateWithCode: (int) code
{
  if (isActive == NO)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: _(@"-[WKWizardPanel deactivateWithCode:]: "
                            @"panel not active.")];
    }

  if (runsInModalSession == NO)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: _(@"-[WKWizardPanel deactivateWithCode]: "
        @"panel was not run in modal session. Use \"-deactivate:\" to "
        @"deactivate a non-modal panel instead.")];
    }

  isActive = NO;

  [NSApp stopModalWithCode: code];
}

/**
 * Returns whether the receiver is active or not.
 *
 * @return YES if the receiver is active, NO otherwise.
 */
- (BOOL) isActive
{
  return isActive;
}

// NSCoding protocol

- (void) encodeWithCoder: (NSCoder*) aCoder
{
  [super encodeWithCoder: aCoder];

  if ([aCoder allowsKeyedCoding])
    {
      [aCoder encodeBool: runsInModalSession
                  forKey: @"WKRunsInModalSessionFlag"];
      [aCoder encodeBool: runsInModalSession
                  forKey: @"WKCentersBeforeActivatingFlag"];

      [aCoder encodeObject: stages forKey: @"WKStages"];
      [aCoder encodeObject: initialStage forKey: @"WKInitialStage"];
    }
  else
    {
      [aCoder encodeValueOfObjCType: @encode(BOOL)
                                 at: &runsInModalSession];
      [aCoder encodeValueOfObjCType: @encode(BOOL)
                                 at: &centersBeforeActivating];

      [aCoder encodeObject: stages];
      [aCoder encodeObject: initialStage];
    }
}

- (id) initWithCoder: (NSCoder*) aDecoder
{
  if ((self = [super initWithCoder: aDecoder]) != nil)
    {
      if ([aDecoder allowsKeyedCoding])
        {
          runsInModalSession = [aDecoder decodeBoolForKey:
            @"WKRunsInModalSessionFlag"];
          runsInModalSession = [aDecoder decodeBoolForKey:
            @"WKCentersBeforeActivatingFlag"];

          ASSIGN(stages, [aDecoder decodeObjectForKey: @"WKStages"]);
          ASSIGN(initialStage, [aDecoder
            decodeObjectForKey: @"WKInitialStage"]);
        }
      else
        {
          [aDecoder decodeValueOfObjCType: @encode(BOOL)
                                     at: &runsInModalSession];
          [aDecoder decodeValueOfObjCType: @encode(BOOL)
                                     at: &centersBeforeActivating];

          ASSIGN(stages, [aDecoder decodeObject]);
          ASSIGN(initialStage, [aDecoder decodeObject]);
        }
    }

  return self;
}

@end

/**
 * Private methods of WKWizardPanel.
 *
 * Do not invoke these methods directly.
 */
@implementation WKWizardPanel (Private)

/**
 * Makes the receiver set it's interface to display a certain stage.
 *
 * @param aStage The stage which to display.
 */
- (void) setupForStage: (NSString *) aStage
{
  [self setContentView: [[self delegate] wizardPanel: self
                                        viewForStage: aStage]];
  [self setInitialFirstResponder: [[self delegate] wizardPanel: self
                                 initialFirstResponderForStage: aStage]];
}

@end

/**
 * This informal protocol defines what methods the delegate of a wizard
 * panel may or must implement to control it's appearance to the user.
 */
@implementation NSObject (WKWizardPanelDelegate)

/**
 * Requests a wizard panel's delegate to provide a view which will be
 * put into the panel's main area when in a certain stage. The delegate
 * object will receive this message for every stage switch, i.e. the
 * returned view isn't cached, so that the delegate can change the view
 * which will be used in the same stage. Overriding this method is mandatory.
 *
 * @param sender The wizard panel which sent this message.
 * @param aStageName The stage in which the panel will use the
 *      returned value.
 *
 * @return A view object, which will be put into the panel when in the
 *      specified stage.
 */
- (NSView *) wizardPanel: (WKWizardPanel *) sender
            viewForStage: (NSString *) aStageName
{
  [NSException raise: NSInternalInconsistencyException
              format: _(@"Wizard delegate %@ didn't override %@."),
    [self className], NSStringFromSelector(_cmd)];

  return nil;
}

/**
 * Requests the wizard panel's delegate to provide a view which
 * will be set as the panel's initial first responder at the
 * beginning of a stage. Overriding this method is optional.
 *
 * @return A view object which will be set as the panel's initial
 *      first responder. Returning `nil' indicates `no initial first
 *      responder'.
 */
- (NSView *)       wizardPanel: (WKWizardPanel *) sender
 initialFirstResponderForStage: (NSString *) aStageName
{
  return nil;
}

@end
