/* GStreamer
 * Copyright (C) 2004 Zaheer Abbas Merali <zaheerabbas at merali dot org>
 * Copyright (C) 2007 Pioneers of the Inevitable <songbird@songbirdnest.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 * The development of this code was made possible due to the involvement of Pioneers
 * of the Inevitable, the creators of the Songbird Music player
 *
 */

/* inspiration gained from looking at source of osx video out of xine and vlc
 * and is reflected in the code
 */


#include <Cocoa/Cocoa.h>
#include <gst/gst.h>
#import "cocoawindow.h"
#import "osxvideosink.h"

#include <OpenGL/OpenGL.h>
#include <OpenGL/gl.h>
#include <OpenGL/glext.h>

#include <Carbon/Carbon.h>

/* Debugging category */
#include <gst/gstinfo.h>

static
const gchar* gst_keycode_to_keyname(gint16 keycode)
{
    switch (keycode)
    {
      case kVK_ANSI_A:
        return "a";
      case kVK_ANSI_S:
        return "s";
      case kVK_ANSI_D:
        return "d";
      case kVK_ANSI_F:
        return "f";
      case kVK_ANSI_H:
        return "h";
      case kVK_ANSI_G:
        return "g";
      case kVK_ANSI_Z:
        return "z";
      case kVK_ANSI_X:
        return "x";
      case kVK_ANSI_C:
        return "c";
      case kVK_ANSI_V:
        return "v";
      case kVK_ANSI_B:
        return "b";
      case kVK_ANSI_Q:
        return "q";
      case kVK_ANSI_W:
        return "w";
      case kVK_ANSI_E:
        return "e";
      case kVK_ANSI_R:
        return "r";
      case kVK_ANSI_Y:
        return "y";
      case kVK_ANSI_T:
        return "t";
      case kVK_ANSI_1:
        return "1";
      case kVK_ANSI_2:
        return "2";
      case kVK_ANSI_3:
        return "3";
      case kVK_ANSI_4:
        return "4";
      case kVK_ANSI_6:
        return "6";
      case kVK_ANSI_5:
        return "5";
      case kVK_ANSI_Equal:
        return "equal";
      case kVK_ANSI_9:
        return "9";
      case kVK_ANSI_7:
        return "7";
      case kVK_ANSI_Minus:
        return "minus";
      case kVK_ANSI_8:
        return "8";
      case kVK_ANSI_0:
        return "0";
      case kVK_ANSI_RightBracket:
        return "bracketright";
      case kVK_ANSI_O:
        return "0";
      case kVK_ANSI_U:
        return "u";
      case kVK_ANSI_LeftBracket:
        return "bracketleft";
      case kVK_ANSI_I:
        return "i";
      case kVK_ANSI_P:
        return "p";
      case kVK_ANSI_L:
        return "l";
      case kVK_ANSI_J:
        return "j";
      case kVK_ANSI_Quote:
        return "apostrophe";
      case kVK_ANSI_K:
        return "k";
      case kVK_ANSI_Semicolon:
        return "semicolon";
      case kVK_ANSI_Backslash:
        return "backslash";
      case kVK_ANSI_Comma:
        return "comma";
      case kVK_ANSI_Slash:
        return "slash";
      case kVK_ANSI_N:
        return "n";
      case kVK_ANSI_M:
        return "m";
      case kVK_ANSI_Period:
        return "period";
      case kVK_ANSI_Grave:
        return "grave";
      case kVK_ANSI_KeypadDecimal:
        return "KP_Delete";
      case kVK_ANSI_KeypadMultiply:
        return "KP_Multiply";
      case kVK_ANSI_KeypadPlus:
        return "KP_Add";
      case kVK_ANSI_KeypadClear:
        return "KP_Clear";
      case kVK_ANSI_KeypadDivide:
        return "KP_Divide";
      case kVK_ANSI_KeypadEnter:
        return "KP_Enter";
      case kVK_ANSI_KeypadMinus:
        return "KP_Subtract";
      case kVK_ANSI_KeypadEquals:
        return "KP_Equals";
      case kVK_ANSI_Keypad0:
        return "KP_Insert";
      case kVK_ANSI_Keypad1:
        return "KP_End";
      case kVK_ANSI_Keypad2:
        return "KP_Down";
      case kVK_ANSI_Keypad3:
        return "KP_Next";
      case kVK_ANSI_Keypad4:
        return "KP_Left";
      case kVK_ANSI_Keypad5:
        return "KP_Begin";
      case kVK_ANSI_Keypad6:
        return "KP_Right";
      case kVK_ANSI_Keypad7:
        return "KP_Home";
      case kVK_ANSI_Keypad8:
        return "KP_Up";
      case kVK_ANSI_Keypad9:
        return "KP_Prior";

    /* keycodes for keys that are independent of keyboard layout*/

      case kVK_Return:
        return "Return";
      case kVK_Tab:
        return "Tab";
      case kVK_Space:
        return "space";
      case kVK_Delete:
        return "Backspace";
      case kVK_Escape:
        return "Escape";
      case kVK_Command:
        return "Command";
      case kVK_Shift:
        return "Shift_L";
      case kVK_CapsLock:
        return "Caps_Lock";
      case kVK_Option:
        return "Option_L";
      case kVK_Control:
        return "Control_L";
      case kVK_RightShift:
        return "Shift_R";
      case kVK_RightOption:
        return "Option_R";
      case kVK_RightControl:
        return "Control_R";
      case kVK_Function:
        return "Function";
      case kVK_F17:
        return "F17";
      case kVK_VolumeUp:
        return "VolumeUp";
      case kVK_VolumeDown:
        return "VolumeDown";
      case kVK_Mute:
        return "Mute";
      case kVK_F18:
        return "F18";
      case kVK_F19:
        return "F19";
      case kVK_F20:
        return "F20";
      case kVK_F5:
        return "F5";
      case kVK_F6:
        return "F6";
      case kVK_F7:
        return "F7";
      case kVK_F3:
        return "F3";
      case kVK_F8:
        return "F8";
      case kVK_F9:
        return "F9";
      case kVK_F11:
        return "F11";
      case kVK_F13:
        return "F13";
      case kVK_F16:
        return "F16";
      case kVK_F14:
        return "F14";
      case kVK_F10:
        return "F10";
      case kVK_F12:
        return "F12";
      case kVK_F15:
        return "F15";
      case kVK_Help:
        return "Help";
      case kVK_Home:
        return "Home";
      case kVK_PageUp:
        return "Prior";
      case kVK_ForwardDelete:
        return "Delete";
      case kVK_F4:
        return "F4";
      case kVK_End:
        return "End";
      case kVK_F2:
        return "F2";
      case kVK_PageDown:
        return "Next";
      case kVK_F1:
        return "F1";
      case kVK_LeftArrow:
        return "Left";
      case kVK_RightArrow:
        return "Right";
      case kVK_DownArrow:
        return "Down";
      case kVK_UpArrow:
        return "Up";
    default:
        return "";
  };
}

