/* 
 * Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
 *
 * 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; version 2 of the
 * License.
 * 
 * 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 "MFPopover.h"
#import "MFMForms.h"

//--------------------------------------------------------------------------------------------------

@interface PopoverFrameView : NSView
{
  NSBezierPath* outline;
}

@property (nonatomic, retain) NSBezierPath* outline;

@end

//--------------------------------------------------------------------------------------------------

@implementation PopoverFrameView

@synthesize outline;

- (void)drawRect: (NSRect)rect
{
  NSGraphicsContext *context = [NSGraphicsContext currentContext];
  [context saveGraphicsState];
  
  [[NSColor colorWithDeviceWhite: 232 / 255.0 alpha: 1] set];
  [outline fill];
	
  // Something is drawing the border for us. Can't find out which part does it, though.
  //[[NSColor colorWithDeviceWhite: 118 / 255.0 alpha: 1] set];
	//[outline stroke];
  
  [context restoreGraphicsState];
}

@end

//--------------------------------------------------------------------------------------------------

@implementation MFPopover

- (id) initWithContentRect: (NSRect)contentRect
                 styleMask: (NSUInteger)windowStyle
                   backing: (NSBackingStoreType)bufferingType
                     defer: (BOOL)deferCreation
{
	self = [super initWithContentRect: contentRect
                          styleMask: windowStyle
                            backing: bufferingType
                              defer: deferCreation];
	if (self)
	{
		[self setOpaque: NO];
		[self setBackgroundColor: [NSColor clearColor]];
	}
	return self;
}

//--------------------------------------------------------------------------------------------------

- (BOOL)canBecomeKeyWindow
{
  return YES;
}

//--------------------------------------------------------------------------------------------------

/**
 * Resize the frame view to accomodate the content view.
 */
- (void)setContentSize: (NSSize)newSize
{
  mBaseSize = newSize;

	NSSize sizeDelta = newSize;
	NSSize childBoundsSize = [mChildContentView bounds].size;
	sizeDelta.width -= childBoundsSize.width;
	sizeDelta.height -= childBoundsSize.height;
	
	PopoverFrameView* frameView = [super contentView];
	NSSize newFrameSize = [frameView bounds].size;
	newFrameSize.width += sizeDelta.width;
	newFrameSize.height += sizeDelta.height;
	
	[super setContentSize: newFrameSize];
}

//--------------------------------------------------------------------------------------------------

/**
 * Set the given view as content view, that is, as child of our frame view.
 */
- (void) setContentView: (NSView *)aView
{
	if ([mChildContentView isEqualTo: aView])
		return;
	
	NSRect bounds = [self frame];
	bounds.origin = NSZeroPoint;

	PopoverFrameView* frameView = [super contentView];
	if (!frameView)
	{
		frameView = [[[PopoverFrameView alloc] initWithFrame: bounds] autorelease];
		[super setContentView: frameView];
	}
	
	if (mChildContentView != nil)
		[mChildContentView removeFromSuperview];

	mChildContentView = aView; // No retain required here, the frame view controls the lifetime.
	[mChildContentView setFrame: [self contentRectForFrameRect: bounds]];
	[mChildContentView setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable];
	[frameView addSubview: mChildContentView];
}

//--------------------------------------------------------------------------------------------------

- (NSView *)contentView
{
	return mChildContentView;
}

//--------------------------------------------------------------------------------------------------

#define DEFAULT_PADDING 7 // Padding on all sides.

#define ARROW_SIZE 16  // Number of pixels from arrow base to arrow tip.
#define ARROW_BASE 32  // Number of pixels the base line of the arrow is wide.
#define CORNER_RADIUS 14 // Size of the rounded corners.

- (NSRect)contentRectForFrameRect:(NSRect)windowFrame
{
  return NSMakeRect(windowFrame.origin.x + mPadding.left, windowFrame.origin.y + mPadding.bottom,
                    windowFrame.size.width - mPadding.horizontal(), windowFrame.size.height - mPadding.vertical());
}

//--------------------------------------------------------------------------------------------------

