/* Clutter -  An OpenGL based 'interactive canvas' library.
 * OSX backend - integration with NSWindow and NSView
 *
 * Copyright (C) 2007  Tommi Komulainen <tommi.komulainen@iki.fi>
 * Copyright (C) 2007  OpenedHand Ltd.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */
#include "config.h"

#include "clutter-osx.h"
#include "clutter-stage-osx.h"
#include "clutter-backend-osx.h"
#import <AppKit/AppKit.h>

#include <clutter/clutter-debug.h>
#include <clutter/clutter-private.h>

G_DEFINE_TYPE (ClutterStageOSX, clutter_stage_osx, CLUTTER_TYPE_STAGE)

/* FIXME: this should be in clutter-stage.c */
static void
clutter_stage_osx_state_update (ClutterStageOSX   *self,
                                ClutterStageState  unset_flags,
                                ClutterStageState  set_flags);

#define CLUTTER_OSX_FULLSCREEN_WINDOW_LEVEL (NSMainMenuWindowLevel + 1)

/*************************************************************************/
@interface ClutterGLWindow : NSWindow
{
  ClutterStageOSX *stage;
}
@end

@implementation ClutterGLWindow
- (id)initWithView:(NSView *)aView UTF8Title:(const char *)aTitle stage:(ClutterStageOSX *)aStage
{
  if ((self = [super initWithContentRect: [aView frame]
                               styleMask: NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask
                                 backing: NSBackingStoreBuffered
                                   defer: NO]) != nil)
    {
      [self setDelegate: self];
      [self useOptimizedDrawing: YES];
      [self setContentView: aView];
      [self setTitle:[NSString stringWithUTF8String: aTitle]];
      stage = aStage;
    }
  return self;
}

- (BOOL) windowShouldClose: (id) sender
{
  CLUTTER_NOTE (BACKEND, "windowShouldClose");

  ClutterEvent event;
  event.type = CLUTTER_DELETE;
  clutter_event_put (&event);

  return NO;
}

- (NSRect) constrainFrameRect:(NSRect)frameRect toScreen:(NSScreen*)aScreen
{
  /* in fullscreen mode we don't want to be constrained by menubar or dock
   * FIXME: calculate proper constraints depending on fullscreen mode
   */

  return frameRect;
}

- (void) windowDidBecomeKey:(NSNotification*)aNotification
{
  CLUTTER_NOTE (BACKEND, "windowDidBecomeKey");

  if (stage->stage_state & CLUTTER_STAGE_STATE_FULLSCREEN)
    [self setLevel: CLUTTER_OSX_FULLSCREEN_WINDOW_LEVEL];

  clutter_stage_osx_state_update (stage, 0, CLUTTER_STAGE_STATE_ACTIVATED);
}

- (void) windowDidResignKey:(NSNotification*)aNotification
{
  CLUTTER_NOTE (BACKEND, "windowDidResignKey");

  if (stage->stage_state & CLUTTER_STAGE_STATE_FULLSCREEN)
    {
      [self setLevel: NSNormalWindowLevel];
      [self orderBack: nil];
    }

  clutter_stage_osx_state_update (stage, CLUTTER_STAGE_STATE_ACTIVATED, 0);
}
@end

/*************************************************************************/
@interface ClutterGLView : NSOpenGLView
{
  ClutterActor *stage;
}
- (void) drawRect: (NSRect) bounds;
@end

@implementation ClutterGLView
- (id) initWithFrame: (NSRect)aFrame pixelFormat:(NSOpenGLPixelFormat*)aFormat stage:(ClutterActor*)aStage
{
  if ((self = [super initWithFrame:aFrame pixelFormat:aFormat]) != nil)
    {
      self->stage = aStage;
    }
  return self;
}

- (void) drawRect: (NSRect) bounds
{
  clutter_actor_paint (self->stage);
  [[self openGLContext] flushBuffer];
}

/* In order to receive key events */
- (BOOL) acceptsFirstResponder
{
  return YES;
}

/* We want 0,0 top left */
- (BOOL) isFlipped
{
  return YES;
}

