/* xscreensaver, Copyright © 2006-2023 Jamie Zawinski <jwz@jwz.org>
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or 
 * implied warranty.
 */

/* This program serves three purposes:

   First, It is a test harness for screen savers.  When it launches, it
   looks around for .saver bundles (in the current directory, and then in
   the standard directories) and puts up a pair of windows that allow you
   to select the saver to run.  This is less clicking than running them
   through System Preferences.  This is the "SaverTester.app" program.

   Second, it can be used to transform any screen saver into a standalone
   program.  Just put one (and only one) .saver bundle into the app
   bundle's Contents/Resources/ directory, and it will load and run that
   saver at start-up (without the saver-selection menu or other chrome).
   This is how the "Phosphor.app" and "Apple2.app" programs work.

   Third, it is the scaffolding which turns a set of screen savers into
   a single iPhone / iPad program.  In that case, all of the savers are
   linked in to this executable, since iOS does not allow dynamic loading
   of bundles that have executable code in them.  Bleh.
 */

#import <TargetConditionals.h>
#import "SaverRunner.h"
#import "SaverListController.h"
#import "XScreenSaverGLView.h"
#import "yarandom.h"
#import "nslog.h"

#ifdef HAVE_IPHONE

# ifndef __IPHONE_8_0
#  define UIInterfaceOrientationUnknown UIDeviceOrientationUnknown
# endif
# ifndef NSFoundationVersionNumber_iOS_7_1
#  define NSFoundationVersionNumber_iOS_7_1 1047.25
# endif
# ifndef NSFoundationVersionNumber_iOS_8_0
#  define NSFoundationVersionNumber_iOS_8_0 1134.10
# endif

@interface RotateyViewController : UINavigationController
{
  BOOL allowRotation;
}
@end

@implementation RotateyViewController

/* This subclass exists so that we can ask that the SaverListController and
   preferences panels be auto-rotated by the system.  Note that the 
   XScreenSaverView is not auto-rotated because it is on a different UIWindow.
 */

- (id)initWithRotation:(BOOL)rotatep
{
  self = [super init];
  allowRotation = rotatep;
  return self;
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
{
  return allowRotation;				/* Deprecated in iOS 6 */
}
#pragma clang diagnostic pop

# ifndef HAVE_TVOS
- (BOOL)shouldAutorotate			/* Added in iOS 6 */
{
  return allowRotation;
}

- (UIInterfaceOrientationMask)supportedInterfaceOrientations	/* Added in iOS 6 */
{
  return UIInterfaceOrientationMaskAll;
}
# endif // !HAVE_TVOS

@end


@implementation SaverViewController

- (id)initWithSaverRunner:(SaverRunner *)parent
             showAboutBox:(BOOL)showAboutBox
{
  self = [super init];
  if (self) {
    _parent = parent;
    _showAboutBox = showAboutBox;

    self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;

# ifndef __IPHONE_7_0
    self.wantsFullScreenLayout = YES;    // Deprecated as of iOS 7
# endif
  }
  return self;
}

# ifndef HAVE_TVOS
- (BOOL) prefersStatusBarHidden
{
  // Requires UIViewControllerBasedStatusBarAppearance = true in plist
  return YES;
}
# endif // !HAVE_TVOS


- (void)dealloc
{
  [_saver_title release];
  // iOS: When a UIView deallocs, it doesn't do [UIView removeFromSuperView]
  // for its subviews, so the subviews end up with a dangling pointer in their
  // superview properties.
  [aboutBox removeFromSuperview];
  [aboutBox release];
  [_saverView removeFromSuperview];
  [_saverView release];
  [super dealloc];
}


- (void)loadView
{
  // The UIViewController's view must never change, so it gets set here to
  // a plain black background.

  // This background view doesn't block the status bar, but that's probably
  // OK, because it's never on screen for more than a fraction of a second.
  UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectNull];
  backgroundView.backgroundColor = [UIColor blackColor];
  self.view = backgroundView;
  [backgroundView release];
}

