//
//  MBUnlockViewController.m
//  MasterPassword
//
//  Created by Maarten Billemont on 22/02/12.
//  Copyright (c) 2012 Lyndir. All rights reserved.
//

#import <QuartzCore/QuartzCore.h>
#import <Social/Social.h>
#import <CoreGraphics/CoreGraphics.h>

#import "MPUnlockViewController.h"
#import "MPiOSAppDelegate.h"
#import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h"


@interface MPUnlockViewController()

@property(strong, nonatomic) NSMutableDictionary *avatarToUserOID;
@property(nonatomic) BOOL wordWallAnimating;
@property(nonatomic, strong) NSArray *wordList;

@property(nonatomic, strong) NSOperationQueue *emergencyQueue;
@property(nonatomic, strong) MPKey *emergencyKey;

@property(nonatomic, strong) NSTimer *marqueeTipTimer;
@property(nonatomic) NSUInteger marqueeTipTextIndex;
@property(nonatomic, strong) NSArray *marqueeTipTexts;
@end


@implementation MPUnlockViewController {
    NSManagedObjectID *_selectedUserOID;
}

- (void)initializeAvatarAlert:(UIAlertView *)alert forUser:(MPUserEntity *)user inContext:(NSManagedObjectContext *)moc {

    UIScrollView *alertAvatarScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake( 12, 30, 260, 150 )];
    alertAvatarScrollView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
    [alertAvatarScrollView flashScrollIndicatorsContinuously];
    [alert addSubview:alertAvatarScrollView];

    CGPoint selectedOffset = CGPointZero;
    for (int a = 0; a < MPAvatarCount; ++a) {
        UIButton *avatar = [self.avatarTemplate cloneAddedTo:alertAvatarScrollView];

        avatar.tag = a;
        avatar.hidden = NO;
        avatar.center = CGPointMake(
                (20 + self.avatarTemplate.bounds.size.width / 2) * (a + 1) + self.avatarTemplate.bounds.size.width / 2 * a,
                20 + self.avatarTemplate.bounds.size.height / 2 );
        [avatar setBackgroundImage:[UIImage imageNamed:PearlString( @"avatar-%d", a )] forState:UIControlStateNormal];
        [avatar setSelectionInSuperviewCandidate:YES isClearable:NO];

        avatar.layer.cornerRadius = avatar.bounds.size.height / 2;
        avatar.layer.shadowColor = [UIColor blackColor].CGColor;
        avatar.layer.shadowOpacity = 1;
        avatar.layer.shadowRadius = 5;
        avatar.backgroundColor = [UIColor clearColor];

        [avatar onHighlightOrSelect:^(BOOL highlighted, BOOL selected) {
            if (highlighted || selected)
                avatar.backgroundColor = self.avatarTemplate.backgroundColor;
            else
                avatar.backgroundColor = [UIColor clearColor];
        }                   options:0];
        [avatar onSelect:^(BOOL selected) {
            if (selected)
                [moc performBlock:^{
                    user.avatar = (unsigned)avatar.tag;
                }];
        }        options:0];
        avatar.selected = (a == user.avatar);
        if (avatar.selected)
            selectedOffset = CGPointMake( avatar.center.x - alertAvatarScrollView.bounds.size.width / 2, 0 );
    }

    [alertAvatarScrollView autoSizeContent];
    [alertAvatarScrollView setContentOffset:selectedOffset animated:YES];
}

- (void)initializeConfirmationAlert:(UIAlertView *)alert forUser:(MPUserEntity *)user {

    UIView *container = [[UIView alloc] initWithFrame:CGRectMake( 12, 70, 260, 110 )];
    [alert addSubview:container];

    UIButton *alertAvatar = [self.avatarTemplate cloneAddedTo:container];
    alertAvatar.center = CGPointMake( 130, 55 );
    alertAvatar.hidden = NO;
    alertAvatar.layer.shadowColor = [UIColor blackColor].CGColor;
    alertAvatar.layer.shadowOpacity = 1;
    alertAvatar.layer.shadowRadius = 5;
    alertAvatar.backgroundColor = [UIColor clearColor];
    [alertAvatar setBackgroundImage:[UIImage imageNamed:PearlString( @"avatar-%d", user.avatar )] forState:UIControlStateNormal];

    UILabel *alertNameLabel = [self.nameLabel cloneAddedTo:container];
    alertNameLabel.center = alertAvatar.center;
    alertNameLabel.text = user.name;
    alertNameLabel.bounds = CGRectSetHeight( alertNameLabel.bounds,
            [alertNameLabel.text sizeWithFont:self.nameLabel.font
                            constrainedToSize:CGSizeMake( alertNameLabel.bounds.size.width - 10,
                                    100 )
                                lineBreakMode:self.nameLabel.lineBreakMode].height );
    alertNameLabel.layer.cornerRadius = 5;
    alertNameLabel.backgroundColor = [UIColor blackColor];
}

