#include "macmouseover.hh"
#include <AppKit/NSTouch.h>
#include <AppKit/NSEvent.h>
#include <AppKit/NSScreen.h>
#include <Foundation/NSAutoreleasePool.h>
#include <Foundation/Foundation.h>

#ifndef AVAILABLE_MAC_OS_X_VERSION_10_11_AND_LATER
#define kAXValueTypeCGPoint kAXValueCGPointType
#define kAXValueTypeCFRange kAXValueCFRangeType
#endif

const int mouseOverInterval = 300;

CGEventRef eventCallback( CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon )
{
(void) proxy;
  if( type != kCGEventMouseMoved )
    return event;
  static_cast< MacMouseOver * >( refcon )->mouseMoved();
  return event;
}

static CGPoint carbonScreenPointFromCocoaScreenPoint( NSPoint cocoaPoint )
{
  NSScreen *foundScreen = nil;
  CGPoint thePoint;

  for (NSScreen *screen in [NSScreen screens]) {
    if (NSPointInRect(cocoaPoint, [screen frame])) {
        foundScreen = screen;
    }
  }

  if (foundScreen) {
    CGFloat screenHeight = [foundScreen frame].size.height;
    thePoint = CGPointMake(cocoaPoint.x, screenHeight - cocoaPoint.y - 1);
  }
  else
    thePoint = CGPointMake(0.0, 0.0);

  return thePoint;
}

MacMouseOver & MacMouseOver::instance()
{
  static MacMouseOver m;

  return m;
}

MacMouseOver::MacMouseOver() :
  pPref(NULL)
, tapRef( 0 )
, loop( 0 )
{
  mouseTimer.setSingleShot( true );
  connect( &mouseTimer, SIGNAL( timeout() ), this, SLOT( timerShot() ) );

  elementSystemWide = AXUIElementCreateSystemWide();
}

MacMouseOver::~MacMouseOver()
{
  disableMouseOver();

  if( tapRef )
    CFRelease( tapRef );

  if( loop )
    CFRelease( loop );

  if( elementSystemWide )
    CFRelease( elementSystemWide );
}

QString MacMouseOver::CFStringRefToQString( CFStringRef str )
{
  int length = CFStringGetLength( str );
  if( length == 0 )
    return QString();

  UniChar *chars = new UniChar[ length ];
  CFStringGetCharacters( str, CFRangeMake( 0, length ), chars );

  QString result = QString::fromUtf16( chars, length );

  delete[] chars;
  return result;
}

void MacMouseOver::mouseMoved()
{
  mouseTimer.start( mouseOverInterval );
}

void MacMouseOver::enableMouseOver()
{
  mouseTimer.stop();
  if( !isAXAPIEnabled() )
    return;
  if( !tapRef )
    tapRef = CGEventTapCreate( kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap,
                               kCGEventTapOptionListenOnly,
                               CGEventMaskBit( kCGEventMouseMoved ),
                               eventCallback, this );
  if( !tapRef )
    return;
  if( !loop )
    loop = CFMachPortCreateRunLoopSource( kCFAllocatorDefault, tapRef, 0 );
  if( loop )
    CFRunLoopAddSource( CFRunLoopGetMain(), loop, kCFRunLoopCommonModes );
}

void MacMouseOver::disableMouseOver()
{
  mouseTimer.stop();
  if( loop )
    CFRunLoopRemoveSource( CFRunLoopGetMain(), loop, kCFRunLoopCommonModes );
}

void MacMouseOver::timerShot()
{
  if( mouseMutex.tryLock( 0 ) )
    mouseMutex.unlock();
  else
    return;
  if( !pPref )
    return;
  if( !pPref->enableScanPopupModifiers || checkModifiersPressed( pPref->scanPopupModifiers ) )
    handlePosition();
}

