//
//  HFRepresenterTextViewCallout.m
//  HexFiend_2
//
//  Copyright 2011 ridiculous_fish. All rights reserved.
//

#import "HFRepresenterTextViewCallout.h"
#import "HFRepresenterTextView.h"

static const CGFloat HFTeardropRadius = 12;
static const CGFloat HFTeadropTipScale = 2.5;

static const CGFloat HFShadowXOffset = -6;
static const CGFloat HFShadowYOffset = 0;
static const CGFloat HFShadowOffscreenHack = 3100;

static NSPoint rotatePoint(NSPoint center, NSPoint point, CGFloat percent) {
    CGFloat radians = percent * M_PI * 2;
    CGFloat x = point.x - center.x;
    CGFloat y = point.y - center.y;
    CGFloat newX = x * cos(radians) + y * sin(radians);
    CGFloat newY = x * -sin(radians) + y * cos(radians);
    return NSMakePoint(center.x + newX, center.y + newY);
}

static NSPoint scalePoint(NSPoint center, NSPoint point, CGFloat percent) {
    CGFloat x = point.x - center.x;
    CGFloat y = point.y - center.y;
    CGFloat newX = x * percent;
    CGFloat newY = y * percent;
    return NSMakePoint(center.x + newX, center.y + newY);
}

static NSBezierPath *copyTeardropPath(void) {
    static NSBezierPath *sPath = nil;
    if (! sPath) {
        
        CGFloat radius = HFTeardropRadius;
        CGFloat rotation = 0;
        CGFloat droppiness = .15;
        CGFloat tipScale = HFTeadropTipScale;
        CGFloat tipLengthFromCenter = radius * tipScale;
        NSPoint bulbCenter = NSMakePoint(-tipLengthFromCenter, 0);
        
        NSPoint triangleCenter = rotatePoint(bulbCenter, NSMakePoint(bulbCenter.x + radius, bulbCenter.y), rotation);
        NSPoint dropCorner1 = rotatePoint(bulbCenter, triangleCenter, droppiness / 2);
        NSPoint dropCorner2 = rotatePoint(bulbCenter, triangleCenter, -droppiness / 2);
        NSPoint dropTip = scalePoint(bulbCenter, triangleCenter, tipScale);
        
        NSBezierPath *path = [[NSBezierPath alloc] init];
        [path appendBezierPathWithArcWithCenter:bulbCenter radius:radius startAngle:-rotation * 360 + droppiness * 180. endAngle:-rotation * 360 - droppiness * 180. clockwise:NO];
        
        [path moveToPoint:dropCorner1];
        [path lineToPoint:dropTip];
        [path lineToPoint:dropCorner2];
        [path closePath];
        
        sPath = path;
    }
    return [sPath retain];
}


@implementation HFRepresenterTextViewCallout

/* A helpful struct for representing a wedge (portion of a circle). Wedges are counterclockwise. */
typedef struct {
    double offset; // 0 <= offset < 1
    double length; // 0 <= length <= 1
} Wedge_t;


static inline double normalizeAngle(double x) {
    /* Convert an angle to the range [0, 1). We typically only generate angles that are off by a full rotation, so a loop isn't too bad. */
    while (x >= 1.) x -= 1.;
    while (x < 0.) x += 1.;
    return x;
}

static inline double distanceCCW(double a, double b) { return normalizeAngle(b-a); }

static inline double wedgeMax(Wedge_t wedge) {
    return normalizeAngle(wedge.offset + wedge.length);
}

/* Computes the smallest wedge containing the two given wedges. Compute the wedge from the min of one to the furthest part of the other, and pick the smaller. */
static Wedge_t wedgeUnion(Wedge_t wedge1, Wedge_t wedge2) {
    // empty wedges don't participate
    if (wedge1.length <= 0) return wedge2;
    if (wedge2.length <= 0) return wedge1;
    
    Wedge_t union1 = wedge1;
    union1.length = fmin(1., fmax(union1.length, distanceCCW(union1.offset, wedge2.offset) + wedge2.length));
    
    Wedge_t union2 = wedge2;
    union2.length = fmin(1., fmax(union2.length, distanceCCW(union2.offset, wedge1.offset) + wedge1.length));
    
    Wedge_t result = (union1.length <= union2.length ? union1 : union2);
    HFASSERT(result.length <= 1);
    return result;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        // Initialization code here.
    }
    
    return self;
}