- (BOOL)shouldAutorotate {

    return YES;
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {

    return UIInterfaceOrientationPortrait;
}

- (NSUInteger)supportedInterfaceOrientations {

    return UIInterfaceOrientationMaskPortrait;
}

- (void)viewDidLoad {

    NSString *newsURL = PearlString( @"http://www.masterpasswordapp.com/news.html?version=%@", [[PearlInfoPlist get] CFBundleVersion] );
    [self.newsView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:newsURL]]];

    self.avatarToUserOID = [NSMutableDictionary dictionaryWithCapacity:3];

    [self.avatarsView addGestureRecognizer:self.targetedUserActionGesture];
    self.avatarsView.decelerationRate = UIScrollViewDecelerationRateFast;
    self.avatarsView.clipsToBounds = NO;
    self.tip.text = @"";
    self.nameLabel.layer.cornerRadius = 5;
    self.avatarTemplate.hidden = YES;
    self.passwordTipView.hidden = NO;
    self.createPasswordTipView.hidden = NO;
    [self.emergencyPassword setTitle:@"" forState:UIControlStateNormal];
    self.emergencyGeneratorContainer.alpha = 0;
    self.emergencyGeneratorContainer.hidden = YES;
    self.emergencyQueue = [NSOperationQueue new];
    [self.emergencyCounterStepper addTargetBlock:^(id sender, UIControlEvents event) {
        self.emergencyCounter.text = PearlString( @"%d", (NSUInteger)self.emergencyCounterStepper.value );

        [self updateEmergencyPassword];
    }                           forControlEvents:UIControlEventValueChanged];
    [self.emergencyTypeControl addTargetBlock:^(id sender, UIControlEvents event) {
        [self updateEmergencyPassword];
    }                        forControlEvents:UIControlEventValueChanged];
    self.emergencyContentTipContainer.alpha = 0;
    self.emergencyContentTipContainer.hidden = NO;
    self.marqueeTipTexts = @[
            @"Tap and hold to delete or reset user.",
            @"Shake for emergency generator."
    ];

    NSMutableArray *wordListLines = [NSMutableArray arrayWithCapacity:27413];
    [[[NSString alloc] initWithData:[NSData dataWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"dictionary" withExtension:@"lst"]]
                           encoding:NSUTF8StringEncoding] enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {
        [wordListLines addObject:line];
    }];
    self.wordList = wordListLines;

    self.wordWall.alpha = 0;
    [self.wordWall enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
        UILabel *wordLabel = (UILabel *)subview;

        [self initializeWordLabel:wordLabel];
    }                        recurse:NO];

    [[NSNotificationCenter defaultCenter] addObserverForName:USMStoreDidChangeNotification object:nil
                                                       queue:[NSOperationQueue mainQueue] usingBlock:
            ^(NSNotification *note) {
                [self updateUsers];
            }];
    [[NSNotificationCenter defaultCenter] addObserverForName:USMStoreDidImportChangesNotification object:nil
                                                       queue:[NSOperationQueue mainQueue] usingBlock:
            ^(NSNotification *note) {
                [self updateUsers];
            }];
    [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillResignActiveNotification object:nil
                                                       queue:[NSOperationQueue mainQueue] usingBlock:
            ^(NSNotification *note) {
                [self emergencyCloseAnimated:NO];
                self.uiContainer.alpha = 0;
            }];
    [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:nil
                                                       queue:[NSOperationQueue mainQueue] usingBlock:
            ^(NSNotification *note) {
                [self updateLayoutAnimated:NO allowScroll:NO completion:nil];
                [UIView animateWithDuration:1 animations:^{
                    self.uiContainer.alpha = 1;
                }];
            }];

    [self updateLayoutAnimated:NO allowScroll:YES completion:nil];

    [super viewDidLoad];
}

- (void)viewWillAppear:(BOOL)animated {

    inf(@"Lock screen will appear");
    [self.navigationController setNavigationBarHidden:YES animated:animated];
    [[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationSlide];

    [[MPiOSAppDelegate get] signOutAnimated:NO];

    _selectedUserOID = nil;
    [self updateUsers];

    self.uiContainer.alpha = 0;
    self.spinner.alpha = 0;

    [super viewWillAppear:animated];
}

- (void)viewDidAppear:(BOOL)animated {

    [self becomeFirstResponder];

    if (!animated && !self.navigationController.presentedViewController)
        [[self findTargetedAvatar] setSelected:YES];
    else
        [self updateLayoutAnimated:YES allowScroll:YES completion:nil];

    [UIView animateWithDuration:0.5 animations:^{
        self.uiContainer.alpha = 1;
    }];

    [self.marqueeTipTimer invalidate];
    self.marqueeTipTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(marqueeTip) userInfo:nil repeats:YES];

    [[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Unlock"];

    [super viewDidAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated {

    inf(@"Lock screen will disappear");
    [self emergencyCloseAnimated:animated];

    [self.marqueeTipTimer invalidate];

    [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationSlide];
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [self.navigationController setNavigationBarHidden:NO animated:animated];
    }];

    [super viewWillDisappear:animated];
}

- (BOOL)canBecomeFirstResponder {

    return YES;
}

- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {

    if (motion == UIEventSubtypeMotionShake) {
        MPCheckpoint( MPCheckpointEmergencyGenerator, nil );
        [[self.view findFirstResponderInHierarchy] resignFirstResponder];

        self.emergencyGeneratorContainer.alpha = 0;
        self.emergencyGeneratorContainer.hidden = NO;
        self.emergencyGeneratorContainer.frame = CGRectSetX( self.emergencyGeneratorContainer.frame,
                self.emergencyGeneratorContainer.frame.origin.x - 100 );
        [UIView animateWithDuration:0.3 animations:^{
            self.emergencyGeneratorContainer.frame = CGRectSetX( self.emergencyGeneratorContainer.frame,
                    self.emergencyGeneratorContainer.frame.origin.x + 150 );
            self.emergencyGeneratorContainer.alpha = 1;
        }                completion:^(BOOL finished) {
            if (!finished)
                return;

            [self.emergencyName becomeFirstResponder];
            [UIView animateWithDuration:0.2 animations:^{
                self.emergencyGeneratorContainer.frame = CGRectSetX( self.emergencyGeneratorContainer.frame,
                        self.emergencyGeneratorContainer.frame.origin.x - 50 );
            }];
        }];
    }
}

- (void)marqueeTip {

    [UIView animateWithDuration:0.5 animations:^{
        self.tip.alpha = 0;
    }                completion:^(BOOL finished) {
        if (!finished)
            return;

        self.tip.text = self.marqueeTipTexts[++self.marqueeTipTextIndex % [self.marqueeTipTexts count]];
        [UIView animateWithDuration:0.5 animations:^{
            self.tip.alpha = 1;
        }];
    }];
}

- (void)updateUsers {

    [MPiOSAppDelegate managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) {
        NSError *error = nil;
        NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPUserEntity class] )];
        fetchRequest.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO] ];
        NSArray *users = [context executeFetchRequest:fetchRequest error:&error];
        if (!users)
        err(@"Failed to load users: %@", error);

        // Clean up avatars.
        for (UIView *subview in [self.avatarsView subviews])
            if ([[self.avatarToUserOID allKeys] containsObject:[NSValue valueWithNonretainedObject:subview]])
                    // This subview is a former avatar.
                [subview removeFromSuperview];
        [self.avatarToUserOID removeAllObjects];

        // Create avatars.
        for (MPUserEntity *user in users)
            [self setupAvatar:[self.avatarTemplate clone] forUser:user];
        [self setupAvatar:[self.avatarTemplate clone] forUser:nil];
    }];

    // Scroll view's content changed, update its content size.
    [self.avatarsView autoSizeContentIgnoreHidden:YES ignoreInvisible:YES limitPadding:NO ignoreSubviews:nil];

    [self updateLayoutAnimated:YES allowScroll:YES completion:nil];
}

