//
//  NSTextView+Autocomplete.m
//  MacDown
//
//  Created by Tzu-ping Chung  on 11/06/2014.
//  Copyright (c) 2014 Tzu-ping Chung . All rights reserved.
//

#import "NSTextView+Autocomplete.h"
#import "NSString+Lookup.h"
#import "MPUtilities.h"


static const unichar kMPLeftSingleQuotation  = L'\u2018';
static const unichar kMPRightSingleQuotation = L'\u2019';
static const unichar kMPLeftDoubleQuotation  = L'\u201c';
static const unichar kMPRightDoubleQuotation = L'\u201d';

static const unichar kMPMatchingCharactersMap[][2] = {
    {L'(', L')'},
    {L'[', L']'},
    {L'{', L'}'},
    {L'<', L'>'},
    {L'\'', L'\''},
    {L'\"', L'\"'},
    {L'\uff08', L'\uff09'},     // full-width parentheses
    {L'\u300c', L'\u300d'},     // corner brackets
    {L'\u300e', L'\u300f'},     // white corner brackets
    {kMPLeftSingleQuotation, kMPRightSingleQuotation},
    {kMPLeftDoubleQuotation, kMPRightDoubleQuotation},
    {L'\0', L'\0'},
};

static const unichar kMPStrikethroughCharacter = L'~';

static const unichar kMPMarkupCharacters[] = {
    L'*', L'_', L'`', L'=', L'\0',
};

static NSString * const kMPListLineHeadPattern =
    @"^(\\s*)((?:(?:\\*|\\+|-|)\\s+)?)((?:\\d+\\.\\s+)?)(\\S)?";
static NSString * const kMPBlockquoteLinePattern = @"^((?:\\> ?)+).*$";


@implementation NSTextView (Autocomplete)

- (BOOL)substringInRange:(NSRange)range isSurroundedByPrefix:(NSString *)prefix
                  suffix:(NSString *)suffix
{
    NSString *content = self.string;
    NSUInteger location = range.location;
    NSUInteger length = range.length;
    if (content.length < location + length + suffix.length)
        return NO;
    if (location < prefix.length)
        return NO;

    if (![[content substringFromIndex:location + length] hasPrefix:suffix]
        || ![[content substringToIndex:location] hasSuffix:prefix])
        return NO;

    // Emphasis (*) requires special treatment because we need to eliminate
    // strong (**) but not strong-emphasis (***).
    if (![prefix isEqualToString:@"*"] || ![suffix isEqualToString:@"*"])
        return YES;
    if ([self substringInRange:range isSurroundedByPrefix:@"***" suffix:@"***"])
        return YES;
    if ([self substringInRange:range isSurroundedByPrefix:@"**" suffix:@"**"])
        return NO;
    return YES;
}


- (void)insertSpacesForTab
{
    NSString *spaces = @"    ";
    NSUInteger currentLocation = self.selectedRange.location;
    NSInteger p = [self.string locationOfFirstNewlineBefore:currentLocation];

    // Calculate how deep we need to go.
    NSUInteger offset = (currentLocation - p - 1) % 4;
    if (offset)
        spaces = [spaces substringFromIndex:offset];
    [self insertText:spaces];
}

- (BOOL)completeMatchingCharactersForTextInRange:(NSRange)range
                                      withString:(NSString *)str
                            strikethroughEnabled:(BOOL)strikethrough
{
    NSUInteger stringLength = str.length;

    // Character insert without selection.
    if (range.length == 0 && stringLength == 1)
    {
        NSUInteger location = range.location;
        if ([self completeMatchingCharacterForText:str
                                        atLocation:location])
            return YES;
    }
    // Character insert with selection (i.e. select and replace).
    else if (range.length > 0 && stringLength == 1)
    {
        unichar character = [str characterAtIndex:0];
        if ([self wrapMatchingCharactersOfCharacter:character
                                  aroundTextInRange:range
                               strikethroughEnabled:strikethrough])
            return YES;
    }
    return NO;
}