- (void)dealloc {
    [_representedObject release];
    [_color release];
    [_label release];
    [super dealloc];
}

- (NSComparisonResult)compare:(HFRepresenterTextViewCallout *)callout {
    return [_representedObject compare:callout.representedObject];
}

static Wedge_t computeForbiddenAngle(double distanceFromEdge, double angleToEdge) {
    Wedge_t newForbiddenAngle;
    
    /* This is how far it is to the center of our teardrop */
    const double teardropLength = HFTeardropRadius * HFTeadropTipScale;
    
    if (distanceFromEdge <= 0) {
        /* We're above or below. */
        if (-distanceFromEdge >= (teardropLength + HFTeardropRadius)) {
            /* We're so far above or below we won't be visible at all. No hope. */
            newForbiddenAngle = (Wedge_t){.offset = 0, .length = 1};
        } else { 
            /* We're either above or below the bounds, but there's a hope we can be visible */
            
            double invertedAngleToEdge = normalizeAngle(angleToEdge + .5);
            double requiredAngle;
            if (-distanceFromEdge >= teardropLength) {
                // We're too far north or south that all we can do is point in the right direction
                requiredAngle = 0;
            } else {
                // By confining ourselves to required angles, we can make ourselves visible
                requiredAngle = acos(-distanceFromEdge / teardropLength) / (2 * M_PI);
            }
            // Require at least a small spread
            requiredAngle = fmax(requiredAngle, .04);
            
            double requiredMin = invertedAngleToEdge - requiredAngle;
            double requiredMax = invertedAngleToEdge + requiredAngle;
            
            newForbiddenAngle = (Wedge_t){.offset = requiredMax, .length = distanceCCW(requiredMax, requiredMin) };
        }
    } else if (distanceFromEdge < teardropLength) {
        // We're onscreen, but some angle will be forbidden
        double forbiddenAngle = acos(distanceFromEdge / teardropLength) / (2 * M_PI);
        
        // This is a wedge out of the top (or bottom)
        newForbiddenAngle = (Wedge_t){.offset = angleToEdge - forbiddenAngle, .length = 2 * forbiddenAngle};
    } else {
        /* Nothing prohibited at all */
        newForbiddenAngle = (Wedge_t){0, 0};
    }
    return newForbiddenAngle;
}


static double distanceMod1(double a, double b) {
    /* Assuming 0 <= a, b < 1, returns the distance between a and b, mod 1 */
    if (a > b) {
        return fmin(a-b, b-a+1);
    } else {
        return fmin(b-a, a-b+1);
    }
}