- (void) computeCoordinatesAndPadding: (mforms::StartPosition) position
{
  // The base size is the size of the main part, without arrow.
  NSSize actualSize = mBaseSize;
  actualSize.width += 2 * DEFAULT_PADDING;
  actualSize.height += 2 * DEFAULT_PADDING;

  // Add the arrow size to either width or height, depending on the proposed relative position.
  if (position == mforms::Left || position == mforms::Right)
    actualSize.width += ARROW_SIZE;
  else
    actualSize.height += ARROW_SIZE;

  // The initial position of the arrow is not the center on its side but only 1/3 of side's size
  // for a more appealing look. Additionally, add the arrow's size to the padding on this size to
  // exclude its area from the main content area.
  NSPoint newLocation;
  switch (position)
  {
    case mforms::Left:
      newLocation.x = mHotSpot.x - actualSize.width;
      newLocation.y = mHotSpot.y - actualSize.height / 3;
      mPadding = base::Padding(DEFAULT_PADDING, DEFAULT_PADDING,
                                          DEFAULT_PADDING + ARROW_SIZE, DEFAULT_PADDING);
      break;
    case mforms::Right:
      newLocation.x = mHotSpot.x;
      newLocation.y = mHotSpot.y - actualSize.height / 3;
      mPadding = base::Padding(DEFAULT_PADDING + ARROW_SIZE, DEFAULT_PADDING,
                                          DEFAULT_PADDING, DEFAULT_PADDING);
      break;
    case mforms::Above:
      newLocation.x = mHotSpot.x - actualSize.width / 3;
      newLocation.y = mHotSpot.y;
      mPadding = base::Padding(DEFAULT_PADDING, DEFAULT_PADDING,
                                          DEFAULT_PADDING, DEFAULT_PADDING + ARROW_SIZE);
      break;
    case mforms::Below:
      newLocation.x = mHotSpot.x - actualSize.width / 3;
      newLocation.y = mHotSpot.y - actualSize.height;
      mPadding = base::Padding(DEFAULT_PADDING, DEFAULT_PADDING + ARROW_SIZE,
                                          DEFAULT_PADDING, DEFAULT_PADDING);
      break;
  }

  NSScreen* currentScreen = [NSScreen mainScreen];
  NSRect screenBounds = [currentScreen visibleFrame];

  // Check the control's bounds and determine the amount of pixels we have to move it make
  // it fully appear on screen. This will usually not move the hot spot, unless the movement
  // of the control is so much that it would leave the arrow outside its bounds.
  int deltaX = 0;
  int deltaY = 0;
  NSRect frame = [self frame];
  if (newLocation.x < screenBounds.origin.x)
    deltaX = screenBounds.origin.x - newLocation.x;
  if (newLocation.x + frame.size.width > NSMaxX(screenBounds))
    deltaX = NSMaxX(screenBounds) - (newLocation.x + frame.size.width);

  if (newLocation.y < NSMinY(screenBounds))
    deltaY = NSMinY(screenBounds) - newLocation.y;
  if (newLocation.y + frame.size.height > NSMaxY(screenBounds))
    deltaY = NSMaxY(screenBounds) - (newLocation.y + frame.size.height);
  newLocation.x += deltaX;
  newLocation.y += deltaY;

  // Now that we have the final location check the arrow again.
  switch (position)
  {
  case mforms::Left:
  case mforms::Right:
    mHotSpot.x += deltaX;
    if ((mHotSpot.y - ARROW_BASE / 2) < (newLocation.y + CORNER_RADIUS))
      mHotSpot.y = newLocation.y + CORNER_RADIUS + ARROW_BASE / 2;
    if ((mHotSpot.y + ARROW_BASE / 2) > (newLocation.y + actualSize.height - CORNER_RADIUS))
      mHotSpot.y = newLocation.y + actualSize.height - CORNER_RADIUS - ARROW_BASE / 2;
    break;
  case mforms::Above:
  case mforms::Below:
    if ((mHotSpot.x - ARROW_BASE / 2) < (newLocation.x + CORNER_RADIUS))
      mHotSpot.x = newLocation.x + CORNER_RADIUS + ARROW_BASE / 2;
    if ((mHotSpot.x + ARROW_BASE / 2) > (newLocation.x + actualSize.width - CORNER_RADIUS))
      mHotSpot.x = newLocation.x + actualSize.width - CORNER_RADIUS - ARROW_BASE / 2;
    mHotSpot.y += deltaY;
    break;
  }

  [self setFrameOrigin: newLocation];
}