- (BOOL)completeMatchingCharacterForText:(NSString *)string
                              atLocation:(NSUInteger)location
{
    static NSCharacterSet *boundaryCharacters = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSMutableCharacterSet *s =
            [NSMutableCharacterSet whitespaceAndNewlineCharacterSet];
        [s formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
        boundaryCharacters = [s copy];
    });
    NSString *content = self.string;
    NSUInteger contentLength = content.length;

    BOOL hasMarkedText = self.hasMarkedText;
    unichar c = [string characterAtIndex:0];
    unichar n = ' ';
    unichar p = ' ';
    if (location < contentLength)
        n = [content characterAtIndex:location];
    if (location > 0 && location <= contentLength)
        p = [content characterAtIndex:location - 1];

    for (const unichar *cs = kMPMatchingCharactersMap[0]; *cs != 0; cs += 2)
    {
        // Ignore IM input of ASCII charaters.
        if (hasMarkedText && cs[0] < L'\u0100')
            continue;

        // First part of matching characters.
        if ([boundaryCharacters characterIsMember:n] && c == cs[0]
            && ([boundaryCharacters characterIsMember:p] || cs[0] != cs[1]))
        {
            NSRange range = NSMakeRange(location, 0);
            NSString *completion = [NSString stringWithCharacters:cs length:2];
            // Mimic OS X's quote substitution if it's on.
            if (self.isAutomaticQuoteSubstitutionEnabled)
            {
                unichar c = L'\0';
                switch (cs[0])
                {
                    case L'\"':
                        c = kMPLeftDoubleQuotation;
                        break;
                    case L'\'':
                        c = kMPLeftSingleQuotation;
                        break;
                    default:
                        break;
                }
                if (c != L'\0')
                    completion = [NSString stringWithCharacters:&c length:1];
            }
            [self insertText:completion replacementRange:range];

            range.location += string.length;
            self.selectedRange = range;
            return YES;
        }
        // Second part of matching characters (shift without really inserting).
        else if (c == cs[1] && n == cs[1])
        {
            NSRange range = NSMakeRange(location + 1, 0);
            self.selectedRange = range;
            return YES;
        }
    }
    return NO;
}

- (void)wrapTextInRange:(NSRange)range withPrefix:(unichar)prefix
                 suffix:(unichar)suffix
{
    NSString *string = [self.string substringWithRange:range];
    NSString *p = [NSString stringWithCharacters:&prefix length:1];
    NSString *s = [NSString stringWithCharacters:&suffix length:1];
    NSString *wrapped = [NSString stringWithFormat:@"%@%@%@", p, string, s];
    [self insertText:wrapped replacementRange:range];

    range.location += 1;
    self.selectedRange = range;
}

- (BOOL)wrapMatchingCharactersOfCharacter:(unichar)character
                        aroundTextInRange:(NSRange)range
                     strikethroughEnabled:(BOOL)isStrikethroughEnabled
{
    for (const unichar *cs = kMPMatchingCharactersMap[0]; *cs != 0; cs += 2)
    {
        if (character == cs[0])
        {
            [self wrapTextInRange:range withPrefix:cs[0] suffix:cs[1]];
            return YES;
        }
    }
    for (size_t i = 0; kMPMarkupCharacters[i] != 0; i++)
    {
        if (character == kMPMarkupCharacters[i])
        {
            [self wrapTextInRange:range withPrefix:character suffix:character];
            return YES;
        }
    }
    if (isStrikethroughEnabled && character == kMPStrikethroughCharacter)
    {
        [self wrapTextInRange:range withPrefix:character suffix:character];
        return YES;
    }
    return NO;
}

- (BOOL)deleteMatchingCharactersAround:(NSUInteger)location
{
    NSString *string = self.string;
    if (location == 0 || location >= string.length)
        return NO;

    unichar f = [string characterAtIndex:location - 1];
    unichar b = [string characterAtIndex:location];

    for (const unichar *cs = kMPMatchingCharactersMap[0]; *cs != 0; cs += 2)
    {
        if (f == cs[0] && b == cs[1])
        {
            NSRange range = NSMakeRange(location - 1, 2);
            [self shouldChangeTextInRange:range replacementString:@""];
            [self replaceCharactersInRange:range withString:@""];
            return YES;
        }
    }
    return NO;
}

