// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/cocoa/javascript_app_modal_dialog_cocoa.h"

#import <Cocoa/Cocoa.h>
#include <stddef.h>

#include "base/i18n/rtl.h"
#include "base/logging.h"
#import "base/mac/foundation_util.h"
#include "base/macros.h"
#include "base/strings/sys_string_conversions.h"
#import "chrome/browser/chrome_browser_application_mac.h"
#include "chrome/browser/ui/app_modal/chrome_javascript_native_dialog_factory.h"
#include "chrome/browser/ui/blocked_content/app_modal_dialog_helper.h"
#include "components/app_modal/javascript_app_modal_dialog.h"
#include "components/app_modal/javascript_dialog_manager.h"
#include "components/app_modal/javascript_native_dialog_factory.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "grit/components_strings.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/ui_base_types.h"
#include "ui/gfx/text_elider.h"
#include "ui/strings/grit/ui_strings.h"

namespace {

const int kSlotsPerLine = 50;
const int kMessageTextMaxSlots = 2000;

}  // namespace

// Helper object that receives the notification that the dialog/sheet is
// going away. Is responsible for cleaning itself up.
@interface JavaScriptAppModalDialogHelper : NSObject<NSAlertDelegate> {
 @private
  base::scoped_nsobject<NSAlert> alert_;
  JavaScriptAppModalDialogCocoa* nativeDialog_;  // Weak.
  base::scoped_nsobject<NSTextField> textField_;
  BOOL alertShown_;
}

// Creates an NSAlert if one does not already exist. Otherwise returns the
// existing NSAlert.
- (NSAlert*)alert;
- (void)addTextFieldWithPrompt:(NSString*)prompt;

// Presents an AppKit blocking dialog.
- (void)showAlert;

// Selects the first button of the alert, which should accept it.
- (void)acceptAlert;

// Selects the second button of the alert, which should cancel it.
- (void)cancelAlert;

// Closes the window, and the alert along with it.
- (void)closeWindow;

// Designated initializer.
- (instancetype)initWithNativeDialog:(JavaScriptAppModalDialogCocoa*)dialog;

@end

@implementation JavaScriptAppModalDialogHelper

- (instancetype)init {
  NOTREACHED();
  return nil;
}

- (instancetype)initWithNativeDialog:(JavaScriptAppModalDialogCocoa*)dialog {
  DCHECK(dialog);
  self = [super init];
  if (self)
    nativeDialog_ = dialog;
  return self;
}

- (NSAlert*)alert {
  if (!alert_) {
    alert_.reset([[NSAlert alloc] init]);
    if (!nativeDialog_->dialog()->is_before_unload_dialog()) {
      // Set a blank icon for dialogs with text provided by the page.
      // "onbeforeunload" dialogs don't have text provided by the page, so it's
      // OK to use the app icon.
      NSImage* image =
          [[[NSImage alloc] initWithSize:NSMakeSize(1, 1)] autorelease];
      [alert_ setIcon:image];
    }
  }
  return alert_;
}

- (void)addTextFieldWithPrompt:(NSString*)prompt {
  DCHECK(!textField_);
  textField_.reset(
      [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 22)]);
  [[textField_ cell] setLineBreakMode:NSLineBreakByTruncatingTail];
  [[self alert] setAccessoryView:textField_];
  [[alert_ window] setInitialFirstResponder:textField_];

  [textField_ setStringValue:prompt];
}

// |contextInfo| is the JavaScriptAppModalDialogCocoa that owns us.
- (void)alertDidEnd:(NSAlert*)alert
         returnCode:(int)returnCode
        contextInfo:(void*)contextInfo {
  switch (returnCode) {
    case NSAlertFirstButtonReturn:  {  // OK
      [self sendAcceptToNativeDialog];
      break;
    }
    case NSAlertSecondButtonReturn:  {  // Cancel
      // If the user wants to stay on this page, stop quitting (if a quit is in
      // progress).
      [self sendCancelToNativeDialog];
      break;
    }
    case NSRunStoppedResponse: {  // Window was closed underneath us
      // Need to call OnClose() because there is some cleanup that needs
      // to be done.  It won't call back to the javascript since the
      // JavaScriptAppModalDialog knows that the WebContents was destroyed.
      [self sendCloseToNativeDialog];
      break;
    }
    default:  {
      NOTREACHED();
    }
  }
}

