//
//  MBFingerTipWindow.m
//
//  Copyright 2011-2017 Mapbox, Inc. All rights reserved.
//

#import "MBFingerTipWindow.h"

// This file must be built with ARC.
//
#if !__has_feature(objc_arc)
    #error "ARC must be enabled for MBFingerTipWindow.m"
#endif

@interface MBFingerTipView : UIImageView

@property (nonatomic, assign) NSTimeInterval timestamp;
@property (nonatomic, assign) BOOL shouldAutomaticallyRemoveAfterTimeout;
@property (nonatomic, assign, getter=isFadingOut) BOOL fadingOut;

@end

#pragma mark -

@interface MBFingerTipOverlayWindow : UIWindow
@end

#pragma mark -

@interface MBFingerTipWindow ()

@property (nonatomic, strong) UIWindow *overlayWindow;
@property (nonatomic, assign) BOOL active;
@property (nonatomic, assign) BOOL fingerTipRemovalScheduled;

- (void)MBFingerTipWindow_commonInit;
- (BOOL)anyScreenIsMirrored;
- (void)updateFingertipsAreActive;
- (void)scheduleFingerTipRemoval;
- (void)cancelScheduledFingerTipRemoval;
- (void)removeInactiveFingerTips;
- (void)removeFingerTipWithHash:(NSUInteger)hash animated:(BOOL)animated;
- (BOOL)shouldAutomaticallyRemoveFingerTipForTouch:(UITouch *)touch;

@end

#pragma mark -

@implementation MBFingerTipWindow

@synthesize touchImage=_touchImage;

- (id)initWithCoder:(NSCoder *)decoder
{
    // This covers NIB-loaded windows.
    //
    self = [super initWithCoder:decoder];

    if (self != nil)
        [self MBFingerTipWindow_commonInit];
    
    return self;
}

- (id)initWithFrame:(CGRect)rect
{
    // This covers programmatically-created windows.
    //
    self = [super initWithFrame:rect];
    
    if (self != nil)
        [self MBFingerTipWindow_commonInit];
    
    return self;
}

- (void)MBFingerTipWindow_commonInit
{
    self.strokeColor = [UIColor blackColor];
    self.fillColor = [UIColor whiteColor];
    
    self.touchAlpha   = 0.5;
    self.fadeDuration = 0.3;
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(screenConnect:)
                                                 name:UIScreenDidConnectNotification
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(screenDisconnect:)
                                                 name:UIScreenDidDisconnectNotification
                                               object:nil];

    // Set up active now, in case the screen was present before the window was created (or application launched).
    //
    [self updateFingertipsAreActive];
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIScreenDidConnectNotification    object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIScreenDidDisconnectNotification object:nil];
}

#pragma mark -

- (UIWindow *)overlayWindow
{
    if ( ! _overlayWindow)
    {
        _overlayWindow = [[MBFingerTipOverlayWindow alloc] initWithFrame:self.frame];
        
        _overlayWindow.userInteractionEnabled = NO;
#if !TARGET_OS_TV
        _overlayWindow.windowLevel = UIWindowLevelStatusBar;
#endif
        _overlayWindow.backgroundColor = [UIColor clearColor];
        _overlayWindow.hidden = NO;
    }
    
    return _overlayWindow;
}

- (UIImage *)touchImage
{
    if ( ! _touchImage)
    {
        UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 50.0, 50.0)];
        
        UIGraphicsBeginImageContextWithOptions(clipPath.bounds.size, NO, 0);

        UIBezierPath *drawPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(25.0, 25.0) 
                                                                radius:22.0
                                                            startAngle:0
                                                              endAngle:2 * M_PI
                                                             clockwise:YES];

        drawPath.lineWidth = 2.0;
        
        [self.strokeColor setStroke];
        [self.fillColor setFill];

        [drawPath stroke];
        [drawPath fill];
        
        [clipPath addClip];
        
        _touchImage = UIGraphicsGetImageFromCurrentImageContext();
        
        UIGraphicsEndImageContext();
    }
        
    return _touchImage;
}

#pragma mark - Setter

- (void)setAlwaysShowTouches:(BOOL)flag
{
	if (_alwaysShowTouches != flag)
	{
		_alwaysShowTouches = flag;

        [self updateFingertipsAreActive];
	}
}

#pragma mark -
#pragma mark Screen notifications

- (void)screenConnect:(NSNotification *)notification
{
    [self updateFingertipsAreActive];
}

- (void)screenDisconnect:(NSNotification *)notification
{
    [self updateFingertipsAreActive];
}

- (BOOL)anyScreenIsMirrored
{
    if ( ! [UIScreen instancesRespondToSelector:@selector(mirroredScreen)])
        return NO;

    for (UIScreen *screen in [UIScreen screens])
    {
        if ([screen mirroredScreen] != nil)
            return YES;
    }

    return NO;
}

- (void)updateFingertipsAreActive;
{
    if (self.alwaysShowTouches || ([[[[NSProcessInfo processInfo] environment] objectForKey:@"DEBUG_FINGERTIP_WINDOW"] boolValue]))
    {
        self.active = YES;
    }
    else
    {
        self.active = [self anyScreenIsMirrored];
    }
}

#pragma mark -
#pragma mark UIWindow overrides

