// Copyright (c) 2011 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/shell_dialogs.h"

#include "base/callback.h"
#include "base/file_path.h"
#include "base/json/json_reader.h"
#include "base/memory/scoped_ptr.h"
#include "base/string_util.h"
#include "base/sys_string_conversions.h"
#include "base/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_dialogs.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/views/html_dialog_view.h"
#include "chrome/browser/ui/webui/html_dialog_ui.h"
#include "chrome/common/url_constants.h"
#include "content/browser/browser_thread.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "grit/generated_resources.h"
#include "ui/base/l10n/l10n_util.h"
#include "views/window/non_client_view.h"
#include "views/window/window.h"

namespace {

const char kKeyNamePath[] = "path";
const int kSaveCompletePageIndex = 2;

}  // namespace

// Implementation of SelectFileDialog that shows an UI for choosing a file
// or folder using FileBrowseUI.
class SelectFileDialogImpl : public SelectFileDialog {
 public:
  explicit SelectFileDialogImpl(Listener* listener);

  // BaseShellDialog implementation.
  virtual bool IsRunning(gfx::NativeWindow parent_window) const;
  virtual void ListenerDestroyed();

  virtual void set_browser_mode(bool value) {
    browser_mode_ = value;
  }

 protected:
  // SelectFileDialog implementation.
  // |params| is user data we pass back via the Listener interface.
  virtual void SelectFileImpl(Type type,
                              const string16& title,
                              const FilePath& default_path,
                              const FileTypeInfo* file_types,
                              int file_type_index,
                              const FilePath::StringType& default_extension,
                              gfx::NativeWindow owning_window,
                              void* params);

 private:
  virtual ~SelectFileDialogImpl();

  class FileBrowseDelegate : public HtmlDialogUIDelegate {
   public:
    FileBrowseDelegate(SelectFileDialogImpl* owner,
                       Type type,
                       const std::wstring& title,
                       const FilePath& default_path,
                       const FileTypeInfo* file_types,
                       int file_type_index,
                       const FilePath::StringType& default_extension,
                       gfx::NativeWindow parent,
                       void* params);

    // Owner of this FileBrowseDelegate.
    scoped_refptr<SelectFileDialogImpl> owner_;

    // Parent window.
    gfx::NativeWindow parent_;

    // The type of dialog we are showing the user.
    Type type_;

    // The dialog title.
    std::wstring title_;

    // Default path of the file dialog.
    FilePath default_path_;

    // The file filters.
    FileTypeInfo file_types_;

    // The index of the default selected file filter.
    // Note: This starts from 1, not 0.
    int file_type_index_;

    // Default extension to be added to file if user does not type one.
    FilePath::StringType default_extension_;

    // Associated user data.
    void* params_;

   private:
    ~FileBrowseDelegate();

    // Overridden from HtmlDialogUI::Delegate:
    virtual bool IsDialogModal() const;
    virtual std::wstring GetDialogTitle() const;
    virtual GURL GetDialogContentURL() const;
    virtual void GetWebUIMessageHandlers(
        std::vector<WebUIMessageHandler*>* handlers) const;
    virtual void GetDialogSize(gfx::Size* size) const;
    virtual std::string GetDialogArgs() const;
    virtual void OnDialogClosed(const std::string& json_retval);
    virtual void OnCloseContents(TabContents* source, bool* out_close_dialog) {
    }
    virtual bool ShouldShowDialogTitle() const { return true; }
    virtual bool HandleContextMenu(const ContextMenuParams& params) {
      return true;
    }

    DISALLOW_COPY_AND_ASSIGN(FileBrowseDelegate);
  };

  class FileBrowseDelegateHandler : public WebUIMessageHandler {
   public:
    explicit FileBrowseDelegateHandler(FileBrowseDelegate* delegate);

    // WebUIMessageHandler implementation.
    virtual void RegisterMessages();

    // Callback for the "setDialogTitle" message.
    void HandleSetDialogTitle(const ListValue* args);

   private:
    FileBrowseDelegate* delegate_;