# ifndef HAVE_TVOS
- (void)aboutPanel:(UIView *)saverView
       orientation:(UIInterfaceOrientation)orient
{
  if (!_showAboutBox)
    return;

  NSString *year = [_parent makeDesc:_saver_title yearOnly:YES];

  CGRect frame = [saverView frame];
  CGFloat rot;
  CGFloat pt1 = 24;
  CGFloat pt2 = 14;
  UIFont *font1 = [UIFont boldSystemFontOfSize:  pt1];
  UIFont *font2 = [UIFont italicSystemFontOfSize:pt2];

# ifdef __IPHONE_7_0
  CGSize s = CGSizeMake(frame.size.width, frame.size.height);
  CGSize tsize1 = [[[NSAttributedString alloc]
                     initWithString:_saver_title
                     attributes:@{ NSFontAttributeName: font1 }]
                    boundingRectWithSize: s
                    options: NSStringDrawingUsesLineFragmentOrigin
                    context: nil].size;
  CGSize tsize2 = [[[NSAttributedString alloc]
                     initWithString:_saver_title
                     attributes:@{ NSFontAttributeName: font2 }]
                    boundingRectWithSize: s
                    options: NSStringDrawingUsesLineFragmentOrigin
                    context: nil].size;
# else // iOS 6 or Cocoa
  CGSize tsize1 = [name sizeWithFont:font1
                   constrainedToSize:CGSizeMake(frame.size.width,
                                                frame.size.height)];
  CGSize tsize2 = [year sizeWithFont:font2
                   constrainedToSize:CGSizeMake(frame.size.width,
                                                frame.size.height)];
# endif

  CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ?
                             tsize1.width : tsize2.width,
                             tsize1.height + tsize2.height);

  tsize.width  = ceilf(tsize.width);
  tsize.height = ceilf(tsize.height);

  // Don't know how to find inner margin of UITextView.
  CGFloat margin = 10;
  tsize.width  += margin * 4;
  tsize.height += margin * 2;

  if ([saverView frame].size.width >= 768)
    tsize.height += pt1 * 3;  // extra bottom margin on iPad

  frame = CGRectMake (0, 0, tsize.width, tsize.height);

  /* Get the text oriented properly, and move it to the bottom of the
     screen, since many savers have action in the middle.
   */
  switch (orient) {
  case UIInterfaceOrientationLandscapeLeft:
    rot = -M_PI/2;
    frame.origin.x = ([saverView frame].size.width
                      - (tsize.width - tsize.height) / 2
                      - tsize.height);
    frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
    break;
  case UIInterfaceOrientationLandscapeRight:
    rot = M_PI/2;
    frame.origin.x = -(tsize.width - tsize.height) / 2;
    frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
    break;
  case UIInterfaceOrientationPortraitUpsideDown:
    rot = M_PI;
    frame.origin.x = ([saverView frame].size.width  - tsize.width) / 2;
    frame.origin.y = 0;
    break;
  default:
    rot = 0;
    frame.origin.x = ([saverView frame].size.width  - tsize.width) / 2;
    frame.origin.y =  [saverView frame].size.height - tsize.height;
    break;
  }

  if (aboutBox) {
    [aboutBox removeFromSuperview];
    [aboutBox release];
  }

  aboutBox = [[UIView alloc] initWithFrame:frame];

  aboutBox.transform = CGAffineTransformMakeRotation (rot);
  aboutBox.backgroundColor = [UIColor clearColor];

  /* There seems to be no easy way to stroke the font, so instead draw
     it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add
     a black shadow to each.  (You'd think the shadow alone would be
     enough, but there's no way to make it dark enough to be legible.)
   */
  for (int i = 0; i < 5; i++) {
    UITextView *textview;
    int off = 1;
    frame.origin.x = frame.origin.y = 0;
    switch (i) {
      case 0: frame.origin.x = -off; break;
      case 1: frame.origin.x =  off; break;
      case 2: frame.origin.y = -off; break;
      case 3: frame.origin.y =  off; break;
    }

    for (int j = 0; j < 2; j++) {

      frame.origin.y = (j == 0 ? 0 : pt1);
      textview = [[UITextView alloc] initWithFrame:frame];
      textview.font = (j == 0 ? font1 : font2);
      textview.text = (j == 0 ? _saver_title : year);
      textview.textAlignment = NSTextAlignmentCenter;
      textview.showsHorizontalScrollIndicator = NO;
      textview.showsVerticalScrollIndicator   = NO;
      textview.scrollEnabled = NO;
      textview.editable = NO;
      textview.userInteractionEnabled = NO;
      textview.backgroundColor = [UIColor clearColor];
      textview.textColor = (i == 4 
                            ? [UIColor yellowColor]
                            : [UIColor blackColor]);

      CALayer *textLayer = (CALayer *)
        [textview.layer.sublayers objectAtIndex:0];
      textLayer.shadowColor   = [UIColor blackColor].CGColor;
      textLayer.shadowOffset  = CGSizeMake(0, 0);
      textLayer.shadowOpacity = 1;
      textLayer.shadowRadius  = 2;

      [aboutBox addSubview:textview];
    }
  }

  CABasicAnimation *anim = 
    [CABasicAnimation animationWithKeyPath:@"opacity"];
  anim.duration     = 0.3;
  anim.repeatCount  = 1;
  anim.autoreverses = NO;
  anim.fromValue    = [NSNumber numberWithFloat:0.0];
  anim.toValue      = [NSNumber numberWithFloat:1.0];
  [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];

  [saverView addSubview:aboutBox];

  if (splashTimer)
    [splashTimer invalidate];

  splashTimer =
    [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2
             target:self
             selector:@selector(aboutOff)
             userInfo:nil
             repeats:NO];
}
# endif // !HAVE_TVOS


- (void)aboutOff
{
  [self aboutOff:FALSE];
}

- (void)aboutOff:(BOOL)fast
{
  if (aboutBox) {
    if (splashTimer) {
      [splashTimer invalidate];
      splashTimer = 0;
    }
    if (fast) {
      aboutBox.layer.opacity = 0;
      return;
    }

    CABasicAnimation *anim = 
      [CABasicAnimation animationWithKeyPath:@"opacity"];
    anim.duration     = 0.3;
    anim.repeatCount  = 1;
    anim.autoreverses = NO;
    anim.fromValue    = [NSNumber numberWithFloat: 1];
    anim.toValue      = [NSNumber numberWithFloat: 0];
    // anim.delegate     = self;
    aboutBox.layer.opacity = 0;
    [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
  }
}


- (void)createSaverView
{
  UIView *parentView = self.view;

  if (_saverView) {
    [_saverView removeFromSuperview];
    [_saverView release];
  }

  _saverView = [_parent newSaverView: _saver_title
                            withSize: parentView.bounds.size];

  if (! _saverView) {
    UIAlertController *c = [UIAlertController
                             alertControllerWithTitle:
                               NSLocalizedString(@"Unable to load!", @"")
                             message:@""
                             preferredStyle:UIAlertControllerStyleAlert];
    [c addAction: [UIAlertAction actionWithTitle:
                                   NSLocalizedString(@"Bummer", @"")
                                 style: UIAlertActionStyleDefault
                                 handler: ^(UIAlertAction *a) {
      // #### Should expose the SaverListController...
    }]];
    [self presentViewController:c animated:YES completion:nil];

    return;
  }

  _saverView.delegate = _parent;
  _saverView.autoresizingMask =
    UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

  [self.view addSubview:_saverView];

  // The first responder must be set only after the view was placed in the view
  // heirarchy.
  [_saverView becomeFirstResponder]; // For shakes on iOS 6.
  [_saverView startAnimation];
# ifndef HAVE_TVOS
  [self aboutPanel:_saverView
       orientation: UIInterfaceOrientationPortrait];
# endif // !HAVE_TVOS
}


- (void)viewDidAppear:(BOOL)animated
{
  [super viewDidAppear:animated];
  [self createSaverView];
}


# ifndef HAVE_TVOS

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
{
  return NO;					/* Deprecated in iOS 6 */
}
#pragma clang diagnostic pop


- (BOOL)shouldAutorotate			/* Added in iOS 6 */
{
  return
    NSFoundationVersionNumber < NSFoundationVersionNumber_iOS_8_0 ?
    ![_saverView suppressRotationAnimation] :
    YES;
}


- (UIInterfaceOrientationMask)supportedInterfaceOrientations	/* Added in iOS 6 */
{
  // Lies from the iOS docs:
  // "This method is only called if the view controller's shouldAutorotate
  // method returns YES."
  return UIInterfaceOrientationMaskAll;
}


/*
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
  return UIInterfaceOrientationPortrait;
}
*/

# endif // !HAVE_TVOS


- (void)setSaverTitle:(NSString *)title
{
  [title retain];
  [_saver_title release];
  _saver_title = title;
  if (_saverView)
    [self createSaverView];
}


- (void)setSaver_title:(NSString *)title
{
  [self setSaverTitle: title];
}
- (void)setSaver_Title:(NSString *)title
{
  abort();
}


- (void)viewWillTransitionToSize: (CGSize)size
       withTransitionCoordinator: 
        (id<UIViewControllerTransitionCoordinator>) coordinator
{
  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
 
  if (!_saverView)
    return;

  [CATransaction begin];

  // Completely suppress the rotation animation, since we
  // will not (visually) be rotating at all.
  if ([_saverView suppressRotationAnimation])
    [CATransaction setDisableActions:YES];

  [self aboutOff:TRUE];  // It does goofy things if we rotate while it's up

# if 1
  [CATransaction commit];
  [_saverView orientationChanged];
  return;
# endif

  // BOOL queued =
  [coordinator animateAlongsideTransition:^
               (id <UIViewControllerTransitionCoordinatorContext> context) {
    // This executes repeatedly during the rotation.
  } completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
    // This executes once when the rotation has finished.
    [CATransaction commit];
    [_saverView orientationChanged];
  }];
  // No code goes here, as it would execute before the above completes.
}

