
/**
 * Scintilla source code edit control
 * ScintillaCocoa.mm - Cocoa subclass of ScintillaBase
 * 
 * Mike Lischke <mlischke@sun.com>
 *
 * Loosely based on ScintillaMacOSX.cxx.
 * Copyright 2003 by Evan Jones <ejones@uwaterloo.ca>
 * Based on ScintillaGTK.cxx Copyright 1998-2002 by Neil Hodgson <neilh@scintilla.org>
 * The License.txt file describes the conditions under which this software may be distributed.
  *
 * Copyright 2009 Sun Microsystems, Inc. All rights reserved.
 * This file is dual licensed under LGPL v2.1 and the Scintilla license (http://www.scintilla.org/License.txt).
 */

#import <Cocoa/Cocoa.h>

#import <Carbon/Carbon.h> // Temporary

#include "ScintillaView.h"
#include "PlatCocoa.h"

using namespace Scintilla;

#ifndef WM_UNICHAR
#define WM_UNICHAR 0x0109
#endif

NSString* ScintillaRecPboardType = @"com.scintilla.utf16-plain-text.rectangular";

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

// Define keyboard shortcuts (equivalents) the Mac way.
#define SCI_CMD ( SCI_ALT | SCI_CTRL)
#define SCI_SCMD ( SCI_CMD | SCI_SHIFT)

static const KeyToCommand macMapDefault[] =
{
  {SCK_DOWN,      SCI_CMD,    SCI_DOCUMENTEND},
  {SCK_UP,        SCI_CMD,    SCI_DOCUMENTSTART},
  {SCK_LEFT,      SCI_CMD,    SCI_VCHOME},
  {SCK_LEFT,      SCI_SCMD,   SCI_VCHOMEEXTEND},
  {SCK_RIGHT,     SCI_CMD,    SCI_LINEEND},
  {SCK_RIGHT,     SCI_SCMD,   SCI_LINEENDEXTEND},
  {SCK_DOWN,      SCI_NORM,   SCI_LINEDOWN},
  {SCK_DOWN,      SCI_SHIFT,  SCI_LINEDOWNEXTEND},
  {SCK_DOWN,      SCI_CTRL,   SCI_LINESCROLLDOWN},
  {SCK_DOWN,      SCI_ASHIFT, SCI_LINEDOWNRECTEXTEND},
  {SCK_UP,        SCI_NORM,   SCI_LINEUP},
  {SCK_UP,        SCI_SHIFT,  SCI_LINEUPEXTEND},
  {SCK_UP,        SCI_CTRL,   SCI_LINESCROLLUP},
  {SCK_UP,        SCI_ASHIFT, SCI_LINEUPRECTEXTEND},
  {'[',           SCI_CTRL,   SCI_PARAUP},
  {'[',           SCI_CSHIFT, SCI_PARAUPEXTEND},
  {']',           SCI_CTRL,   SCI_PARADOWN},
  {']',           SCI_CSHIFT, SCI_PARADOWNEXTEND},
  {SCK_LEFT,      SCI_NORM,   SCI_CHARLEFT},
  {SCK_LEFT,      SCI_SHIFT,  SCI_CHARLEFTEXTEND},
  {SCK_LEFT,      SCI_ALT,    SCI_WORDLEFT},
  {SCK_LEFT,      SCI_CSHIFT, SCI_WORDLEFTEXTEND},
  {SCK_LEFT,      SCI_ASHIFT, SCI_CHARLEFTRECTEXTEND},
  {SCK_RIGHT,     SCI_NORM,   SCI_CHARRIGHT},
  {SCK_RIGHT,     SCI_SHIFT,  SCI_CHARRIGHTEXTEND},
  {SCK_RIGHT,     SCI_ALT,    SCI_WORDRIGHT},
  {SCK_RIGHT,     SCI_CSHIFT, SCI_WORDRIGHTEXTEND},
  {SCK_RIGHT,     SCI_ASHIFT, SCI_CHARRIGHTRECTEXTEND},
  {'/',           SCI_CTRL,   SCI_WORDPARTLEFT},
  {'/',           SCI_CSHIFT, SCI_WORDPARTLEFTEXTEND},
  {'\\',          SCI_CTRL,   SCI_WORDPARTRIGHT},
  {'\\',          SCI_CSHIFT, SCI_WORDPARTRIGHTEXTEND},
  {SCK_HOME,      SCI_NORM,   SCI_VCHOME},
  {SCK_HOME,      SCI_SHIFT,  SCI_VCHOMEEXTEND},
  {SCK_HOME,      SCI_CTRL,   SCI_DOCUMENTSTART},
  {SCK_HOME,      SCI_CSHIFT, SCI_DOCUMENTSTARTEXTEND},
  {SCK_HOME,      SCI_ALT,    SCI_HOMEDISPLAY},
  {SCK_HOME,      SCI_ASHIFT, SCI_VCHOMERECTEXTEND},
  {SCK_END,       SCI_NORM,   SCI_LINEEND},
  {SCK_END,       SCI_SHIFT,  SCI_LINEENDEXTEND},
  {SCK_END,       SCI_CTRL,   SCI_DOCUMENTEND},
  {SCK_END,       SCI_CSHIFT, SCI_DOCUMENTENDEXTEND},
  {SCK_END,       SCI_ALT,    SCI_LINEENDDISPLAY},
  {SCK_END,       SCI_ASHIFT, SCI_LINEENDRECTEXTEND},
  {SCK_PRIOR,     SCI_NORM,   SCI_PAGEUP},
  {SCK_PRIOR,     SCI_SHIFT,  SCI_PAGEUPEXTEND},
  {SCK_PRIOR,     SCI_ASHIFT, SCI_PAGEUPRECTEXTEND},
  {SCK_NEXT,      SCI_NORM,   SCI_PAGEDOWN},
  {SCK_NEXT,      SCI_SHIFT,  SCI_PAGEDOWNEXTEND},
  {SCK_NEXT,      SCI_ASHIFT, SCI_PAGEDOWNRECTEXTEND},
  {SCK_DELETE,    SCI_NORM,   SCI_CLEAR},
  {SCK_DELETE,    SCI_SHIFT,  SCI_CUT},
  {SCK_DELETE,    SCI_CTRL,   SCI_DELWORDRIGHT},
  {SCK_DELETE,    SCI_CSHIFT, SCI_DELLINERIGHT},
  {SCK_INSERT,    SCI_NORM,   SCI_EDITTOGGLEOVERTYPE},
  {SCK_INSERT,    SCI_SHIFT,  SCI_PASTE},
  {SCK_INSERT,    SCI_CTRL,   SCI_COPY},
  {SCK_ESCAPE,    SCI_NORM,   SCI_CANCEL},
  {SCK_BACK,      SCI_NORM,   SCI_DELETEBACK},
  {SCK_BACK,      SCI_SHIFT,  SCI_DELETEBACK},
  {SCK_BACK,      SCI_CTRL,   SCI_DELWORDLEFT},
  {SCK_BACK,      SCI_ALT,    SCI_UNDO},
  {SCK_BACK,      SCI_CSHIFT, SCI_DELLINELEFT},
  {'z',           SCI_CMD,    SCI_UNDO},
  {'y',           SCI_CMD,    SCI_REDO},
  {'x',           SCI_CMD,    SCI_CUT},
  {'c',           SCI_CMD,    SCI_COPY},
  {'v',           SCI_CMD,    SCI_PASTE},
  {'a',           SCI_CMD,    SCI_SELECTALL},
  {SCK_TAB,       SCI_NORM,   SCI_TAB},
  {SCK_TAB,       SCI_SHIFT,  SCI_BACKTAB},
  {SCK_RETURN,    SCI_NORM,   SCI_NEWLINE},
  {SCK_RETURN,    SCI_SHIFT,  SCI_NEWLINE},
  {SCK_ADD,       SCI_CMD,    SCI_ZOOMIN},
  {SCK_SUBTRACT,  SCI_CMD,    SCI_ZOOMOUT},
  {SCK_DIVIDE,    SCI_CMD,    SCI_SETZOOM},
  {'l',           SCI_CMD,    SCI_LINECUT},
  {'l',           SCI_CSHIFT, SCI_LINEDELETE},
  {'t',           SCI_CSHIFT, SCI_LINECOPY},
  {'t',           SCI_CTRL,   SCI_LINETRANSPOSE},
  {'d',           SCI_CTRL,   SCI_SELECTIONDUPLICATE},
  {'u',           SCI_CTRL,   SCI_LOWERCASE},
  {'u',           SCI_CSHIFT, SCI_UPPERCASE},
  {0, 0, 0},
};

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

