//
//  MPExpressionLayout.m
//  MathKit
//
//  Created by Kim Wittenburg on 07.08.14.
//  Copyright (c) 2014 Kim Wittenburg. All rights reserved.
//

#import "MPExpressionLayout.h"

#import "MPExpression.h"
#import "MPFunction.h"
#import "MPFunctionLayout.h"
#import "MPPowerFunctionLayout.h"

#import "MPToken.h"
#import "NSIndexPath+MPAdditions.h"



@interface MPExpressionLayout (MPLineGeneration)

- (CTLineRef)lineForElementAtIndex:(NSUInteger)index;

@end



@implementation MPExpressionLayout (MPLineGeneration)

- (CTLineRef)lineForElementAtIndex:(NSUInteger)index
{
    id element = [self.expression elementAtIndex:index];
    if (![element isString]) {
        return NULL;
    }
    NSRange tokensRange = [self.expression convertRange:NSMakeRange(index, 1)
                                     fromReferenceFrame:MPElementReferenceFrame
                                       toReferenceFrame:MPTokenReferenceFrame];
    NSArray *tokens = [self.expression itemsInRange:tokensRange
                                     referenceFrame:MPTokenReferenceFrame];
    id lineObject = [self cachableObjectForIndex:index generator:^id{
        NSMutableAttributedString *text = [[NSMutableAttributedString alloc] init];
        for (id<MPToken> token in tokens) {
            NSFont *font;
            if (token.tokenType == MPElementaryFunctionToken) {
                font = [self specialFontWithSize:self.contextInferredFontSize];
            } else {
                font = [self normalFontWithSize:self.contextInferredFontSize];
            }
            NSAttributedString *tokenText = [[NSAttributedString alloc] initWithString:token.stringValue
                                                                            attributes:@{NSFontAttributeName: font}];
            [text appendAttributedString:tokenText];
        }
        CFAttributedStringRef attributedString = CFBridgingRetain(text);
        CTLineRef line = CTLineCreateWithAttributedString(attributedString);
        CFRelease(attributedString);
        return CFBridgingRelease(line);
    }];
    return (__bridge CTLineRef)lineObject;
}

@end



@implementation MPExpressionLayout

# pragma mark Creation Methods


- (instancetype)initWithExpression:(MPExpression *)expression
                            parent:(MPFunctionLayout *)parent
{
    self = [super initWithParent:parent];
    if (self) {
        _expression = expression;
    }
    return self;
}


#pragma mark Cache Methods


- (NSUInteger)numberOfChildren
{
    return self.expression.countElements;
}


- (MPLayout *)childLayoutAtIndex:(NSUInteger)index
{
    id cachedObject = [self cachableObjectForIndex:index generator:^id{
        MPFunction *function = (MPFunction *)[self.expression elementAtIndex:index];
        MPFunctionLayout *layout = [MPFunctionLayout functionLayoutForFunction:function
                                                                        parent:self];
        layout.flipped = self.flipped;
        layout.usesSmallSize = self.usesSmallSize;
        return layout;
    }];
    if ([cachedObject isKindOfClass:[MPLayout class]]) {
        return cachedObject;
    }
    return nil;
}


- (NSRect)boundsOfElementAtIndex:(NSUInteger)index
{
    id symbol = [self.expression elementAtIndex:index];
    if ([symbol isString]) {
        CTLineRef line = [self lineForElementAtIndex:index];
        CFRetain(line);
        CGRect bounds = CTLineGetBoundsWithOptions(line, 0);
        CFRelease(line);
        return bounds;
    } else {
        return [self childLayoutAtIndex:index].bounds;
    }
}


#pragma mark Drawing Methods


- (NSRect)generateBounds
{
    if (self.expression.countElements == 0) {
        return NSMakeRect(0, kMPEmptyBoxYOrigin, kMPEmptyBoxWidth, kMPEmptyBoxHeight);
    }
    CGRect bounds = NSMakeRect(0, 0, 0, 0);
    for (NSUInteger index = 0; index < self.expression.countElements; index++) {
        NSRect elementBounds = [self boundsOfElementAtIndex:index];
        elementBounds.origin.x = bounds.size.width;
        bounds = NSUnionRect(bounds, elementBounds);
    }
    return bounds;
}