- (void) setFrameSize: (NSSize) aSize
{
  CLUTTER_NOTE (BACKEND, "setFrameSize: %dx%d",
                (int)aSize.width, (int)aSize.height);

  [super setFrameSize: aSize];

  clutter_actor_set_size (self->stage, (int)aSize.width, (int)aSize.height);

  CLUTTER_SET_PRIVATE_FLAGS(self->stage, CLUTTER_ACTOR_SYNC_MATRICES);
}

/* Simply forward all events that reach our view to clutter. */

#define EVENT_HANDLER(event) -(void)event:(NSEvent *)theEvent { \
  _clutter_event_osx_put (theEvent);                            \
}
EVENT_HANDLER(mouseDown)
EVENT_HANDLER(mouseDragged)
EVENT_HANDLER(mouseUp)
EVENT_HANDLER(mouseMoved)
EVENT_HANDLER(mouseEntered)
EVENT_HANDLER(mouseExited)
EVENT_HANDLER(rightMouseDown)
EVENT_HANDLER(rightMouseDragged)
EVENT_HANDLER(rightMouseUp)
EVENT_HANDLER(otherMouseDown)
EVENT_HANDLER(otherMouseDragged)
EVENT_HANDLER(otherMouseUp)
EVENT_HANDLER(scrollWheel)
EVENT_HANDLER(keyDown)
EVENT_HANDLER(keyUp)
EVENT_HANDLER(flagsChanged)
EVENT_HANDLER(helpRequested)
EVENT_HANDLER(tabletPoint)
EVENT_HANDLER(tabletProximity)

#undef EVENT_HANDLER
@end

/*************************************************************************/
static void
clutter_stage_osx_state_update (ClutterStageOSX   *self,
                                ClutterStageState  unset_flags,
                                ClutterStageState  set_flags)
{
  ClutterStageStateEvent event;

  event.new_state = self->stage_state;
  event.new_state |= set_flags;
  event.new_state &= ~unset_flags;

  if (event.new_state == self->stage_state)
    return;

  event.changed_mask = event.new_state ^ self->stage_state;

  self->stage_state = event.new_state;

  event.type = CLUTTER_STAGE_STATE;
  clutter_event_put ((ClutterEvent*)&event);
}

static void
clutter_stage_osx_save_frame (ClutterStageOSX *self)
{
  if (CLUTTER_ACTOR_IS_REALIZED (CLUTTER_ACTOR (self)))
    {
      g_assert (self->window != NULL);

      self->normalFrame = [self->window frame];
      self->haveNormalFrame = TRUE;
    }
}

static void
clutter_stage_osx_set_frame (ClutterStageOSX *self)
{
  g_assert (CLUTTER_ACTOR_IS_REALIZED (CLUTTER_ACTOR (self)));
  g_assert (self->window != NULL);

  if (self->stage_state & CLUTTER_STAGE_STATE_FULLSCREEN)
    {
      /* Raise above the menubar (and dock) covering the whole screen.
       *
       * NOTE: This effectively breaks Option-Tabbing as our window covers
       * all other applications completely. However we deal with the situation
       * by lowering the window to the bottom of the normal level stack on
       * windowDidResignKey notification.
       */
      [self->window setLevel: CLUTTER_OSX_FULLSCREEN_WINDOW_LEVEL];

      [self->window setFrame: [self->window frameRectForContentRect: [[self->window screen] frame]] display: NO];
    }
  else
    {
      [self->window setLevel: NSNormalWindowLevel];

      if (self->haveNormalFrame)
        [self->window setFrame: self->normalFrame display: NO];
      else
        /* looks better than positioning to 0,0 (bottom right) */
        [self->window center];
    }
}