@implementation TimerTarget

- (id) init: (void*) target
{
  [super init];
  if (self != nil)
  {
    mTarget = target;

    // Get the default notification queue for the thread which created the instance (usually the
    // main thread). We need that later for idle event processing.
    NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 
    notificationQueue = [[NSNotificationQueue alloc] initWithNotificationCenter: center];
    [center addObserver: self selector: @selector(idleTriggered:) name: @"Idle" object: nil]; 
  }
  return self;
}

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

/**
 * Method called by a timer installed by ScintillaCocoa. This two step approach is needed because
 * a native Obj-C class is required as target for the timer.
 */
- (void) timerFired: (NSTimer*) timer
{
  reinterpret_cast<ScintillaCocoa*>(mTarget)->TimerFired(timer);
}

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

/**
 * Another timer callback for the idle timer.
 */
- (void) idleTimerFired: (NSTimer*) timer
{
  // Idle timer event.
  // Post a new idle notification, which gets executed when the run loop is idle.
  // Since we are coalescing on name and sender there will always be only one actual notification
  // even for multiple requests.
  NSNotification *notification = [NSNotification notificationWithName: @"Idle" object: self]; 
  [notificationQueue enqueueNotification: notification
                            postingStyle: NSPostWhenIdle
                            coalesceMask: (NSNotificationCoalescingOnName | NSNotificationCoalescingOnSender)
                                forModes: nil]; 
}

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

/**
 * Another step for idle events. The timer (for idle events) simply requests a notification on
 * idle time. Only when this notification is send we actually call back the editor.
 */
- (void) idleTriggered: (NSNotification*) notification
{
  reinterpret_cast<ScintillaCocoa*>(mTarget)->IdleTimerFired();
}

@end

//----------------- ScintillaCocoa -----------------------------------------------------------------

ScintillaCocoa::ScintillaCocoa(NSView* view)
{
  wMain= [view retain];
  timerTarget = [[[TimerTarget alloc] init: this] retain];
  Initialise();
}

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

ScintillaCocoa::~ScintillaCocoa()
{
  SetTicking(false);
  [timerTarget release];
  NSView* container = ContentView();
  [container release];
}

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

/**
 * Core initialization of the control. Everything that needs to be set up happens here.
 */
void ScintillaCocoa::Initialise() 
{
  notifyObj = NULL;
  notifyProc = NULL;
  
  capturedMouse = false;
  
  // Tell Scintilla not to buffer: Quartz buffers drawing for us.
  WndProc(SCI_SETBUFFEREDDRAW, 0, 0);
  
  // We are working with Unicode exclusively.
  WndProc(SCI_SETCODEPAGE, SC_CP_UTF8, 0);

  // Add Mac specific key bindings.
  for (int i = 0; macMapDefault[i].key; i++) 
    kmap.AssignCmdKey(macMapDefault[i].key, macMapDefault[i].modifiers, macMapDefault[i].msg);
  
}

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

/**
 * We need some clean up. Do it here.
 */
void ScintillaCocoa::Finalise()
{
  SetTicking(false);
  ScintillaBase::Finalise();
}

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

/**
 * Helper function to get the outer container which represents the Scintilla editor on application side.
 */
ScintillaView* ScintillaCocoa::TopContainer()
{
  NSView* container = static_cast<NSView*>(wMain.GetID());
  return static_cast<ScintillaView*>([container superview]);
}

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