- (BOOL)unindentForSpacesBefore:(NSUInteger)location
{
    NSString *string = self.string;

    NSUInteger whitespaceCount = 0;
    while (location - whitespaceCount > 0
           && [string characterAtIndex:location - whitespaceCount - 1] == L' ')
    {
        whitespaceCount++;
        if (whitespaceCount >= 4)
            break;
    }
    if (whitespaceCount < 2)
        return NO;

    NSUInteger lineStart = [string locationOfFirstNewlineBefore:location] + 1;
    if (location <= lineStart)
        return NO;

    NSUInteger offset = (location - lineStart) % 4;
    if (offset == 0)
        offset = 4;
    if (whitespaceCount < offset)
        offset = whitespaceCount;

    NSRange range = NSMakeRange(location - offset, offset);
    [self shouldChangeTextInRange:range replacementString:@""];
    [self replaceCharactersInRange:range withString:@""];
    return YES;
}

- (BOOL)toggleForMarkupPrefix:(NSString *)prefix suffix:(NSString *)suffix
{
    NSRange range = self.selectedRange;
    NSString *selection = [self.string substringWithRange:range];
    BOOL isOn = NO;

    // Selection is already marked-up. Clear markup and maintain selection.
    NSUInteger poff = prefix.length;
    if ([self substringInRange:range isSurroundedByPrefix:prefix
                        suffix:suffix])
    {
        NSRange sub = NSMakeRange(range.location - poff,
                                  selection.length + poff + suffix.length);
        [self insertText:selection replacementRange:sub];
        range.location = sub.location;
        isOn = NO;
    }
    // Selection is normal. Mark it up and maintain selection.
    else
    {
        NSString *text = [NSString stringWithFormat:@"%@%@%@",
                          prefix, selection, suffix];
        [self insertText:text replacementRange:range];
        range.location += poff;
        isOn = YES;
    }
    self.selectedRange = range;
    return isOn;
}

- (void)toggleBlockWithPattern:(NSString *)pattern prefix:(NSString *)prefix
{
    NSRegularExpression *regex =
        [[NSRegularExpression alloc] initWithPattern:pattern options:0
                                               error:NULL];
    NSString *content = self.string;
    NSRange selectedRange = self.selectedRange;
    NSRange lineRange = [content lineRangeForRange:selectedRange];

    NSString *toProcess = [content substringWithRange:lineRange];
    BOOL hasTrailingNewline = NO;
    if ([toProcess hasSuffix:@"\n"])
    {
        toProcess = [toProcess substringToIndex:(toProcess.length - 1)];
        hasTrailingNewline = YES;
    }

    NSArray *lines = [toProcess componentsSeparatedByString:@"\n"];

    BOOL isMarked = YES;
    for (NSString *line in lines)
    {
        NSRange matchRange =
            [regex rangeOfFirstMatchInString:line options:0
                                       range:NSMakeRange(0, line.length)];
        if (matchRange.location == NSNotFound)
        {
            isMarked = NO;
            break;
        }
    }

    NSUInteger prefixLength = prefix.length;
    NSMutableArray *modLines = [NSMutableArray arrayWithCapacity:lines.count];

    __block NSUInteger totalShift = 0;
    [lines enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
        NSString *line = obj;
        if (line.length)
            totalShift += prefixLength;
        if (!isMarked)
            line = [prefix stringByAppendingString:line];
        else
            line = [line substringFromIndex:prefixLength];
        [modLines addObject:line];
    }];

    NSString *processed = [modLines componentsJoinedByString:@"\n"];
    if (hasTrailingNewline)
        processed = [NSString stringWithFormat:@"%@\n", processed];
    [self insertText:processed replacementRange:lineRange];

    if (!isMarked)
    {
        selectedRange.location += prefixLength;
        if (selectedRange.length + totalShift >= prefixLength)
            selectedRange.length += totalShift - prefixLength;
        else    // Underflow.
            selectedRange.length = 0;
    }
    else
    {
        if (prefixLength <= selectedRange.location)
            selectedRange.location -= prefixLength;
        else    // Underflow.
            selectedRange.location = 0;
        if (totalShift - prefixLength <= selectedRange.length)
            selectedRange.length -= totalShift - prefixLength;
        else    // Underflow.
            selectedRange.length = 0;

        if (selectedRange.location < lineRange.location)
        {
            selectedRange.length -= lineRange.location - selectedRange.location;
            selectedRange.location = lineRange.location;
        }
    }
    self.selectedRange = selectedRange;
}