- (UIButton *)setupAvatar:(UIButton *)avatar forUser:(MPUserEntity *)user {

    avatar.center = CGPointMake( avatar.center.x + [self.avatarToUserOID count] * 160, avatar.center.y );
    avatar.hidden = NO;
    avatar.layer.cornerRadius = avatar.bounds.size.height / 2;
    avatar.layer.shadowColor = [UIColor blackColor].CGColor;
    avatar.layer.shadowOpacity = 1;
    avatar.layer.shadowRadius = 20;
    avatar.layer.masksToBounds = NO;
    avatar.backgroundColor = [UIColor clearColor];
    avatar.tag = user.avatar;

    [avatar setBackgroundImage:[UIImage imageNamed:PearlString( @"avatar-%u", user.avatar )]
                      forState:UIControlStateNormal];
    [avatar setSelectionInSuperviewCandidate:YES isClearable:YES];
    [avatar onHighlightOrSelect:^(BOOL highlighted, BOOL selected) {
        if (highlighted || selected)
            avatar.backgroundColor = self.avatarTemplate.backgroundColor;
        else
            avatar.backgroundColor = [UIColor clearColor];
    }                   options:0];
    [avatar onSelect:^(BOOL selected) {
        if (!selected) {
            self.selectedUser = nil;
            [self didToggleUserSelection];
        }
        else if ((self.selectedUser = user))
            [self didToggleUserSelection];
        else
            [self didSelectNewUserAvatar:avatar];
    }        options:0];

    [self.avatarToUserOID setObject:NilToNSNull([user objectID]) forKey:[NSValue valueWithNonretainedObject:avatar]];

    if ([_selectedUserOID isEqual:[user objectID]])
        avatar.selected = YES;

    return avatar;
}

- (void)didToggleUserSelection {

    NSAssert([[NSThread currentThread] isMainThread], @"User selection should only be toggled from the main thread.");

    NSManagedObjectContext *context = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady];
    MPUserEntity *selectedUser = [self selectedUserInContext:context];
    if (!selectedUser)
        [self.passwordField resignFirstResponder];
    else if ([[MPiOSAppDelegate get] signInAsUser:selectedUser saveInContext:context usingMasterPassword:nil]) {
        [self performSegueWithIdentifier:@"MP_Unlock" sender:self];
        return;
    }

    [self updateLayoutAnimated:YES allowScroll:YES completion:^(BOOL finished) {
        if ([self selectedUserForThread])
            [self.passwordField becomeFirstResponder];
    }];
}

- (void)didSelectNewUserAvatar:(UIButton *)newUserAvatar {

    if (![MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
        MPUserEntity *newUser = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass( [MPUserEntity class] )
                                                              inManagedObjectContext:context];

        [self showNewUserNameAlertFor:newUser saveInContext:context completion:^(BOOL finished) {
            newUserAvatar.selected = NO;
        }];
    }])
        newUserAvatar.selected = NO;
}

- (void)showNewUserNameAlertFor:(MPUserEntity *)newUser saveInContext:(NSManagedObjectContext *)context
                     completion:(void (^)(BOOL finished))completion {

    [PearlAlert showAlertWithTitle:@"Enter Your Name"
                           message:nil viewStyle:UIAlertViewStylePlainTextInput
                         initAlert:^(UIAlertView *alert, UITextField *firstField) {
                             firstField.autocapitalizationType = UITextAutocapitalizationTypeWords;
                             firstField.keyboardType = UIKeyboardTypeAlphabet;
                             firstField.text = newUser.name;
                             firstField.placeholder = @"eg. Robert Lee Mitchell";
                             firstField.enablesReturnKeyAutomatically = YES;
                         }
                 tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
                     if (buttonIndex == [alert cancelButtonIndex]) {
                         completion( NO );
                         return;
                     }
                     NSString *name = [alert textFieldAtIndex:0].text;
                     if (!name.length) {
                         [PearlAlert showAlertWithTitle:@"Name Is Required" message:nil viewStyle:UIAlertViewStyleDefault initAlert:nil
                                      tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
                                          [self showNewUserNameAlertFor:newUser saveInContext:context completion:completion];
                                      }     cancelTitle:@"Try Again" otherTitles:nil];
                         return;
                     }

                     // Save
                     [context performBlockAndWait:^{
                         newUser.name = name;
                     }];
                     [self showNewUserAvatarAlertFor:newUser saveInContext:context completion:completion];
                 }
                       cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonSave, nil];
}