/**
 * Helper function to get the inner container which represents the actual "canvas" we work with.
 */
NSView* ScintillaCocoa::ContentView()
{
  return static_cast<NSView*>(wMain.GetID());
}

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

/**
 * Instead of returning the size of the inner view we have to return the visible part of it
 * in order to make scrolling working properly.
 */
PRectangle ScintillaCocoa::GetClientRectangle()
{
  NSView* host = ContentView();
  NSSize size = [host frame].size;
  return PRectangle(0, 0, size.width, size.height);
}

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

/**
 * Converts the given point from base coordinates to local coordinates and at the same time into
 * a native Point structure. Base coordinates are used for the top window used in the view hierarchy. 
 */
Scintilla::Point ScintillaCocoa::ConvertPoint(NSPoint point)
{
  NSView* container = ContentView();
  NSPoint result = [container convertPoint: point fromView: nil];
  
  return Point(result.x, result.y);
}

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

/**
 * A function to directly execute code that would usually go the long way via window messages.
 * However this is a Windows metapher and not used here, hence we just call our fake
 * window proc. The given parameters directly reflect the message parameters used on Windows.
 *
 * @param sciThis The target which is to be called.
 * @param iMessage A code that indicates which message was sent.
 * @param wParam One of the two free parameters for the message. Traditionally a word sized parameter 
 *               (hence the w prefix).
 * @param lParam The other of the two free parameters. A signed long.
 */
sptr_t ScintillaCocoa::DirectFunction(ScintillaCocoa *sciThis, unsigned int iMessage, uptr_t wParam, 
                                      sptr_t lParam)
{
  return sciThis->WndProc(iMessage, wParam, lParam);
}

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

/**
 * This method is very similar to DirectFunction. On Windows it sends a message (not in the Obj-C sense)
 * to the target window. Here we simply call our fake window proc.
 */
sptr_t scintilla_send_message(void* sci, unsigned int iMessage, uptr_t wParam, sptr_t lParam)
{
  ScintillaView *control = reinterpret_cast<ScintillaView*>(sci);
  ScintillaCocoa* scintilla = [control backend];
  return scintilla->WndProc(iMessage, wParam, lParam);
}

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

/** 
 * That's our fake window procedure. On Windows each window has a dedicated procedure to handle
 * commands (also used to synchronize UI and background threads), which is not the case in Cocoa.
 * 
 * Messages handled here are almost solely for special commands of the backend. Everything which
 * would be sytem messages on Windows (e.g. for key down, mouse move etc.) are handled by
 * directly calling appropriate handlers.
 */
sptr_t ScintillaCocoa::WndProc(unsigned int iMessage, uptr_t wParam, sptr_t lParam)
{
  switch (iMessage)
  {
    case SCI_GETDIRECTFUNCTION:
      return reinterpret_cast<sptr_t>(DirectFunction);
      
    case SCI_GETDIRECTPOINTER:
      return reinterpret_cast<sptr_t>(this);
      
    case SCI_GRABFOCUS:
      // TODO: implement it
      break;

    case WM_UNICHAR: 
      // Special case not used normally. Characters passed in this way will be inserted
      // regardless of their value or modifier states. That means no command interpretation is
      // performed.
      if (IsUnicodeMode())
      {
        NSString* input = [[NSString stringWithCharacters: (const unichar*) &wParam length: 1] autorelease];
        const char* utf8 = [input UTF8String];
        AddCharUTF((char*) utf8, strlen(utf8), false);
        return 1;
      }
      return 0;
      
    default:
      unsigned int r = ScintillaBase::WndProc(iMessage, wParam, lParam);
      
      return r;
  }
  return 0l;
}

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

/**
 * In Windows lingo this is the handler which handles anything that wasn't handled in the normal 
 * window proc which would usually send the message back to generic window proc that Windows uses.
 */
sptr_t ScintillaCocoa::DefWndProc(unsigned int, uptr_t, sptr_t)
{
  return 0;
}

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

/**
 * Enables or disables a timer that can trigger background processing at a regular interval, like
 * drag scrolling or caret blinking.
 */
void ScintillaCocoa::SetTicking(bool on)
{
  if (timer.ticking != on)
  {
    timer.ticking = on;
    if (timer.ticking)
    {
      // Scintilla ticks = milliseconds
      // Using userInfo as flag to distinct between tick and idle timer.
      NSTimer* tickTimer = [NSTimer scheduledTimerWithTimeInterval: timer.tickSize / 1000.0
                                                            target: timerTarget
                                                          selector: @selector(timerFired:)
                                                          userInfo: nil
                                                           repeats: YES];
      timer.tickerID = reinterpret_cast<TickerID>(tickTimer);
    }
    else
      if (timer.tickerID != NULL)
      {
        [reinterpret_cast<NSTimer*>(timer.tickerID) invalidate];
        timer.tickerID = 0;
      }
  }
  timer.ticksToWait = caret.period;
}

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

bool ScintillaCocoa::SetIdle(bool on)
{
  if (idler.state != on)
  {
    idler.state = on;
    if (idler.state)
    {
      // Scintilla ticks = milliseconds
      NSTimer* idleTimer = [NSTimer scheduledTimerWithTimeInterval: timer.tickSize / 1000.0
                                                            target: timerTarget
                                                          selector: @selector(idleTimerFired:)
                                                          userInfo: nil
                                                           repeats: YES];
      idler.idlerID = reinterpret_cast<IdlerID>(idleTimer);
    }
    else
      if (idler.idlerID != NULL)
      {
        [reinterpret_cast<NSTimer*>(idler.idlerID) invalidate];
        idler.idlerID = 0;
      }
  }
  return true;
}

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

void ScintillaCocoa::CopyToClipboard(const SelectionText &selectedText)
{
  SetPasteboardData([NSPasteboard generalPasteboard], selectedText);
}

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

void ScintillaCocoa::Copy()
{
  if (currentPos != anchor)
  {
    SelectionText selectedText;
    CopySelectionRange(&selectedText);
    CopyToClipboard(selectedText);
  }
}

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