- (NSRect)boundingRectForRange:(NSRange)range
{
    NSUInteger startOffset;
    NSUInteger startElementIndex = [self.expression convertIndex:range.location
                                              fromReferenceFrame:MPSymbolReferenceFrame
                                                toReferenceFrame:MPElementReferenceFrame
                                                          offset:&startOffset];
    // Calculate x position
    CGFloat x = 0, width = 0;
    for (NSUInteger index = 0; index < startElementIndex; index++) {
        x += [self boundsOfElementAtIndex:index].size.width;
    }

    if (startOffset > 0) {
        CTLineRef line = [self lineForElementAtIndex:startElementIndex];
        CFRetain(line);
        CGFloat xOffset = CTLineGetOffsetForStringIndex(line, startOffset, NULL);
        x += xOffset;
        width += CTLineGetBoundsWithOptions(line, 0).size.width - xOffset;
        CFRelease(line);
    } else if (startElementIndex < self.expression.countElements) { // Otherwise the selection is after the last symbol
        width += [self boundsOfElementAtIndex:startElementIndex].size.width;
    }
    
    // If we search the caret position we are done
    if (range.length == 0) {
        return NSMakeRect(x, self.bounds.origin.y, 0, self.bounds.size.height);
    }

    NSUInteger endOffset;
    NSUInteger endElementIndex = [self.expression convertIndex:NSMaxRange(range)
                                            fromReferenceFrame:MPSymbolReferenceFrame
                                              toReferenceFrame:MPElementReferenceFrame
                                                        offset:&endOffset];

    // Selection is inside of one string element
    if (startElementIndex == endElementIndex) {
        CTLineRef line = [self lineForElementAtIndex:endElementIndex];
        CFRetain(line);
        CGFloat xStart = CTLineGetOffsetForStringIndex(line, startOffset, NULL);
        CGFloat xEnd = CTLineGetOffsetForStringIndex(line, endOffset, NULL);
        width = xEnd - xStart;
        CFRelease(line);
        return NSMakeRect(x, self.bounds.origin.y, width, self.bounds.size.height);
    }
    
    // Calculate width
    for (NSUInteger index = startElementIndex + 1; index < endElementIndex; index++) {
        width += [self boundsOfElementAtIndex:index].size.width;
    }
    if (endOffset > 0) {
        CTLineRef line = [self lineForElementAtIndex:endElementIndex];
        CFRetain(line);
        width += CTLineGetOffsetForStringIndex(line, endOffset, NULL);
        CFRelease(line);
    }
    return NSMakeRect(x, self.bounds.origin.y, width, self.bounds.size.height);
}


- (NSPoint)offsetOfChildLayoutAtIndex:(NSUInteger)index
{
    CGFloat x = 0;
    for (NSUInteger i = 0; i < index; i++) {
        x += [self boundsOfElementAtIndex:i].size.width;
    }
    return NSMakePoint(x, 0);
}


- (NSIndexPath *)indexPathForMousePoint:(NSPoint)point
{
    NSUInteger currentPosition = 0;
    for (NSUInteger index = 0; index < self.expression.countElements; index++) {
        NSRect elementBounds = [self boundsOfElementAtIndex:index];
        NSPoint elementOffset = [self offsetOfChildLayoutAtIndex:index];
        elementBounds.origin.x += elementOffset.x;
        elementBounds.origin.y += elementOffset.y;
        
        // Only the horizontal location is to consider for hit testing
        elementBounds.size.height = CGFLOAT_MAX;
        
        id<MPExpressionElement> element = [self.expression elementAtIndex:index];
        if (NSMouseInRect(point, elementBounds, self.flipped)) {
            NSPoint pointInElement = NSMakePoint(point.x - elementOffset.x, point.y + elementOffset.y);
            if ([element isString]) {
                CTLineRef line = [self lineForElementAtIndex:index];
                CFRetain(line);
                CFIndex localIndex = CTLineGetStringIndexForPosition(line, pointInElement);
                CFRelease(line);
                return [NSIndexPath indexPathWithIndex:currentPosition+localIndex];
            } else {
                NSIndexPath *subPath = [[self childLayoutAtIndex:index] indexPathForMousePoint:pointInElement];
                if (subPath.length == 1) {
                    // A single index is used to communicate back wether the
                    // selection should be before or after the function.
                    // A 0 means before, a 1 means after.
                    return [NSIndexPath indexPathWithIndex:currentPosition + [subPath indexAtPosition:0]];
                } else {
                    return [subPath indexPathByPreceedingIndex:index];
                }
            }
        }
        currentPosition += element.length;
    }
    if (point.x < self.bounds.size.width / 2) {
        return [NSIndexPath indexPathWithIndex:0];
    } else {
        return [NSIndexPath indexPathWithIndex:self.expression.countSymbols];
    }
}


- (BOOL)drawsChildrenManually
{
    return YES;
}


- (void)draw
{
    CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
    CGContextSaveGState(context);
    
    [[NSColor textColor] set];
    if (self.expression.countElements == 0) {
        CGContextRestoreGState(context);
        NSBezierPath *path = [NSBezierPath bezierPathWithRect:NSMakeRect(0, kMPEmptyBoxDrawingYOrigin, kMPEmptyBoxDrawingWidth, kMPEmptyBoxDrawingHeight)];
        path.lineWidth = 0.5;
        [path stroke];
        return;
    }
    
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    
    CGFloat x = 0;
    NSRect lastElementBounds = NSMakeRect(0, kMPEmptyBoxYOrigin, kMPEmptyBoxWidth, kMPEmptyBoxHeight);
    
    for (NSUInteger index = 0; index < self.expression.countElements; index++) {
        id element = [self.expression elementAtIndex:index];
        NSRect elementBounds = [self boundsOfElementAtIndex:index];
        
        if ([element isString]) {
            CTLineRef line = [self lineForElementAtIndex:index];
            CFRetain(line);
            
            CGContextSetTextPosition(context, x, 0);
            CTLineDraw(line, context);
            
            CFRelease(line);
        } else {
            MPLayout *layout = [self childLayoutAtIndex:index];
            if ([layout isKindOfClass:[MPPowerFunctionLayout class]]) {
                ((MPPowerFunctionLayout *)layout).baseBounds = lastElementBounds;
            }
            [layout drawAtPoint:NSMakePoint(x, 0)];
        }
        x += elementBounds.size.width;
        lastElementBounds = elementBounds;
    }
    CGContextRestoreGState(context);
}

@end