- (void)showNewUserAvatarAlertFor:(MPUserEntity *)newUser saveInContext:(NSManagedObjectContext *)context
                       completion:(void (^)(BOOL finished))completion {

    [PearlAlert showAlertWithTitle:@"Choose Your Avatar"
                           message:@"\n\n\n\n\n\n" viewStyle:UIAlertViewStyleDefault
                         initAlert:^(UIAlertView *_alert, UITextField *_firstField) {
                             [self initializeAvatarAlert:_alert forUser:newUser inContext:context];
                         }
                 tappedButtonBlock:^(UIAlertView *_alert, NSInteger _buttonIndex) {

                     // Okay
                     [self showNewUserConfirmationAlertFor:newUser saveInContext:context completion:completion];
                 }     cancelTitle:nil otherTitles:[PearlStrings get].commonButtonOkay, nil];
}

- (void)showNewUserConfirmationAlertFor:(MPUserEntity *)newUser saveInContext:(NSManagedObjectContext *)context
                             completion:(void (^)(BOOL finished))completion {

    [PearlAlert showAlertWithTitle:@"Is this correct?"
                           message:
                                   @"Please double-check your name.\n"
                                           @"\n\n\n\n\n\n"
                         viewStyle:UIAlertViewStyleDefault
                         initAlert:^void(UIAlertView *__alert, UITextField *__firstField) {
                             [self initializeConfirmationAlert:__alert forUser:newUser];
                         }
                 tappedButtonBlock:^void(UIAlertView *__alert, NSInteger __buttonIndex) {
                     if (__buttonIndex == [__alert cancelButtonIndex]) {
                         [self showNewUserNameAlertFor:newUser saveInContext:context completion:completion];
                         return;
                     }

                     // Confirm
                     [context performBlockAndWait:^{
                         [context saveToStore];
                         NSError *error = nil;
                         if (![context obtainPermanentIDsForObjects:@[ newUser ] error:&error])
                         err(@"Failed to obtain permanent object ID for new user: %@", error);
                         self.selectedUser = newUser;
                     }];
                     completion( YES );

                     [self updateUsers];
                 }
                       cancelTitle:@"Change" otherTitles:@"Confirm", nil];
}

- (void)updateLayoutAnimated:(BOOL)animated allowScroll:(BOOL)allowScroll completion:(void (^)(BOOL finished))completion {

    if (animated) {
        self.oldNameLabel.text = self.nameLabel.text;
        self.oldNameLabel.alpha = 1;
        self.nameLabel.alpha = 0;

        [UIView animateWithDuration:0.5f animations:^{
            [self updateLayoutAnimated:NO allowScroll:allowScroll completion:nil];

            self.oldNameLabel.alpha = 0;
            self.nameLabel.alpha = 1;
        }                completion:^(BOOL finished) {
            if (completion)
                completion( finished );
        }];
        return;
    }

    // Lay out password entry and user selection views.
    MPUserEntity *selectedUser = [self selectedUserForThread];
    if (selectedUser && !self.passwordView.alpha) {
        // User was just selected.
        self.passwordView.alpha = 1;
        self.avatarsView.frame = CGRectSetY( self.avatarsView.frame, 16 );
        self.avatarsView.scrollEnabled = NO;
        self.nameLabel.center = CGPointMake( 160, 94 );
        self.nameLabel.backgroundColor = [UIColor blackColor];
        self.oldNameLabel.center = self.nameLabel.center;
        self.avatarShadowColor = [UIColor whiteColor];
    }
    else if (!selectedUser && self.passwordView.alpha == 1) {
        // User was just deselected.
        self.passwordField.text = nil;
        self.passwordView.alpha = 0;
        self.avatarsView.frame = CGRectSetY( self.avatarsView.frame, 140 );
        self.avatarsView.scrollEnabled = YES;
        self.nameLabel.center = CGPointMake( 160, 296 );
        self.nameLabel.backgroundColor = [UIColor clearColor];
        self.oldNameLabel.center = self.nameLabel.center;
        self.avatarShadowColor = [UIColor lightGrayColor];
    }

    // Lay out the word wall.
    if (!selectedUser || selectedUser.keyID) {
        self.passwordFieldLabel.text = @"Enter your master password:";

        self.wordWall.alpha = 0;
        self.createPasswordTipView.alpha = 0;
        self.wordWallAnimating = NO;
    }
    else {
        self.passwordFieldLabel.text = @"Create your master password:";

        if (!self.wordWallAnimating) {
            self.wordWallAnimating = YES;
            self.wordWall.alpha = 1;
            self.createPasswordTipView.alpha = 1;

            dispatch_async( dispatch_get_main_queue(), ^{
                // Jump out of our UIView animation block.
                [self beginWordWallAnimation];
            } );
            dispatch_after( dispatch_time( DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC ), dispatch_get_main_queue(), ^{
                [UIView animateWithDuration:1 animations:^{
                    self.createPasswordTipView.alpha = 0;
                }];
            } );
        }
    }

    // Lay out user targeting.
    UIButton *selectedAvatar = [self avatarForUser:selectedUser];
    MPUserEntity *targetedUser = selectedUser;
    UIButton *targetedAvatar = selectedAvatar;
    if (!targetedAvatar) {
        targetedAvatar = [self findTargetedAvatar];
        targetedUser = [self userForAvatar:targetedAvatar inContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
    }

    [self.avatarsView enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
        if (![[self.avatarToUserOID allKeys] containsObject:[NSValue valueWithNonretainedObject:subview]])
                // This subview is not one of the user avatars.
            return;
        UIButton *avatar = (UIButton *)subview;

        BOOL isTargeted = avatar == targetedAvatar;

        avatar.userInteractionEnabled = isTargeted;
        avatar.alpha = isTargeted? 1: [self selectedUserForThread]? 0.1: 0.4;

        [self updateAvatarShadowColor:avatar isTargeted:isTargeted];
    }                           recurse:NO];

    if (allowScroll) {
        CGPoint targetContentOffset = CGPointMake(
                MAX(0, targetedAvatar.center.x - self.avatarsView.bounds.size.width / 2),
                self.avatarsView.contentOffset.y );
        if (!CGPointEqualToPoint( self.avatarsView.contentOffset, targetContentOffset ))
            [self.avatarsView setContentOffset:targetContentOffset animated:animated];
    }

    // Lay out user name label.
    self.nameLabel.text = targetedAvatar? (targetedUser? targetedUser.name: @"New User"): nil;
    self.nameLabel.bounds = CGRectSetHeight( self.nameLabel.bounds,
            [self.nameLabel.text sizeWithFont:self.nameLabel.font
                            constrainedToSize:CGSizeMake( self.nameLabel.bounds.size.width - 10, 100 )
                                lineBreakMode:self.nameLabel.lineBreakMode].height );
    self.oldNameLabel.bounds = self.nameLabel.bounds;
    if (completion)
        completion( YES );
}