    DISALLOW_COPY_AND_ASSIGN(FileBrowseDelegateHandler);
  };

  // Notification from FileBrowseDelegate when file browse UI is dismissed.
  void OnDialogClosed(FileBrowseDelegate* delegate, const std::string& json);

  // Callback method to open HTML
  void OpenHtmlDialog(gfx::NativeWindow owning_window,
                      FileBrowseDelegate* file_browse_delegate);

  // The set of all parent windows for which we are currently running dialogs.
  std::set<gfx::NativeWindow> parents_;

  // The set of all FileBrowseDelegate that we are currently running.
  std::set<FileBrowseDelegate*> delegates_;

  // True when opening in browser, otherwise in OOBE/login mode.
  bool browser_mode_;

  DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImpl);
};

// static
SelectFileDialog* SelectFileDialog::Create(Listener* listener) {
  DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::IO));
  DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::FILE));
  return new SelectFileDialogImpl(listener);
}

SelectFileDialogImpl::SelectFileDialogImpl(Listener* listener)
    : SelectFileDialog(listener),
      browser_mode_(true) {
}

SelectFileDialogImpl::~SelectFileDialogImpl() {
  // All dialogs should be dismissed by now.
  DCHECK(parents_.empty() && delegates_.empty());
}

bool SelectFileDialogImpl::IsRunning(gfx::NativeWindow parent_window) const {
  return parent_window && parents_.find(parent_window) != parents_.end();
}

void SelectFileDialogImpl::ListenerDestroyed() {
  listener_ = NULL;
}

void SelectFileDialogImpl::SelectFileImpl(
    Type type,
    const string16& title,
    const FilePath& default_path,
    const FileTypeInfo* file_types,
    int file_type_index,
    const FilePath::StringType& default_extension,
    gfx::NativeWindow owning_window,
    void* params) {
  std::wstring title_string;
  if (title.empty()) {
    int string_id;
    switch (type) {
      case SELECT_FOLDER:
        string_id = IDS_SELECT_FOLDER_DIALOG_TITLE;
        break;
      case SELECT_OPEN_FILE:
      case SELECT_OPEN_MULTI_FILE:
        string_id = IDS_OPEN_FILE_DIALOG_TITLE;
        break;
      case SELECT_SAVEAS_FILE:
        string_id = IDS_SAVE_AS_DIALOG_TITLE;
        break;
      default:
        NOTREACHED();
        return;
    }
    title_string = UTF16ToWide(l10n_util::GetStringUTF16(string_id));
  } else {
    title_string = UTF16ToWide(title);
  }

  if (owning_window)
    parents_.insert(owning_window);

  FileBrowseDelegate* file_browse_delegate = new FileBrowseDelegate(this,
      type, title_string, default_path, file_types, file_type_index,
      default_extension, owning_window, params);
  delegates_.insert(file_browse_delegate);

  if (browser_mode_) {
    Browser* browser = BrowserList::GetLastActive();
    // As SelectFile may be invoked after a delay, it is entirely possible for
    // it be invoked when no browser is around. Silently ignore this case.
    if (browser)
      browser->BrowserShowHtmlDialog(file_browse_delegate, owning_window);
  } else {
    BrowserThread::PostTask(
        BrowserThread::UI,
        FROM_HERE,
        NewRunnableMethod(this,
                          &SelectFileDialogImpl::OpenHtmlDialog,
                          owning_window,
                          file_browse_delegate));
  }
}