+ (void)layoutCallouts:(NSArray *)callouts inView:(HFRepresenterTextView *)textView {
    
    // Keep track of how many drops are at a given location
    NSCountedSet *dropsPerByteLoc = [[NSCountedSet alloc] init];
    
    const CGFloat lineHeight = [textView lineHeight];
    const NSRect bounds = [textView bounds];
    
    NSMutableArray *remainingCallouts = [[callouts mutableCopy] autorelease];
    [remainingCallouts sortUsingSelector:@selector(compare:)];
    
    while ([remainingCallouts count] > 0) {
        /* Get the next callout to lay out */
        const NSInteger byteLoc = [remainingCallouts[0] byteOffset];
        
        /* Get all the callouts that share that byteLoc */
        NSMutableArray *sharedCallouts = [NSMutableArray array];
        FOREACH(HFRepresenterTextViewCallout *, testCallout, remainingCallouts) {
            if ([testCallout byteOffset] == byteLoc) {
                [sharedCallouts addObject:testCallout];
            }
        }
        
        /* We expect to get at least one */
        const NSUInteger calloutCount = [sharedCallouts count];
        HFASSERT(calloutCount > 0);
        
        /* Get the character origin */
        const NSPoint characterOrigin = [textView originForCharacterAtByteIndex:byteLoc];

        Wedge_t forbiddenAngle = {0, 0};
        
        // Compute how far we are from the top (or bottom)
        BOOL isNearerTop = (characterOrigin.y < NSMidY(bounds));
        double verticalDistance = (isNearerTop ? characterOrigin.y - NSMinY(bounds) : NSMaxY(bounds) - characterOrigin.y);
        forbiddenAngle = wedgeUnion(forbiddenAngle, computeForbiddenAngle(verticalDistance, (isNearerTop ? .25 : .75)));
        
        // Compute how far we are from the left (or right)
        BOOL isNearerLeft = (characterOrigin.x < NSMidX(bounds));
        double horizontalDistance = (isNearerLeft ? characterOrigin.x - NSMinX(bounds) : NSMaxX(bounds) - characterOrigin.x);
        forbiddenAngle = wedgeUnion(forbiddenAngle, computeForbiddenAngle(horizontalDistance, (isNearerLeft ? .5 : 0.)));
        
        
        /* How much will each callout rotate? No more than 1/8th. */
        HFASSERT(forbiddenAngle.length <= 1);
        double changeInRotationPerCallout = fmin(.125, (1. - forbiddenAngle.length) / calloutCount);
        double totalConsumedAmount = changeInRotationPerCallout * calloutCount;
        
        /* We would like to center around .375. */
        const double goalCenter = .375;
        
        /* We're going to pretend to work on a line segment that extends from the max prohibited angle all the way back to min */
        double segmentLength = 1. - forbiddenAngle.length;
        double goalSegmentCenter = normalizeAngle(goalCenter - wedgeMax(forbiddenAngle)); //may exceed segmentLength!
                
        /* Now center us on the goal, or as close as we can get. */
        double consumedSegmentCenter;
        
        /* We only need to worry about wrapping around if we have some prohibited angle */
        if (forbiddenAngle.length <= 0) { //never expect < 0, but be paranoid
            consumedSegmentCenter = goalSegmentCenter;
        } else {
            
            /* The consumed segment center is confined to the segment range [amount/2, length - amount/2] */
            double consumedSegmentCenterMin = totalConsumedAmount/2;
            double consumedSegmentCenterMax = segmentLength - totalConsumedAmount/2;
            if (goalSegmentCenter >= consumedSegmentCenterMin && goalSegmentCenter < consumedSegmentCenterMax) {
                /* We can hit our goal */
                consumedSegmentCenter = goalSegmentCenter;
            } else {
                /* Pick either the min or max location, depending on which one gets us closer to the goal segment center mod 1. */
                if (distanceMod1(goalSegmentCenter, consumedSegmentCenterMin) <= distanceMod1(goalSegmentCenter, consumedSegmentCenterMax)) {
                    consumedSegmentCenter = consumedSegmentCenterMin;
                } else {
                    consumedSegmentCenter = consumedSegmentCenterMax;
                }
                
            }
        }
        
        /* Now convert this back to an angle */
        double consumedAngleCenter = normalizeAngle(wedgeMax(forbiddenAngle) + consumedSegmentCenter);
        
        // move us slightly towards the character
        NSPoint teardropTipOrigin = NSMakePoint(characterOrigin.x + 1, characterOrigin.y + floor(lineHeight / 8.));
        
        // make the pin
        NSPoint pinStart, pinEnd;
        pinStart = NSMakePoint(characterOrigin.x + .25, characterOrigin.y);
        pinEnd = NSMakePoint(pinStart.x, pinStart.y + lineHeight);
        
        // store it all, invalidating as necessary
        NSInteger i = 0;
        FOREACH(HFRepresenterTextViewCallout *, callout, sharedCallouts) {
            
            /* Compute the rotation */
            double seq = (i+1)/2; //0, 1, -1, 2, -2...
            if ((i & 1) == 0) seq = -seq;
            //if we've got an even number of callouts, we want -.5, .5, -1.5, 1.5...
            if (! (calloutCount & 1)) seq -= .5;
            // compute the angle of rotation
            double angle = consumedAngleCenter + seq * changeInRotationPerCallout;
            // our notion of rotation has 0 meaning pointing right and going counterclockwise, but callouts with 0 pointing left and going clockwise, so convert
            angle = normalizeAngle(.5 - angle);

            
            NSRect beforeRect = [callout rect];
            
            callout->rotation = angle;
            callout->tipOrigin = teardropTipOrigin;
            callout->pinStart = pinStart;
            callout->pinEnd = pinEnd;
            
            // Only the first gets a pin
            pinStart = pinEnd = NSZeroPoint;
            
            NSRect afterRect = [callout rect];
            
            if (! NSEqualRects(beforeRect, afterRect)) {
                [textView setNeedsDisplayInRect:beforeRect];
                [textView setNeedsDisplayInRect:afterRect];
            }
            
            i++;
        }

        
        /* We're done laying out these callouts */
        [remainingCallouts removeObjectsInArray:sharedCallouts];
    }
    
    [dropsPerByteLoc release];
}

