blockchain: register name in account settings

- Show field of blockchain server. No edition possible yet since this
feature has not been tested well enough
- Display register name window

Change-Id: I2b19ce83e40de9a6cda0e0ec601516b75c01702b
Tuleap: #1158
diff --git a/src/AccRingVC.mm b/src/AccRingVC.mm
index 4b9dc1d..1705e57 100644
--- a/src/AccRingVC.mm
+++ b/src/AccRingVC.mm
@@ -21,26 +21,34 @@
 #import <accountmodel.h>
 #import <qitemselectionmodel.h>
 
-@interface AccRingVC ()
+#import "RegisterNameWC.h"
 
-@property (assign) IBOutlet NSTextField *aliasTextField;
-@property (assign) IBOutlet NSTextField *bootstrapField;
-@property (assign) IBOutlet NSTextField *hashField;
+@interface AccRingVC () <RegisterNameDelegate>
 
-@property (assign) IBOutlet NSButton *upnpButton;
-@property (assign) IBOutlet NSButton *autoAnswerButton;
-@property (assign) IBOutlet NSButton *userAgentButton;
-@property (assign) IBOutlet NSTextField *userAgentTextField;
+@property (unsafe_unretained) IBOutlet NSTextField *aliasTextField;
+@property (unsafe_unretained) IBOutlet NSTextField *bootstrapField;
+@property (unsafe_unretained) IBOutlet NSTextField *blockchainField;
+@property (unsafe_unretained) IBOutlet NSTextField *ringIDField;
+@property (unsafe_unretained) IBOutlet NSButton *registerBlockchainNameButton;
+@property (unsafe_unretained) IBOutlet NSTextField *registeredNameField;
+
+@property (unsafe_unretained) IBOutlet NSButton *upnpButton;
+@property (unsafe_unretained) IBOutlet NSButton *autoAnswerButton;
+@property (unsafe_unretained) IBOutlet NSButton *userAgentButton;
+@property (unsafe_unretained) IBOutlet NSTextField *userAgentTextField;
 @property (unsafe_unretained) IBOutlet NSButton *allowUnknown;
 @property (unsafe_unretained) IBOutlet NSButton *allowHistory;
 @property (unsafe_unretained) IBOutlet NSButton *allowContacts;
 
+@property AbstractLoadingWC* accountModal;
+
 @end
 
 @implementation AccRingVC
 @synthesize bootstrapField;
-@synthesize hashField;
+@synthesize ringIDField;
 @synthesize aliasTextField;
+@synthesize blockchainField;
 @synthesize upnpButton;
 @synthesize autoAnswerButton;
 @synthesize userAgentButton;
@@ -51,6 +59,7 @@
     ALIAS = 0,
     HOSTNAME,
     USERAGENT,
+    BLOCKCHAIN,
 };
 
 - (void)awakeFromNib
@@ -59,6 +68,7 @@
     [aliasTextField setTag:TagViews::ALIAS];
     [userAgentTextField setTag:TagViews::USERAGENT];
     [bootstrapField setTag:TagViews::HOSTNAME];
