// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "chrome/browser/ui/cocoa/constrained_window/constrained_window_sheet_controller.h"

#include <map>

#include "base/logging.h"
#import "chrome/browser/ui/cocoa/constrained_window/constrained_window_sheet.h"
#include "chrome/browser/ui/cocoa/constrained_window/constrained_window_sheet_info.h"

namespace {

// Maps parent windows to sheet controllers.
NSMutableDictionary* g_sheetControllers;

// Get a value for the given window that can be used as a key in a dictionary.
NSValue* GetKeyForParentWindow(NSWindow* parent_window) {
  return [NSValue valueWithNonretainedObject:parent_window];
}

}  // namespace

// An invisible overlay window placed on top of the sheet's parent view.
// This window blocks interaction with the underlying view.
@interface CWSheetOverlayWindow : NSWindow {
  base::scoped_nsobject<ConstrainedWindowSheetController> controller_;
}
@end

@interface ConstrainedWindowSheetController ()
- (id)initWithParentWindow:(NSWindow*)parentWindow;
- (ConstrainedWindowSheetInfo*)findSheetInfoForParentView:(NSView*)parentView;
- (ConstrainedWindowSheetInfo*)
    findSheetInfoForSheet:(id<ConstrainedWindowSheet>)sheet;
- (void)onParentWindowWillClose:(NSNotification*)note;
- (void)onParentViewFrameDidChange:(NSNotification*)note;
- (void)updateSheetPosition:(NSView*)parentView;
- (NSRect)overlayWindowFrameForParentView:(NSView*)parentView;
- (NSPoint)originForSheetSize:(NSSize)sheetSize
              inContainerRect:(NSRect)containerRect;
- (void)onOverlayWindowMouseDown:(CWSheetOverlayWindow*)overlayWindow;
- (void)closeSheet:(ConstrainedWindowSheetInfo*)info
     withAnimation:(BOOL)withAnimation;
@end

@implementation CWSheetOverlayWindow

- (id)initWithContentRect:(NSRect)rect
               controller:(ConstrainedWindowSheetController*)controller {
  if ((self = [super initWithContentRect:rect
                               styleMask:NSBorderlessWindowMask
                                 backing:NSBackingStoreBuffered
                                   defer:NO])) {
    [self setOpaque:NO];
    [self setBackgroundColor:[NSColor clearColor]];
    [self setIgnoresMouseEvents:NO];
    [self setReleasedWhenClosed:NO];
    controller_.reset([controller retain]);
  }
  return self;
}

- (void)mouseDown:(NSEvent*)event {
  [controller_ onOverlayWindowMouseDown:self];
}

@end

@implementation ConstrainedWindowSheetController

+ (ConstrainedWindowSheetController*)
    controllerForParentWindow:(NSWindow*)parentWindow {
  DCHECK(parentWindow);
  ConstrainedWindowSheetController* controller =
      [g_sheetControllers objectForKey:GetKeyForParentWindow(parentWindow)];
  if (controller)
    return controller;

  base::scoped_nsobject<ConstrainedWindowSheetController> new_controller(
      [[ConstrainedWindowSheetController alloc]
          initWithParentWindow:parentWindow]);
  if (!g_sheetControllers)
    g_sheetControllers = [[NSMutableDictionary alloc] init];
  [g_sheetControllers setObject:new_controller
                         forKey:GetKeyForParentWindow(parentWindow)];
  return new_controller;
}

+ (ConstrainedWindowSheetController*)
    controllerForSheet:(id<ConstrainedWindowSheet>)sheet {
  for (ConstrainedWindowSheetController* controller in
       [g_sheetControllers objectEnumerator]) {
    if ([controller findSheetInfoForSheet:sheet])
      return controller;
  }
  return nil;
}

+ (id<ConstrainedWindowSheet>)sheetForOverlayWindow:(NSWindow*)overlayWindow {
  for (ConstrainedWindowSheetController* controller in
          [g_sheetControllers objectEnumerator]) {
    for (ConstrainedWindowSheetInfo* info in controller->sheets_.get()) {
      if ([overlayWindow isEqual:[info overlayWindow]])
        return [info sheet];
    }
  }
  return nil;
}

- (id)initWithParentWindow:(NSWindow*)parentWindow {
  if ((self = [super init])) {
    parentWindow_.reset([parentWindow retain]);
    sheets_.reset([[NSMutableArray alloc] init]);

    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(onParentWindowWillClose:)
               name:NSWindowWillCloseNotification
             object:parentWindow_];
  }
  return self;
}

- (void)showSheet:(id<ConstrainedWindowSheet>)sheet
    forParentView:(NSView*)parentView {
  DCHECK(sheet);
  DCHECK(parentView);
  if (!activeView_.get())
    activeView_.reset([parentView retain]);

  // Observer the parent view's frame.
  [parentView setPostsFrameChangedNotifications:YES];
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(onParentViewFrameDidChange:)
             name:NSViewFrameDidChangeNotification
           object:parentView];

  // Create an invisible overlay window.
  NSRect rect = [self overlayWindowFrameForParentView:parentView];
  base::scoped_nsobject<NSWindow> overlayWindow(
      [[CWSheetOverlayWindow alloc] initWithContentRect:rect controller:self]);
  [parentWindow_ addChildWindow:overlayWindow
                        ordered:NSWindowAbove];

  // Add an entry for the sheet.
  base::scoped_nsobject<ConstrainedWindowSheetInfo> info(
      [[ConstrainedWindowSheetInfo alloc] initWithSheet:sheet
                                             parentView:parentView
                                          overlayWindow:overlayWindow]);
  [sheets_ addObject:info];

  // Show or hide the sheet.
  if ([activeView_ isEqual:parentView])
    [info showSheet];
  else
    [info hideSheet];
}