- (CGAffineTransform)teardropTransform {
    CGAffineTransform trans = CGAffineTransformMakeTranslation(tipOrigin.x, tipOrigin.y);
    trans = CGAffineTransformRotate(trans, rotation * M_PI * 2);
    return trans;
}

- (NSRect)teardropBaseRect {
    NSSize teardropSize = NSMakeSize(HFTeardropRadius * (1 + HFTeadropTipScale), HFTeardropRadius*2);
    NSRect result = NSMakeRect(-teardropSize.width, -teardropSize.height/2, teardropSize.width, teardropSize.height);
    return result;
}

- (CGAffineTransform)shadowTransform {
    CGFloat shadowXOffset = HFShadowXOffset;
    CGFloat shadowYOffset = HFShadowYOffset;
    CGFloat offscreenOffset = HFShadowOffscreenHack;
    
    // Figure out how much movement the shadow offset produces
    CGFloat shadowTranslationDistance = hypot(shadowXOffset, shadowYOffset);
    
    CGAffineTransform transform = CGAffineTransformIdentity;
    transform = CGAffineTransformTranslate(transform, tipOrigin.x + offscreenOffset - shadowXOffset, tipOrigin.y - shadowYOffset);
    transform = CGAffineTransformRotate(transform, rotation * M_PI * 2 - atan2(shadowTranslationDistance, 2*HFTeardropRadius /* bulbHeight */));
    return transform;
}

- (void)drawShadowWithClip:(NSRect)clip {
    USE(clip);
    CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort];
    
    // Set the shadow. Note that these shadows are pretty unphysical for high rotations.
    NSShadow *shadow = [[NSShadow alloc] init];
    [shadow setShadowBlurRadius:5.];
    [shadow setShadowOffset:NSMakeSize(HFShadowXOffset - HFShadowOffscreenHack, HFShadowYOffset)];
    [shadow setShadowColor:[NSColor colorWithDeviceWhite:0. alpha:.5]];
    [shadow set];
    [shadow release];
    
    // Draw the shadow first and separately
    CGAffineTransform transform = [self shadowTransform];
    CGContextConcatCTM(ctx, transform);
    
    NSBezierPath *teardrop = copyTeardropPath();
    [teardrop fill];
    [teardrop release];
    
    // Clear the shadow
    CGContextSetShadowWithColor(ctx, CGSizeZero, 0, NULL);
    
    // Undo the transform
    CGContextConcatCTM(ctx, CGAffineTransformInvert(transform));
}