- (void)beginWordWallAnimation {

    [self.wordWall enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
        UILabel *wordLabel = (UILabel *)subview;

        if (wordLabel.frame.origin.x < -self.wordWall.frame.size.width / 3) {
            wordLabel.frame = CGRectSetX( wordLabel.frame, wordLabel.frame.origin.x + self.wordWall.frame.size.width );
            [self initializeWordLabel:wordLabel];
        }
    }                        recurse:NO];

    if (self.wordWallAnimating)
        [UIView animateWithDuration:15 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
            [self.wordWall enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
                UILabel *wordLabel = (UILabel *)subview;

                wordLabel.frame = CGRectSetX( wordLabel.frame, wordLabel.frame.origin.x - self.wordWall.frame.size.width / 3 );
            }                        recurse:NO];
        }                completion:^(BOOL finished) {
            if (finished)
                [self beginWordWallAnimation];
        }];
}

- (void)initializeWordLabel:(UILabel *)wordLabel {

    wordLabel.alpha = 0.05 + (random() % 35) / 100.0F;
    wordLabel.text = [self.wordList objectAtIndex:(NSUInteger)random() % [self.wordList count]];
}

- (void)setPasswordTip:(NSString *)string {

    if (string.length)
        self.passwordTipLabel.text = string;

    [UIView animateWithDuration:0.3f animations:^{
        self.passwordTipView.alpha = string.length? 1: 0;
    }];
}

- (void)tryMasterPassword {

    if (![self selectedUserForThread])
            // No user selected, can't try sign-in.
        return;

    [self setSpinnerActive:YES];

    [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) {
        BOOL unlocked = [[MPiOSAppDelegate get] signInAsUser:[self selectedUserInContext:moc] saveInContext:moc
                                         usingMasterPassword:self.passwordField.text];

        dispatch_async( dispatch_get_main_queue(), ^{
            if (unlocked)
                [self performSegueWithIdentifier:@"MP_Unlock" sender:self];

            else {
                if (self.passwordField.text.length)
                    [self setPasswordTip:@"Incorrect password."];

                [self setSpinnerActive:NO];
            }
        } );
    }];
}

- (UIButton *)findTargetedAvatar {

    CGFloat xOfMiddle = self.avatarsView.contentOffset.x + self.avatarsView.bounds.size.width / 2;
    return (UIButton *)[PearlUIUtils viewClosestTo:CGPointMake( xOfMiddle, self.avatarsView.contentOffset.y )
                                           ofArray:self.avatarsView.subviews];
}

- (UIButton *)avatarForUser:(MPUserEntity *)user {

    NSManagedObjectID *userOID = [user objectID];
    __block UIButton *avatar = nil;
    if (userOID)
        [self.avatarToUserOID enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
            if ([obj isEqual:userOID])
                avatar = [key nonretainedObjectValue];
        }];

    return avatar;
}

- (MPUserEntity *)userForAvatar:(UIButton *)avatar inContext:(NSManagedObjectContext *)context {

    NSManagedObjectID *userOID = NSNullToNil([self.avatarToUserOID objectForKey:[NSValue valueWithNonretainedObject:avatar]]);
    if (!userOID)
        return nil;

    NSError *error;
    MPUserEntity *user = (MPUserEntity *)[context existingObjectWithID:userOID error:&error];
    if (!user)
    err(@"Failed retrieving user for avatar: %@", error);

    return user;
}

- (void)setSpinnerActive:(BOOL)active {

    PearlMainThread(^{
        CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
        rotate.toValue = [NSNumber numberWithDouble:2 * M_PI];
        rotate.duration = 5.0;

        if (active) {
            rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
            rotate.fromValue = [NSNumber numberWithFloat:0];
            rotate.repeatCount = MAXFLOAT;
        }
        else {
            rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
            rotate.repeatCount = 1;
        }

        [self.spinner.layer removeAnimationForKey:@"rotation"];
        [self.spinner.layer addAnimation:rotate forKey:@"rotation"];

        [UIView animateWithDuration:0.3f animations:^{
            self.spinner.alpha = active? 1: 0;

            if (active)
                [self avatarForUser:[self selectedUserForThread]].backgroundColor = [UIColor clearColor];
            else
                [self avatarForUser:[self selectedUserForThread]].backgroundColor = self.avatarTemplate.backgroundColor;
        }];
    });
}