- (void)showAlert {
  DCHECK(nativeDialog_);
  DCHECK(!alertShown_);
  alertShown_ = YES;
  NSAlert* alert = [self alert];
  [alert beginSheetModalForWindow:nil  // nil here makes it app-modal
                    modalDelegate:self
                   didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
                      contextInfo:NULL];
}

- (void)acceptAlert {
  DCHECK(nativeDialog_);
  if (!alertShown_) {
    [self sendAcceptToNativeDialog];
    return;
  }
  NSButton* first = [[[self alert] buttons] objectAtIndex:0];
  [first performClick:nil];
}

- (void)cancelAlert {
  DCHECK(nativeDialog_);
  if (!alertShown_) {
    [self sendCancelToNativeDialog];
    return;
  }
  DCHECK_GE([[[self alert] buttons] count], 2U);
  NSButton* second = [[[self alert] buttons] objectAtIndex:1];
  [second performClick:nil];
}

- (void)closeWindow {
  DCHECK(nativeDialog_);
  if (!alertShown_) {
    [self sendCloseToNativeDialog];
    return;
  }
  [NSApp endSheet:[[self alert] window]];
}

- (void)sendAcceptToNativeDialog {
  DCHECK(nativeDialog_);
  nativeDialog_->dialog()->OnAccept([self input], [self shouldSuppress]);
  [self destroyNativeDialog];
}

- (void)sendCancelToNativeDialog {
  DCHECK(nativeDialog_);
  // If the user wants to stay on this page, stop quitting (if a quit is in
  // progress).
  if (nativeDialog_->dialog()->is_before_unload_dialog())
    chrome_browser_application_mac::CancelTerminate();
  nativeDialog_->dialog()->OnCancel([self shouldSuppress]);
  [self destroyNativeDialog];
}

- (void)sendCloseToNativeDialog {
  DCHECK(nativeDialog_);
  nativeDialog_->dialog()->OnClose();
  [self destroyNativeDialog];
}

- (void)destroyNativeDialog {
  DCHECK(nativeDialog_);
  JavaScriptAppModalDialogCocoa* nativeDialog = nativeDialog_;
  nativeDialog_ = nil;  // Need to fail on DCHECK if something wrong happens.
  delete nativeDialog;  // Careful, this will delete us.
}

- (base::string16)input {
  if (textField_)
    return base::SysNSStringToUTF16([textField_ stringValue]);
  return base::string16();
}

- (bool)shouldSuppress {
  if ([[self alert] showsSuppressionButton])
    return [[[self alert] suppressionButton] state] == NSOnState;
  return false;
}

@end

////////////////////////////////////////////////////////////////////////////////
// JavaScriptAppModalDialogCocoa, public:

