/** 
 *  Yudit Unicode Editor Source File
 *
 *  GNU Copyright (C) 1997-2023  Gaspar Sinai <gaspar@yudit.org>  
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License, version 2,
 *  dated June 1991. See file COPYYING for details.
 *
 *  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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

// https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TextEditing/TextEditing.html
// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextEditing/Tasks/TextViewTask.html

#import <objc/objc-runtime.h>
#import <AppKit/Appkit.h>
#import <Foundation/Foundation.h>
#import <swindow/sosx/SCocoaInput.h>

#define DEBUG 0

/* 
 Normal japanese input example:
 selectedRange 9223372036854775807,0
 hasMarkedText
 firstRectForCharacterRange helper
 validAttributesForMarkedText
 setMarkedText s{
    NSUnderline = 1;
    NSUnderlineColor = "NSCalibratedWhiteColorSpace 0.17 1";
 }
 calling replaementMarkedRange
 selectedRange 1,0
 hasMarkedText
 setMarkedText し{
     NSUnderline = 1;
     NSUnderlineColor = "NSCalibratedWhiteColorSpace 0.17 1";
 }
 selectedRange 1,0
 hasMarkedText
 setMarkedText しn{
    NSUnderline = 1;
    NSUnderlineColor = "NSCalibratedWhiteColorSpace 0.17 1";
}
 selectedRange 2,0
*/ 

/*
 Moved selection to suru

replaementMarkedRange short
replaceCharactersInRange 品川に行って買いもの{
    NSUnderline = 1;
    NSUnderlineColor = "NSCalibratedWhiteColorSpace 0.17 1";
}する{
    NSUnderline = 2;
    NSUnderlineColor = "Generic Gray Gamma 2.2 Profile colorspace 0 1";
}
selectedRange 13,2
*/

/*
 * For Alt-latin 2019
    NSTextInputClient hasMarkedText
    NSTextInputClient validAttributesForMarkedText
    setMarkedText ˝
    replaementMarkedRange short
    replaceCharactersInRange ˝
    NSTextInputClient hasMarkedText
    NSTextInputClient insertText ő
    replaceCharactersInRange ő
 */

// From https://gist.github.com/ishikawa/23049
static const NSRange kEmptyRange = {NSNotFound, 0};

@implementation CocoaInput {
  NSMutableAttributedString *_text; // NSTextInputClient

  // [oldText][selectedRange[markedRange]..]
  NSRange _selectedRange;
  NSRange _markedRange;

  SPreEditor* preEditor; 
  bool cocoaActive;
}
-(id) initWithFrame:(NSRect)frameRect {
    self = [super initWithFrame:frameRect];
    _text = [[NSMutableAttributedString alloc] init];
    _selectedRange = _markedRange = kEmptyRange;
    cocoaActive = false;
    preEditor = nil;
    return self;
}

- (void) appendCharacters: (id) aString {
  // FIXME
  if ([aString isKindOfClass: [NSAttributedString class]]) {
    [_text appendAttributedString: aString];
  } else {
    [[_text mutableString] appendString: aString];
  }
#if DEBUG
  NSLog(@"appendCharacters %@", aString);
#endif
  [self cocoaFireTextChanged];
}

- (void) removeMarkedText {
  if (_markedRange.location != NSNotFound) {
    if (NSMaxRange(_markedRange) <= [_text length])
      [_text deleteCharactersInRange: _markedRange];
    _markedRange = _selectedRange = kEmptyRange;
  }
}

- (void) replaceCharactersInRange: (NSRange) aRange
                         withText: (id) aString
                   effectiveRange: (NSRangePointer) effectiveRange
{
#if DEBUG
  NSLog(@"replaceCharactersInRange %@", aString);
#endif
  NSRange replacementRange = aRange;

  if (replacementRange.location == NSNotFound) {
    replacementRange.location = [_text length];
    replacementRange.length = 0;
  }
    // FIXME
  if (NSMaxRange(replacementRange) > [_text length]) {
    NSLog (@"Out of bounds: %@ for length %lu",
        NSStringFromRange(replacementRange),
        (unsigned long) [_text length]);
     return;
  }
  if ([aString isKindOfClass: [NSAttributedString class]]) {
    [_text replaceCharactersInRange: replacementRange
               withAttributedString: aString];
  } else {
    [_text replaceCharactersInRange: replacementRange
                         withString: aString];
  }

  if (effectiveRange != NULL) {
    *effectiveRange = NSMakeRange(replacementRange.location, [aString length]);
  }
}