bool ScintillaCocoa::CanPaste()
{
  if (!Editor::CanPaste())
    return false;
  
  bool ok = GetPasteboardData([NSPasteboard generalPasteboard], NULL);
  return ok;
}

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

void ScintillaCocoa::Paste()
{
  Paste(false);
}

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

/**
 * Pastes data from the paste board into the editor.
 */
void ScintillaCocoa::Paste(bool forceRectangular)
{
  SelectionText selectedText;
  bool ok = GetPasteboardData([NSPasteboard generalPasteboard], &selectedText);
  if (forceRectangular)
    selectedText.rectangular = forceRectangular;
  
  if (!ok || !selectedText.s)
    // No data or no flavor we support.
    return;
  
  pdoc->BeginUndoAction();
  ClearSelection();
  if (selectedText.rectangular)
  {
    int selStart = SelectionStart();
    PasteRectangular(selStart, selectedText.s, selectedText.len);
  }
  else 
    if (pdoc->InsertString(currentPos, selectedText.s, selectedText.len))
      SetEmptySelection(currentPos + selectedText.len);
  
  pdoc->EndUndoAction();
  
  Redraw();
  EnsureCaretVisible();
}

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

void ScintillaCocoa::CreateCallTipWindow(PRectangle rc)
{
#if 0
  // create a calltip window
  if (!ct.wCallTip.Created()) {
    WindowClass windowClass = kHelpWindowClass;
    WindowAttributes attributes = kWindowNoAttributes;
    Rect contentBounds;
    WindowRef outWindow;
    
    // convert PRectangle to Rect
    // this adjustment gets the calltip window placed in the correct location relative
    // to our editor window
    Rect bounds;
    OSStatus err;
    err = GetWindowBounds( this->GetOwner(), kWindowGlobalPortRgn, &bounds );
    assert( err == noErr );
    contentBounds.top = rc.top + bounds.top;
    contentBounds.bottom = rc.bottom + bounds.top;
    contentBounds.right = rc.right + bounds.left;
    contentBounds.left = rc.left + bounds.left;
    
    // create our calltip hiview
    HIViewRef ctw = scintilla_calltip_new();
    CallTip* objectPtr = &ct;
    ScintillaCocoa* sciThis = this;
    SetControlProperty( ctw, scintillaMacOSType, 0, sizeof( this ), &sciThis );
    SetControlProperty( ctw, scintillaCallTipType, 0, sizeof( objectPtr ), &objectPtr );
    
    CreateNewWindow(windowClass, attributes, &contentBounds, &outWindow);
    ControlRef root;
    CreateRootControl(outWindow, &root);
    
    HIViewRef hiroot = HIViewGetRoot (outWindow);
    HIViewAddSubview(hiroot, ctw);
    
    HIRect boundsRect;
    HIViewGetFrame(hiroot, &boundsRect);
    HIViewSetFrame( ctw, &boundsRect );
    
    // bind the size of the calltip to the size of it's container window
    HILayoutInfo layout = {
      kHILayoutInfoVersionZero,
      {
        { NULL, kHILayoutBindTop, 0 },
        { NULL, kHILayoutBindLeft, 0 },
        { NULL, kHILayoutBindBottom, 0 },
        { NULL, kHILayoutBindRight, 0 }
      },
      {
        { NULL, kHILayoutScaleAbsolute, 0 },
        { NULL, kHILayoutScaleAbsolute, 0 }
        
      },
      {
        { NULL, kHILayoutPositionTop, 0 },
        { NULL, kHILayoutPositionLeft, 0 }
      }
    };
    HIViewSetLayoutInfo(ctw, &layout);
    
    ct.wCallTip = root;
    ct.wDraw = ctw;
    ct.wCallTip.SetWindow(outWindow);
    HIViewSetVisible(ctw,true);
    
  }
#endif
}


void ScintillaCocoa::AddToPopUp(const char *label, int cmd, bool enabled)
{
  // Translate stuff into menu item attributes
  MenuItemAttributes attributes = 0;
  if (label[0] == '\0')
    attributes |= kMenuItemAttrSeparator;
  if (!enabled)
    attributes |= kMenuItemAttrDisabled;
  
  // Translate Scintilla commands into Mac OS commands
  // TODO: If I create an AEDesc, OS X may insert these standard
  // text editing commands into the menu for me.
  MenuCommand macCommand;
  switch (cmd)
  {
    case idcmdUndo:
      macCommand = kHICommandUndo;
      break;
    case idcmdRedo:
      macCommand = kHICommandRedo;
      break;
    case idcmdCut:
      macCommand = kHICommandCut;
      break;
    case idcmdCopy:
      macCommand = kHICommandCopy;
      break;
    case idcmdPaste:
      macCommand = kHICommandPaste;
      break;
    case idcmdDelete:
      macCommand = kHICommandClear;
      break;
    case idcmdSelectAll:
      macCommand = kHICommandSelectAll;
      break;
    case 0:
      macCommand = 0;
      break;
    default:
      assert( false );
      return;
  }
  
//  NSMenu *menu= reinterpret_cast<NSMenu*>(popup.GetID());
  
  //XXX TODO:XXX
//  [menu addItemWithTitle:[NSString stringWithUTF8String:label]
//                  action:@selector()
//           keyEquivalent:@""];
}

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

void ScintillaCocoa::ClaimSelection()
{
  // Mac OS X does not have a primary selection.
}

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

/**
 * Returns the current caret position (which is tracked as an offset into the entire text string)
 * to a row:column pair. The result is zero-based.
 */
NSPoint ScintillaCocoa::GetCaretPosition()
{
  NSPoint result;

  result.y = pdoc->LineFromPosition(currentPos);
  result.x = currentPos - pdoc->LineStart(result.y);
  return result;
}

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

#pragma segment Drag

/**
 * Triggered by the tick timer on a regular basis to scroll the content during a drag operation.
 */