JavaScriptAppModalDialogCocoa::JavaScriptAppModalDialogCocoa(
    app_modal::JavaScriptAppModalDialog* dialog)
    : dialog_(dialog),
      popup_helper_(new AppModalDialogHelper(dialog->web_contents())),
      is_showing_(false) {
  // Determine the names of the dialog buttons based on the flags. "Default"
  // is the OK button. "Other" is the cancel button. We don't use the
  // "Alternate" button in NSRunAlertPanel.
  NSString* default_button = l10n_util::GetNSStringWithFixup(IDS_APP_OK);
  NSString* other_button = l10n_util::GetNSStringWithFixup(IDS_APP_CANCEL);
  bool text_field = false;
  bool one_button = false;
  switch (dialog_->javascript_message_type()) {
    case content::JAVASCRIPT_MESSAGE_TYPE_ALERT:
      one_button = true;
      break;
    case content::JAVASCRIPT_MESSAGE_TYPE_CONFIRM:
      if (dialog_->is_before_unload_dialog()) {
        if (dialog_->is_reload()) {
          default_button = l10n_util::GetNSStringWithFixup(
              IDS_BEFORERELOAD_MESSAGEBOX_OK_BUTTON_LABEL);
          other_button = l10n_util::GetNSStringWithFixup(
              IDS_BEFORERELOAD_MESSAGEBOX_CANCEL_BUTTON_LABEL);
        } else {
          default_button = l10n_util::GetNSStringWithFixup(
              IDS_BEFOREUNLOAD_MESSAGEBOX_OK_BUTTON_LABEL);
          other_button = l10n_util::GetNSStringWithFixup(
              IDS_BEFOREUNLOAD_MESSAGEBOX_CANCEL_BUTTON_LABEL);
        }
      }
      break;
    case content::JAVASCRIPT_MESSAGE_TYPE_PROMPT:
      text_field = true;
      break;

    default:
      NOTREACHED();
  }

  // Create a helper which will receive the sheet ended selector. It will
  // delete itself when done.
  helper_.reset(
      [[JavaScriptAppModalDialogHelper alloc] initWithNativeDialog:this]);

  // Show the modal dialog.
  if (text_field) {
    [helper_ addTextFieldWithPrompt:base::SysUTF16ToNSString(
        dialog_->default_prompt_text())];
  }
  [GetAlert() setDelegate:helper_];
  NSString* informative_text =
      base::SysUTF16ToNSString(dialog_->message_text());

  // Truncate long JS alerts - crbug.com/331219
  NSCharacterSet* newline_char_set = [NSCharacterSet newlineCharacterSet];
  for (size_t index = 0, slots_count = 0; index < informative_text.length;
      ++index) {
    unichar current_char = [informative_text characterAtIndex:index];
    if ([newline_char_set characterIsMember:current_char])
      slots_count += kSlotsPerLine;
    else
      slots_count++;
    if (slots_count > kMessageTextMaxSlots) {
      base::string16 info_text = base::SysNSStringToUTF16(informative_text);
      informative_text = base::SysUTF16ToNSString(
          gfx::TruncateString(info_text, index, gfx::WORD_BREAK));
      break;
    }
  }

  [GetAlert() setInformativeText:informative_text];
  NSString* message_text =
      base::SysUTF16ToNSString(dialog_->title());
  [GetAlert() setMessageText:message_text];
  [GetAlert() addButtonWithTitle:default_button];
  if (!one_button) {
    NSButton* other = [GetAlert() addButtonWithTitle:other_button];
    [other setKeyEquivalent:@"\e"];
  }
  if (dialog_->display_suppress_checkbox()) {
    [GetAlert() setShowsSuppressionButton:YES];
    NSString* suppression_title = l10n_util::GetNSStringWithFixup(
        IDS_JAVASCRIPT_MESSAGEBOX_SUPPRESS_OPTION);
    [[GetAlert() suppressionButton] setTitle:suppression_title];
  }

  // Fix RTL dialogs.
  //
  // Mac OS X will always display NSAlert strings as LTR. A workaround is to
  // manually set the text as attributed strings in the implementing
  // NSTextFields. This is a basic correctness issue.
  //
  // In addition, for readability, the overall alignment is set based on the
  // directionality of the first strongly-directional character.
  //
  // If the dialog fields are selectable then they will scramble when clicked.
  // Therefore, selectability is disabled.
  //
  // See http://crbug.com/70806 for more details.

  bool message_has_rtl =
      base::i18n::StringContainsStrongRTLChars(dialog_->title());
  bool informative_has_rtl =
      base::i18n::StringContainsStrongRTLChars(dialog_->message_text());

  NSTextField* message_text_field = nil;
  NSTextField* informative_text_field = nil;
  if (message_has_rtl || informative_has_rtl) {
    // Force layout of the dialog. NSAlert leaves its dialog alone once laid
    // out; if this is not done then all the modifications that are to come will
    // be un-done when the dialog is finally displayed.
    [GetAlert() layout];

    // Locate the NSTextFields that implement the text display. These are
    // actually available as the ivars |_messageField| and |_informationField|
    // of the NSAlert, but it is safer (and more forward-compatible) to search
    // for them in the subviews.
    for (NSView* view in [[[GetAlert() window] contentView] subviews]) {
      NSTextField* text_field = base::mac::ObjCCast<NSTextField>(view);
      if ([[text_field stringValue] isEqualTo:message_text])
        message_text_field = text_field;
      else if ([[text_field stringValue] isEqualTo:informative_text])
        informative_text_field = text_field;
    }

    // This may fail in future OS releases, but it will still work for shipped
    // versions of Chromium.
    DCHECK(message_text_field);
    DCHECK(informative_text_field);
  }

  if (message_has_rtl && message_text_field) {
    base::scoped_nsobject<NSMutableParagraphStyle> alignment(
        [[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
    [alignment setAlignment:NSRightTextAlignment];

    NSDictionary* alignment_attributes =
        @{ NSParagraphStyleAttributeName : alignment };
    base::scoped_nsobject<NSAttributedString> attr_string(
        [[NSAttributedString alloc] initWithString:message_text
                                        attributes:alignment_attributes]);

    [message_text_field setAttributedStringValue:attr_string];
    [message_text_field setSelectable:NO];
  }

  if (informative_has_rtl && informative_text_field) {
    base::i18n::TextDirection direction =
        base::i18n::GetFirstStrongCharacterDirection(dialog_->message_text());
    base::scoped_nsobject<NSMutableParagraphStyle> alignment(
        [[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
    [alignment setAlignment:
        (direction == base::i18n::RIGHT_TO_LEFT) ? NSRightTextAlignment
                                                 : NSLeftTextAlignment];

    NSDictionary* alignment_attributes =
        @{ NSParagraphStyleAttributeName : alignment };
    base::scoped_nsobject<NSAttributedString> attr_string(
        [[NSAttributedString alloc] initWithString:informative_text
                                        attributes:alignment_attributes]);

    [informative_text_field setAttributedStringValue:attr_string];
    [informative_text_field setSelectable:NO];
  }
}

JavaScriptAppModalDialogCocoa::~JavaScriptAppModalDialogCocoa() {
  [NSObject cancelPreviousPerformRequestsWithTarget:helper_.get()];
}

////////////////////////////////////////////////////////////////////////////////
// JavaScriptAppModalDialogCocoa, private:

NSAlert* JavaScriptAppModalDialogCocoa::GetAlert() const {
  return [helper_ alert];
}

////////////////////////////////////////////////////////////////////////////////
// JavaScriptAppModalDialogCocoa, NativeAppModalDialog implementation:

int JavaScriptAppModalDialogCocoa::GetAppModalDialogButtons() const {
  // From the above, it is the case that if there is 1 button, it is always the
  // OK button.  The second button, if it exists, is always the Cancel button.
  int num_buttons = [[GetAlert() buttons] count];
  switch (num_buttons) {
    case 1:
      return ui::DIALOG_BUTTON_OK;
    case 2:
      return ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL;
    default:
      NOTREACHED();
      return 0;
  }
}

void JavaScriptAppModalDialogCocoa::ShowAppModalDialog() {
  is_showing_ = true;

  // Dispatch the method to show the alert back to the top of the CFRunLoop.
  // This fixes an interaction bug with NSSavePanel. http://crbug.com/375785
  // When this object is destroyed, outstanding performSelector: requests
  // should be cancelled.
  [helper_.get() performSelector:@selector(showAlert)
                      withObject:nil
                      afterDelay:0];
}

void JavaScriptAppModalDialogCocoa::ActivateAppModalDialog() {
}

void JavaScriptAppModalDialogCocoa::CloseAppModalDialog() {
  [helper_ closeWindow];
}

void JavaScriptAppModalDialogCocoa::AcceptAppModalDialog() {
  [helper_ acceptAlert];
}

void JavaScriptAppModalDialogCocoa::CancelAppModalDialog() {
  [helper_ cancelAlert];
}

bool JavaScriptAppModalDialogCocoa::IsShowing() const {
  return is_showing_;
}

namespace {

class ChromeJavaScriptNativeDialogCocoaFactory
    : public app_modal::JavaScriptNativeDialogFactory {
 public:
  ChromeJavaScriptNativeDialogCocoaFactory() {}
  ~ChromeJavaScriptNativeDialogCocoaFactory() override {}

 private:
  app_modal::NativeAppModalDialog* CreateNativeJavaScriptDialog(
      app_modal::JavaScriptAppModalDialog* dialog) override {
    app_modal::NativeAppModalDialog* d =
        new JavaScriptAppModalDialogCocoa(dialog);
    dialog->web_contents()->GetDelegate()->ActivateContents(
        dialog->web_contents());
    return d;
  }

  DISALLOW_COPY_AND_ASSIGN(ChromeJavaScriptNativeDialogCocoaFactory);
};

}  // namespace

void InstallChromeJavaScriptNativeDialogFactory() {
  app_modal::JavaScriptDialogManager::GetInstance()->
      SetNativeDialogFactory(
          make_scoped_ptr(new ChromeJavaScriptNativeDialogCocoaFactory));
}