- (void) setMarkedText: (id) aString
         selectedRange: (NSRange) selectedRange
      replacementRange: (NSRange) replacementRange
{
#if DEBUG
  NSLog(@"setMarkedText %@", aString);
#endif
  NSRange effectiveRange;

  [self replaceCharactersInRange: 
        [self replacementMarkedRange: replacementRange]
                        withText: aString
                  effectiveRange: &effectiveRange];
  if (selectedRange.location != NSNotFound) selectedRange.location += effectiveRange.location;
  _selectedRange = selectedRange;
  _markedRange = effectiveRange;
  if ([aString length] == 0) [self removeMarkedText];
  [self cocoaFireTextChanged];
}

- (NSRange) replacementMarkedRange: (NSRange) replacementRange {
  NSRange markedRange = _markedRange;

#if DEBUG
  NSLog(@"replacementMarkedRange short");
#endif
  if (markedRange.location == NSNotFound) markedRange = _selectedRange;
  if (replacementRange.location != NSNotFound) {
    NSRange newRange = markedRange;
    newRange.location += replacementRange.location;
    newRange.length += replacementRange.length;
    if (NSMaxRange(newRange) <= NSMaxRange(markedRange)) {
      markedRange = newRange;
    }
  }

  return markedRange;
}
//----------------------------- NSResponder
//X1
- (void) deleteBackward: (id) sender {
  const NSUInteger length = [_text length];
  if (length > 0) {
    [_text deleteCharactersInRange: NSMakeRange(length - 1, 1)];
    [self cocoaFireTextChanged];
  } else {
    self.cppWindow->listener->keyPressed (self.cppWindow, 
        SWindowListener::Key_BackSpace, "", false, false, false); 
  }
}

- (void) insertNewline: (id) sender {
  const NSUInteger length = [_text length];
  if (length > 0) {
    [self appendCharacters: @"\n"];
    [self cocoaFireTextChanged];
  } else {
    self.cppWindow->listener->keyPressed (self.cppWindow, 
        SWindowListener::Key_Enter, "", false, false, false); 
  }
}

- (void) insertTab: (id) sender {
  const NSUInteger length = [_text length];
// FIMXE
  if (length > 0) {
    [self appendCharacters: @"\t"];
  } else {
    self.cppWindow->listener->keyPressed (self.cppWindow, 
        SWindowListener::Key_Tab, "\t", false, false, false); 
  }
}

- (void) insertText: (id) aString {
  const NSUInteger length = [_text length];
// FIMXE
  if (length > 0) {
    [self appendCharacters: aString];
  } else {
    //preEditor->preEditClearMarkedText();
    self.cppWindow->listener->keyPressed (self.cppWindow, 
        SWindowListener::Key_Undefined, [aString UTF8String], 
        false, false, false); 
  }
}

// http://mirror.informatimago.com/next/developer.apple.com/documentation/Cocoa/Reference/ApplicationKit/Java/Protocols/NSTextInput.html#//apple_ref/doc/uid/20000610/BAJBDHAD

// ---------------------------- NSTextInputClient Begin ---------------------

/*
Returns attributed string at theRange. This method allows input mangers to query any range in text storage.
An implementation of this method should be prepared theRange to be out-of-bounds. The InkWell text input service can ask for the contents of the text input client that extends beyond the document’s range. In this case, you should return the intersection of the document’s range and theRange. If the location of theRange is completely outside of the document’s range, return null.
*/
- (NSAttributedString *) attributedSubstringFromRange: (NSRange) theRange {
#if DEBUG
//    NSLog(@"NSTextInputClient attributedSubstringFromRange in");
#endif
    NSAttributedString* ret =  [self attributedSubstringForProposedRange:theRange actualRange:NULL];
#if DEBUG
    NSLog(@"NSTextInputClient attributedSubstringFromRange ret %@", ret);
#endif
    return ret;
}
/*
   Could not find doc, but needed.
   Seriously: this is a security risk, return nothing.
 */
