/* xdaliclock - a melting digital clock
 * Copyright (c) 1991-2013 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.
 */

#import "AppController.h"
#import "DaliClockWindow.h"

char *progname = "Dali Clock";   // digital.c wants this for error messages.

@interface AppController (ForwardDeclarations)
- (void)createDaliClockWindow;
@end

@implementation AppController

+ (void)initialize;
{
  static BOOL initialized_p = NO;
  if (initialized_p)
    return;
  initialized_p = YES;

  NSUserDefaultsController *controller =
    [NSUserDefaultsController sharedUserDefaultsController];
  NSUserDefaults *defs = [controller defaults];

  /* Tell the DaliClockView class to initialize its default preferences.
   */
  [DaliClockView registerDefaults:defs];


  /* Now that the DaliClockView class has initialized its preferences,
     set the defaults for those preferences handled by AppController
     rather than by DaliClockView.
   */
  NSDictionary *extras = [NSDictionary dictionaryWithObjectsAndKeys:
    @"NO", @"windowTitlebarIsHidden",
    @"NO", @"dockIconIsHidden",
    @"0", @"windowLevel",  // default to "normal window"
    nil];
  NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:100];
  [dict addEntriesFromDictionary:[defs dictionaryRepresentation]];
  [dict addEntriesFromDictionary:extras];
  [defs registerDefaults:dict];

  [DaliClockView setUserDefaultsController: controller];
}


/* 2018: As of macOS 10.11 or so, the "On top of screen saver too" option
   no longer works. Normal applications can't open windows in spaces other
   than their own (full-screen-mode being a kind of "space"). Since
   Dali Clock is not a member of (for example) the special iTunes
   full-screen space, it can't map itself inside that space. The same
   thing is true of the lock screen, but moreso.

   "Background" applications (the kind that do not have a dock icon or a
   menubar) can open their windows in whatever the current space is, but
   apps can't switch back and forth between the two without an edit to the
   Info.plist file, so that's out too.
 */

// Convert preferences radio button value to real window level.
static long
real_window_level(long pref) {
  return (pref == 0 ? NSNormalWindowLevel        :  // normal
          pref == 1 ? NSNormalWindowLevel-1      :  // below all
          pref == 2 ? NSFloatingWindowLevel      :  // above all
        pref == 3 ? NSScreenSaverWindowLevel+1 :  // above saver
//          pref == 3 ? CGShieldingWindowLevel()   :  // above saver
          NSNormalWindowLevel);                     // unknown
}

- (void)createDaliClockWindow;
{
  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];

  BOOL title_p = ![prefs boolForKey:@"windowTitlebarIsHidden"];
  BOOL was_fullscreen_p = (fullscreen_window_level != 0);

  long level = real_window_level ([prefs integerForKey:@"windowLevel"]);

  /* CGDisplayCaptureWithOptions (kCGDirectMainDisplay, kCGCaptureNoFill)
     is supposed to grab the screen without blanking it, but it blanks anyway.
     As of 10.7, the new way to do full-screen is
     [app setPresentationOptions:NSApplicationPresentationFullScreen]
     but that doesn't seem to do anything.  E.g., it doesn't grab the mouse.
     So let's just not do anything.
   */

# if 0
  NSApplication *app = [NSApplication sharedApplication];
  if (was_fullscreen_p) {
    if ([app respondsToSelector:@selector(setPresentationOptions:)]) { // 10.7
      [app setPresentationOptions: NSApplicationPresentationDefault];
    } else {
      if (CGDisplayRelease (kCGDirectMainDisplay) != kCGErrorSuccess)
        NSLog ( @"Couldn't release the display!");
    }
  }

  if (fullscreen_p) {
    if ([app respondsToSelector:@selector(setPresentationOptions:)]) { // 10.7
      [app setPresentationOptions:(NSApplicationPresentationHideDock|
                                   NSApplicationPresentationHideMenuBar|
                                   NSApplicationPresentationFullScreen)];
    } else {
      if (CGDisplayCaptureWithOptions (kCGDirectMainDisplay, kCGCaptureNoFill)
          != kCGErrorSuccess) {
        NSLog(@"Couldn't capture the main display!");
        fullscreen_p = NO;
      }
    }
  }