/*************************************************************************/
static void
clutter_stage_osx_realize (ClutterActor *actor)
{
  ClutterStageOSX *self = CLUTTER_STAGE_OSX (actor);
  ClutterBackendOSX *backend_osx;

  CLUTTER_NOTE (BACKEND, "realize");

  CLUTTER_OSX_POOL_ALLOC();

  if (CLUTTER_ACTOR_CLASS (clutter_stage_osx_parent_class)->realize)
    CLUTTER_ACTOR_CLASS (clutter_stage_osx_parent_class)->realize (actor);

  backend_osx = CLUTTER_BACKEND_OSX (self->backend);

  NSRect rect = NSMakeRect(0, 0, self->requisition_width, self->requisition_height);

  self->view = [[ClutterGLView alloc]
                initWithFrame: rect
                  pixelFormat: backend_osx->pixel_format
                        stage: actor];

  self->window = [[ClutterGLWindow alloc]
                  initWithView: self->view
                     UTF8Title: clutter_stage_get_title (CLUTTER_STAGE (self))
                         stage: self];

  /* looks better than positioning to 0,0 (bottom right) */
  [self->window center];

  /* To not miss all textures created with the context created in the backend
   * make sure we share the context. (By default NSOpenGLView creates its own
   * context.)
   */
  NSOpenGLContext *context = backend_osx->context;

  [self->view setOpenGLContext: context];
  [context setView: self->view];

  CLUTTER_OSX_POOL_RELEASE();

  CLUTTER_SET_PRIVATE_FLAGS(self, CLUTTER_ACTOR_SYNC_MATRICES);
}

static void
clutter_stage_osx_unrealize (ClutterActor *actor)
{
  ClutterStageOSX *self = CLUTTER_STAGE_OSX (actor);

  CLUTTER_NOTE (BACKEND, "unrealize");

  CLUTTER_OSX_POOL_ALLOC();

  [self->view release];
  [self->window close];

  self->view = NULL;
  self->window = NULL;

  CLUTTER_OSX_POOL_RELEASE();

  if (CLUTTER_ACTOR_CLASS (clutter_stage_osx_parent_class)->unrealize)
    CLUTTER_ACTOR_CLASS (clutter_stage_osx_parent_class)->unrealize (actor);
}

static void
clutter_stage_osx_show (ClutterActor *actor)
{
  ClutterStageOSX *self = CLUTTER_STAGE_OSX (actor);

  CLUTTER_NOTE (BACKEND, "show");

  if (CLUTTER_ACTOR_CLASS (clutter_stage_osx_parent_class)->show)
    CLUTTER_ACTOR_CLASS (clutter_stage_osx_parent_class)->show (actor);

  CLUTTER_OSX_POOL_ALLOC();

  clutter_stage_osx_set_frame (self);

  [self->window makeKeyAndOrderFront: nil];

  CLUTTER_OSX_POOL_RELEASE();
}

static void
clutter_stage_osx_hide (ClutterActor *actor)
{
  ClutterStageOSX *self = CLUTTER_STAGE_OSX (actor);

  CLUTTER_NOTE (BACKEND, "hide");

  CLUTTER_OSX_POOL_ALLOC();

  [self->window orderOut: nil];

  CLUTTER_OSX_POOL_RELEASE();

  if (CLUTTER_ACTOR_CLASS (clutter_stage_osx_parent_class)->hide)
    CLUTTER_ACTOR_CLASS (clutter_stage_osx_parent_class)->hide (actor);
}

static void
clutter_stage_osx_query_coords (ClutterActor    *actor,
                                ClutterActorBox *box)
{
  ClutterStageOSX *self = CLUTTER_STAGE_OSX (actor);

  CLUTTER_OSX_POOL_ALLOC();

  box->x1 = 0;
  box->y1 = 0;
  box->x2 = box->x1 + CLUTTER_UNITS_FROM_FLOAT (self->requisition_width);
  box->y2 = box->y1 + CLUTTER_UNITS_FROM_FLOAT (self->requisition_height);

  CLUTTER_OSX_POOL_RELEASE();
}