- (NSPoint)originForSheet:(id<ConstrainedWindowSheet>)sheet
           withWindowSize:(NSSize)size {
  ConstrainedWindowSheetInfo* info = [self findSheetInfoForSheet:sheet];
  DCHECK(info);
  NSRect containerRect =
      [self overlayWindowFrameForParentView:[info parentView]];
  return [self originForSheetSize:size inContainerRect:containerRect];
}

- (void)closeSheet:(id<ConstrainedWindowSheet>)sheet {
  ConstrainedWindowSheetInfo* info = [self findSheetInfoForSheet:sheet];
  DCHECK(info);
  [self closeSheet:info withAnimation:YES];
}

- (void)parentViewDidBecomeActive:(NSView*)parentView {
  [[self findSheetInfoForParentView:activeView_] hideSheet];
  activeView_.reset([parentView retain]);
  [self updateSheetPosition:parentView];
  [[self findSheetInfoForParentView:activeView_] showSheet];
}

- (void)pulseSheet:(id<ConstrainedWindowSheet>)sheet {
  ConstrainedWindowSheetInfo* info = [self findSheetInfoForSheet:sheet];
  DCHECK(info);
  if ([activeView_ isEqual:[info parentView]])
    [[info sheet] pulseSheet];
}

- (int)sheetCount {
  return [sheets_ count];
}

- (ConstrainedWindowSheetInfo*)findSheetInfoForParentView:(NSView*)parentView {
  for (ConstrainedWindowSheetInfo* info in sheets_.get()) {
    if ([parentView isEqual:[info parentView]])
      return info;
  }
  return NULL;
}

- (ConstrainedWindowSheetInfo*)
    findSheetInfoForSheet:(id<ConstrainedWindowSheet>)sheet {
  for (ConstrainedWindowSheetInfo* info in sheets_.get()) {
    if ([sheet isEqual:[info sheet]])
      return info;
  }
  return NULL;
}

- (void)onParentWindowWillClose:(NSNotification*)note {
  [[NSNotificationCenter defaultCenter]
      removeObserver:self
                name:NSWindowWillCloseNotification
              object:parentWindow_];

  // Close all sheets.
  NSArray* sheets = [NSArray arrayWithArray:sheets_];
  for (ConstrainedWindowSheetInfo* info in sheets)
    [self closeSheet:info withAnimation:NO];

  // Delete this instance.
  [g_sheetControllers removeObjectForKey:GetKeyForParentWindow(parentWindow_)];
  if (![g_sheetControllers count]) {
    [g_sheetControllers release];
    g_sheetControllers = nil;
  }
}

- (void)onParentViewFrameDidChange:(NSNotification*)note {
  NSView* parentView = [note object];
  if (![activeView_ isEqual:parentView])
    return;
  [self updateSheetPosition:parentView];
}

- (void)updateSheetPosition:(NSView*)parentView {
  ConstrainedWindowSheetInfo* info =
      [self findSheetInfoForParentView:parentView];
  if (!info)
    return;

  NSRect rect = [self overlayWindowFrameForParentView:parentView];
  [[info overlayWindow] setFrame:rect display:YES];
  [[info sheet] updateSheetPosition];
}

- (NSRect)overlayWindowFrameForParentView:(NSView*)parentView {
  NSRect viewFrame = [parentView convertRect:[parentView bounds] toView:nil];

  id<NSWindowDelegate> delegate = [[parentView window] delegate];
  if ([delegate respondsToSelector:@selector(window:
                                  willPositionSheet:
                                          usingRect:)]) {
    NSRect sheetFrame = NSZeroRect;
    // This API needs Y to be the distance from the bottom of the overlay to
    // the top of the sheet. X, width, and height are ignored.
    sheetFrame.origin.y = NSMaxY(viewFrame);
    NSRect customSheetFrame = [delegate window:[parentView window]
                             willPositionSheet:nil
                                     usingRect:sheetFrame];
    viewFrame.size.height += NSMinY(customSheetFrame) - NSMinY(sheetFrame);
  }

  viewFrame.origin = [[parentView window] convertBaseToScreen:viewFrame.origin];
  return viewFrame;
}

- (NSPoint)originForSheetSize:(NSSize)sheetSize
              inContainerRect:(NSRect)containerRect {
  NSPoint origin;
  origin.x = roundf(NSMinX(containerRect) +
                    (NSWidth(containerRect) - sheetSize.width) / 2.0);
  origin.y = NSMaxY(containerRect) + 5 - sheetSize.height;
  return origin;
}

- (void)onOverlayWindowMouseDown:(CWSheetOverlayWindow*)overlayWindow {
  for (ConstrainedWindowSheetInfo* curInfo in sheets_.get()) {
    if ([overlayWindow isEqual:[curInfo overlayWindow]]) {
      [self pulseSheet:[curInfo sheet]];
      [[curInfo sheet] makeSheetKeyAndOrderFront];
      break;
    }
  }
}

- (void)closeSheet:(ConstrainedWindowSheetInfo*)info
     withAnimation:(BOOL)withAnimation {
  if (![sheets_ containsObject:info])
    return;

  [[NSNotificationCenter defaultCenter]
      removeObserver:self
                name:NSViewFrameDidChangeNotification
              object:[info parentView]];

  [parentWindow_ removeChildWindow:[info overlayWindow]];
  [[info sheet] closeSheetWithAnimation:withAnimation];
  [[info overlayWindow] close];
  [sheets_ removeObject:info];
}

@end