# endif // 0

  if (fullscreen_p) {
    level = CGShieldingWindowLevel();
    fullscreen_window_level = level;
    title_p = NO;
    [NSCursor hide];
    CGAssociateMouseAndMouseCursorPosition (false); // locks mouse position
  } else {
    [NSCursor unhide];
    CGAssociateMouseAndMouseCursorPosition (true);  // normal mouse
  }

  NSScreen *screen = [NSScreen mainScreen];

  // Set default size/position of the window if there is no autosave.
  //
  NSRect rect;
  rect.size.width = 625;
  rect.size.height = 128;
  rect.origin.x = ([screen frame].size.width  - rect.size.width)  / 2;
  rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;

  NSRect default_rect = rect;

  // Nuke the old window if we're re-creating it.
  //
  BOOL keep_old_size_p = NO;
  if (window) {
    keep_old_size_p = !was_fullscreen_p;
    rect = [window contentRectForFrameRect:[window frame]];
    [window close];
    [window setFrameAutosaveName:@""];  // two windows can't have the same one
    [window autorelease];
    window = 0;
  }

  window = [[DaliClockWindow alloc]
            initWithContentRect:rect
                      styleMask:(title_p
                                 ? (NSTitledWindowMask |
                                    NSClosableWindowMask |
                                    NSMiniaturizableWindowMask |
                                    NSResizableWindowMask)
                                 : (NSBorderlessWindowMask))
                        backing:NSBackingStoreBuffered
                          defer:YES
                         screen:screen];
  [window setTitle:@"Dali Clock"];
  [window setOpaque:NO];
  [window setHasShadow:NO];  // windows with shadows flicker
  [window setLevel:level];
  [window setMovableByWindowBackground:!fullscreen_p];
  [window setReleasedWhenClosed:NO];
  [window setDelegate:self]; // for the "cancel" method

  if (fullscreen_p) {
    // don't save window sizes for fullscreen windows.
    [window setFrameAutosaveName:@""];
  } else {
    [window setFrameAutosaveName:@"clockWindow"];
    // need "force" for size to be updated when borderless.
    if (! [window setFrameUsingName:[window frameAutosaveName] force:YES]) {
      // If we weren't able to set the frame, then that means that no size
      // was yet saved; so instead of leaving it at the "full screen" size,
      // use the default size and position instead.
      [window setFrame:[window frameRectForContentRect:default_rect]
               display:NO];
    }
  }

  if (fullscreen_p)
    [window setFrame:[screen frame] display:NO];
  else if (keep_old_size_p)
    [window setFrame:[window frameRectForContentRect:rect] display:NO];

  // if we're re-creating the window, just move the existing view
  // from the previous window.
  if (! view)
    view = [[DaliClockView alloc] initWithFrame:[window frame]];

  // Without this, toggling the title bar kills transparency until restart.
  [window setBackgroundColor:[NSColor clearColor]];

  [window setContentView:view];
}


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


/* Connect the various window/app-related preferences to accessor methods.
   Note that DaliClockView has its own set of preferences it deals with.
 */
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
  [self createDaliClockWindow];
  if (!view || !window) abort();

  NSUserDefaultsController *userDefaultsController =
    [NSUserDefaultsController sharedUserDefaultsController];

  [userDefaultsController addObserver:self
                           forKeyPath:@"values.windowLevel"
                              options:0
                              context:@selector(windowLevelDidChange:)];
  [userDefaultsController addObserver:self
                           forKeyPath:@"values.windowTitlebarIsHidden"
                              options:0
                         context:@selector(windowTitlebarIsHiddenDidChange:)];

  [window makeKeyAndOrderFront:self];


#if 0
  /* For this to work, AppInfo.plist must have LSBackgroundOnly=1.
     (LSUIElement=1 does not work).  When that is set, the app comes
     up with no icon by default, and we are able to turn it on (but
     cannot turn it off).

     But, then there's no way out, because the window will not take
     focus.  It has no menubar, and typing Command-, does not bring
     up preferences!

     If LSUIElement=1, then Command-, does bring up preferences...
     but TransformProcessType() returns error -50 ("error in user
     parameter list").

     So, as far as I can see, there's no way to make this preference
     not be completely horrible.
   */
  if (![[NSUserDefaults standardUserDefaults]
         boolForKey:@"dockIconIsHidden"]) {
    // Dock icon is hidden by default; turn it on if it should be on.
    ProcessSerialNumber psn = { 0, kCurrentProcess }; 
    OSStatus status = 
      TransformProcessType (&psn, kProcessTransformToForegroundApplication);
    if (status)
      NSLog (@"Could not turn on the dock icon: error %d", status);
  }
  [NSMenu setMenuBarVisible:YES];  // this doesn't work.
#endif
}


- (void)windowLevelDidChange:(NSDictionary *)change
{
  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
  long level = real_window_level ([prefs integerForKey:@"windowLevel"]);
  [window setLevel:level];
}


- (void)windowTitlebarIsHiddenDidChange:(NSDictionary *)change
{
  // re-create the window when the "Title Bar" preference changes.
  [self createDaliClockWindow];
  [window makeKeyAndOrderFront:self];
}


- (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];
  }
}


/* Toggle, called by the "Window / Full Screen" menu item.
   This doesn't go through the preferences because it should not be saved.
 */
- (void)performFullscreen:(id)sender
{
  fullscreen_p = !fullscreen_p;
  [self createDaliClockWindow];
  [window makeKeyAndOrderFront:self];
}


/* "Window / Minimize".
   Normally this happens in NSWindow via FirstResponder, but we bind this to
   the menu directly so that it works when the window is borderless or full
   screen.  (To minimize a full-screen window, we have to take it out of
   full-screen mode first, which is why this can't be in DaliClockWindow
   like performClose is).
 */
- (void)performMiniaturize:(id)sender
{
  if (fullscreen_p)
    // turn off fullscreen mode before miniaturizing.
    // (it does not automatically turn back on when unminiaturizing.)
    [self performFullscreen:sender];

  //[window performMiniaturize:sender];  // beeps when borderless
  [window miniaturize:sender];
}


/* This is sent by ESC and Cmd-.
   Take us out of full-screen if it is active.
 */
- (void)cancel:(id)sender
{
  if (fullscreen_p) {
    [self performFullscreen:sender];
  }
}

/* The About button in the menu bar.
 */
- (IBAction)aboutClick:(id)sender
{
  [view aboutClick:sender];
}

@end