- (void)updateAvatarShadowColor:(UIButton *)avatar isTargeted:(BOOL)targeted {

    if (targeted) {
        if (![avatar.layer animationForKey:@"targetedShadow"]) {
            CABasicAnimation *toShadowColorAnimation = [CABasicAnimation animationWithKeyPath:@"shadowColor"];
            toShadowColorAnimation.toValue = (__bridge id)(avatar.selected? self.avatarTemplate.backgroundColor
                                                           : [UIColor whiteColor]).CGColor;
            toShadowColorAnimation.beginTime = 0.0f;
            toShadowColorAnimation.duration = 0.5f;
            toShadowColorAnimation.fillMode = kCAFillModeForwards;

            CABasicAnimation *toShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
            toShadowOpacityAnimation.toValue = PearlFloat(0.2);
            toShadowOpacityAnimation.duration = 0.5f;

            CABasicAnimation *pulseShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
            pulseShadowOpacityAnimation.fromValue = PearlFloat(0.2);
            pulseShadowOpacityAnimation.toValue = PearlFloat(0.6);
            pulseShadowOpacityAnimation.beginTime = 0.5f;
            pulseShadowOpacityAnimation.duration = 2.0f;
            pulseShadowOpacityAnimation.autoreverses = YES;
            pulseShadowOpacityAnimation.repeatCount = MAXFLOAT;

            CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
            group.animations = @[ toShadowColorAnimation, toShadowOpacityAnimation, pulseShadowOpacityAnimation ];
            group.duration = MAXFLOAT;

            [avatar.layer removeAnimationForKey:@"inactiveShadow"];
            [avatar.layer addAnimation:group forKey:@"targetedShadow"];
        }
    }
    else {
        if ([avatar.layer animationForKey:@"targetedShadow"]) {
            CABasicAnimation *toShadowColorAnimation = [CABasicAnimation animationWithKeyPath:@"shadowColor"];
            toShadowColorAnimation.toValue = (__bridge id)[UIColor blackColor].CGColor;
            toShadowColorAnimation.duration = 0.5f;

            CABasicAnimation *toShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
            toShadowOpacityAnimation.toValue = PearlFloat(1);
            toShadowOpacityAnimation.duration = 0.5f;

            CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
            group.animations = @[ toShadowColorAnimation, toShadowOpacityAnimation ];
            group.duration = 0.5f;

            [avatar.layer removeAnimationForKey:@"targetedShadow"];
            [avatar.layer addAnimation:group forKey:@"inactiveShadow"];
        }
    }
}

#pragma mark - UITextFieldDelegate

- (void)textFieldDidBeginEditing:(UITextField *)textField {

    [self setPasswordTip:nil];
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField {

    [textField resignFirstResponder];

    if (textField == self.emergencyName) {
        if (![self.emergencyMasterPassword.text length])
            [self.emergencyMasterPassword becomeFirstResponder];
    }

    else if (textField == self.emergencyMasterPassword) {
        if (![self.emergencySite.text length])
            [self.emergencySite becomeFirstResponder];
    }

    else if (textField == self.passwordField) {
        [self setSpinnerActive:YES];

        if ([self selectedUserForThread].keyID)
            [self tryMasterPassword];

        else
            [PearlAlert showAlertWithTitle:@"New Master Password"
                                   message:@"Please confirm the spelling of this new master password."
                                 viewStyle:UIAlertViewStyleSecureTextInput
                                 initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
                [self setSpinnerActive:NO];

                if (buttonIndex == [alert cancelButtonIndex])
                    return;

                if (![[alert textFieldAtIndex:0].text isEqualToString:textField.text]) {
                    [PearlAlert showAlertWithTitle:@"Incorrect Master Password"
                                           message:
                                                   @"The password you entered doesn't match with the master password you tried to use.  "
                                                           @"You've probably mistyped one of them.\n\n"
                                                           @"Give it another try."
                                         viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:nil
                                       cancelTitle:[PearlStrings get].commonButtonOkay otherTitles:nil];
                    return;
                }

                [self tryMasterPassword];
            }
                               cancelTitle:[PearlStrings get].commonButtonCancel
                               otherTitles:[PearlStrings get].commonButtonContinue, nil];
    }

    return YES;
}

- (void)textFieldDidEndEditing:(UITextField *)textField {

    if (textField == self.emergencyName || textField == self.emergencyMasterPassword)
        [self updateEmergencyKey];

    if (textField == self.emergencySite)
        [self updateEmergencyPassword];
}

- (void)updateEmergencyKey {

    if (![self.emergencyMasterPassword.text length] || ![self.emergencyName.text length])
        return;

    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [self.emergencyActivity startAnimating];
        [self.emergencyPassword setTitle:@"" forState:UIControlStateNormal];

        NSString *masterPassword = self.emergencyMasterPassword.text;
        NSString *userName = self.emergencyName.text;

        [self.emergencyQueue addOperationWithBlock:^{
            self.emergencyKey = [MPAlgorithmDefault keyForPassword:masterPassword ofUserNamed:userName];

            [self updateEmergencyPassword];
        }];
    }];
}