//--------------------------------------------------------------------------------------------------

- (void) computeOutline
{
  PopoverFrameView* frameView = [super contentView];
  
  NSRect bounds = [frameView bounds];
  NSPoint localHotSpot = [self convertScreenToBase: mHotSpot];
  localHotSpot.x += 0.5;

  // The path is constructed counterclockwise.
  NSBezierPath* outline = [NSBezierPath bezierPath];
  [outline setLineWidth: 1];
  
  CGFloat leftOffset = 0;
  CGFloat topOffset = 0;
  CGFloat rightOffset = 0;
  CGFloat bottomOffset = 0;

  switch (mRelativePosition)
  {
    case mforms::Left:
    {
      rightOffset = ARROW_SIZE;
      break;
    }
    case mforms::Right:
    {
      leftOffset = ARROW_SIZE;
      break;
    }
    case mforms::Above:
    {
      bottomOffset = ARROW_SIZE;
      break;
    }
    case mforms::Below:
    {
      topOffset = ARROW_SIZE;
      break;
    }
  }

  // Left-bottom corner.
  [outline appendBezierPathWithArcWithCenter: NSMakePoint(NSMinX(bounds) + CORNER_RADIUS + leftOffset, NSMinY(bounds) + CORNER_RADIUS + bottomOffset)
                                      radius: CORNER_RADIUS
                                  startAngle: 180
                                    endAngle: -90];
  if (bottomOffset > 0)
  {
    [outline lineToPoint: NSMakePoint(localHotSpot.x - ARROW_BASE / 2, NSMinY(bounds) + bottomOffset)];
    [outline lineToPoint: NSMakePoint(localHotSpot.x, localHotSpot.y)];
    [outline lineToPoint: NSMakePoint(localHotSpot.x + ARROW_BASE / 2, NSMinY(bounds) + bottomOffset)];
  }

  // Right-bottom corner.
  [outline appendBezierPathWithArcWithCenter: NSMakePoint(NSMaxX(bounds) - CORNER_RADIUS - rightOffset, NSMinY(bounds) + CORNER_RADIUS + bottomOffset)
                                      radius: CORNER_RADIUS
                                  startAngle: -90
                                    endAngle: 0];
  if (rightOffset > 0)
  {
    [outline lineToPoint: NSMakePoint(NSMaxX(bounds) - rightOffset, localHotSpot.y - ARROW_BASE / 2)];
    [outline lineToPoint: localHotSpot];
    [outline lineToPoint: NSMakePoint(NSMaxX(bounds) - rightOffset, localHotSpot.y + ARROW_BASE / 2)];
  }

  // Right-top corner.
  [outline appendBezierPathWithArcWithCenter: NSMakePoint(NSMaxX(bounds) - CORNER_RADIUS - rightOffset, NSMaxY(bounds) - CORNER_RADIUS - topOffset)
                                      radius: CORNER_RADIUS
                                  startAngle: 0
                                    endAngle: 90];
  if (topOffset > 0)
  {
    [outline lineToPoint: NSMakePoint(localHotSpot.x + ARROW_BASE / 2, NSMaxY(bounds) - topOffset)];
    [outline lineToPoint: localHotSpot];
    [outline lineToPoint: NSMakePoint(localHotSpot.x - ARROW_BASE / 2, NSMaxY(bounds) - topOffset)];
  }

  // Left-top corner.
  [outline appendBezierPathWithArcWithCenter: NSMakePoint(NSMinX(bounds) + CORNER_RADIUS + leftOffset, NSMaxY(bounds) - CORNER_RADIUS - topOffset)
                                      radius: CORNER_RADIUS
                                  startAngle: 90
                                    endAngle: 180];
  if (leftOffset > 0)
  {
    [outline lineToPoint: NSMakePoint(NSMinX(bounds) + leftOffset, localHotSpot.y + ARROW_BASE / 2)];
    [outline lineToPoint: localHotSpot];
    [outline lineToPoint: NSMakePoint(NSMinX(bounds) + leftOffset, localHotSpot.y - ARROW_BASE / 2)];
  }

  frameView.outline = outline;
  [frameView setNeedsDisplay: YES];
}