- (void)indentSelectedLinesWithPadding:(NSString *)padding
{
    NSString *content = self.string;
    NSRange selectedRange = self.selectedRange;
    NSRange lineRange = [content lineRangeForRange:selectedRange];

    NSString *toProcess = [content substringWithRange:lineRange];
    NSArray *lines = [toProcess componentsSeparatedByString:@"\n"];
    NSMutableArray *modLines = [NSMutableArray arrayWithCapacity:lines.count];
    NSUInteger paddingLength = padding.length;

    __block NSUInteger totalShift = 0;
    [lines enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
        NSString *line = obj;
        if (line.length)
            totalShift += paddingLength;
        [modLines addObject:[padding stringByAppendingString:line]];
    }];
    if ([modLines.lastObject isEqualToString:padding])
    {
        [modLines removeLastObject];
        [modLines addObject:@""];
    }
    NSString *processed = [modLines componentsJoinedByString:@"\n"];
    [self insertText:processed replacementRange:lineRange];

    selectedRange.location += paddingLength;
    selectedRange.length +=
        (totalShift > paddingLength) ? totalShift - paddingLength : 0;
    self.selectedRange = selectedRange;
}

- (void)unindentSelectedLines
{
    NSString *content = self.string;
    NSRange selectedRange = self.selectedRange;
    NSRange lineRange = [content lineRangeForRange:selectedRange];

    // Get the lines to unindent.
    NSString *toProcess = [content substringWithRange:lineRange];
    NSArray *lines = [toProcess componentsSeparatedByString:@"\n"];

    // This will hold the modified lines.
    NSMutableArray *modLines = [NSMutableArray arrayWithCapacity:lines.count];

    // Unindent the lines one by one, and put them in the new array.
    __block NSUInteger firstShift = 0;      // Indentation of the first line.
    __block NSUInteger totalShift = 0;      // Indents removed in total.
    [lines enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
        NSString *line = obj;
        NSUInteger lineLength = line.length;
        NSUInteger shift = 0;

        for (shift = 0; shift < 4; shift++)
        {
            if (shift >= lineLength)
                break;
            unichar c = [line characterAtIndex:shift];
            if (c == '\t')
                shift++;
            if (c != ' ')
                break;
        }
        if (index == 0)
            firstShift += shift;
        totalShift += shift;
        if (shift && shift < lineLength)
            line = [line substringFromIndex:shift];
        [modLines addObject:line];
    }];

    // Join the processed lines, and replace the original with them.
    NSString *processed = [modLines componentsJoinedByString:@"\n"];
    [self insertText:processed replacementRange:lineRange];

    // Modify the selection range so that the same text (minus removed spaces)
    // are selected.
    selectedRange.location -= firstShift;
    selectedRange.length -= totalShift - firstShift;
    self.selectedRange = selectedRange;
}

- (BOOL)insertMappedContent
{
    NSString *content = self.string;
    NSUInteger contentLength = content.length;
    if (contentLength > 20)
        return NO;

    static NSDictionary *map = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        map = MPGetDataMap(@"data");
    });
    NSData *mapped = map[content];
    if (!mapped)
        return NO;
    NSArray *components = @[NSTemporaryDirectory(),
                             [NSString stringWithFormat:@"%lu", content.hash]];
    NSString *path = [NSString pathWithComponents:components];
    [mapped writeToFile:path atomically:NO];
    NSString *text = [NSString stringWithFormat:@"![%@](%@)", content, path];
    [self insertText:text replacementRange:NSMakeRange(0, contentLength)];
    self.selectedRange = NSMakeRange(2, contentLength);
    return YES;
}

