// 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/chromeos/login/update_screen.h"

#include "base/file_util.h"
#include "base/logging.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/chromeos/cros/cros_library.h"
#include "chrome/browser/chromeos/login/screen_observer.h"
#include "chrome/browser/chromeos/login/update_view.h"
#include "chrome/browser/chromeos/login/wizard_controller.h"
#include "content/browser/browser_thread.h"

namespace {

// Progress bar stages. Each represents progress bar value
// at the beginning of each stage.
// TODO(nkostylev): Base stage progress values on approximate time.
// TODO(nkostylev): Animate progress during each state.
const int kBeforeUpdateCheckProgress = 7;
const int kBeforeDownloadProgress = 14;
const int kBeforeVerifyingProgress = 74;
const int kBeforeFinalizingProgress = 81;
const int kProgressComplete = 100;

// Defines what part of update progress does download part takes.
const int kDownloadProgressIncrement = 60;

// Considering 10px shadow from each side.
const int kUpdateScreenWidth = 580;
const int kUpdateScreenHeight = 305;

const char kUpdateDeadlineFile[] = "/tmp/update-check-response-deadline";

}  // anonymous namespace

namespace chromeos {


// static
UpdateScreen::InstanceSet& UpdateScreen::GetInstanceSet() {
  static std::set<UpdateScreen*> instance_set;
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));  // not threadsafe.
  return instance_set;
}

// static
bool UpdateScreen::HasInstance(UpdateScreen* inst) {
  InstanceSet& instance_set = GetInstanceSet();
  InstanceSet::iterator found = instance_set.find(inst);
  return (found != instance_set.end());
}

UpdateScreen::UpdateScreen(WizardScreenDelegate* delegate)
    : DefaultViewScreen<chromeos::UpdateView>(delegate,
                                              kUpdateScreenWidth,
                                              kUpdateScreenHeight),
      checking_for_update_(true),
      reboot_check_delay_(0),
      is_downloading_update_(false),
      is_all_updates_critical_(true) { // See http://crosbug.com/10068
  GetInstanceSet().insert(this);
}

UpdateScreen::~UpdateScreen() {
  // Remove pointer to this object from view.
  if (view())
    view()->set_controller(NULL);
  CrosLibrary::Get()->GetUpdateLibrary()->RemoveObserver(this);
  GetInstanceSet().erase(this);
}

void UpdateScreen::UpdateStatusChanged(UpdateLibrary* library) {
  UpdateStatusOperation status = library->status().status;
  if (checking_for_update_ && status > UPDATE_STATUS_CHECKING_FOR_UPDATE) {
    checking_for_update_ = false;
  }

  switch (status) {
    case UPDATE_STATUS_CHECKING_FOR_UPDATE:
      // Do nothing in these cases, we don't want to notify the user of the
      // check unless there is an update.
      break;
    case UPDATE_STATUS_UPDATE_AVAILABLE:
      MakeSureScreenIsShown();
      view()->SetProgress(kBeforeDownloadProgress);
      if (!HasCriticalUpdate()) {
        LOG(INFO) << "Noncritical update available: "
                  << library->status().new_version;
        ExitUpdate(REASON_UPDATE_NON_CRITICAL);
      } else {
        LOG(INFO) << "Critical update available: "
                  << library->status().new_version;
        view()->ShowPreparingUpdatesInfo(true);
        view()->ShowCurtain(false);
      }
      break;
    case UPDATE_STATUS_DOWNLOADING:
      {
        MakeSureScreenIsShown();
        if (!is_downloading_update_) {
          // Because update engine doesn't send UPDATE_STATUS_UPDATE_AVAILABLE
          // we need to is update critical on first downloading notification.
          is_downloading_update_ = true;
          if (!HasCriticalUpdate()) {
            LOG(INFO) << "Non-critical update available: "
                      << library->status().new_version;
            ExitUpdate(REASON_UPDATE_NON_CRITICAL);
          } else {
            LOG(INFO) << "Critical update available: "
                      << library->status().new_version;
          }
        }
        view()->ShowPreparingUpdatesInfo(false);
        view()->ShowCurtain(false);
        int download_progress = static_cast<int>(
            library->status().download_progress * kDownloadProgressIncrement);
        view()->SetProgress(kBeforeDownloadProgress + download_progress);
      }
      break;
    case UPDATE_STATUS_VERIFYING:
      MakeSureScreenIsShown();
      view()->SetProgress(kBeforeVerifyingProgress);
      break;
    case UPDATE_STATUS_FINALIZING:
      MakeSureScreenIsShown();
      view()->SetProgress(kBeforeFinalizingProgress);
      break;
    case UPDATE_STATUS_UPDATED_NEED_REBOOT:
      MakeSureScreenIsShown();
      // Make sure that first OOBE stage won't be shown after reboot.
      WizardController::MarkOobeCompleted();
      view()->SetProgress(kProgressComplete);
      if (HasCriticalUpdate()) {
        view()->ShowCurtain(false);
        VLOG(1) << "Initiate reboot after update";
        CrosLibrary::Get()->GetUpdateLibrary()->RebootAfterUpdate();
        reboot_timer_.Start(base::TimeDelta::FromSeconds(reboot_check_delay_),
                            this,
                            &UpdateScreen::OnWaitForRebootTimeElapsed);
      } else {
        ExitUpdate(REASON_UPDATE_NON_CRITICAL);
      }
      break;
    case UPDATE_STATUS_IDLE:
    case UPDATE_STATUS_ERROR:
    case UPDATE_STATUS_REPORTING_ERROR_EVENT:
      ExitUpdate(REASON_UPDATE_ENDED);
      break;
    default:
      NOTREACHED();
      break;
  }
}