- (MPElementType)emergencyType {

    switch (self.emergencyTypeControl.selectedSegmentIndex) {
        case 0:
            return MPElementTypeGeneratedMaximum;
        case 1:
            return MPElementTypeGeneratedLong;
        case 2:
            return MPElementTypeGeneratedMedium;
        case 3:
            return MPElementTypeGeneratedBasic;
        case 4:
            return MPElementTypeGeneratedShort;
        case 5:
            return MPElementTypeGeneratedPIN;
        default:
            Throw(@"Unsupported type index: %d", self.emergencyTypeControl.selectedSegmentIndex);
    }
}

- (void)updateEmergencyPassword {

    if (!self.emergencyKey || ![self.emergencySite.text length])
        return;

    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [self.emergencyPassword setTitle:@"" forState:UIControlStateNormal];

        NSString *name = self.emergencySite.text;
        NSUInteger counter = (NSUInteger)self.emergencyCounterStepper.value;

        [self.emergencyQueue addOperationWithBlock:^{
            NSString *content = [MPAlgorithmDefault generateContentNamed:name ofType:[self emergencyType]
                                                             withCounter:counter usingKey:self.emergencyKey];

            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                [self.emergencyActivity stopAnimating];
                [self.emergencyPassword setTitle:content forState:UIControlStateNormal];
            }];
        }];
    }];
}

- (IBAction)emergencyClose:(UIButton *)sender {

    [self emergencyCloseAnimated:YES];
}

- (IBAction)emergencyCopy:(UIButton *)sender {

    inf(@"Copying emergency password for: %@", self.emergencyName.text);
    [UIPasteboard generalPasteboard].string = [self.emergencyPassword titleForState:UIControlStateNormal];

    [UIView animateWithDuration:0.3f animations:^{
        self.emergencyContentTipContainer.alpha = 1;
    }                completion:^(BOOL finished) {
        if (finished) {
            dispatch_time_t popTime = dispatch_time( DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC );
            dispatch_after( popTime, dispatch_get_main_queue(), ^(void) {
                [UIView animateWithDuration:0.2f animations:^{
                    self.emergencyContentTipContainer.alpha = 0;
                }];
            } );
        }
    }];

    MPCheckpoint( MPCheckpointCopyToPasteboard, @{
            @"type"      : [MPAlgorithmDefault nameOfType:self.emergencyType],
            @"version"   : @MPAlgorithmDefaultVersion,
            @"emergency" : @YES,
    } );
}

- (void)emergencyCloseAnimated:(BOOL)animated {

    [[self.emergencyGeneratorContainer findFirstResponderInHierarchy] resignFirstResponder];

    if (animated) {
        [UIView animateWithDuration:0.2 animations:^{
            self.emergencyGeneratorContainer.alpha = 0;
        }                completion:^(BOOL finished) {
            [self emergencyCloseAnimated:NO];
        }];
        return;
    }

    self.emergencyName.text = @"";
    self.emergencyMasterPassword.text = @"";
    self.emergencySite.text = @"";
    self.emergencyCounterStepper.value = 0;
    [self.emergencyPassword setTitle:@"" forState:UIControlStateNormal];
    [self.emergencyActivity stopAnimating];
    self.emergencyGeneratorContainer.alpha = 0;
    self.emergencyGeneratorContainer.hidden = YES;
}

#pragma mark - UIScrollViewDelegate

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity
              targetContentOffset:(inout CGPoint *)targetContentOffset {

    CGFloat xOfMiddle = targetContentOffset->x + scrollView.bounds.size.width / 2;
    UIButton *middleAvatar = (UIButton *)[PearlUIUtils viewClosestTo:CGPointMake( xOfMiddle, targetContentOffset->y )
                                                             ofArray:scrollView.subviews];
    *targetContentOffset = CGPointMake( middleAvatar.center.x - scrollView.bounds.size.width / 2, targetContentOffset->y );

    [self updateLayoutAnimated:NO allowScroll:NO completion:nil];
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {

    [self updateLayoutAnimated:YES allowScroll:YES completion:nil];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

    [self updateLayoutAnimated:NO allowScroll:NO completion:nil];
}

#pragma mark - UIWebViewDelegate

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
 navigationType:(UIWebViewNavigationType)navigationType {

    if (navigationType == UIWebViewNavigationTypeLinkClicked) {
        [[UIApplication sharedApplication] openURL:[request URL]];
        return NO;
    }

    return YES;
}

#pragma mark - IBActions

- (IBAction)targetedUserAction:(UILongPressGestureRecognizer *)sender {

    if (sender.state != UIGestureRecognizerStateBegan)
        return;

    if ([self selectedUserForThread])
        return;

    [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
        MPUserEntity *targetedUser = [self userForAvatar:[self findTargetedAvatar] inContext:context];
        if (!targetedUser)
            return;

        [PearlSheet showSheetWithTitle:targetedUser.name
                             viewStyle:UIActionSheetStyleBlackTranslucent
                             initSheet:nil tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) {
            if (buttonIndex == [sheet cancelButtonIndex])
                return;

            if (buttonIndex == [sheet destructiveButtonIndex]) {
                [context performBlock:^{
                    [context deleteObject:targetedUser];
                    [context saveToStore];

                    dispatch_async( dispatch_get_main_queue(), ^{
                        [self updateUsers];
                    } );
                }];
                return;
            }

            if (buttonIndex == [sheet firstOtherButtonIndex])
                [[MPiOSAppDelegate get] changeMasterPasswordFor:targetedUser saveInContext:context didResetBlock:^{
                    dispatch_async( dispatch_get_main_queue(), ^{
                        [[self avatarForUser:targetedUser] setSelected:YES];
                    } );
                }];
        }                  cancelTitle:[PearlStrings get].commonButtonCancel
                      destructiveTitle:@"Delete User" otherTitles:@"Reset Password", nil];
    }];
}