void ScintillaCocoa::DragScroll()
{
  if (posDrag == invalidPosition)
  {
    scrollSpeed = 1;
    scrollTicks = 2000;
    return;
  }

  // TODO: does not work for wrapped lines, fix it.
  int line = pdoc->LineFromPosition(posDrag);
  int currentVisibleLine = cs.DisplayFromDoc(line);
  int lastVisibleLine = Platform::Minimum(topLine + LinesOnScreen(), cs.LinesDisplayed()) - 2;
    
  if (currentVisibleLine <= topLine && topLine > 0)
    ScrollTo(topLine - scrollSpeed);
  else
    if (currentVisibleLine >= lastVisibleLine)
      ScrollTo(topLine + scrollSpeed);
    else
    {
      scrollSpeed = 1;
      scrollTicks = 2000;
      return;
    }
  
  // TODO: also handle horizontal scrolling.
  
  if (scrollSpeed == 1)
  {
    scrollTicks -= timer.tickSize;
    if (scrollTicks <= 0)
    {
      scrollSpeed = 5;
      scrollTicks = 2000;
    }
  }
  
}

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

/**
 * Called when a drag operation was initiated from within Scintilla.
 */
void ScintillaCocoa::StartDrag()
{
  if (currentPos == anchor)
    return;

  // Put the data to be dragged on the drag pasteboard.
  SelectionText selectedText;
  NSPasteboard* pasteboard = [NSPasteboard pasteboardWithName: NSDragPboard];
  CopySelectionRange(&selectedText);
  SetPasteboardData(pasteboard, selectedText);
  
  // Prepare drag image.
  PRectangle localRectangle = RectangleFromRange(SelectionStart(), SelectionEnd());
  NSRect selectionRectangle = PRectangleToNSRect(localRectangle);
  
  NSView* content = ContentView();

#if 0 // TODO: fix initialization of the drag image with CGImageRef.
  // To get a bitmap of the text we're dragging, we just use Paint on a pixmap surface.
  SurfaceImpl *sw = new SurfaceImpl();
  SurfaceImpl *pixmap = NULL;
  
  bool lastHideSelection = hideSelection;
  hideSelection = true;
  if (sw)
  {
    pixmap = new SurfaceImpl();
    if (pixmap)
    {
      PRectangle client = GetClientRectangle();
      PRectangle imageRect = NSRectToPRectangle(selectionRectangle);
      paintState = painting;
      //sw->InitPixMap(client.Width(), client.Height(), NULL, NULL);
      sw->InitPixMap(imageRect.Width(), imageRect.Height(), NULL, NULL);
      paintingAllText = true;
      Paint(sw, imageRect);
      paintState = notPainting;
      
      pixmap->InitPixMap(imageRect.Width(), imageRect.Height(), NULL, NULL);
      
      CGContextRef gc = pixmap->GetContext(); 
      
      // To make Paint() work on a bitmap, we have to flip our coordinates and translate the origin
      CGContextTranslateCTM(gc, 0, imageRect.Height());
      CGContextScaleCTM(gc, 1.0, -1.0);
      
      pixmap->CopyImageRectangle(*sw, imageRect, PRectangle(0, 0, imageRect.Width(), imageRect.Height()));
      // XXX TODO: overwrite any part of the image that is not part of the
      //           selection to make it transparent.  right now we just use
      //           the full rectangle which may include non-selected text.
    }
    sw->Release();
    delete sw;
  }
  hideSelection = lastHideSelection;
  
  NSBitmapImageRep* bitmap = NULL;
  if (pixmap)
  {
    bitmap = [[[NSBitmapImageRep alloc] initWithCGImage: pixmap->GetImage()] autorelease];
    pixmap->Release();
    delete pixmap;
  }
#else
  
  // Poor man's drag image: take a snapshot of the content view.
  [content lockFocus];
  NSBitmapImageRep* bitmap = [[[NSBitmapImageRep alloc] initWithFocusedViewRect: selectionRectangle] autorelease];
  [bitmap setColorSpaceName: NSDeviceRGBColorSpace];
  [content unlockFocus];
  
#endif
  
  NSImage* image = [[[NSImage alloc] initWithSize: selectionRectangle.size] autorelease];
  [image addRepresentation: bitmap];
  
  NSImage* dragImage = [[[NSImage alloc] initWithSize: selectionRectangle.size] autorelease];
  [dragImage setBackgroundColor: [NSColor clearColor]];
  [dragImage lockFocus];
  [image dissolveToPoint: NSMakePoint(0.0, 0.0) fraction: 0.5];
  [dragImage unlockFocus];
  
  NSPoint startPoint;
  startPoint.x = selectionRectangle.origin.x;
  startPoint.y = selectionRectangle.origin.y + selectionRectangle.size.height;
  [content dragImage: dragImage 
                  at: startPoint
              offset: NSZeroSize
               event: lastMouseEvent // Set in MouseMove.
          pasteboard: pasteboard
              source: content
           slideBack: YES];
}

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

/**
 * Called when a drag operation reaches the control which was initiated outside.
 */
NSDragOperation ScintillaCocoa::DraggingEntered(id <NSDraggingInfo> info)
{
  inDragDrop = ddDragging;
  return DraggingUpdated(info);
}

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

/**
 * Called frequently during a drag operation if we are the target. Keep telling the caller
 * what drag operation we accept and update the drop caret position to indicate the
 * potential insertion point of the dragged data.
 */
NSDragOperation ScintillaCocoa::DraggingUpdated(id <NSDraggingInfo> info)
{
  // Convert the drag location from window coordinates to view coordinates and 
  // from there to a text position to finally set the drag position.
  Point location = ConvertPoint([info draggingLocation]);
  SetDragPosition(PositionFromLocation(location));
  
  NSDragOperation sourceDragMask = [info draggingSourceOperationMask];
  if (sourceDragMask == NSDragOperationNone)
    return sourceDragMask;
  
  NSPasteboard* pasteboard = [info draggingPasteboard];
  
  // Return what type of operation we will perform. Prefer move over copy.
  if ([[pasteboard types] containsObject: NSStringPboardType] ||
      [[pasteboard types] containsObject: ScintillaRecPboardType])
    return (sourceDragMask & NSDragOperationMove) ? NSDragOperationMove : NSDragOperationCopy;
  
  if ([[pasteboard types] containsObject: NSFilenamesPboardType])
    return (sourceDragMask & NSDragOperationGeneric);
  return NSDragOperationNone;
}

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