- (BOOL)completeNextListItem:(BOOL)autoIncrement
{
    NSRange selectedRange = self.selectedRange;
    NSUInteger location = selectedRange.location;
    NSString *content = self.string;
    if (selectedRange.length || !content.length)
        return NO;

    NSInteger start = [content locationOfFirstNewlineBefore:location] + 1;
    NSUInteger end = location;
    NSUInteger nonwhitespace =
        [content locationOfFirstNonWhitespaceCharacterInLineBefore:location];

    // No non-whitespace character at this line.
    if (nonwhitespace == location)
        return NO;

    NSRange range = NSMakeRange(start, end - start);
    NSString *line = [self.string substringWithRange:range];

    NSRegularExpressionOptions options = NSRegularExpressionAnchorsMatchLines;
    NSRegularExpression *regex =
        [[NSRegularExpression alloc] initWithPattern:kMPListLineHeadPattern
                                             options:options
                                               error:NULL];
    NSTextCheckingResult *result =
        [regex firstMatchInString:line options:0
                            range:NSMakeRange(0, line.length)];
    if (!result || result.range.location == NSNotFound)
        return NO;

    NSString *t = nil;
    BOOL isUl = ([result rangeAtIndex:2].length != 0);
    BOOL isOl = ([result rangeAtIndex:3].length != 0);
    BOOL previousLineEmpty = ([result rangeAtIndex:4].length == 0);
    if (previousLineEmpty)
    {
        NSRange replaceRange = NSMakeRange(NSNotFound, 0);
        if (isUl)
            replaceRange = [result rangeAtIndex:2];
        else if (isOl)
            replaceRange = [result rangeAtIndex:3];
        if (replaceRange.length)
        {
            replaceRange.location += start;
            [self shouldChangeTextInRange:range replacementString:@""];
            [self replaceCharactersInRange:range withString:@""];
        }
        t = @"";
    }
    else if (isUl)
    {
        NSRange range = [result rangeAtIndex:2];
        range.length -= 1;      // Exclude trailing whitespace.
        t = [line substringWithRange:range];
    }
    else if (isOl)
    {
        NSRange range = [result rangeAtIndex:3];
        range.length -= 1;      // Exclude trailing space.
        NSString *captured = [line substringWithRange:range];
        NSInteger i = captured.integerValue;
        if (autoIncrement)
            i += 1;
        t = [NSString stringWithFormat:@"%ld.", i];
    }
    if (!t)
        return NO;

    [self insertNewline:self];
    location += 1;  // Shift for inserted newline.

    NSString *indent = [line substringWithRange:[result rangeAtIndex:1]];
    NSUInteger contentLength = content.length;

    // Has matching list item. Only insert indent.
    NSRange r = NSMakeRange(location, t.length);
    if (contentLength > location + t.length
            && [[content substringWithRange:r] isEqualToString:t])
    {
        [self insertText:indent];
        return YES;
    }

    NSString *it = [NSString stringWithFormat:@"%@%@", indent, t];

    // Has indent and matching list item. Accept it.
    r = NSMakeRange(location, it.length);
    if (contentLength > location + it.length
            && [[content substringWithRange:r] isEqualToString:it])
        return YES;

    // Insert completion for normal cases.
    if (t.length)
        it = [NSString stringWithFormat:@"%@ ", it];
    [self insertText:it];
    return YES;
}