/* Not called
- (void)willTransitionToTraitCollection:(UITraitCollection *)collection
              withTransitionCoordinator:
                (id<UIViewControllerTransitionCoordinator>)coordinator
{
  NSLog(@"#### %@ %@", collection, coordinator);
}
*/

@end

#endif // HAVE_IPHONE


@implementation SaverRunner

- (ScreenSaverView *) newSaverView: (NSString *) title
                          withSize: (NSSize) size
{
  Class new_class = 0;

  NSString *classname = [saverNames objectForKey: title];

# ifndef HAVE_IPHONE

  // Load the XScreenSaverView subclass and code from a ".saver" bundle.

  NSString *path = [[saverDir stringByAppendingPathComponent:classname]
                     stringByAppendingPathExtension:@"saver"];
  if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) {
    NSLog(@"bundle \"%@\" does not exist", path);
    return 0;
  }

  NSLog(@"Loading %@", path);

  // NSBundle *obundle = saverBundle;

  saverBundle = [NSBundle bundleWithPath:path];
  if (saverBundle)
    new_class = [saverBundle principalClass];

  // Not entirely unsurprisingly, this tends to break the world.
  // if (obundle && obundle != saverBundle)
  //  [obundle unload];

# else  // HAVE_IPHONE

  // Determine whether to create an X11 view or an OpenGL view by
  // looking for the "gl" tag in the xml file.  This is kind of awful.

  NSString *file = [classname lowercaseString];
  NSString *path = [saverDir
                     stringByAppendingPathComponent:
                       [file stringByAppendingPathExtension:@"xml"]];
  NSData *xmld = [NSData dataWithContentsOfFile:path];
  NSAssert (xmld, @"no XML: %@", path);
  NSString *xml = [XScreenSaverView decompressXML:xmld];
  Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0);

  new_class = (gl_p
               ? [XScreenSaverGLView class]
               : [XScreenSaverView class]);

# endif // HAVE_IPHONE

  if (! new_class)
    return 0;

  NSRect rect;
  rect.origin.x = rect.origin.y = 0;
  rect.size.width  = size.width;
  rect.size.height = size.height;

  NSObject *instance = [new_class alloc];
  if ([instance respondsToSelector:
                  @selector(initWithFrame:title:isPreview:)])
    // If it's one of ours, also pass the title.
    instance = [(XScreenSaverView *) instance
                   initWithFrame: rect
                           title: title
                       isPreview: YES];
  else if ([instance respondsToSelector: @selector(initWithFrame:isPreview:)])
    instance = [(ScreenSaverView *) instance
                   initWithFrame: rect
                       isPreview: YES];
  else
    instance = 0;

  if (! instance) {
    NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, saver_title);
    return 0;
  }


  /* KLUGE: Inform the underlying program that we're in "standalone"
     mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver".
     This is kind of horrible but I haven't thought of a more sensible
     way to make this work.
   */
# ifndef HAVE_IPHONE
  if ([saverNames count] == 1) {
    setenv ("XSCREENSAVER_STANDALONE", "1", 1);
  }
# endif

  return (ScreenSaverView *) instance;
}


#ifndef HAVE_IPHONE

static ScreenSaverView *
find_saverView_child (NSView *v)
{
  NSArray *kids = [v subviews];
  NSUInteger nkids = [kids count];
  NSUInteger i;
  for (i = 0; i < nkids; i++) {
    NSObject *kid = [kids objectAtIndex:i];
    if ([kid isKindOfClass:[ScreenSaverView class]]) {
      return (ScreenSaverView *) kid;
    } else {
      ScreenSaverView *sv = find_saverView_child ((NSView *) kid);
      if (sv) return sv;
    }
  }
  return 0;
}


static ScreenSaverView *
find_saverView (NSView *v)
{
  while (1) {
    NSView *p = [v superview];
    if (p) v = p;
    else break;
  }
  return find_saverView_child (v);
}


/* Changes the contents of the menubar menus to correspond to
   the running saver.  Desktop only.
 */
static void
relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
{
  if ([v isKindOfClass:[NSMenu class]]) {
    NSMenu *m = (NSMenu *)v;
    [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str
                            withString:new_str]];
    NSArray *kids = [m itemArray];
    NSUInteger nkids = [kids count];
    NSUInteger i;
    for (i = 0; i < nkids; i++) {
      relabel_menus ([kids objectAtIndex:i], old_str, new_str);
    }
  } else if ([v isKindOfClass:[NSMenuItem class]]) {
    NSMenuItem *mi = (NSMenuItem *)v;
    [mi setTitle: [[mi title] stringByReplacingOccurrencesOfString:old_str
                              withString:new_str]];
    NSMenu *m = [mi submenu];
    if (m) relabel_menus (m, old_str, new_str);
  }
}