/**
 * Resets the current drag position as we are no longer the drag target.
 */
void ScintillaCocoa::DraggingExited(id <NSDraggingInfo> info)
{
  SetDragPosition(invalidPosition);
  inDragDrop = ddNone;
}

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

/**
 * Here is where the real work is done. Insert the text from the pasteboard.
 */
bool ScintillaCocoa::PerformDragOperation(id <NSDraggingInfo> info)
{
  NSPasteboard* pasteboard = [info draggingPasteboard];
  
  if ([[pasteboard types] containsObject: NSFilenamesPboardType])
  {
    NSArray* files = [pasteboard propertyListForType: NSFilenamesPboardType];
    for (NSString* uri in files)
      NotifyURIDropped([uri UTF8String]);
  }
  else
  {
    SelectionText text;
    GetPasteboardData(pasteboard, &text);
    
    if (text.len > 0)
    {
      NSDragOperation operation = [info draggingSourceOperationMask];
      bool moving = (operation & NSDragOperationMove) != 0;
      
      DropAt(posDrag, text.s, moving, text.rectangular);
    };
  }

  return true;
}

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

void ScintillaCocoa::SetPasteboardData(NSPasteboard* board, const SelectionText &selectedText)
{
  if (selectedText.len == 0)
    return;

  NSString *string;
  string = [NSString stringWithUTF8String: selectedText.s];

  [board declareTypes:[NSArray arrayWithObjects:
                       NSStringPboardType,
                       selectedText.rectangular ? ScintillaRecPboardType : nil,
                       nil] owner:nil];
  
  if (selectedText.rectangular)
  {
    // This is specific to scintilla, allows us to drag rectangular selections around the document.
    [board setString: string forType: ScintillaRecPboardType];
  }
  
  [board setString: string forType: NSStringPboardType];
  
}

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

/**
 * Helper method to retrieve the best fitting alternative from the general pasteboard.
 */
bool ScintillaCocoa::GetPasteboardData(NSPasteboard* board, SelectionText* selectedText)
{
  NSArray* supportedTypes = [NSArray arrayWithObjects: ScintillaRecPboardType, 
                             NSStringPboardType, 
                             nil];
  NSString *bestType = [board availableTypeFromArray: supportedTypes];
  NSString* data = [board stringForType: bestType];
  
  if (data != nil)
  {
    if (selectedText != nil)
    {
      char* text = (char*) [data UTF8String];
      bool rectangular = bestType == ScintillaRecPboardType;
      selectedText->Copy(text, strlen(text), 0, 0, rectangular, false);
    }
    return true;
  }
  
  return false;
}

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

void ScintillaCocoa::SetMouseCapture(bool on)
{
  capturedMouse = on;
  /*
  if (mouseDownCaptures)
  {
    if (capturedMouse)
      WndProc(SCI_SETCURSOR, Window::cursorArrow, 0);
    else
      // Reset to normal. Actual image will be set on mouse move.
      WndProc(SCI_SETCURSOR, (unsigned int) SC_CURSORNORMAL, 0);
  }
   */
}

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

bool ScintillaCocoa::HaveMouseCapture()
{
  return capturedMouse;
}

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

/**
 * Synchronously paint a rectangle of the window.
 */
void ScintillaCocoa::SyncPaint(void* gc, PRectangle rc)
{
  paintState = painting;
  rcPaint = rc;
  PRectangle rcText = GetTextRectangle();
  paintingAllText = rcPaint.Contains(rcText);
  Surface *sw = Surface::Allocate();
  if (sw)
  {
    sw->Init(gc, wMain.GetID());
    Paint(sw, rc);
    if (paintState == paintAbandoned)
    {
      // Do a full paint.
      rcPaint = GetClientRectangle();
      paintState = painting;
      paintingAllText = true;
      Paint(sw, rcPaint);
    }
    sw->Release();
    delete sw;
  }
  paintState = notPainting;
}

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

/**
 * Modfies the vertical scroll position to make the current top line show up as such.
 */
void ScintillaCocoa::SetVerticalScrollPos()
{
  ScintillaView* topContainer = TopContainer();
  
  // Convert absolute coordinate into the range [0..1]. Keep in mind that the visible area
  // does *not* belong to the scroll range.
  float relativePosition = (float) topLine / MaxScrollPos();
  [topContainer setVerticalScrollPosition: relativePosition];
}

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

void ScintillaCocoa::SetHorizontalScrollPos()
{
  ScintillaView* topContainer = TopContainer();
  PRectangle textRect = GetTextRectangle();
  
  // Convert absolute coordinate into the range [0..1]. Keep in mind that the visible area
  // does *not* belong to the scroll range.
  float relativePosition = (float) xOffset / (scrollWidth - textRect.Width());
  [topContainer setHorizontalScrollPosition: relativePosition];
}

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

/**
 * Used to adjust both scrollers to reflect the current scroll range and position in the editor.
 *
 * @param nMax Number of lines in the editor.
 * @param nPage Number of lines per scroll page.
 * @return True if there was a change, otherwise false.
 */
bool ScintillaCocoa::ModifyScrollBars(int nMax, int nPage)
{
  // Input values are given in lines, not pixels, so we have to convert.
  int lineHeight = WndProc(SCI_TEXTHEIGHT, 0, 0);
  NSView* container = ContentView();
  NSRect bounds = [container frame];
  ScintillaView* topContainer = TopContainer();

  // Set page size to the same value as the scroll range to hide the scrollbar.
  int scrollRange = lineHeight * (nMax + 1); // +1 because the caller subtracted one.
  int pageSize;
  if (verticalScrollBarVisible)
    pageSize = bounds.size.height;
  else
    pageSize = scrollRange;
  bool verticalChange = [topContainer setVerticalScrollRange: scrollRange page: pageSize];
  
  scrollRange = scrollWidth;
  if (horizontalScrollBarVisible)
    pageSize = bounds.size.width;
  else
    pageSize = scrollRange;
  bool horizontalChange = [topContainer setHorizontalScrollRange: scrollRange page: pageSize];
  
  return verticalChange || horizontalChange;
}

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