void SelectFileDialogImpl::OnDialogClosed(FileBrowseDelegate* delegate,
                                          const std::string& json) {
  // Nothing to do if listener_ is gone.

  if (!listener_)
    return;

  bool notification_fired = false;

  if (!json.empty()) {
    scoped_ptr<Value> value(base::JSONReader::Read(json, false));
    if (!value.get() || !value->IsType(Value::TYPE_DICTIONARY)) {
      // Bad json value returned.
      NOTREACHED();
    } else {
      const DictionaryValue* dict = static_cast<DictionaryValue*>(value.get());
      if (delegate->type_ == SELECT_OPEN_FILE ||
          delegate->type_ == SELECT_SAVEAS_FILE ||
          delegate->type_ == SELECT_FOLDER) {
        std::string path_string;
        if (dict->HasKey(kKeyNamePath) &&
            dict->GetString(kKeyNamePath, &path_string)) {
#if defined(OS_WIN)
          FilePath path(base::SysUTF8ToWide(path_string));
#else
          FilePath path(
              base::SysWideToNativeMB(base::SysUTF8ToWide(path_string)));
#endif
          listener_->
              FileSelected(path, kSaveCompletePageIndex, delegate->params_);
          notification_fired = true;
        }
      } else if (delegate->type_ == SELECT_OPEN_MULTI_FILE) {
        ListValue* paths_value = NULL;
        if (dict->HasKey(kKeyNamePath) &&
            dict->GetList(kKeyNamePath, &paths_value) &&
            paths_value) {
          std::vector<FilePath> paths;
          paths.reserve(paths_value->GetSize());
          for (size_t i = 0; i < paths_value->GetSize(); ++i) {
            std::string path_string;
            if (paths_value->GetString(i, &path_string) &&
                !path_string.empty()) {
#if defined(OS_WIN)
              FilePath path(base::SysUTF8ToWide(path_string));
#else
              FilePath path(
                  base::SysWideToNativeMB(base::SysUTF8ToWide(path_string)));
#endif
              paths.push_back(path);
            }
          }

          listener_->MultiFilesSelected(paths, delegate->params_);
          notification_fired = true;
        }
      } else {
        NOTREACHED();
      }
    }
  }

  // Always notify listener when dialog is dismissed.
  if (!notification_fired)
    listener_->FileSelectionCanceled(delegate->params_);

  parents_.erase(delegate->parent_);
  delegates_.erase(delegate);
}

void SelectFileDialogImpl::OpenHtmlDialog(
    gfx::NativeWindow owning_window,
    FileBrowseDelegate* file_browse_delegate) {
  browser::ShowHtmlDialog(owning_window,
                          ProfileManager::GetDefaultProfile(),
                          file_browse_delegate);
}

SelectFileDialogImpl::FileBrowseDelegate::FileBrowseDelegate(
    SelectFileDialogImpl* owner,
    Type type,
    const std::wstring& title,
    const FilePath& default_path,
    const FileTypeInfo* file_types,
    int file_type_index,
    const FilePath::StringType& default_extension,
    gfx::NativeWindow parent,
    void* params)
  : owner_(owner),
    parent_(parent),
    type_(type),
    title_(title),
    default_path_(default_path),
    file_type_index_(file_type_index),
    default_extension_(default_extension),
    params_(params) {
  if (file_types)
    file_types_ = *file_types;
  else
    file_types_.include_all_files = true;
}

SelectFileDialogImpl::FileBrowseDelegate::~FileBrowseDelegate() {
}

bool SelectFileDialogImpl::FileBrowseDelegate::IsDialogModal() const {
  return true;
}

std::wstring SelectFileDialogImpl::FileBrowseDelegate::GetDialogTitle() const {
  return title_;
}

GURL SelectFileDialogImpl::FileBrowseDelegate::GetDialogContentURL() const {
  std::string url_string(chrome::kChromeUIFileBrowseURL);

  return GURL(url_string);
}

void SelectFileDialogImpl::FileBrowseDelegate::GetWebUIMessageHandlers(
    std::vector<WebUIMessageHandler*>* handlers) const {
  handlers->push_back(new FileBrowseDelegateHandler(
      const_cast<FileBrowseDelegate*>(this)));
  return;
}

void SelectFileDialogImpl::FileBrowseDelegate::GetDialogSize(
    gfx::Size* size) const {
  size->SetSize(320, 240);
}