- (void) openPreferences: (id) sender
{
  ScreenSaverView *sv;
  if ([sender isKindOfClass:[NSView class]]) {	// Sent from button
    sv = find_saverView ((NSView *) sender);
  } else {
    long i;
    NSWindow *w = 0;
    for (i = [windows count]-1; i >= 0; i--) {	// Sent from menubar
      w = [windows objectAtIndex:i];
      if ([w isKeyWindow]) break;
    }
    sv = find_saverView ([w contentView]);
  }

  NSAssert (sv, @"no saver view");
  if (!sv) return;
  NSWindow *prefs = [sv configureSheet];
  NSAssert (prefs, @"no configureSheet in %@", sv);

  [sv.window beginSheet:prefs
      completionHandler:^(NSInteger code) {
      [NSApp stopModalWithCode: code];
    }];

  NSUInteger code = [NSApp runModalForWindow:prefs];
  
  /* Restart the animation if the "OK" button was hit, but not if "Cancel".
     We have to restart *both* animations, because the xlockmore-style
     ones will blow up if one re-inits but the other doesn't.
   */
  if (code != NSModalResponseCancel) {
    if ([sv isAnimating])
      [sv stopAnimation];
    [sv startAnimation];
  }
}


#else  // HAVE_IPHONE


- (UIImage *) screenshot
{
  return saved_screenshot;
}

- (void) saveScreenshot
{
  // Most of this is from:
  // http://developer.apple.com/library/ios/#qa/qa1703/_index.html
  // The rotation stuff is by me.

  CGSize size = [[UIScreen mainScreen] bounds].size;

  // iOS 7: Needs to be [[window rootViewController] interfaceOrientation].
  // iOS 8: Needs to be UIInterfaceOrientationPortrait.
  // (interfaceOrientation deprecated in iOS 8)

# ifndef HAVE_TVOS
  UIInterfaceOrientation orient = UIInterfaceOrientationPortrait;
  /* iOS 8 broke -[UIScreen bounds]. */

  if (orient == UIInterfaceOrientationLandscapeLeft ||
      orient == UIInterfaceOrientationLandscapeRight) {
    // Rotate the shape of the canvas 90 degrees.
    double s = size.width;
    size.width = size.height;
    size.height = s;
  }
# endif // !HAVE_TVOS


  // Create a graphics context with the target size
  // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to
  // take the scale into consideration
  // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext

  UIGraphicsBeginImageContextWithOptions (size, NO, 0);

  CGContextRef ctx = UIGraphicsGetCurrentContext();


# ifndef HAVE_TVOS
  // Rotate the graphics context to match current hardware rotation.
  //
  switch (orient) {
  case UIInterfaceOrientationPortraitUpsideDown:
    CGContextTranslateCTM (ctx,  [window center].x,  [window center].y);
    CGContextRotateCTM (ctx, M_PI);
    CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
    break;
  case UIInterfaceOrientationLandscapeLeft:
  case UIInterfaceOrientationLandscapeRight:
    CGContextTranslateCTM (ctx,  
                           ([window frame].size.height -
                            [window frame].size.width) / 2,
                           ([window frame].size.width -
                            [window frame].size.height) / 2);
    CGContextTranslateCTM (ctx,  [window center].x,  [window center].y);
    CGContextRotateCTM (ctx, 
                        (orient == UIInterfaceOrientationLandscapeLeft
                         ?  M_PI/2
                         : -M_PI/2));
    CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
    break;
  default:
    break;
  }
# endif // !HAVE_TVOS


  // Iterate over every window from back to front
  //
  for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
    if (![win respondsToSelector:@selector(screen)] ||
        [win screen] == [UIScreen mainScreen]) {

      // -renderInContext: renders in the coordinate space of the layer,
      // so we must first apply the layer's geometry to the graphics context
      CGContextSaveGState (ctx);

      // Center the context around the window's anchor point
      CGContextTranslateCTM (ctx, [win center].x, [win center].y);

      // Apply the window's transform about the anchor point
      CGContextConcatCTM (ctx, [win transform]);

      // Offset by the portion of the bounds left of and above anchor point
      CGContextTranslateCTM (ctx,
        -[win bounds].size.width  * [[win layer] anchorPoint].x,
        -[win bounds].size.height * [[win layer] anchorPoint].y);

      // Render the layer hierarchy to the current context
      [[win layer] renderInContext:ctx];

      // Restore the context
      CGContextRestoreGState (ctx);
    }
  }

  if (saved_screenshot)
    [saved_screenshot release];
  saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain];

  UIGraphicsEndImageContext();
}


- (void) openPreferences: (NSString *) saver
{
  ScreenSaverView *saverView = [self newSaverView:saver
                                         withSize:CGSizeMake(0, 0)];
  if (! saverView) return;

  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
  [prefs setObject:saver forKey:@"selectedSaverName"];
  [prefs synchronize];

  [rotating_nav pushViewController: [saverView configureView]
                      animated:YES];
}


#endif // HAVE_IPHONE