void ScintillaCocoa::Resize()
{
  ChangeSize();
}

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

/**
 * Called by the frontend control when the user manipulates one of the scrollers.
 *
 * @param position The relative position of the scroller in the range of [0..1].
 * @param part Specifies which part was clicked on by the user, so we can handle thumb tracking
 *             as well as page and line scrolling.
 * @param horizontal True if the horizontal scroller was hit, otherwise false.
 */
void ScintillaCocoa::DoScroll(float position, NSScrollerPart part, bool horizontal)
{
  // If the given scroller part is not the knob (or knob slot) then the given position is not yet
  // current and we have to update it.
  if (horizontal)
  {
    // Horizontal offset is given in pixels.
    PRectangle textRect = GetTextRectangle();
    int offset = (int) (position * (scrollWidth - textRect.Width()));
    int smallChange = (int) (textRect.Width() / 30);
    if (smallChange < 5)
      smallChange = 5;
    switch (part)
    {
      case NSScrollerDecrementLine:
        offset -= smallChange;
        break;
      case NSScrollerDecrementPage:
        offset -= textRect.Width();
        break;
      case NSScrollerIncrementLine:
        offset += smallChange;
        break;
      case NSScrollerIncrementPage:
        offset += textRect.Width();
        break;
    };
    HorizontalScrollTo(offset);
  }
  else
  {
    // VerticalScrolling is by line.
    int topLine = (int) (position * MaxScrollPos());
    int page = LinesOnScreen();
    switch (part)
    {
      case NSScrollerDecrementLine:
        topLine--;
        break;
      case NSScrollerDecrementPage:
        topLine -= page;
        break;
      case NSScrollerIncrementLine:
        topLine++;
        break;
      case NSScrollerIncrementPage:
        topLine += page;
        break;
    };
    ScrollTo(topLine, true);
  }
}

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

/**
 * Used to register a callback function for a given window. This is used to emulate the way
 * Windows notfies other controls (mainly up in the view hierarchy) about certain events.
 *
 * @param windowid A handle to a window. That value is generic and can be anything. It is passed
 *                 through to the callback.
 * @param callback The callback function to be used for future notifications. If NULL then no
 *                 notifications will be sent anymore.
 */
void ScintillaCocoa::RegisterNotifyCallback(intptr_t windowid, SciNotifyFunc callback)
{
  notifyObj = windowid;
  notifyProc = callback;
}

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

void ScintillaCocoa::NotifyChange()
{
  if (notifyProc != NULL)
    notifyProc(notifyObj, WM_COMMAND, (uintptr_t) (SCEN_CHANGE << 16), (uintptr_t) this);
}

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

void ScintillaCocoa::NotifyFocus(bool focus)
{
  if (notifyProc != NULL)
    notifyProc(notifyObj, WM_COMMAND, (uintptr_t) ((focus ? SCEN_SETFOCUS : SCEN_KILLFOCUS) << 16), (uintptr_t) this);
}

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

/**
 * Used to send a notification (as WM_NOTIFY call) to the procedure, which has been set by the call
 * to RegisterNotifyCallback (so it is not necessarily the parent window).
 *
 * @param scn The notification to send.
 */
void ScintillaCocoa::NotifyParent(SCNotification scn)
{ 
  if (notifyProc != NULL)
  {
    scn.nmhdr.hwndFrom = (void*) this;
    scn.nmhdr.idFrom = (unsigned int) wMain.GetID();
    notifyProc(notifyObj, WM_NOTIFY, (uintptr_t) 0, (uintptr_t) &scn);
  }
}

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

void ScintillaCocoa::NotifyURIDropped(const char *uri)
{
  SCNotification scn;
  scn.nmhdr.code = SCN_URIDROPPED;
  scn.text = uri;
  
  NotifyParent(scn);
}

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

bool ScintillaCocoa::HasSelection()
{
  return (SelectionEnd() - SelectionStart()) > 0;
}

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

bool ScintillaCocoa::CanUndo()
{
  return pdoc->CanUndo();
}

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

bool ScintillaCocoa::CanRedo()
{
  return pdoc->CanRedo();
}

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

void ScintillaCocoa::TimerFired(NSTimer* timer)
{
  Tick();
  DragScroll();
}

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

void ScintillaCocoa::IdleTimerFired()
{
  bool more = Idle();
  if (!more)
    SetIdle(false);
}

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

/**
 * Main entry point for drawing the control.
 *
 * @param rect The area to paint, given in the sender's coordinate.
 * @param gc The context we can use to paint.
 */
void ScintillaCocoa::Draw(NSRect rect, CGContextRef gc)
{
  SyncPaint(gc, NSRectToPRectangle(rect));
}

//--------------------------------------------------------------------------------------------------
    
/**
 * Helper function to translate OS X key codes to Scintilla key codes.
 */
static inline UniChar KeyTranslate(UniChar unicodeChar)
{
  switch (unicodeChar)
  {
    case NSDownArrowFunctionKey:
      return SCK_DOWN;
    case NSUpArrowFunctionKey:
      return SCK_UP;
    case NSLeftArrowFunctionKey:
      return SCK_LEFT;
    case NSRightArrowFunctionKey:
      return SCK_RIGHT;
    case NSHomeFunctionKey:
      return SCK_HOME;
    case NSEndFunctionKey:
      return SCK_END;
    case NSPageUpFunctionKey:
      return SCK_PRIOR;
    case NSPageDownFunctionKey:
      return SCK_NEXT;
    case NSDeleteFunctionKey:
      return SCK_DELETE;
    case NSInsertFunctionKey:
      return SCK_INSERT;
    case '\n':
    case 3:
      return SCK_RETURN;
    case 27:
      return SCK_ESCAPE;
    case 127:
      return SCK_BACK;
    case '\t':
    case 25: // Shift tab, return to unmodified tab and handle that via modifiers.
      return SCK_TAB;
    default:
      return unicodeChar;
  }
}

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