@ implementation GstOSXVideoSinkWindow

/* The object has to be released */
- (id) initWithContentNSRect: (NSRect) rect
		 styleMask: (unsigned int) styleMask
		   backing: (NSBackingStoreType) bufferingType
		     defer: (BOOL) flag
		    screen:(NSScreen *) aScreen
{
  self = [super initWithContentRect: rect
		styleMask: styleMask
		backing: bufferingType
		defer: flag
		screen:aScreen];

  GST_DEBUG ("Initializing GstOSXvideoSinkWindow");

  gstview = [[GstGLView alloc] initWithFrame:rect];

  if (gstview)
    [self setContentView:gstview];
  [self setTitle:@"GStreamer Video Output"];

  return self;
}

- (void) setContentSize:(NSSize) size {
  width = size.width;
  height = size.height;

  [super setContentSize:size];
}

- (GstGLView *) gstView {
  return gstview;
}

- (void) awakeFromNib {
  [self setAcceptsMouseMovedEvents:YES];
}

@end


//
// OpenGL implementation
//

@ implementation GstGLView

- (id) initWithFrame:(NSRect) frame {
  NSOpenGLPixelFormat *fmt;
  NSOpenGLPixelFormatAttribute attribs[] = {
    NSOpenGLPFANoRecovery,
    NSOpenGLPFADoubleBuffer,
    NSOpenGLPFAColorSize, 24,
    NSOpenGLPFAAlphaSize, 8,
    NSOpenGLPFADepthSize, 24,
    NSOpenGLPFAWindow,
    0
  };

  fmt = [[NSOpenGLPixelFormat alloc]
	  initWithAttributes:attribs];

  if (!fmt) {
    GST_WARNING ("Cannot create NSOpenGLPixelFormat");
    return nil;
  }

  self = [super initWithFrame: frame pixelFormat:fmt];
  [fmt release];

   actualContext = [self openGLContext];
   [actualContext makeCurrentContext];
   [actualContext update];

  /* Black background */
  glClearColor (0.0, 0.0, 0.0, 0.0);

  pi_texture = 0;
  data = nil;
  width = frame.size.width;
  height = frame.size.height;
  drawingBounds = NSMakeRect(0, 0, width, height);

  GST_LOG ("Width: %d Height: %d", width, height);

  trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds]
      options: (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow)
      owner:self
      userInfo:nil];

  [self addTrackingArea:trackingArea];
  mainThread = [NSThread mainThread];

  [self initTextures];
  return self;
}