- (void)loadSaver:(NSString *)title
{
# ifndef HAVE_IPHONE

  if (saver_title && [saver_title isEqualToString: title]) {
    for (NSWindow *win in windows) {
      ScreenSaverView *sv = find_saverView ([win contentView]);
      if (![sv isAnimating])
        [sv startAnimation];
    }
    return;
  }

  [title retain];
  [saver_title release];
  saver_title = title;

  for (NSWindow *win in windows) {
    NSView *cv = [win contentView];
    NSString *old_title = [win title];
    if (!old_title) old_title = @"XScreenSaver";
    [win setTitle: title];
    relabel_menus (menubar, old_title, title);

    ScreenSaverView *old_view = find_saverView (cv);
    NSView *sup = old_view ? [old_view superview] : cv;

    if (old_view) {
      if ([old_view isAnimating])
        [old_view stopAnimation];
      [old_view removeFromSuperview];
    }

    NSSize size = [sup frame].size;
    ScreenSaverView *new_view = [self newSaverView: title withSize: size];
    NSAssert (new_view, @"unable to make a saver view");

    [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
    [sup addSubview: new_view];
    [win makeFirstResponder:new_view];
    [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
    [new_view startAnimation];
    [new_view release];
  }

  NSUserDefaultsController *ctl =
    [NSUserDefaultsController sharedUserDefaultsController];
  [ctl save:self];

#  if 0
  // Dump the entire resource database.
  NSBundle *nsb = [NSBundle bundleForClass:[self class]];
  NSLog(@"SaverRunner userDefaults for %@", [nsb bundleIdentifier]);
  NSDictionary *d = [[ctl defaults] dictionaryRepresentation];
  for (NSObject *key in [[d allKeys]
                          sortedArrayUsingSelector:@selector(compare:)]) {
    NSObject *val = [d objectForKey:key];
    NSLog (@"%@ = %@", key, val);
  }
#  endif

# else  // HAVE_IPHONE

  [title retain];
  [saver_title release];
  saver_title = title;

#  if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
  NSLog (@"selecting saver \"%@\"", saver_title);
#  endif

  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
  [prefs setObject:saver_title forKey:@"selectedSaverName"];
  [prefs synchronize];

/* Cacheing this screws up rotation when starting a saver twice in a row.
  if (saver_title && [saver_title isEqualToString: title]) {
    if ([saverView isAnimating])
      return;
    else
      goto LAUNCH;
  }
*/

  [title retain];
  [saver_title release];
  saver_title = title;

  if (nonrotating_controller) {
    nonrotating_controller.saver_title = saver_title;
    return;
  }

# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
  UIScreen *screen = [UIScreen mainScreen];

  /* 'nativeScale' is very confusing.

     iPhone 4s:
        bounds:        320x480   scale:        2
        nativeBounds:  640x960   nativeScale:  2
     iPhone 5s:
        bounds:        320x568   scale:        2
        nativeBounds:  640x1136  nativeScale:  2
     iPad 2:
        bounds:       768x1024   scale:        1
        nativeBounds: 768x1024   nativeScale:  1
     iPad Retina/Air:
        bounds:       768x1024   scale:        2
        nativeBounds: 1536x2048  nativeScale:  2
     iPhone 6:
        bounds:        320x568   scale:        2
        nativeBounds:  640x1136  nativeScale:  2
     iPhone 6+:
        bounds:        320x568   scale:        2
        nativeBounds:  960x1704  nativeScale:  3

     According to a StackOverflow comment:

       The iPhone 6+ renders internally using @3x assets at a virtual
       resolution of 2208x1242 (with 736x414 points), then samples that down
       for display. The same as using a scaled resolution on a Retina MacBook
       -- it lets them hit an integral multiple for pixel assets while still
       having e.g. 12pt text look the same size on the screen.

       The 6, the 5s, the 5, the 4s and the 4 are all 326 pixels per inch,
       and use @2x assets to stick to the approximately 160 points per inch
       of all previous devices.

       The 6+ is 401 pixels per inch. So it'd hypothetically need roughly
       @2.46x assets. Instead Apple uses @3x assets and scales the complete
       output down to about 84% of its natural size.

       In practice Apple has decided to go with more like 87%, turning the
       1080 into 1242. No doubt that was to find something as close as
       possible to 84% that still produced integral sizes in both directions
       -- 1242/1080 = 2208/1920 exactly, whereas if you'd turned the 1080
       into, say, 1286, you'd somehow need to render 2286.22 pixels
       vertically to scale well.
   */

  NSLog(@"screen: %.0fx%0.f",
        [[screen currentMode] size].width,
        [[screen currentMode] size].height);
  NSLog(@"bounds: %.0fx%0.f x %.1f = %.0fx%0.f",
        [screen bounds].size.width,
        [screen bounds].size.height,
        [screen scale],
        [screen scale] * [screen bounds].size.width,
        [screen scale] * [screen bounds].size.height);

#  ifdef __IPHONE_8_0
  if ([screen respondsToSelector:@selector(nativeBounds)])
    NSLog(@"native: %.0fx%0.f / %.1f = %.0fx%0.f",
          [screen nativeBounds].size.width,
          [screen nativeBounds].size.height,
          [screen nativeScale],
          [screen nativeBounds].size.width  / [screen nativeScale],
          [screen nativeBounds].size.height / [screen nativeScale]);
#  endif
# endif // TARGET_IPHONE_SIMULATOR

  // Take the screen shot before creating the screen saver view, because this
  // can screw with the layout.
  [self saveScreenshot];

  // iOS 3.2. Before this were iPhones (and iPods) only, which always did modal
  // presentation full screen.
  rotating_nav.modalPresentationStyle = UIModalPresentationFullScreen;

  nonrotating_controller = [[SaverViewController alloc]
                            initWithSaverRunner:self
                            showAboutBox:[saverNames count] != 1];
  nonrotating_controller.saver_title = title;

  // Necessary to prevent "card"-like presentation on Xcode 11 with iOS 13:
  nonrotating_controller.modalPresentationStyle =
    UIModalPresentationFullScreen;

  /* LAUNCH: */

  [rotating_nav presentViewController:nonrotating_controller animated:NO completion:nil];

  // Doing this makes savers cut back to the list instead of fading,
  // even though [XScreenSaverView stopAndClose] does setHidden:NO first.
  // [window setHidden:YES];

# endif // HAVE_IPHONE
}


#ifndef HAVE_IPHONE

- (void)aboutPanel:(id)sender
{
  NSDictionary *bd = [saverBundle infoDictionary];
  NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];

  [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
  [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
  [d setValue:[bd objectForKey:@"CFBundleShortVersionString"] 
     forKey:@"ApplicationVersion"];
  [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
  NSAttributedString *s = [[NSAttributedString alloc]
                           initWithString: (NSString *)
                           [bd objectForKey:@"CFBundleGetInfoString"]];
  [d setValue:s forKey:@"Credits"];
  [s release];
  
  [[NSApplication sharedApplication]
    orderFrontStandardAboutPanelWithOptions:d];
}

#endif // !HAVE_IPHONE



- (void)selectedSaverDidChange:(NSDictionary *)change
{
  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
  NSString *title = [prefs stringForKey:@"selectedSaverName"];

  if (! title) return;

  if (! [saverNames objectForKey:title]) {
    NSLog (@"saver \"%@\" does not exist", title);
    return;
  }

  [self loadSaver: title];
}


- (NSDictionary *) listSaverBundleNamesInDir:(NSString *)dir
{
# ifndef HAVE_IPHONE
  NSString *ext = @"saver";
# else
  NSString *ext = @"xml";
# endif

  NSArray *files = [[NSFileManager defaultManager]
                     contentsOfDirectoryAtPath:dir error:nil];
  if (! files) return 0;
  NSMutableDictionary *result =
    [NSMutableDictionary dictionaryWithCapacity:[files count]+1];

  for (NSString *p in files) {
    if ([[p pathExtension] caseInsensitiveCompare: ext])
      continue;

    p = [dir stringByAppendingPathComponent: p];

    NSString *classname = [[p lastPathComponent] stringByDeletingPathExtension];
    NSString *title = classname;

    // Get the title's capitalization right by reading the XML file.

# ifdef HAVE_IPHONE
    NSData *xmld = [NSData dataWithContentsOfFile:p];
    NSAssert (xmld, @"no XML: %@", p);
    NSString *xml = [XScreenSaverView decompressXML:xmld];

    NSRange r = [xml rangeOfString:@"_label=\"" options:0];
    NSAssert1 (r.length, @"no name in %@", p);
    if (r.length) {
      xml = [xml substringFromIndex: r.location + r.length];
      r = [xml rangeOfString:@"\"" options:0];
      NSAssert1 (r.length, @"no name in %@", p);
      if (r.length)
        title = [xml substringToIndex: r.location];
    }
# else  // !HAVE_IPHONE
    NSBundle *nsb = [NSBundle bundleWithPath:p];
    title = [[nsb infoDictionary] objectForKey:@"CFBundleName"];
# endif // !HAVE_IPHONE

    [result setObject:classname forKey:title];
  }

  return [result count] ? result : 0;
}



- (void) listSaverBundleNames
{
  NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];

# ifndef HAVE_IPHONE
  // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
  // directories in the bundle.
  [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
                      stringByAppendingPathComponent:@"Contents"]
                     stringByAppendingPathComponent:@"Resources"]];
  [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];

  // Also look in the same directory as the executable.
  [dirs addObject: [[[NSBundle mainBundle] bundlePath]
                     stringByDeletingLastPathComponent]];

  // Finally, look in standard MacOS screensaver directories.
//  [dirs addObject: [@"~/Library/Screen Savers" 
//                     stringByExpandingTildeInPath]];
//  [dirs addObject: [@"/Library/Screen Savers"
//                     stringByExpandingTildeInPath]];
//  [dirs addObject: [@"/System/Library/Screen Savers"
//                     stringByExpandingTildeInPath]];

# else  // HAVE_IPHONE

  // On iOS, only look in the bundle's root directory.
  [dirs addObject: [[NSBundle mainBundle] bundlePath]];

# endif // HAVE_IPHONE

  int i;
  for (i = 0; i < [dirs count]; i++) {
    NSString *dir = [dirs objectAtIndex:i];
    NSDictionary *names = [self listSaverBundleNamesInDir:dir];
    if (! names) continue;
    saverDir   = [dir retain];
    saverNames = [names retain];
    return;
  }

  NSString *err = @"no .saver bundles found in: ";
  for (i = 0; i < [dirs count]; i++) {
    if (i) err = [err stringByAppendingString:@", "];
    err = [err stringByAppendingString:[[dirs objectAtIndex:i] 
                                         stringByAbbreviatingWithTildeInPath]];
    err = [err stringByAppendingString:@"/"];
  }
  NSLog (@"%@", err);
}


/* Create the popup menu of available saver names.
 */
#ifndef HAVE_IPHONE

- (NSPopUpButton *) makeMenu
{
  NSRect rect;
  rect.origin.x = rect.origin.y = 0;
  rect.size.width = 10;
  rect.size.height = 10;
  NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
                                                    pullsDown:NO];
  float max_width = 0;
  NSArray *titles = [[saverNames allKeys]
                        sortedArrayUsingSelector:
                       @selector(localizedCaseInsensitiveCompare:)];
  for (NSString *title in titles) {
    [popup addItemWithTitle: title];
    [[popup itemWithTitle: title] setRepresentedObject: title];
    [popup sizeToFit];
    NSRect r = [popup frame];
    if (r.size.width > max_width) max_width = r.size.width;
  }

  // Bind the menu to preferences, and trigger a callback when an item
  // is selected.
  //
  NSString *key = @"values.selectedSaverName";
  NSUserDefaultsController *prefs =
    [NSUserDefaultsController sharedUserDefaultsController];
  [prefs addObserver:self
         forKeyPath:key
            options:0
            context:@selector(selectedSaverDidChange:)];
  [popup   bind:@"selectedObject"
       toObject:prefs
    withKeyPath:key
        options:nil];
  [prefs setAppliesImmediately:YES];

  NSRect r = [popup frame];
  r.size.width = max_width;
  [popup setFrame:r];
  [popup autorelease];
  return popup;
}