static void
clutter_stage_osx_request_coords (ClutterActor    *actor,
                                  ClutterActorBox *box)
{
  ClutterStageOSX *self = CLUTTER_STAGE_OSX (actor);

  CLUTTER_NOTE (BACKEND, "request_coords: %d,%d %dx%d",
                CLUTTER_UNITS_TO_INT (box->x1),
                CLUTTER_UNITS_TO_INT (box->y1),
                CLUTTER_UNITS_TO_INT (box->x2 - box->x1),
                CLUTTER_UNITS_TO_INT (box->y2 - box->y1));

  self->requisition_width  = CLUTTER_UNITS_TO_INT (box->x2 - box->x1);
  self->requisition_height = CLUTTER_UNITS_TO_INT (box->y2 - box->y1);

  if (CLUTTER_ACTOR_IS_REALIZED (actor))
    {
      CLUTTER_OSX_POOL_ALLOC();

      NSSize size = NSMakeSize(self->requisition_width,
                               self->requisition_height);
      [self->window setContentSize: size];

      CLUTTER_OSX_POOL_RELEASE();
    }
}

/*************************************************************************/
static void
clutter_stage_osx_set_title (ClutterStage *stage,
                             const char   *title)
{
  ClutterStageOSX *self = CLUTTER_STAGE_OSX (stage);

  CLUTTER_NOTE (BACKEND, "set_title: %s", title);

  CLUTTER_OSX_POOL_ALLOC();

  if (CLUTTER_ACTOR_IS_REALIZED (CLUTTER_ACTOR (stage)))
    [self->window setTitle:[NSString stringWithUTF8String:title]];

  CLUTTER_OSX_POOL_RELEASE();
}

static void
clutter_stage_osx_set_fullscreen (ClutterStage *stage,
                                  gboolean      fullscreen)
{
  ClutterStageOSX *self = CLUTTER_STAGE_OSX (stage);

  CLUTTER_NOTE (BACKEND, "set_fullscreen: %u", fullscreen);

  CLUTTER_OSX_POOL_ALLOC();

  /* Make sure to update the state before clutter_stage_osx_set_frame.
   *
   * Toggling fullscreen isn't atomic, there's two "events" involved:
   *  - stage state change (via state_update)
   *  - stage size change (via set_frame -> setFrameSize / set_size)
   *
   * We do state change first. Not sure there's any difference.
   */
  if (fullscreen)
    clutter_stage_osx_state_update (self, 0, CLUTTER_STAGE_STATE_FULLSCREEN);
  else
    clutter_stage_osx_state_update (self, CLUTTER_STAGE_STATE_FULLSCREEN, 0);

  if (CLUTTER_ACTOR_IS_REALIZED (CLUTTER_ACTOR (self)))
    {
      if (fullscreen)
        clutter_stage_osx_save_frame (self);

      clutter_stage_osx_set_frame (self);
    }
  else if (fullscreen)
    {
      /* FIXME: if you go fullscreen before realize we throw away the normal
       * stage size and can't return. Need to maintain them separately.
       */
      NSSize size = [[NSScreen mainScreen] frame].size;

      clutter_actor_set_size (CLUTTER_ACTOR (self),
                              (int)size.width, (int)size.height);
    }

  CLUTTER_OSX_POOL_RELEASE();
}

/*************************************************************************/
ClutterActor *
clutter_stage_osx_new (ClutterBackend *backend)
{
  ClutterStageOSX *self;

  self = g_object_new (CLUTTER_TYPE_STAGE_OSX, NULL);
  self->backend = backend;

  return CLUTTER_ACTOR(self);
}

/*************************************************************************/
static void
clutter_stage_osx_init (ClutterStageOSX *self)
{
  self->requisition_width  = 640;
  self->requisition_height = 480;

  CLUTTER_SET_PRIVATE_FLAGS(self, CLUTTER_ACTOR_SYNC_MATRICES);
}

static void
clutter_stage_osx_class_init (ClutterStageOSXClass *klass)
{
  ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
  ClutterStageClass *stage_class = CLUTTER_STAGE_CLASS (klass);

  actor_class->realize   = clutter_stage_osx_realize;
  actor_class->unrealize = clutter_stage_osx_unrealize;
  actor_class->show      = clutter_stage_osx_show;
  actor_class->hide      = clutter_stage_osx_hide;

  actor_class->query_coords   = clutter_stage_osx_query_coords;
  actor_class->request_coords = clutter_stage_osx_request_coords;

  stage_class->set_title = clutter_stage_osx_set_title;
  stage_class->set_fullscreen = clutter_stage_osx_set_fullscreen;
}