- (void)drawWithClip:(NSRect)clip {
    USE(clip);
    CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort];
    // Here's the font we'll use
    CTFontRef ctfont = CTFontCreateWithName(CFSTR("Helvetica-Bold"), 1., NULL);
    if (ctfont) {
        // Set the font
        [(NSFont *)ctfont set];
            
        // Get characters
        NSUInteger labelLength = MIN([_label length], kHFRepresenterTextViewCalloutMaxGlyphCount);
        UniChar calloutUniLabel[kHFRepresenterTextViewCalloutMaxGlyphCount];
        [_label getCharacters:calloutUniLabel range:NSMakeRange(0, labelLength)];
        
        // Get our glyphs and advances
        CGGlyph glyphs[kHFRepresenterTextViewCalloutMaxGlyphCount];
        CGSize advances[kHFRepresenterTextViewCalloutMaxGlyphCount];
        CTFontGetGlyphsForCharacters(ctfont, calloutUniLabel, glyphs, labelLength);
        CTFontGetAdvancesForGlyphs(ctfont, kCTFontHorizontalOrientation, glyphs, advances, labelLength);

        // Count our glyphs. Note: this won't work with any label containing spaces, etc.
        NSUInteger glyphCount;
        for (glyphCount = 0; glyphCount < labelLength; glyphCount++) {
            if (glyphs[glyphCount] == 0) break;
        }
                
        // Set our color.
        [_color set];
        
        // Draw the pin first
        if (! NSEqualPoints(pinStart, pinEnd)) {
            [NSBezierPath setDefaultLineWidth:1.25];
            [NSBezierPath strokeLineFromPoint:pinStart toPoint:pinEnd];
        }
        
        CGContextSaveGState(ctx);
        CGContextBeginTransparencyLayerWithRect(ctx, NSRectToCGRect([self rect]), NULL);

        // Rotate and translate in preparation for drawing the teardrop
        CGContextConcatCTM(ctx, [self teardropTransform]);
        
        // Draw the teardrop
        NSBezierPath *teardrop = copyTeardropPath();
        [teardrop fill];
        [teardrop release];
        
        // Draw the text with white and alpha.  Use blend mode copy so that we clip out the shadow, and when the transparency layer is ended we'll composite over the text.
        CGFloat textScale = (glyphCount == 1 ? 24 : 20);
        
        // we are flipped by default, so invert the rotation's sign to get the text direction. Use a little slop so we don't get jitter.
        const CGFloat textDirection = (rotation <= .27 || rotation >= .73) ? -1 : 1;
        
        CGPoint positions[kHFRepresenterTextViewCalloutMaxGlyphCount];
        CGFloat totalAdvance = 0;
        for (NSUInteger i=0; i < glyphCount; i++) {
            // make sure to provide negative advances if necessary
            positions[i].x = copysign(totalAdvance, -textDirection);
            positions[i].y = 0;
            CGFloat advance = advances[i].width;
            // Workaround 5834794
            advance *= textScale;
            // Tighten up the advances a little
            advance *= .85;
            totalAdvance += advance;
        }
        
        
        // Compute the vertical offset
        CGFloat textYOffset = (glyphCount == 1 ? 4 : 5);                
        // LOL
        if ([_label isEqualToString:@"6"]) textYOffset -= 1;
        
        
        // Apply this text matrix
        NSRect bulbRect = [self teardropBaseRect];
        CGAffineTransform textMatrix = CGAffineTransformMakeScale(-copysign(textScale, textDirection), copysign(textScale, textDirection)); //roughly the font size we want
        textMatrix.tx = NSMinX(bulbRect) + HFTeardropRadius + copysign(totalAdvance/2, textDirection);
        

        if (textDirection < 0) {
            textMatrix.ty = NSMaxY(bulbRect) - textYOffset;
        } else {
            textMatrix.ty = NSMinY(bulbRect) + textYOffset;
        }
        
        // Draw
        CGContextSetTextMatrix(ctx, textMatrix);
        CGContextSetTextDrawingMode(ctx, kCGTextClip);
        CGContextShowGlyphsAtPositions(ctx, glyphs, positions, glyphCount);
        
        CGContextSetBlendMode(ctx, kCGBlendModeCopy);
        CGContextSetGrayFillColor(ctx, 1., .66); //faint white fill
        CGContextFillRect(ctx, NSRectToCGRect(NSInsetRect(bulbRect, -20, -20)));
        
        // Done drawing, so composite
        CGContextEndTransparencyLayer(ctx);
        CGContextRestoreGState(ctx); // this also restores the clip, which is important
        
        // Done with the font
        CFRelease(ctfont);
    }
}

- (NSRect)rect {
    // get the transformed teardrop rect
    NSRect result = NSRectFromCGRect(CGRectApplyAffineTransform(NSRectToCGRect([self teardropBaseRect]), [self teardropTransform]));
    
    // outset a bit for the shadow
    result = NSInsetRect(result, -8, -8);
    return result;
}

@end