#else  // HAVE_IPHONE

- (NSString *) makeDesc:(NSString *) title
               yearOnly:(BOOL) yearp
{
  NSString *desc = 0;
  NSString *classname = [saverNames objectForKey: title];
  NSString *file = [classname lowercaseString];
  NSString *path = [[saverDir stringByAppendingPathComponent: file]
                     stringByAppendingPathExtension:@"xml"];
  NSData *xmld = [NSData dataWithContentsOfFile:path];
  if (! xmld) goto FAIL;
  desc = [XScreenSaverView decompressXML:xmld];
  if (! desc) goto FAIL;

  NSRange r = [desc rangeOfString:@"<_description>"
                          options:NSCaseInsensitiveSearch];
  if (r.length == 0) {
    desc = 0;
    goto FAIL;
  }
  desc = [desc substringFromIndex: r.location + r.length];
  r = [desc rangeOfString:@"</_description>"
            options:NSCaseInsensitiveSearch];
  if (r.length > 0)
    desc = [desc substringToIndex: r.location];

  // Leading and trailing whitespace.
  desc = [desc stringByTrimmingCharactersInSet:
                 [NSCharacterSet whitespaceAndNewlineCharacterSet]];

  // Let's see if we can find a year on the last line.
  r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
  NSString *year = 0;
  for (NSString *word in
         [[desc substringFromIndex:r.location + r.length]
           componentsSeparatedByCharactersInSet:
             [NSCharacterSet characterSetWithCharactersInString:
                               @" \t\n-."]]) {
    int n = [word doubleValue];
    if (n > 1970 && n < 2100)
      year = word;
  }

  // Delete everything after the first blank line.
  //
  r = [desc rangeOfString:@"\n\n" options:0];
  if (r.length > 0)
    desc = [desc substringToIndex: r.location];

  // Unwrap lines and compress whitespace.
  {
    NSString *result = @"";
    for (NSString *s in [desc componentsSeparatedByCharactersInSet:
                          [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
      if ([result length] == 0)
        result = s;
      else if ([s length] > 0)
        result = [NSString stringWithFormat: @"%@ %@", result, s];
      desc = result;
    }
  }

  if (year)
    desc = [year stringByAppendingString:
                   [@": " stringByAppendingString: desc]];

  if (yearp)
    desc = year ? year : @"";

FAIL:
  if (! desc) {
    if ([saverNames count] > 1)
      desc = @"Oops, this module appears to be incomplete.";
    else
      desc = @"";
  }

  return desc;
}

- (NSString *) makeDesc:(NSString *)saver
{
  return [self makeDesc:saver yearOnly:NO];
}



/* Create a dictionary of one-line descriptions of every saver,
   for display on the UITableView.
 */
- (NSDictionary *)makeDescTable
{
  NSMutableDictionary *dict = 
    [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
  for (NSString *title in [saverNames allKeys]) {
    [dict setObject:[self makeDesc:title] forKey:title];
  }
  return dict;
}


// Called from iOS XScreenSaverView when the "Stop" or "Settings"
// buttons are pressed.
//
- (void) wantsFadeOut:(XScreenSaverView *)sender
{
  rotating_nav.view.hidden = NO; // In case it was hidden during startup.

  /* Make sure the most-recently-run saver is visible.  Sometimes it ends
     up scrolled half a line off the bottom of the screen.
   */
  if (saver_title) {
    for (UIViewController *v in [rotating_nav viewControllers]) {
      if ([v isKindOfClass:[SaverListController class]]) {
        [(SaverListController *)v scrollTo: saver_title];
        break;
      }
    }
  }

  [rotating_nav dismissViewControllerAnimated:YES completion:^() {
    [nonrotating_controller release];
    nonrotating_controller = nil;
    [[rotating_nav view] becomeFirstResponder];
  }];
}


// Called from iOS XScreenSaverView when the cycle timer fires.
//
- (void) didShake:(XScreenSaverView *)sender
{
# if TARGET_IPHONE_SIMULATOR
  NSLog (@"simulating shake on saver list");
# endif
  [[rotating_nav topViewController] motionEnded: UIEventSubtypeMotionShake
                                      withEvent: nil];
}


#endif // HAVE_IPHONE



/* This is called when the "selectedSaverName" pref changes, e.g.,
   when a menu selection is made.
 */
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
  SEL dispatchSelector = (SEL)context;
  if (dispatchSelector != NULL) {
    [self performSelector:dispatchSelector withObject:change];
  } else {
    [super observeValueForKeyPath:keyPath
                         ofObject:object
                           change:change
                          context:context];
  }
}


# ifndef HAVE_IPHONE

/* Create the desktop window shell, possibly including a preferences button.
 */
- (NSWindow *) makeWindow
{
  NSRect rect;
  static int count = 0;
  Bool simple_p = ([saverNames count] == 1);
  NSButton *pb = 0;
  NSPopUpButton *menu = 0;
  NSBox *gbox = 0;
  NSBox *pbox = 0;

  NSRect sv_rect;
  sv_rect.origin.x = sv_rect.origin.y = 0;
  sv_rect.size.width = 320;
  sv_rect.size.height = 240;
  ScreenSaverView *sv = [[ScreenSaverView alloc]  // dummy placeholder
                          initWithFrame:sv_rect
                          isPreview:YES];

  // make a "Preferences" button
  //
  if (! simple_p) {
    rect.origin.x = 0;
    rect.origin.y = 0;
    rect.size.width = rect.size.height = 10;
    pb = [[NSButton alloc] initWithFrame:rect];
    [pb setTitle:NSLocalizedString(@"Preferences", @"")];
    [pb setBezelStyle:NSRoundedBezelStyle];
    [pb sizeToFit];

    rect.origin.x = ([sv frame].size.width -
                     [pb frame].size.width) / 2;
    [pb setFrameOrigin:rect.origin];
  
    // grab the click
    //
    [pb setTarget:self];
    [pb setAction:@selector(openPreferences:)];

    // Make a saver selection menu
    //
    menu = [self makeMenu];
    rect.origin.x = 2;
    rect.origin.y = 2;
    [menu setFrameOrigin:rect.origin];

    // make a box to wrap the saverView
    //
    rect = [sv frame];
    rect.origin.x = 0;
    rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
    gbox = [[NSBox alloc] initWithFrame:rect];
    rect.size.width = rect.size.height = 10;
    [gbox setContentViewMargins:rect.size];
    [gbox setTitlePosition:NSNoTitle];
    [gbox addSubview:sv];
    [gbox sizeToFit];

    // make a box to wrap the other two boxes
    //
    rect.origin.x = rect.origin.y = 0;
    rect.size.width  = [gbox frame].size.width;
    rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
    pbox = [[NSBox alloc] initWithFrame:rect];
    [pbox setTitlePosition:NSNoTitle];
    [pbox setBorderType:NSNoBorder];
    [pbox addSubview:gbox];
    [gbox release];
    if (menu) [pbox addSubview:menu];
    if (pb)   [pbox addSubview:pb];
    [pb release];
    [pbox sizeToFit];

    [pb   setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
    [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
    [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
    [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
  }

  [sv     setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];


  // and make a window to hold that.
  //
  NSScreen *screen = [NSScreen mainScreen];
  rect = pbox ? [pbox frame] : [sv frame];
  rect.origin.x = ([screen frame].size.width  - rect.size.width)  / 2;
  rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
  
  rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
  
  NSWindow *win = [[NSWindow alloc]
                      initWithContentRect:rect
                                styleMask:(NSWindowStyleMaskTitled |
                                           NSWindowStyleMaskClosable |
                                           NSWindowStyleMaskMiniaturizable |
                                           NSWindowStyleMaskResizable)
                                  backing:NSBackingStoreBuffered
                                    defer:YES
                                   screen:screen];
//  [win setMinSize:[win frameRectForContentRect:rect].size];
  [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
  [pbox release];

  [win makeKeyAndOrderFront:win];
  
  [sv startAnimation]; // this is the dummy saver
  [sv autorelease];

  count++;

  return win;
}


- (void) animTimer
{
  for (NSWindow *win in windows) {
    ScreenSaverView *sv = find_saverView ([win contentView]);
    BOOL is = [sv isAnimating];
    if (is)
      [sv animateOneFrame];
  }
}

# endif // !HAVE_IPHONE


- (void)applicationDidFinishLaunching:
# ifndef HAVE_IPHONE
    (NSNotification *) notif
# else  // HAVE_IPHONE
    (UIApplication *) application
# endif // HAVE_IPHONE
{
  [self listSaverBundleNames];

  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];

# ifndef HAVE_IPHONE
  int window_count = ([saverNames count] <= 1 ? 1 : 2);
  NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
                        retain];
  windows = a;

  int i;
  // Create either one window (for standalone, e.g. Phosphor.app)
  // or two windows for SaverTester.app.
  for (i = 0; i < window_count; i++) {
    NSWindow *win = [self makeWindow];
    [win setDelegate:self];
    // Get the last-saved window position out of preferences.
    [win setFrameAutosaveName:
           [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
    [win setFrameUsingName:[win frameAutosaveName]];
    [a addObject: win];
    // This prevents clicks from being seen by savers.
    // [win setMovableByWindowBackground:YES];
    win.releasedWhenClosed = NO;
    [win release];
  }
# else  // HAVE_IPHONE

# undef ya_rand_init
  ya_rand_init (0);	// Now's a good time.


# ifndef HAVE_TVOS
  /* iOS docs say:
     "You must call this method before attempting to get orientation data from
      the receiver. This method enables the device's accelerometer hardware
      and begins the delivery of acceleration events to the receiver."

     Adding or removing this doesn't seem to make any difference. It's
     probably getting called by the UINavigationController. Still... */
  [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
# endif // !HAVE_TVOS

  rotating_nav = [[[RotateyViewController alloc] initWithRotation:YES]
                         retain];

  if ([prefs boolForKey:@"wasRunning"]) // Prevents menu flicker on startup.
    rotating_nav.view.hidden = YES;

  [window setRootViewController: rotating_nav];
  [window setAutoresizesSubviews:YES];
  [window setAutoresizingMask: 
            (UIViewAutoresizingFlexibleWidth | 
             UIViewAutoresizingFlexibleHeight)];

  SaverListController *menu = [[SaverListController alloc] 
                                initWithNames:[saverNames allKeys]
                                descriptions:[self makeDescTable]];
  [rotating_nav pushViewController:menu animated:YES];
  [menu becomeFirstResponder];
  [menu autorelease];

# ifndef HAVE_TVOS
  application.applicationSupportsShakeToEdit = YES;
# endif // !HAVE_TVOS


# endif // HAVE_IPHONE

  NSString *forced = 0;
  /* In the XCode project, each .saver scheme sets this env var when
     launching SaverTester.app so that it knows which one we are
     currently debugging.  If this is set, it overrides the default
     selection in the popup menu.  If unset, that menu persists to
     whatever it was last time.
   */
  const char *f = getenv ("SELECTED_SAVER");
  if (f && *f) {
    NSString *ff = [NSString stringWithCString:(char *)f
                                      encoding:NSUTF8StringEncoding];
    for (NSString *title in [saverNames allKeys]) {
      if ([ff isEqualToString: [saverNames objectForKey:title]]) {
        forced = title;
        break;
      }
    }
    if (!forced)
      NSLog(@"forced saver \"%@\" does not exist", ff);
  }

  // If there's only one saver, run that.
  if (!forced && [saverNames count] == 1)
    forced = [[saverNames allKeys] objectAtIndex:0];

# ifdef HAVE_IPHONE
  NSString *prev = [prefs stringForKey:@"selectedSaverName"];

  if (forced)
    prev = forced;

  if (prev && ! [saverNames objectForKey:prev]) {
    NSLog (@"prev saver \"%@\" does not exist", prev);
    prev = forced = 0;
    [prefs removeObjectForKey:@"selectedSaverName"];
    [prefs removeObjectForKey:@"wasRunning"];
    rotating_nav.view.hidden = NO;
  }

  // If nothing was selected (e.g., this is the first launch)
  // then scroll randomly instead of starting up at "A".
  //
  if (!prev) {
    NSArray *titles = [saverNames allKeys];
    prev = [titles objectAtIndex: (random() % [titles count])];
    [prefs removeObjectForKey:@"wasRunning"];
  }

  if (prev)
    [menu scrollTo: prev];
# endif // HAVE_IPHONE

  if (forced)
    [prefs setObject:forced forKey:@"selectedSaverName"];

# ifdef HAVE_IPHONE
  /* Don't auto-launch the saver unless it was running last time.
     XScreenSaverView manages this, on crash_timer.
     Unless forced.
   */
  if (!forced && ![prefs boolForKey:@"wasRunning"])
    return;
# endif

  [self selectedSaverDidChange:nil];
//  [NSTimer scheduledTimerWithTimeInterval: 0
//           target:self
//           selector:@selector(selectedSaverDidChange:)
//           userInfo:nil
//           repeats:NO];



# ifndef HAVE_IPHONE
  /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
     ScreenSaverView to run its own timer calling animateOneFrame.
     On 10.9, that fails because the private class ScreenSaverModule
     is only initialized properly by ScreenSaverEngine, and in the
     context of SaverRunner, the null ScreenSaverEngine instance
     behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
     So, if it looks like this is the 10.9 version of ScreenSaverModule
     instead of the 10.8 version, we run our own timer here.  This sucks.
   */
  if (!anim_timer) {
    Class ssm = NSClassFromString (@"ScreenSaverModule");
    if (ssm && [ssm instancesRespondToSelector:
                      NSSelectorFromString(@"needsAnimationTimer")]) {
      NSWindow *win = [windows objectAtIndex:0];
      ScreenSaverView *sv = find_saverView ([win contentView]);
      anim_timer = [NSTimer scheduledTimerWithTimeInterval:
                              [sv animationTimeInterval]
                            target:self
                            selector:@selector(animTimer)
                            userInfo:nil
                            repeats:YES];
    }
  }
# endif // !HAVE_IPHONE
}


#ifndef HAVE_IPHONE

/* When the window closes, exit (even if prefs still open.)
 */
- (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
{
  return YES;
}

/* When the window is about to close, stop its animation.
   Without this, timers might fire after the window is dead.
 */
- (void)windowWillClose:(NSNotification *)notification
{
  NSWindow *win = [notification object];
  NSView *cv = win ? [win contentView] : 0;
  ScreenSaverView *sv = cv ? find_saverView (cv) : 0;
  if (sv && [sv isAnimating])
    [sv stopAnimation];
}

# else // HAVE_IPHONE

- (void)applicationWillResignActive:(UIApplication *)app
{
  [(XScreenSaverView *)view setScreenLocked:YES];
}

- (void)applicationDidBecomeActive:(UIApplication *)app
{
  [(XScreenSaverView *)view setScreenLocked:NO];
}

- (void)applicationDidEnterBackground:(UIApplication *)application
{
  [(XScreenSaverView *)view setScreenLocked:YES];
}

#endif // HAVE_IPHONE


@end