namespace {
// Invoked from call to RequestUpdateCheck upon completion of the DBus call.
void StartUpdateCallback(void* user_data,
                         UpdateResult result,
                         const char* msg) {
  if (result != UPDATE_RESULT_SUCCESS) {
    DCHECK(user_data);
    UpdateScreen* screen = static_cast<UpdateScreen*>(user_data);
    if (UpdateScreen::HasInstance(screen))
      screen->ExitUpdate(UpdateScreen::REASON_UPDATE_INIT_FAILED);
  }
}
}  // namespace

void UpdateScreen::StartUpdate() {
  // Reset view if view was created.
  if (view()) {
    view()->Reset();
    view()->set_controller(this);
    is_downloading_update_ = false;
    view()->SetProgress(kBeforeUpdateCheckProgress);
  }

  if (!CrosLibrary::Get()->EnsureLoaded()) {
    LOG(ERROR) << "Error loading CrosLibrary";
    ExitUpdate(REASON_UPDATE_INIT_FAILED);
  } else {
    CrosLibrary::Get()->GetUpdateLibrary()->AddObserver(this);
    VLOG(1) << "Initiate update check";
    CrosLibrary::Get()->GetUpdateLibrary()->RequestUpdateCheck(
        StartUpdateCallback, this);
  }
}

void UpdateScreen::CancelUpdate() {
  // Screen has longer lifetime than it's view.
  // View is deleted after wizard proceeds to the next screen.
  if (view())
    ExitUpdate(REASON_UPDATE_CANCELED);
}

void UpdateScreen::Show() {
  DefaultViewScreen<UpdateView>::Show();
  view()->set_controller(this);
  is_downloading_update_ = false;
  view()->SetProgress(kBeforeUpdateCheckProgress);
}

void UpdateScreen::ExitUpdate(UpdateScreen::ExitReason reason) {
  ScreenObserver* observer = delegate()->GetObserver(this);
  if (CrosLibrary::Get()->EnsureLoaded())
    CrosLibrary::Get()->GetUpdateLibrary()->RemoveObserver(this);

  switch(reason) {
    case REASON_UPDATE_CANCELED:
      observer->OnExit(ScreenObserver::UPDATE_NOUPDATE);
      break;
    case REASON_UPDATE_INIT_FAILED:
      observer->OnExit(ScreenObserver::UPDATE_ERROR_CHECKING_FOR_UPDATE);
      break;
    case REASON_UPDATE_NON_CRITICAL:
    case REASON_UPDATE_ENDED:
      {
        UpdateLibrary* update_library = CrosLibrary::Get()->GetUpdateLibrary();
        switch (update_library->status().status) {
          case UPDATE_STATUS_UPDATE_AVAILABLE:
          case UPDATE_STATUS_UPDATED_NEED_REBOOT:
          case UPDATE_STATUS_DOWNLOADING:
          case UPDATE_STATUS_FINALIZING:
          case UPDATE_STATUS_VERIFYING:
            DCHECK(!HasCriticalUpdate());
            // Noncritical update, just exit screen as if there is no update.
            // no break
          case UPDATE_STATUS_IDLE:
            observer->OnExit(ScreenObserver::UPDATE_NOUPDATE);
            break;
          case UPDATE_STATUS_ERROR:
          case UPDATE_STATUS_REPORTING_ERROR_EVENT:
            observer->OnExit(checking_for_update_ ?
                ScreenObserver::UPDATE_ERROR_CHECKING_FOR_UPDATE :
                ScreenObserver::UPDATE_ERROR_UPDATING);
            break;
          default:
            NOTREACHED();
        }
      }
      break;
    default:
      NOTREACHED();
  }
}

void UpdateScreen::OnWaitForRebootTimeElapsed() {
  LOG(ERROR) << "Unable to reboot - asking user for a manual reboot.";
  MakeSureScreenIsShown();
  view()->ShowManualRebootInfo();
}

void UpdateScreen::MakeSureScreenIsShown() {
  if (!view()) {
    delegate()->ShowCurrentScreen();
  }
}

void UpdateScreen::SetRebootCheckDelay(int seconds) {
  if (seconds <= 0)
    reboot_timer_.Stop();
  DCHECK(!reboot_timer_.IsRunning());
  reboot_check_delay_ = seconds;
}

bool UpdateScreen::HasCriticalUpdate() {
  if (is_all_updates_critical_)
    return true;

  std::string deadline;
  // Checking for update flag file causes us to do blocking IO on UI thread.
  // Temporarily allow it until we fix http://crosbug.com/11106
  base::ThreadRestrictions::ScopedAllowIO allow_io;
  FilePath update_deadline_file_path(kUpdateDeadlineFile);
  if (!file_util::ReadFileToString(update_deadline_file_path, &deadline) ||
      deadline.empty()) {
    return false;
  }

  // TODO(dpolukhin): Analyze file content. Now we can just assume that
  // if the file exists and not empty, there is critical update.
  return true;
}

void UpdateScreen::SetAllUpdatesCritical(bool is_critical) {
  is_all_updates_critical_ = is_critical;
}

}  // namespace chromeos