- (NSAttributedString *) attributedSubstringForProposedRange: (NSRange) aRange
    actualRange: (NSRangePointer) actualRange
{
  NSAttributedString * ret = [[[NSAttributedString alloc] init] autorelease];

#if DEBUG
  NSLog (@"NSTextInputClient attributedSubstringForProposedRange replacementRange -> %@", ret);
#endif
  return ret;
}

/*
 Returns the index of the character whose frame rectangle includes thePoint. The returned index measures from the start of the receiver’s text storage. thePoint is in the screen coordinate system. Returns NSArray.NotFound if the cursor is not within a character.
*/
- (NSUInteger) characterIndexForPoint: (NSPoint) thePoint {
#if DEBUG
  NSLog (@"NSTextInputClient characterIndexForPoint");
#endif
  return 0;
}

/*
Returns a number used to identify the receiver’s context to the input server. Each text view within an application should return a unique identifier (typically its address). However, multiple text views sharing the same text storage must all return the same identifier.
*/
- (NSInteger) conversationIdentifier {
#if DEBUG
  NSLog (@"NSTextInputClient conversationIdentifier");
#endif
  return (NSInteger) self;
}
/*
Invokes aSelector if possible. If aSelector cannot be invoked, then doCommandBySelector should not pass this message up the responder chain. NSResponder also implements this method, and it does forward uninvokable commands up the responder chain, but a text view should not. A text view implementing the NSTextInput interface will inherit from NSView, which inherits from NSResponder, so your implementation of this method will override the one in NSResponder. It should not call super.
See Also: interpretKeyEvents (NSResponder), doCommandBySelector (NSKeyBindingResponder)
*/
// FIXME
- (void) doCommandBySelector: (SEL) aSelector {
#if DEBUG
  NSLog (@"NSTextInputClient doCommandBySelector %@", 
    NSStringFromSelector(aSelector));
#endif
  [super doCommandBySelector: aSelector];
}

/*
Returns the first frame rectangle for characters in theRange, in screen coordinates. If theRange spans multiple lines of text in the text view, the rectangle returned is the one for the characters in the first line. If the length of theRange is 0 (as it would be if there is nothing selected at the insertion point), the rectangle will coincide with the insertion point, and its width will be 0.
*/
- (NSRect) firstRectForCharacterRange: (NSRange) theRange {
#if DEBUG
  NSLog (@"NSTextInputClient firstRectForCharacterRange actual");
#endif
  return [self firstRectForCharacterRange:theRange actualRange:NULL];
}

/*
 * This is what is called.
*/
- (NSRect) firstRectForCharacterRange: (NSRange) aRange
                          actualRange: (NSRangePointer) actualRange
{
#if DEBUG
  NSLog (@"NSTextInputClient firstRectForCharacterRange");
#endif
//SGC1
    if ( _markedRange.location == NSNotFound) {
        return NSZeroRect;
    }
    if ( aRange.location == NSNotFound) {
        return NSZeroRect;
    }
    if (aRange.location < _markedRange.location) {
        return NSZeroRect;
    }
    SRectangle rect = preEditor->preEditGlyphRectangleUTF16(
        aRange.location - _markedRange.location);

//NSLog (@"arange.location=%lu markedrane.location=%lu arange.length=%lu",
//     aRange.location, _markedRange.location, aRange.length); 

    NSRect rectView = NSMakeRect((float)rect.originX, (float)rect.originY,
        (float) rect.width, (float) rect.height);
    NSRect rectWindow = [self convertRect:rectView toView: nil];
    NSRect rectScreen = [[self window] convertRectToScreen: rectWindow];
    // FIXME 
    //if (actualRange != nil) *actualRange = aRange;
    return rectScreen;
    
}

/*
Returns true if the receiver has marked text, false if it doesn’t. Unlike other methods in this protocol, this one is not called by an input server. The text view itself may call this method to determine whether there currently is marked text. NSTextView, for example, disables the Edit>Copy menu item when this method returns true.
See Also: markedRange
 */
- (BOOL) hasMarkedText {
#if DEBUG
  BOOL ret = _markedRange.location != NSNotFound;
  NSLog (@"NSTextInputClient hasMarkedText %d", ret);
#endif
  return _markedRange.location != NSNotFound;
}