//--------------------------------------------------------------------------------------------------

/**
 * Resizes the window to accomodate the content view with the computed padding on each side
 * and then places the content view to the right position.
 */
- (void)adjustWindowSizeAndContentFrame
{
  NSPoint origin = [self frame].origin;
  NSRect newWindowFrame = NSMakeRect(origin.x,
                                     origin.y,
                                     mBaseSize.width + mPadding.horizontal(),
                                     mBaseSize.height + mPadding.vertical());

  [self setFrame: newWindowFrame display: NO animate: [self isVisible]];
  [mChildContentView setFrame: NSMakeRect(mPadding.left, mPadding.bottom, mBaseSize.width, mBaseSize.height)];
}

//--------------------------------------------------------------------------------------------------

- (void) show: (NSPoint)location relativePosition: (mforms::StartPosition)position
{
  mHotSpot = location;
  [self computeCoordinatesAndPadding: position];
  if (![self isVisible] || mRelativePosition != position)
  {
    mRelativePosition = position;
    [self adjustWindowSizeAndContentFrame];
    [self computeOutline];
  }

  if (![self isVisible])
  {
    [NSAnimationContext beginGrouping];
    [self setAlphaValue: 0];
    [self orderFront: nil];
    [[NSAnimationContext currentContext] setDuration: 0.25];
    [[self animator] setAlphaValue: 1];
    [NSAnimationContext endGrouping];
  }
}

//--------------------------------------------------------------------------------------------------

- (void) close
{
  [[NSAnimationContext currentContext] setDuration: 0.25];
  [[self animator] setAlphaValue: 0];
  [self performSelector: @selector(orderOut:) withObject: nil afterDelay: 0.5];
}

//--------------------------------------------------------------------------------------------------

using namespace mforms;

static bool popover_create(Popover* popover)
{
  MFPopover* popoverWindow = [[[MFPopover alloc] initWithContentRect: NSMakeRect(0, 0, 100, 100)
                                                           styleMask: NSBorderlessWindowMask
                                                             backing: NSBackingStoreBuffered
                                                               defer: NO]
                              autorelease];
  [popoverWindow setHasShadow: YES];
  [popoverWindow setLevel: NSPopUpMenuWindowLevel];
  popover->set_data(popoverWindow);
  
  return true;  
}

//--------------------------------------------------------------------------------------------------

static void popover_set_content(Popover* popover, View* content)
{
  [popover->get_data() setContentView: content->get_data()];
}

//--------------------------------------------------------------------------------------------------

static void popover_set_size(Popover* popover, int width, int height)
{
  [popover->get_data() setContentSize: NSMakeSize(width, height)];
}

//--------------------------------------------------------------------------------------------------

static void popover_show(Popover* popover, int x, int y, StartPosition relativePosition)
{
  [popover->get_data() show: NSMakePoint(x, y) relativePosition: relativePosition];
}

//--------------------------------------------------------------------------------------------------

static void popover_close(Popover* popover)
{
  [popover->get_data() close];
}

//--------------------------------------------------------------------------------------------------

void cf_popover_init()
{
  ::mforms::ControlFactory *f = ::mforms::ControlFactory::get_instance();

  f->_popover_impl.create= &popover_create;
  f->_popover_impl.set_content= &popover_set_content;
  f->_popover_impl.set_size= &popover_set_size;
  f->_popover_impl.show= &popover_show;

  f->_popover_impl.close= &popover_close;
}

@end