- (IBAction)facebook:(UIButton *)sender {

    if (![SLComposeViewController isAvailableForServiceType:SLServiceTypeFacebook]) {
        [PearlAlert showAlertWithTitle:@"Facebook Not Enabled" message:@"To send tweets, configure Facebook from Settings."
                             viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:nil cancelTitle:nil otherTitles:@"OK", nil];
        return;
    }

    SLComposeViewController *vc = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeFacebook];
    [vc setInitialText:@"I've started doing passwords properly thanks to Master Password for iOS."];
    [vc addImage:[UIImage imageNamed:@"iTunesArtwork-Rounded"]];
    [vc addURL:[NSURL URLWithString:@"http://masterpasswordapp.com"]];
    [self presentViewController:vc animated:YES completion:nil];
}

- (IBAction)twitter:(UIButton *)sender {

    if (![SLComposeViewController isAvailableForServiceType:SLServiceTypeTwitter]) {
        [PearlAlert showAlertWithTitle:@"Twitter Not Enabled" message:@"To send tweets, configure Twitter from Settings."
                             viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:nil cancelTitle:nil otherTitles:@"OK", nil];
        return;
    }

    SLComposeViewController *vc = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeTwitter];
    [vc setInitialText:@"I've started doing passwords properly thanks to Master Password."];
    [vc addImage:[UIImage imageNamed:@"iTunesArtwork-Rounded"]];
    [vc addURL:[NSURL URLWithString:@"http://masterpasswordapp.com"]];
    [self presentViewController:vc animated:YES completion:nil];
}

- (IBAction)google:(UIButton *)sender {

    id<GPPShareBuilder> shareDialog = [[GPPShare sharedInstance] shareDialog];
    [[[shareDialog setURLToShare:[NSURL URLWithString:@"http://masterpasswordapp.com"]]
            setPrefillText:@"I've started doing passwords properly thanks to Master Password."] open];
}

- (IBAction)mail:(UIButton *)sender {

    [[MPiOSAppDelegate get] showFeedbackWithLogs:NO forVC:self];
}

- (IBAction)add:(UIButton *)sender {

    [PearlSheet showSheetWithTitle:@"Follow Master Password" viewStyle:UIActionSheetStyleBlackTranslucent
                         initSheet:nil tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) {
        if (buttonIndex == [sheet cancelButtonIndex])
            return;

        if (buttonIndex == [sheet firstOtherButtonIndex]) {
            // Google+
            [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://plus.google.com/116256327773442623984/about"]];
            return;
        }
        if (buttonIndex == [sheet firstOtherButtonIndex] + 1) {
            // Facebook
            [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://www.facebook.com/masterpasswordapp"]];
            return;
        }
        if (buttonIndex == [sheet firstOtherButtonIndex] + 2) {
            // Twitter
            UIApplication *application = [UIApplication sharedApplication];
            for (NSString *candidate in @[
                    @"twitter://user?screen_name=%@", // Twitter
                    @"tweetbot:///user_profile/%@", // TweetBot
                    @"echofon:///user_timeline?%@", // Echofon
                    @"twit:///user?screen_name=%@", // Twittelator Pro
                    @"x-seesmic://twitter_profile?twitter_screen_name=%@", // Seesmic
                    @"x-birdfeed://user?screen_name=%@", // Birdfeed
                    @"tweetings:///user?screen_name=%@", // Tweetings
                    @"simplytweet:?link=http://twitter.com/%@", // SimplyTweet
                    @"icebird://user?screen_name=%@", // IceBird
                    @"fluttr://user/%@", // Fluttr
                    @"http://twitter.com/%@"
            ]) {
                NSURL *url = [NSURL URLWithString:PearlString( candidate, @"master_password" )];

                if ([application canOpenURL:url]) {
                    [application openURL:url];
                    break;
                }
            }
            return;
        }
        if (buttonIndex == [sheet firstOtherButtonIndex] + 3) {
            // Mailing List
            [PearlEMail sendEMailTo:@"masterpassword-join@lists.lyndir.com" subject:@"Subscribe"
                               body:@"Press 'Send' now to subscribe to the Master Password mailing list.\n\n"
                                       @"You'll be kept up-to-date on the evolution of and discussions revolving Master Password."];
            return;
        }
        if (buttonIndex == [sheet firstOtherButtonIndex] + 4) {
            // GitHub
            [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/Lyndir/MasterPassword"]];
            return;
        }
    }                  cancelTitle:[PearlStrings get].commonButtonCancel
                  destructiveTitle:nil otherTitles:@"Google+", @"Facebook", @"Twitter", @"Mailing List", @"GitHub", nil];
}

#pragma mark - Core Data

- (MPUserEntity *)selectedUserForThread {

    return [self selectedUserInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
}

- (MPUserEntity *)selectedUserInContext:(NSManagedObjectContext *)moc {

    if (!_selectedUserOID)
        return nil;

    NSError *error;
    MPUserEntity *selectedUser = (MPUserEntity *)[moc existingObjectWithID:_selectedUserOID error:&error];
    if (!selectedUser)
    err(@"Failed to retrieve selected user: %@", error);

    return selectedUser;
}

- (void)setSelectedUser:(MPUserEntity *)selectedUser {

    NSError *error = nil;
    if (selectedUser.objectID.isTemporaryID &&
        ![selectedUser.managedObjectContext obtainPermanentIDsForObjects:@[ selectedUser ] error:&error])
    err(@"Failed to obtain a permanent object ID after setting selected user: %@", error);

    _selectedUserOID = selectedUser.objectID;
}

@end