- (void)sendEvent:(UIEvent *)event
{
    if (self.active)
    {
        NSSet *allTouches = [event allTouches];
        
        for (UITouch *touch in [allTouches allObjects])
        {
            switch (touch.phase)
            {
                case UITouchPhaseBegan:
                case UITouchPhaseMoved:
                case UITouchPhaseStationary:
                {
                    MBFingerTipView *touchView = (MBFingerTipView *)[self.overlayWindow viewWithTag:touch.hash];

                    if (touch.phase != UITouchPhaseStationary && touchView != nil && [touchView isFadingOut])
                    {
                        [touchView removeFromSuperview];
                        touchView = nil;
                    }
                    
                    if (touchView == nil && touch.phase != UITouchPhaseStationary)
                    {
                        touchView = [[MBFingerTipView alloc] initWithImage:self.touchImage];
                        [self.overlayWindow addSubview:touchView];
                    }
            
                    if ( ! [touchView isFadingOut])
                    {
                        touchView.alpha = self.touchAlpha;
                        touchView.center = [touch locationInView:self.overlayWindow];
                        touchView.tag = touch.hash;
                        touchView.timestamp = touch.timestamp;
                        touchView.shouldAutomaticallyRemoveAfterTimeout = [self shouldAutomaticallyRemoveFingerTipForTouch:touch];
                    }
                    break;
                }

                case UITouchPhaseEnded:
                case UITouchPhaseCancelled:
                {
                    [self removeFingerTipWithHash:touch.hash animated:YES];
                    break;
                }
            }
        }
    }
        
    [super sendEvent:event];

    [self scheduleFingerTipRemoval]; // We may not see all UITouchPhaseEnded/UITouchPhaseCancelled events.
}

#pragma mark -
#pragma mark Private

- (void)scheduleFingerTipRemoval
{
    if (self.fingerTipRemovalScheduled)
        return;
    
    self.fingerTipRemovalScheduled = YES;
    [self performSelector:@selector(removeInactiveFingerTips) withObject:nil afterDelay:0.1];
}

- (void)cancelScheduledFingerTipRemoval
{
    self.fingerTipRemovalScheduled = YES;
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(removeInactiveFingerTips) object:nil];
}

- (void)removeInactiveFingerTips
{
    self.fingerTipRemovalScheduled = NO;

    NSTimeInterval now = [[NSProcessInfo processInfo] systemUptime];
    const CGFloat REMOVAL_DELAY = 0.2;

    for (MBFingerTipView *touchView in [self.overlayWindow subviews])
    {
        if ( ! [touchView isKindOfClass:[MBFingerTipView class]])
            continue;
        
        if (touchView.shouldAutomaticallyRemoveAfterTimeout && now > touchView.timestamp + REMOVAL_DELAY)
            [self removeFingerTipWithHash:touchView.tag animated:YES];
    }

    if ([[self.overlayWindow subviews] count] > 0)
        [self scheduleFingerTipRemoval];
}

- (void)removeFingerTipWithHash:(NSUInteger)hash animated:(BOOL)animated;
{
    MBFingerTipView *touchView = (MBFingerTipView *)[self.overlayWindow viewWithTag:hash];
    if ( ! [touchView isKindOfClass:[MBFingerTipView class]])
        return;
    
    if ([touchView isFadingOut])
        return;
        
    BOOL animationsWereEnabled = [UIView areAnimationsEnabled];

    if (animated)
    {
        [UIView setAnimationsEnabled:YES];
        [UIView beginAnimations:nil context:nil];
        [UIView setAnimationDuration:self.fadeDuration];
    }

    touchView.frame = CGRectMake(touchView.center.x - touchView.frame.size.width, 
                                 touchView.center.y - touchView.frame.size.height, 
                                 touchView.frame.size.width  * 2, 
                                 touchView.frame.size.height * 2);
    
    touchView.alpha = 0.0;

    if (animated)
    {
        [UIView commitAnimations];
        [UIView setAnimationsEnabled:animationsWereEnabled];
    }
    
    touchView.fadingOut = YES;
    [touchView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:self.fadeDuration];
}

- (BOOL)shouldAutomaticallyRemoveFingerTipForTouch:(UITouch *)touch;
{
    // We don't reliably get UITouchPhaseEnded or UITouchPhaseCancelled
    // events via -sendEvent: for certain touch events. Known cases
    // include swipe-to-delete on a table view row, and tap-to-cancel
    // swipe to delete. We automatically remove their associated
    // fingertips after a suitable timeout.
    //
    // It would be much nicer if we could remove all touch events after
    // a suitable time out, but then we'll prematurely remove touch and
    // hold events that are picked up by gesture recognizers (since we
    // don't use UITouchPhaseStationary touches for those. *sigh*). So we
    // end up with this more complicated setup.

    UIView *view = [touch view];
    view = [view hitTest:[touch locationInView:view] withEvent:nil];

    while (view != nil)
    {
        if ([view isKindOfClass:[UITableViewCell class]])
        {
            for (UIGestureRecognizer *recognizer in [touch gestureRecognizers])
            {
                if ([recognizer isKindOfClass:[UISwipeGestureRecognizer class]])
                    return YES;
            }
        }

        if ([view isKindOfClass:[UITableView class]])
        {
            if ([[touch gestureRecognizers] count] == 0)
                return YES;
        }

        view = view.superview;
    }

    return NO;
}

@end

#pragma mark -

@implementation MBFingerTipView

@end

#pragma mark -

@implementation MBFingerTipOverlayWindow

// UIKit tries to get the rootViewController from the overlay window. Use the Fingertips window instead. This fixes
// issues with status bar behavior, as otherwise the overlay window would control the status bar.

- (UIViewController *)rootViewController
{
    NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings)
    {
        return [evaluatedObject isKindOfClass:[MBFingerTipWindow class]];
    }];
    UIWindow *mainWindow = [[[[UIApplication sharedApplication] windows] filteredArrayUsingPredicate:predicate] firstObject];
    return mainWindow.rootViewController ?: [super rootViewController];
}

@end