- (BOOL)completeNextBlockquoteLine
{
    NSRange selectedRange = self.selectedRange;
    NSString *content = self.string;
    NSUInteger contentLength = content.length;
    if (selectedRange.length || !contentLength)
        return NO;

    NSRange lineRange = [content lineRangeForRange:selectedRange];
    NSString *line = [content substringWithRange:lineRange];

    NSRegularExpressionOptions options = NSRegularExpressionAnchorsMatchLines;
    NSRegularExpression *regex =
        [[NSRegularExpression alloc] initWithPattern:kMPBlockquoteLinePattern
                                             options:options error:NULL];
    NSTextCheckingResult *result =
        [regex firstMatchInString:line options:0
                            range:NSMakeRange(0, lineRange.length)];
    if (!result || result.range.location == NSNotFound)
        return NO;

    [self insertNewline:self];

    NSRange markersRange = [result rangeAtIndex:1];
    NSString *markers = [line substringWithRange:markersRange];
    NSUInteger nextLineStart = selectedRange.location + 1;

    // Has identical markers. Accept this.
    NSRange nextMarkersRange = NSMakeRange(nextLineStart, markersRange.length);
    if (contentLength > nextLineStart + markersRange.length)
    {
        NSString *nextMarkers = [content substringWithRange:nextMarkersRange];
        if ([nextMarkers isEqualToString:markers])
            return YES;
    }

    // Insert completion.
    [self insertText:markers];
    return YES;
}

- (BOOL)completeNextIndentedLine
{
    NSRange selectedRange = self.selectedRange;
    if (selectedRange.length)
        return NO;

    NSString *content = self.string;
    NSUInteger start = [content lineRangeForRange:selectedRange].location;
    NSUInteger end = [content locationOfFirstNonWhitespaceCharacterInLineBefore:
                      selectedRange.location];
    if (end <= start)
        return NO;

    [self insertNewline:self];
    NSRange indentRange = NSMakeRange(start, end - start);
    [self insertText:[content substringWithRange:indentRange]];
    return YES;
}

- (void)makeHeaderForSelectedLinesWithLevel:(NSUInteger)level
{
    NSAssert(level <= 6, @"Should be 1-6, or 0 (convert to paragraph).");
    NSString *content = self.string;
    NSRange selectedRange = self.selectedRange;
    NSRange lineRange = [content lineRangeForRange:selectedRange];

    NSString *header = [@"###### " substringFromIndex:6 - level];
    if (level == 0)
        header = [header substringToIndex:header.length - 1];
    NSUInteger headerLength = header.length;
    NSUInteger options = NSRegularExpressionDotMatchesLineSeparators;
    NSRegularExpression *regex =
        [[NSRegularExpression alloc] initWithPattern:@"^(#+ )*.*?$"
                                             options:options error:NULL];

    NSMutableArray *processedLines = [NSMutableArray array];
    NSString *toProcess = [content substringWithRange:lineRange];
    NSArray *lines = [toProcess componentsSeparatedByString:@"\n"];
    NSUInteger lineCount = lines.count;
    __block NSInteger firstShift = 0;
    __block NSInteger totalShift = 0;
    [lines enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
        NSString *line = obj;
        NSTextCheckingResult *result =
            [regex firstMatchInString:line options:0
                                range:NSMakeRange(0, line.length)];
        NSUInteger rangeCount = result.numberOfRanges;

        // Don't process empty/whitespace-only lines unless it's the only line.
        if (lineCount > 1)
        {
            NSUInteger sentinel = [line
                locationOfFirstNonWhitespaceCharacterInLineBefore:line.length];
            if (sentinel == line.length)
            {
                [processedLines addObject:line];
                return;
            }
        }

        NSString *lineContent = line;
        NSRange headerRange = NSMakeRange(0, 0);
        if (rangeCount > 1)
            headerRange = [result rangeAtIndex:1];
        if (headerRange.location != NSNotFound)
        {
            NSUInteger start = headerRange.location + headerRange.length;
            lineContent = [line substringFromIndex:start];
        }
        [processedLines addObject:[header stringByAppendingString:lineContent]];

        NSInteger shift = headerLength - headerRange.length;
        if (index == 0)
            firstShift += shift;
        totalShift += shift;
    }];
    NSString *processed = [processedLines componentsJoinedByString:@"\n"];
    [self insertText:processed replacementRange:lineRange];

    selectedRange.location += firstShift;
    selectedRange.length += totalShift - firstShift;
    self.selectedRange = selectedRange;
}

@end