/*
Returns the range of the marked text. The returned range measures from the start of the receiver’s text storage. The return value’s location is NSArray.NotFound, and its length is 0 if and only if hasMarkedText returns false.
See Also: setMarkedTextAndSelectedRange, unmarkText, hasMarkedText
*/
- (NSRange) markedRange {
#if DEBUG
  NSLog (@"NSTextInputClient markedRange");
#endif
  return _markedRange;
}

/*
 Returns the range of selected text. The returned range measures from the start of the receiver’s text storage. If there is no selection, the return value’s location is NSArray.NotFound, and its length is 0.
See Also: setMarkedTextAndSelectedRange
 */
- (NSRange) selectedRange {
#if DEBUG
  NSLog (@"NSTextInputClient selectedRange %lu,%lu", _selectedRange.location, _selectedRange.length);
#endif
  return _selectedRange;
}

/*
 Replaces text in selRange within receiver’s text storage with the contents of aString, which the receiver must display distinctively to indicate that it is marked text. aString must be either a String or an NSAttributedString and not null.
See Also: selectedRange, unmarkText
*/
- (void) setMarkedText: (id) aString selectedRange: (NSRange) selRange {
#if DEBUG
  NSLog (@"NSTextInputClient setMarketText %@", aString);
#endif
  [self setMarkedText: aString
        selectedRange: selRange
     replacementRange: kEmptyRange];
}

/*
 Removes any marking from pending input text, and disposes of the marked text as it wishes. The text view should accept the marked text as if it had been inserted normally.
See Also: selectedRange, setMarkedTextAndSelectedRange
*/ 
- (void) unmarkText {
#if DEBUG
  NSLog (@"NSTextInputClient unmarkText");
#endif
}

/*
Returns an array of String names for the attributes supported by the receiver. The input server may choose to use some of these attributes in the text it inserts or in marked text. Returns an empty array if no attributes are supported. See NSAttributedString for the set of string constants that you could return in the array.
*/
- (NSArray *) validAttributesForMarkedText {
#if DEBUG
    NSLog(@"NSTextInputClient validAttributesForMarkedText");
#endif
  const NSRange entireRange = NSMakeRange(0, [_text length]);
  [_text addAttribute: NSFontAttributeName
                value: [NSFont userFontOfSize: 18.0f]
                range: entireRange];
    NSArray* ret = [NSArray arrayWithObjects: 
        NSUnderlineStyleAttributeName,
        nil
     ];
    return ret;

}

/*
   Could not find doc, but needed.
 */
- (void) insertText: (id) aString
   replacementRange: (NSRange) replacementRange
{
#if DEBUG
  NSLog (@"NSTextInputClient insertText %@ replacementRange %lu,%lu", aString, 
        replacementRange.location, replacementRange.length);
#endif

  preEditor->preEditClearMarkedText();
  self.cppWindow->listener->keyPressed (self.cppWindow, 
        SWindowListener::Key_Undefined, [aString UTF8String], 
        false, false, false); 

  [self removeMarkedText];
  [self replaceCharactersInRange: replacementRange
                        withText: aString
                  effectiveRange: NULL];

}


// Returns the baseline position of a given character relative 
// to the origin of rectangle returned by 
// firstRect(forCharacterRange:actualRange:)
// not required.
#if 0
- (float) baselineDeltaForCharacterAt:(int) aAt {
    // FIXME 
#if DEBUG
    NSLog(@"baselineDeltaForCharacterAt %d", aAt);
#endif
    return 0.0;
}
#endif

#if 0
-(NSInteger) windowLevel {
    // FIXME
    NSLog(@"windowLevel");
    return 0;
}
#endif

#if 0
- (NSAttributedString *) attributedString {
  return _text;
}
#endif

-(BOOL) drawsVerticallyForCharacterAt: (int) aAt {
#if DEBUG
    NSLog(@"NSTextInputClient drawsVerticallyForCharacterAt");
#endif
    return false;
}

- (NSAttributedString *) attributedString {
  return _text;
}

// 
// ---------------------------- NSTextInputClient End -------------------------
- (BOOL) cocoaInputEnabled {
  return cocoaActive;
}