+    [blockchainField setTag:TagViews::BLOCKCHAIN];
 
     QObject::connect(AccountModel::instance().selectionModel(),
                      &QItemSelectionModel::currentChanged,
@@ -90,13 +100,37 @@
     [userAgentTextField setStringValue:account->userAgent().toNSString()];
 
     [bootstrapField setStringValue:account->hostname().toNSString()];
+    [blockchainField setStringValue:account->nameServiceURL().toNSString()];
 
-    if([account->username().toNSString() isEqualToString:@""])
-        [hashField setStringValue:NSLocalizedString(@"Reopen account to see your hash",
+    if([account->username().toNSString() isEqualToString:@""]) {
+        [ringIDField setStringValue:NSLocalizedString(@"Reopen account to see your hash",
                                                     @"Show advice to user")];
-    else
-        [hashField setStringValue:account->username().toNSString()];
+    } else {
+        [ringIDField setStringValue:account->username().toNSString()];
+    }
 
+    [self refreshRegisteredName:account];
+}
+
+- (void) refreshRegisteredName:(Account*) account
+{
+    [self.registerBlockchainNameButton setHidden:!account->registeredName().isEmpty()];
+    [self.registeredNameField setStringValue:account->registeredName().toNSString()];
+}
+
+- (IBAction)startNameRegistration:(id)sender
+{
+    auto registerWC = [[RegisterNameWC alloc] initWithDelegate:self];
+#if MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_9
+    [self.view.window beginSheet:registerWC.window completionHandler:nil];
+#else
+    [NSApp beginSheet: registerWC.window
+       modalForWindow: self.view.window
+        modalDelegate: self
+       didEndSelector: nil
+          contextInfo: nil];
+#endif
+    self.accountModal = registerWC;
 }
 
 - (IBAction)toggleUpnp:(NSButton *)sender {
@@ -146,9 +180,18 @@
         case TagViews::USERAGENT:
             AccountModel::instance().selectedAccount()->setUserAgent([[textField stringValue] UTF8String]);
             break;
+        case TagViews::BLOCKCHAIN:
+            AccountModel::instance().selectedAccount()->setNameServiceURL([[textField stringValue] UTF8String]);
+            break;
         default:
             break;
     }
 }
 
+- (void) didRegisterNameWithSuccess
+{
+    [self.accountModal close];
+    [self refreshRegisteredName:AccountModel::instance().selectedAccount()];
+}
+
 @end
diff --git a/src/RegisterNameWC.h b/src/RegisterNameWC.h
new file mode 100644
index 0000000..ae54e29
--- /dev/null
+++ b/src/RegisterNameWC.h
@@ -0,0 +1,55 @@
+/*
+ *  Copyright (C) 2016 Savoir-faire Linux Inc.
+ *  Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#import <Cocoa/Cocoa.h>
+
+#import "AbstractLoadingWC.h"
+#import "LoadingWCDelegate.h"
+
+@protocol RegisterNameDelegate <LoadingWCDelegate>
+
+@optional
+
+- (void) didRegisterNameWithSuccess;
+
+@end
+
+@interface RegisterNameWC : AbstractLoadingWC
+
+- (id)initWithDelegate:(id <LoadingWCDelegate>) del;
+
+@property (nonatomic, weak) NSWindowController <RegisterNameDelegate>* delegate;
+
+/**
+ * KVO with the registeredNameField
+ */
+@property (nonatomic, weak)NSString* registeredName;
+
+/**
+ * KVO with the passwordField
+ */
+@property (nonatomic, weak)NSString* password;
+
+/**
+ * KVO validators for the UI
+ */
+@property (readonly)BOOL isPasswordValid;
+@property (assign)BOOL isUserNameAvailable;
+
+@end
diff --git a/src/RegisterNameWC.mm b/src/RegisterNameWC.mm
new file mode 100644
index 0000000..063018f
--- /dev/null
+++ b/src/RegisterNameWC.mm
@@ -0,0 +1,248 @@
+/*
+ *  Copyright (C) 2016 Savoir-faire Linux Inc.
+ *  Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#import "RegisterNameWC.h"
+
+
+//Cocoa
+#import <AddressBook/AddressBook.h>
+
+//LRC
+#import <accountmodel.h>
+#import <QItemSelectionModel>
+#import <account.h>
+
+#import "AppDelegate.h"
+
+@interface RegisterNameWC ()
+@end
+
+@implementation RegisterNameWC
+{
+    __unsafe_unretained IBOutlet NSTextField* registeredNameField;
+    __unsafe_unretained IBOutlet NSSecureTextField* passwordField;
+    __unsafe_unretained IBOutlet NSImageView* ivLookupResult;
+    __unsafe_unretained IBOutlet NSProgressIndicator* indicatorLookupResult;
+
+    __unsafe_unretained IBOutlet NSProgressIndicator *registrationProgress;
+
+    QMetaObject::Connection registrationEnded;
+    QMetaObject::Connection registeredNameFound;
+
+    BOOL lookupQueued;
+    NSString* usernameWaitingForLookupResult;
+}
+
+NSInteger const BLOCKCHAIN_NAME_TAG             = 2;
+
+- (id)initWithDelegate:(id <LoadingWCDelegate>) del
+{
+    return [self initWithDelegate:del actionCode:0];
+}
+
+- (id)initWithDelegate:(id <RegisterNameDelegate>) del actionCode:(NSInteger) code
+{
+    return [super initWithWindowNibName:@"RegisterNameWindow" delegate:del actionCode:code];
+}
+
+- (void)windowDidLoad
+{
+    [super windowDidLoad];
+    [registeredNameField setTag:BLOCKCHAIN_NAME_TAG];
+    self.registeredName = @"";
+    [ivLookupResult setHidden:YES];
+    [indicatorLookupResult setHidden:YES];
+}
+
+#pragma mark - Input validation
+
+- (BOOL)isPasswordValid
+{
+    return self.password.length >= 6;
+}
+
+#pragma mark - Username validation delegate methods
+
+- (BOOL)userNameAvailable
+{
+    return (self.registeredName.length > 0 && self.isUserNameAvailable);
+}
+
+- (void)showLookUpAvailable:(BOOL)available andText:(NSString *)message
+{
+    [ivLookupResult setImage:[NSImage imageNamed:(available?@"ic_action_accept":@"ic_action_cancel")]] ;
+    [ivLookupResult setHidden:NO];
+    [ivLookupResult setToolTip:message];
+}
+
+- (void)onUsernameAvailabilityChangedWithNewAvailability:(BOOL)newAvailability
+{
+    self.isUserNameAvailable = newAvailability;
+}
+
+- (void)hideLookupSpinner
+{
+    [indicatorLookupResult setHidden:YES];
+}
+
+- (void)showLookupSpinner
+{
+    [ivLookupResult setHidden:YES];
+    [indicatorLookupResult setHidden:NO];
+    [indicatorLookupResult startAnimation:nil];
+}
+
+- (BOOL)lookupUserName
+{
+    [self showLookupSpinner];
+    QObject::disconnect(registeredNameFound);
+    registeredNameFound = QObject::connect(
+                                           &NameDirectory::instance(),
+                                           &NameDirectory::registeredNameFound,
+                                           [=] ( const Account* account, NameDirectory::LookupStatus status,  const QString& address, const QString& name) {
+                                               NSLog(@"Name lookup ended");
+                                               lookupQueued = NO;
+                                               //If this is the username we are waiting for, we can disconnect.
+                                               if (name.compare(QString::fromNSString(usernameWaitingForLookupResult)) == 0) {
+                                                   QObject::disconnect(registeredNameFound);
+                                               } else {
+                                                   //Keep waiting...
+                                                   return;
+                                               }
+
+                                               //We may now stop the spinner
+                                               [self hideLookupSpinner];
+
+                                               BOOL isAvailable = NO;
+                                               NSString* message;
+                                               switch(status)
+                                               {
+                                                   case NameDirectory::LookupStatus::SUCCESS:
+                                                   {
+                                                       message = NSLocalizedString(@"The entered username is not available",
+                                                                                   @"Text shown to user when his username is already registered");
+                                                       isAvailable = NO;
+                                                       break;
+                                                   }
+                                                   case NameDirectory::LookupStatus::NOT_FOUND:
+                                                   {
+                                                       message = NSLocalizedString(@"The entered username is available",
+                                                                                   @"Text shown to user when his username is available to be registered");
+                                                       isAvailable = YES;
+                                                       break;
+                                                   }
+                                                   case NameDirectory::LookupStatus::INVALID_NAME:
+                                                   {
+                                                       message = NSLocalizedString(@"The entered username is invalid. It must have at leat 3 characters and contains only lowercase alphanumeric characters.",
+                                                                                   @"Text shown to user when his username is invalid to be registered");
+                                                       isAvailable = NO;
+                                                       break;
+                                                   }
+                                                   case NameDirectory::LookupStatus::ERROR:
+                                                   default:
+                                                   {
+                                                       message = NSLocalizedString(@"Failed to perform lookup",
+                                                                                   @"Text shown to user when an error occur at registration");
+                                                       isAvailable = NO;
+                                                       break;
+                                                   }
+                                               }
+                                               [self showLookUpAvailable:isAvailable andText: message];
+                                               [self onUsernameAvailabilityChangedWithNewAvailability:isAvailable];
+
+                                           }
+                                           );
+
+    //Start the lookup in a second so that the UI dosen't seem to freeze
+    BOOL result = NameDirectory::instance().lookupName(nullptr, QString(), QString::fromNSString(usernameWaitingForLookupResult));
+
+}
+
+- (void)controlTextDidChange:(NSNotification *)notif
+{
+    NSTextField* textField = [notif object];
+    if (textField.tag != BLOCKCHAIN_NAME_TAG) {
+        return;
+    }
+    NSString* alias = textField.stringValue;
+
+    [self showLookupSpinner];
+    [self onUsernameAvailabilityChangedWithNewAvailability:NO];
+    [NSObject cancelPreviousPerformRequestsWithTarget:self];
+    [self performSelector:@selector(lookUp:) withObject:alias afterDelay:0.5];
+}
+
+- (void) lookUp:(NSString*) name
+{
+    if (!lookupQueued) {
+        usernameWaitingForLookupResult = name;
+        lookupQueued = YES;
+        [self lookupUserName];
+    }
+}
+
+#pragma mark - Registration process
+
+- (IBAction)registerUsername:(id)sender
+{
+    [registrationProgress startAnimation:nil];
+    [self showLoading];
+    [self setCallback];
+
+    self.isUserNameAvailable = AccountModel::instance().selectedAccount()->registerName(QString::fromNSString(self.password),
+                                                                                        QString::fromNSString(self.registeredName));
+    if (!self.isUserNameAvailable) {
+        NSLog(@"Could not initialize registerName operation");
+        QObject::disconnect(registrationEnded);
+    }
+}
+
+- (void)setCallback
+{
+    QObject::disconnect(registrationEnded);
+    registrationEnded = QObject::connect(AccountModel::instance().selectedAccount(),
+                                         &Account::nameRegistrationEnded,
+                                         [=] (NameDirectory::RegisterNameStatus status,  const QString& name)
+                                         {
+                                             QObject::disconnect(registrationEnded);
+                                             switch(status) {
+                                                 case NameDirectory::RegisterNameStatus::WRONG_PASSWORD:
+                                                 case NameDirectory::RegisterNameStatus::ALREADY_TAKEN:
+                                                 case NameDirectory::RegisterNameStatus::NETWORK_ERROR:
+                                                     [self showError];
+                                                     break;
+                                                 case NameDirectory::RegisterNameStatus::SUCCESS:
+                                                     [self.delegate didRegisterNameWithSuccess];
+                                                     break;
+                                             }
+                                         });
+}
+
+
++ (NSSet *)keyPathsForValuesAffectingUserNameAvailableORNotBlockchain
+{
+    return [NSSet setWithObjects: NSStringFromSelector(@selector(isUserNameAvailable)), nil];
+}
+
++ (NSSet *)keyPathsForValuesAffectingIsPasswordValid
+{
+    return [NSSet setWithObjects:@"password", nil];
+}
+
+@end