std::string SelectFileDialogImpl::FileBrowseDelegate::GetDialogArgs() const {
  // SelectFile inputs as json.
  //   {
  //     "type"            : "open",   // (or "open_multiple", "save", "folder"
  //     "all_files"       : true,
  //     "file_types"      : {
  //                           "exts" : [ ["htm", "html"], ["txt"] ],
  //                           "desc" : [ "HTML files", "Text files" ],
  //                         },
  //     "file_type_index" : 1,    // 1-based file type index.
  //   }
  // See browser/ui/shell_dialogs.h for more details.

  std::string type_string;
  switch (type_) {
    case SELECT_FOLDER:
      type_string = "folder";
      break;
    case SELECT_OPEN_FILE:
      type_string = "open";
      break;
    case SELECT_OPEN_MULTI_FILE:
      type_string = "open_multiple";
      break;
    case SELECT_SAVEAS_FILE:
      type_string = "save";
      break;
    default:
      NOTREACHED();
      return std::string();
  }

  std::string exts_list;
  std::string desc_list;
  for (size_t i = 0; i < file_types_.extensions.size(); ++i) {
    DCHECK(!file_types_.extensions[i].empty());

    std::string exts;
    for (size_t j = 0; j < file_types_.extensions[i].size(); ++j) {
      if (!exts.empty())
        exts.append(",");
      base::StringAppendF(&exts, "\"%s\"",
                          file_types_.extensions[i][j].c_str());
    }

    if (!exts_list.empty())
      exts_list.append(",");
    base::StringAppendF(&exts_list, "[%s]", exts.c_str());

    std::string desc;
    if (i < file_types_.extension_description_overrides.size()) {
      desc = UTF16ToUTF8(file_types_.extension_description_overrides[i]);
    } else {
#if defined(OS_WIN)
      desc = WideToUTF8(file_types_.extensions[i][0]);
#elif defined(OS_POSIX)
      desc = file_types_.extensions[i][0];
#else
      NOTIMPLEMENTED();
#endif
    }

    if (!desc_list.empty())
      desc_list.append(",");
    base::StringAppendF(&desc_list, "\"%s\"", desc.c_str());
  }

  std::string filename = default_path_.BaseName().value();

  return StringPrintf("{"
        "\"type\":\"%s\","
        "\"all_files\":%s,"
        "\"current_file\":\"%s\","
        "\"file_types\":{\"exts\":[%s],\"desc\":[%s]},"
        "\"file_type_index\":%d"
      "}",
      type_string.c_str(),
      file_types_.include_all_files ? "true" : "false",
      filename.c_str(),
      exts_list.c_str(),
      desc_list.c_str(),
      file_type_index_);
}

void SelectFileDialogImpl::FileBrowseDelegate::OnDialogClosed(
    const std::string& json_retval) {
  owner_->OnDialogClosed(this, json_retval);
  delete this;
  return;
}

SelectFileDialogImpl::FileBrowseDelegateHandler::FileBrowseDelegateHandler(
    FileBrowseDelegate* delegate)
    : delegate_(delegate) {
}

void SelectFileDialogImpl::FileBrowseDelegateHandler::RegisterMessages() {
  web_ui_->RegisterMessageCallback("setDialogTitle",
      NewCallback(this, &FileBrowseDelegateHandler::HandleSetDialogTitle));
}

void SelectFileDialogImpl::FileBrowseDelegateHandler::HandleSetDialogTitle(
    const ListValue* args) {
  std::wstring new_title = UTF16ToWideHack(ExtractStringValue(args));
  if (new_title != delegate_->title_) {
    delegate_->title_ = new_title;

    // Notify the containing view about the title change.
    // The current HtmlDialogUIDelegate and HtmlDialogView does not support
    // dynamic title change. We hijacked the mechanism between HTMLDialogUI
    // and HtmlDialogView to get the HtmlDialogView and forced it to update
    // its title.
    // TODO(xiyuan): Change this when the infrastructure is improved.
    HtmlDialogUIDelegate** delegate = HtmlDialogUI::GetPropertyAccessor().
        GetProperty(web_ui_->tab_contents()->property_bag());
    HtmlDialogView* containing_view = static_cast<HtmlDialogView*>(*delegate);
    DCHECK(containing_view);

    containing_view->GetWindow()->UpdateWindowTitle();
    containing_view->GetWindow()->non_client_view()->SchedulePaint();
  }
}