- (void) cocoaClearInput {
    if (![self cocoaInputEnabled]) return;
    if ([_text length]==0) return;
    if (NSTextInputContext *ctxt = [NSTextInputContext currentInputContext]) {
#if DEBUG
        NSLog(@"cocoaClearInput");
#endif
        [ctxt discardMarkedText];
        _selectedRange = _markedRange = kEmptyRange;
        [_text release];
        _text = [[NSMutableAttributedString alloc] init];
        [self cocoaFireTextChanged];
    }
}

- (void) cocoaStartMacKeyboard: (SPreEditor*) aPreEditor {
    preEditor = aPreEditor;
    if (NSTextInputContext *ctxt = [NSTextInputContext currentInputContext]) {
#if DEBUG
        NSLog(@"cocoaStartMacKeyboard: unmark text");
#endif
        [ctxt discardMarkedText];
        // does not work.
        //[ctxt invalidateCharacterCoordinates];
        [ctxt activate];
        //[view unmarkText];
    }
    _selectedRange = _markedRange = kEmptyRange;
    [_text release];
    _text = [[NSMutableAttributedString alloc] init];

#if DEBUG
    NSLog(@"cocoaStartMacKeyboard");
#endif
    cocoaActive = true;
    preEditor->preEditClearMarkedText();
}

- (BOOL) cocoaCommitInput {
    if (NSTextInputContext *ctxt = [NSTextInputContext currentInputContext]) {
        if (_markedRange.location == NSNotFound) {
            return false;
        }
        [ctxt discardMarkedText];
        NSAttributedString* marked = [_text attributedSubstringFromRange:_markedRange];
        [_text release];
        _text = [[NSMutableAttributedString alloc] init];
        _selectedRange = _markedRange = kEmptyRange;
        if ([marked length] > 0) {
            preEditor->preEditClearMarkedText();
            self.cppWindow->listener->keyPressed (self.cppWindow, 
                SWindowListener::Key_Undefined, [[marked string]  UTF8String], 
                false, false, false); 
            return true;
        }
    }
    return false;
}

// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextEditing/Tasks/TextViewTask.html
- (void) cocoaInvalidateCoordinates {
    if (![self cocoaInputEnabled]) return;
#if 0
        // Does not work
    if (NSTextInputContext *ctxt = [NSTextInputContext currentInputContext]) {
        NSLog (@"cocoaInvalidateCoordinates");
        [ctxt invalidateCharacterCoordinates];
    }
    // 
#endif
    [self cocoaCommitInput];
}

- (void) cocoaStopMacKeyboard {
#if DEBUG
   // NSLog(@"cocoaStopMacKeyboard");
#endif
    // previously it was: NSInputManager
    if (NSTextInputContext *ctxt = [NSTextInputContext currentInputContext]) {
        [ctxt invalidateCharacterCoordinates];
        [ctxt discardMarkedText];
        [ctxt deactivate];
    }
    [_text release];
    _text = [[NSMutableAttributedString alloc] init];
    _selectedRange = _markedRange = kEmptyRange;

    cocoaActive = false;
    preEditor->preEditClearMarkedText();
}

- (void) cocoaFireTextChanged {
    preEditor->preEditClearMarkedText();
    if (_markedRange.location == NSNotFound) {
        return;
    }
    NSAttributedString* marked = [_text attributedSubstringFromRange:_markedRange];
    unsigned int length = [_text length];
    NSRange effectiveRange = NSMakeRange(0, 0);
    while (NSMaxRange(effectiveRange) < length) {
        id attr = [marked attribute:NSUnderlineStyleAttributeName
            atIndex:NSMaxRange(effectiveRange) effectiveRange:&effectiveRange];
        NSAttributedString* subs = [marked attributedSubstringFromRange:effectiveRange];
        int n = 1;
        if (attr != nil && [attr isKindOfClass: [NSNumber class]]) {
            n = [(NSNumber*) attr intValue];
            //NSLog(@"Underline is %d", n);
        }
        if (n == 1) {
            preEditor->preEditInsertMarkedText([[subs string] UTF8String], SPreEditor::Style_Default);
        } 
        if (n == 2) {
            preEditor->preEditInsertMarkedText([[subs string] UTF8String], SPreEditor::Style_Selected);
        } 
    }
}

@end