void MacMouseOver::handlePosition()
{
  Mutex::Lock _( mouseMutex );

  QString strToTranslate;

  NSAutoreleasePool * pool = [ [ NSAutoreleasePool alloc ] init ];
  CGPoint pt = carbonScreenPointFromCocoaScreenPoint( [NSEvent mouseLocation] );
  [ pool drain ];

  CFArrayRef names = 0;

  AXUIElementRef elem = 0;
  AXError err = AXUIElementCopyElementAtPosition( elementSystemWide, pt.x, pt.y, &elem );

  if( err != kAXErrorSuccess )
    return;

  for( ; ; )
  {
    CFTypeRef parameter = AXValueCreate( kAXValueTypeCGPoint, &pt );
    CFTypeRef rangeValue;
    err = AXUIElementCopyParameterizedAttributeNames( elem, &names );
    if( err != kAXErrorSuccess )
      break;

    int numOfAttributes = CFArrayGetCount( names );
    if( CFArrayContainsValue( names, CFRangeMake( 0, numOfAttributes ), CFSTR( "AXRangeForPosition" ) ) )
    {
      // Standard interface
      err = AXUIElementCopyParameterizedAttributeValue( elem, kAXRangeForPositionParameterizedAttribute,
                                                            parameter, ( CFTypeRef * )&rangeValue );
      CFRelease( parameter );
      if( err != kAXErrorSuccess )
        break;

      CFStringRef stringValue;

      CFRange decodedRange = CFRangeMake( 0, 0 );
      bool b = AXValueGetValue( (AXValueRef)rangeValue, kAXValueTypeCFRange, &decodedRange );
      CFRelease( rangeValue );
      if( b )
      {
        int fromPos = decodedRange.location - 127;
        if( fromPos < 0 )
          fromPos = 0;
        int wordPos = decodedRange.location - fromPos;  // Cursor position in result string

        CFRange range = CFRangeMake( fromPos, wordPos + 1 );
        parameter = AXValueCreate( kAXValueTypeCFRange, &range );
        err = AXUIElementCopyParameterizedAttributeValue( elem, kAXStringForRangeParameterizedAttribute,
                                                            parameter, (CFTypeRef *)&stringValue );
        CFRelease( parameter );
        if( err != kAXErrorSuccess )
          break;

        strToTranslate = CFStringRefToQString( stringValue );
        CFRelease( stringValue );

        // Read string further
        for( int i = 1; i < 128; i++ )
        {
          range = CFRangeMake( decodedRange.location + i, 1 );
          parameter = AXValueCreate( kAXValueTypeCFRange, &range );
          err = AXUIElementCopyParameterizedAttributeValue( elem, kAXStringForRangeParameterizedAttribute,
                                                              parameter, (CFTypeRef *)&stringValue );
          CFRelease( parameter );

          if( err != kAXErrorSuccess )
            break;

          QString s = CFStringRefToQString( stringValue );
          CFRelease( stringValue );

          if( s[ 0 ].isLetterOrNumber() || s[ 0 ] == '-' )
            strToTranslate += s;
          else
            break;
        }

        handleRetrievedString( strToTranslate, wordPos );
      }
    }
    else if( CFArrayContainsValue( names, CFRangeMake( 0, numOfAttributes ), CFSTR( "AXTextMarkerForPosition" ) ) )
    {
      // Safari interface
      CFTypeRef marker, range;
      CFStringRef str;
      err = AXUIElementCopyParameterizedAttributeValue( elem, CFSTR( "AXTextMarkerForPosition" ),
                                                            parameter, ( CFTypeRef * )&marker );
      CFRelease( parameter );
      if( err != kAXErrorSuccess )
        break;

      err = AXUIElementCopyParameterizedAttributeValue( elem, CFSTR( "AXLeftWordTextMarkerRangeForTextMarker" ),
                                                        marker, ( CFTypeRef * )&range );
      CFRelease( marker );
      if( err != kAXErrorSuccess )
        break;

      err = AXUIElementCopyParameterizedAttributeValue( elem, CFSTR( "AXStringForTextMarkerRange" ),
                                                        range, ( CFTypeRef * )&str );
      CFRelease( range );
      if( err == kAXErrorSuccess )
      {
        strToTranslate = CFStringRefToQString( str );
        CFRelease( str );
        handleRetrievedString( strToTranslate, 0 );
      }
    }
    break;
  }
  if( elem )
    CFRelease( elem );
  if( names )
    CFRelease( names );
}

void MacMouseOver::handleRetrievedString( QString & wordSeq, int wordSeqPos )
{

  if( wordSeq.isEmpty() )
    return;

  // locate the word inside the sequence

  QString word;

  if ( wordSeq[ wordSeqPos ].isSpace() )
  {
    // Currently we ignore such cases
    return;
  }
  else
  if ( !wordSeq[ wordSeqPos ].isLetterOrNumber() )
  {
    // Special case: the cursor points to something which doesn't look like a
    // middle of the word -- assume that it's something that joins two words
    // together.

    int begin = wordSeqPos;

    for( ; begin; --begin )
      if ( !wordSeq[ begin - 1 ].isLetterOrNumber() )
        break;

    int end = wordSeqPos;

    while( ++end < wordSeq.size() )
      if ( !wordSeq[ end ].isLetterOrNumber() )
        break;

    if ( end - begin == 1 )
    {
      // Well, turns out it was just a single non-letter char, discard it
      return;
    }

    word = wordSeq.mid( begin, end - begin );
  }
  else
  {
    // Cursor points to a letter -- cut the word it points to

    int begin = wordSeqPos;

    for( ; begin; --begin )
      if ( !wordSeq[ begin - 1 ].isLetterOrNumber() )
        break;

    int end = wordSeqPos;

    while( ++end < wordSeq.size() )
    {
      if ( !wordSeq[ end ].isLetterOrNumber() )
        break;
    }
    word = wordSeq.mid( begin, end - begin );
  }

  // See if we have an RTL char. Reverse the whole string if we do.

  for( int x = 0; x < word.size(); ++x )
  {
    QChar::Direction d = word[ x ].direction();

    if ( d == QChar::DirR || d == QChar::DirAL ||
         d == QChar::DirRLE || d == QChar::DirRLO )
    {
      std::reverse( word.begin(), word.end() );
      break;
    }
  }

  emit instance().hovered( word, false );
}

bool MacMouseOver::isAXAPIEnabled()
{
  if( NSFoundationVersionNumber >= 1000 )  // MacOS 10.9+
    return AXIsProcessTrusted();

  return AXAPIEnabled();
}