- (NSRect) getDrawingBounds {
  return drawingBounds;
}

- (void) reshape {
  NSRect bounds;
  gdouble frame_par, view_par;
  gint view_height, view_width, c_height, c_width, c_x, c_y;


  GST_LOG ("reshaping");

  if (!initDone) {
    return;
  }

  [actualContext makeCurrentContext];

  bounds = [self bounds];
  view_width = bounds.size.width;
  view_height = bounds.size.height;

  frame_par = (gdouble) width / height;
  view_par = (gdouble) view_width / view_height;
  if (!keepAspectRatio)
    view_par = frame_par;

  if (frame_par == view_par) {
    c_height = view_height;
    c_width = view_width;
    c_x = 0;
    c_y = 0;
  } else if (frame_par < view_par) {
    c_height = view_height;
    c_width = c_height * frame_par;
    c_x = (view_width - c_width) / 2;
    c_y = 0;
  } else {
    c_width = view_width;
    c_height = c_width / frame_par;
    c_x = 0;
    c_y = (view_height - c_height) / 2;
  }

  drawingBounds = NSMakeRect(c_x, c_y, c_width, c_height);
  glViewport (c_x, c_y, (GLint) c_width, (GLint) c_height);
}

- (void) initTextures {

  [actualContext makeCurrentContext];

  /* Free previous texture if any */
  if (pi_texture) {
    glDeleteTextures (1, (GLuint *)&pi_texture);
  }

  if (data) {
    data = g_realloc (data, width * height * sizeof(short)); // short or 3byte?
  } else {
    data = g_malloc0(width * height * sizeof(short));
  }
  /* Create textures */
  glGenTextures (1, (GLuint *)&pi_texture);

  glEnable (GL_TEXTURE_RECTANGLE_EXT);
  glEnable (GL_UNPACK_CLIENT_STORAGE_APPLE);

  glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
  glPixelStorei (GL_UNPACK_ROW_LENGTH, width);

  glBindTexture (GL_TEXTURE_RECTANGLE_EXT, pi_texture);

  /* Use VRAM texturing */
  glTexParameteri (GL_TEXTURE_RECTANGLE_EXT,
		   GL_TEXTURE_STORAGE_HINT_APPLE, GL_STORAGE_CACHED_APPLE);

  /* Tell the driver not to make a copy of the texture but to use
     our buffer */
  glPixelStorei (GL_UNPACK_CLIENT_STORAGE_APPLE, GL_TRUE);

  /* Linear interpolation */
  glTexParameteri (GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri (GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

  /* I have no idea what this exactly does, but it seems to be
     necessary for scaling */
  glTexParameteri (GL_TEXTURE_RECTANGLE_EXT,
		   GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri (GL_TEXTURE_RECTANGLE_EXT,
		   GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  // glPixelStorei (GL_UNPACK_ROW_LENGTH, 0); WHY ??

  glTexImage2D (GL_TEXTURE_RECTANGLE_EXT, 0, GL_RGBA,
		width, height, 0,
		GL_YCBCR_422_APPLE, GL_UNSIGNED_SHORT_8_8_APPLE, data);


  initDone = 1;
}

- (void) reloadTexture {
  if (!initDone) {
    return;
  }

  GST_LOG ("Reloading Texture");

  [actualContext makeCurrentContext];

  glBindTexture (GL_TEXTURE_RECTANGLE_EXT, pi_texture);
  glPixelStorei (GL_UNPACK_ROW_LENGTH, width);

  /* glTexSubImage2D is faster than glTexImage2D
     http://developer.apple.com/samplecode/Sample_Code/Graphics_3D/
     TextureRange/MainOpenGLView.m.htm */
  glTexSubImage2D (GL_TEXTURE_RECTANGLE_EXT, 0, 0, 0,
		   width, height,
		   GL_YCBCR_422_APPLE, GL_UNSIGNED_SHORT_8_8_APPLE, data);    //FIXME
}

- (void) cleanUp {
  initDone = 0;
}

- (void) drawQuad {
  f_x = 1.0;
  f_y = 1.0;

  glBegin (GL_QUADS);
  /* Top left */
  glTexCoord2f (0.0, 0.0);
  glVertex2f (-f_x, f_y);
  /* Bottom left */
  glTexCoord2f (0.0, (float) height);
  glVertex2f (-f_x, -f_y);
  /* Bottom right */
  glTexCoord2f ((float) width, (float) height);
  glVertex2f (f_x, -f_y);
  /* Top right */
  glTexCoord2f ((float) width, 0.0);
  glVertex2f (f_x, f_y);
  glEnd ();
}

- (void) drawRect:(NSRect) rect {
  GLint params[] = { 1 };

  [actualContext makeCurrentContext];

  CGLSetParameter (CGLGetCurrentContext (), kCGLCPSwapInterval, params);

  /* Black background */
  glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  if (!initDone) {
    [actualContext flushBuffer];
    return;
  }

  /* Draw */
  glBindTexture (GL_TEXTURE_RECTANGLE_EXT, pi_texture); // FIXME
  [self drawQuad];
  /* Draw */
  [actualContext flushBuffer];
}

- (void) displayTexture {
  if ([self lockFocusIfCanDraw]) {

    [self drawRect:[self bounds]];
    [self reloadTexture];

    [self unlockFocus];

  }

}

- (char *) getTextureBuffer {
  return data;
}

- (void) setFullScreen:(BOOL) flag {
  if (!fullscreen && flag) {
    // go to full screen
    /* Create the new pixel format */
    NSOpenGLPixelFormat *fmt;
    NSOpenGLPixelFormatAttribute attribs[] = {
      NSOpenGLPFAAccelerated,
      NSOpenGLPFANoRecovery,
      NSOpenGLPFADoubleBuffer,
      NSOpenGLPFAColorSize, 24,
      NSOpenGLPFAAlphaSize, 8,
      NSOpenGLPFADepthSize, 24,
      NSOpenGLPFAFullScreen,
      NSOpenGLPFAScreenMask,
      CGDisplayIDToOpenGLDisplayMask (kCGDirectMainDisplay),
      0
    };

    fmt = [[NSOpenGLPixelFormat alloc]
	    initWithAttributes:attribs];

    if (!fmt) {
      GST_WARNING ("Cannot create NSOpenGLPixelFormat");
      return;
    }

    /* Create the new OpenGL context */
    fullScreenContext = [[NSOpenGLContext alloc]
			  initWithFormat: fmt shareContext:nil];
    if (!fullScreenContext) {
      GST_WARNING ("Failed to create new NSOpenGLContext");
      return;
    }

    actualContext = fullScreenContext;

    /* Capture display, switch to fullscreen */
    if (CGCaptureAllDisplays () != CGDisplayNoErr) {
      GST_WARNING ("CGCaptureAllDisplays() failed");
      return;
    }
    [fullScreenContext setFullScreen];
    [fullScreenContext makeCurrentContext];

    fullscreen = YES;

    [self initTextures];
    [self setNeedsDisplay:YES];

  } else if (fullscreen && !flag) {
    // fullscreen now and needs to go back to normal
    initDone = NO;

    actualContext = [self openGLContext];

    [NSOpenGLContext clearCurrentContext];
    [fullScreenContext clearDrawable];
    [fullScreenContext release];
    fullScreenContext = nil;

    CGReleaseAllDisplays ();

    [self reshape];
    [self initTextures];

    [self setNeedsDisplay:YES];

    fullscreen = NO;
    initDone = YES;
  }
}

- (void) setVideoSize: (int)w : (int)h {
  GST_LOG ("width:%d, height:%d", w, h);

  width = w;
  height = h;

  [self initTextures];
  [self reshape];
}

- (void) setKeepAspectRatio: (BOOL) flag {
  keepAspectRatio = flag;
  [self reshape];
}

- (void) setMainThread: (NSThread *) thread {
  mainThread = thread;
}

- (void) haveSuperviewReal:(NSMutableArray *)closure {
	BOOL haveSuperview = [self superview] != nil;
	[closure addObject:[NSNumber numberWithBool:haveSuperview]];
}

- (BOOL) haveSuperview {
	NSMutableArray *closure = [NSMutableArray arrayWithCapacity:1];
	[self performSelector:@selector(haveSuperviewReal:)
		onThread:mainThread
		withObject:(id)closure waitUntilDone:YES];

	return [[closure objectAtIndex:0] boolValue];
}

- (void) addToSuperviewReal:(NSView *)superview {
	NSRect bounds;
	[superview addSubview:self];
	bounds = [superview bounds];
	[self setFrame:bounds];
	[self setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
}

- (void) addToSuperview: (NSView *)superview {
	[self performSelector:@selector(addToSuperviewReal:)
		onThread:mainThread
		withObject:superview waitUntilDone:YES];
}

- (void) removeFromSuperview: (id)unused
{
	[self removeFromSuperview];
}

- (void) dealloc {
  GST_LOG ("dealloc called");
  if (data) g_free(data);

  if (fullScreenContext) {
    [NSOpenGLContext clearCurrentContext];
    [fullScreenContext clearDrawable];
    [fullScreenContext release];
    if (actualContext == fullScreenContext) actualContext = nil;
    fullScreenContext = nil;
  }

  [super dealloc];
}

- (void)updateTrackingAreas {
  [self removeTrackingArea:trackingArea];
  [trackingArea release];
  trackingArea = [[NSTrackingArea alloc] initWithRect: [self bounds]
      options: (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow)
      owner:self userInfo:nil];
  [self addTrackingArea:trackingArea];
}

- (BOOL)acceptsFirstResponder {
    return YES;
}

- (void) setNavigation:(GstNavigation *)nav
{
  navigation = nav;
}

- (void)sendMouseEvent:(NSEvent *)event : (const char *)event_name
{
  NSPoint location;
  gint button;
  gdouble x, y;

  if (!navigation)
    return;

  switch ([event type]) {
    case NSMouseMoved:
      button = 0;
      break;
    case NSLeftMouseDown:
    case NSLeftMouseUp:
      button = 1;
      break;
    case NSRightMouseDown:
    case NSRightMouseUp:
      button = 2;
      break;
    default:
      button = 3;
      break;
  }

  location = [self convertPoint:[event locationInWindow] fromView:nil];

  x = location.x;
  y = location.y;
  /* invert Y */

  y = (1 - ((gdouble) y) / [self bounds].size.height) * [self bounds].size.height;

  gst_navigation_send_mouse_event (navigation, event_name, button, x, y);
}

- (void)sendKeyEvent:(NSEvent *)event : (const char *)event_name
{
  if (!navigation)
    return;

  gst_navigation_send_key_event(navigation, event_name, gst_keycode_to_keyname([event keyCode]));
}

- (void)sendModifierKeyEvent:(NSEvent *)event
{
  NSUInteger flags = [event modifierFlags];
  const gchar* event_name = flags > savedModifierFlags ? "key-press" : "key-release";
  savedModifierFlags = flags;
  [self sendKeyEvent: event: event_name];
}

- (void)keyDown:(NSEvent *) event;
{
  [self sendKeyEvent: event: "key-press"];
  [super keyDown: event];
}

- (void)keyUp:(NSEvent *) event;
{
  [self sendKeyEvent: event: "key-release"];
  [super keyUp: event];
}

- (void)flagsChanged:(NSEvent *) event;
{
  [self sendModifierKeyEvent: event];
  [super flagsChanged: event];
}

- (void)mouseDown:(NSEvent *) event;
{
  [self sendMouseEvent:event: "mouse-button-press"];
  [super mouseDown: event];
}

- (void)mouseUp:(NSEvent *) event;
{
  [self sendMouseEvent:event: "mouse-button-release"];
  [super mouseUp: event];
}

- (void)mouseMoved:(NSEvent *)event;
{
  [self sendMouseEvent:event: "mouse-move"];
  [super mouseMoved: event];
}

- (void)mouseEntered:(NSEvent *)event;
{
  [super mouseEntered: event];
}

- (void)mouseExited:(NSEvent *)event;
{
  [super mouseExited: event];
}

@end