/**
 * Main keyboard input handling method. It is called for any key down event, including function keys,
 * numeric keypad input and whatnot. 
 *
 * @param event The event instance associated with the key down event.
 * @return True if the input was handled, false otherwise.
 */
bool ScintillaCocoa::KeyboardInput(NSEvent* event)
{
  // For now filter out function keys.
  NSUInteger modifiers = [event modifierFlags];
  
  NSString* input = [event characters];
  
  bool control = (modifiers & NSControlKeyMask) != 0;
  bool shift = (modifiers & NSShiftKeyMask) != 0;
  bool command = (modifiers & NSCommandKeyMask) != 0;
  bool alt = (modifiers & NSAlternateKeyMask) != 0;
  
  bool handled = false;
  
  // Handle each entry individually. Usually we only have one entry anway.
  for (int i = 0; i < input.length; i++)
  {
    const UniChar originalKey = [input characterAtIndex: i];
    UniChar key = KeyTranslate(originalKey);
    
    bool consumed = false; // Consumed as command?
    
    // Signal command as control + alt. This leaves us without command + control and command + alt
    // but that's what we get when we have a modifier key more than other platforms.
    if (KeyDown(key, shift, control || command, alt || command, &consumed))
      handled = true;
    if (consumed)
      handled = true;
  }
  
  return handled;
}

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

/**
 * Used to insert already processed text provided by the Cocoa text input system.
 */
int ScintillaCocoa::InsertText(NSString* input)
{
  const char* utf8 = [input UTF8String];
  AddCharUTF((char*) utf8, strlen(utf8), false);
  return true;
}

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

/**
 * Called by the owning view when the mouse pointer enters the control.
 */
void ScintillaCocoa::MouseEntered(NSEvent* event)
{
  if (!HaveMouseCapture())
  {
    WndProc(SCI_SETCURSOR, (long int)SC_CURSORNORMAL, 0);
    
    // Mouse location is given in screen coordinates and might also be outside of our bounds.
    Point location = ConvertPoint([event locationInWindow]);
    ButtonMove(location);
  }
}

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

void ScintillaCocoa::MouseExited(NSEvent* event)
{
  // Nothing to do here.
}

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

void ScintillaCocoa::MouseDown(NSEvent* event)
{
  Point location = ConvertPoint([event locationInWindow]);
  NSTimeInterval time = [event timestamp];
  bool command = ([event modifierFlags] & NSCommandKeyMask) != 0;
  bool shift = ([event modifierFlags] & NSShiftKeyMask) != 0;
  bool control = ([event modifierFlags] & NSControlKeyMask) != 0;
    
  ButtonDown(Point(location.x, location.y), (int) (time * 1000), shift, control, command);
}

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

void ScintillaCocoa::MouseMove(NSEvent* event)
{
  lastMouseEvent = event;
  
  ButtonMove(ConvertPoint([event locationInWindow]));
}

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

void ScintillaCocoa::MouseUp(NSEvent* event)
{
  NSTimeInterval time = [event timestamp];
  bool control = ([event modifierFlags] & NSControlKeyMask) != 0;

  ButtonUp(ConvertPoint([event locationInWindow]), (int) (time * 1000), control);
}

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

void ScintillaCocoa::MouseWheel(NSEvent* event)
{
  bool command = ([event modifierFlags] & NSCommandKeyMask) != 0;
  bool shift = ([event modifierFlags] & NSShiftKeyMask) != 0;
  int delta;
  if (shift)
    delta = 10 * [event deltaX]; // Arbitrary scale factor.
  else
  {
    // In order to make scrolling with larger offset smoother we scroll less line the larger the 
    // delta value is.
    if ([event deltaY] < 0)
      delta = -(int) sqrt(-10.0 * [event deltaY]);
    else
      delta = (int) sqrt(10.0 * [event deltaY]);
  }
  
  if (command)
  {
    // Zoom! We play with the font sizes in the styles.
    // Number of steps/line is ignored, we just care if sizing up or down.
    if (delta > 0)
      KeyCommand(SCI_ZOOMIN);
    else
      KeyCommand(SCI_ZOOMOUT);
  }
  else
    if (shift)
      HorizontalScrollTo(xOffset - delta);
    else
      ScrollTo(topLine - delta, true);
}

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

// Helper methods for NSResponder actions.

void ScintillaCocoa::SelectAll()
{
  Editor::SelectAll();
}

void ScintillaCocoa::DeleteBackward()
{
  KeyDown(SCK_BACK, false, false, false, nil);
}

void ScintillaCocoa::Cut()
{
  Editor::Cut();
}

void ScintillaCocoa::Undo()
{
  Editor::Undo();
}

void ScintillaCocoa::Redo()
{
  Editor::Undo();
}

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

//OSStatus ScintillaCocoa::ContextualMenuClick( HIPoint& location )
//{
//  // convert screen coords to window relative
//  Rect bounds;
//  OSStatus err;
//  err = GetWindowBounds( this->GetOwner(), kWindowContentRgn, &bounds );
//  assert( err == noErr );
//  location.x += bounds.left;
//  location.y += bounds.top;
//  ContextMenu( Scintilla::Point( static_cast<int>( location.x ), static_cast<int>( location.y ) ) );
//  return noErr;   
//}
//
//OSStatus ScintillaCocoa::ActiveStateChanged()
//{
//  // If the window is being deactivated, lose the focus and turn off the ticking
//  if ( ! this->IsActive() ) {
//    DropCaret();
//    //SetFocusState( false );
//    SetTicking( false );
//  } else {
//    ShowCaretAtCurrentPosition();
//  }
//  return noErr;
//}
//

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