//
// Copyright (C) 2014 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#include "update_engine/update_manager/evaluation_context.h"

#include <algorithm>
#include <memory>
#include <string>

#include <base/bind.h>
#include <base/json/json_writer.h>
#include <base/location.h>
#include <base/strings/string_util.h>
#include <base/values.h>

#include "update_engine/common/utils.h"

using base::Callback;
using base::Closure;
using base::Time;
using base::TimeDelta;
using brillo::MessageLoop;
using chromeos_update_engine::ClockInterface;
using std::string;
using std::unique_ptr;

namespace {

// Returns whether |curr_time| surpassed |ref_time|; if not, also checks whether
// |ref_time| is sooner than the current value of |*reeval_time|, in which case
// the latter is updated to the former.
bool IsTimeGreaterThanHelper(Time ref_time, Time curr_time,
                             Time* reeval_time) {
  if (curr_time > ref_time)
    return true;
  // Remember the nearest reference we've checked against in this evaluation.
  if (*reeval_time > ref_time)
    *reeval_time = ref_time;
  return false;
}

// If |expires| never happens (maximal value), returns the maximal interval;
// otherwise, returns the difference between |expires| and |curr|.
TimeDelta GetTimeout(Time curr, Time expires) {
  if (expires.is_max())
    return TimeDelta::Max();
  return expires - curr;
}

}  // namespace

namespace chromeos_update_manager {

EvaluationContext::EvaluationContext(
    ClockInterface* clock,
    TimeDelta evaluation_timeout,
    TimeDelta expiration_timeout,
    unique_ptr<Callback<void(EvaluationContext*)>> unregister_cb)
    : clock_(clock),
      evaluation_timeout_(evaluation_timeout),
      expiration_timeout_(expiration_timeout),
      unregister_cb_(std::move(unregister_cb)),
      weak_ptr_factory_(this) {
  ResetEvaluation();
  ResetExpiration();
}

EvaluationContext::~EvaluationContext() {
  RemoveObserversAndTimeout();
  if (unregister_cb_.get())
    unregister_cb_->Run(this);
}

unique_ptr<Closure> EvaluationContext::RemoveObserversAndTimeout() {
  for (auto& it : value_cache_) {
    if (it.first->GetMode() == kVariableModeAsync)
      it.first->RemoveObserver(this);
  }
  MessageLoop::current()->CancelTask(timeout_event_);
  timeout_event_ = MessageLoop::kTaskIdNull;

  return unique_ptr<Closure>(callback_.release());
}

TimeDelta EvaluationContext::RemainingTime(Time monotonic_deadline) const {
  if (monotonic_deadline.is_max())
    return TimeDelta::Max();
  TimeDelta remaining = monotonic_deadline - clock_->GetMonotonicTime();
  return std::max(remaining, TimeDelta());
}

Time EvaluationContext::MonotonicDeadline(TimeDelta timeout) {
  return (timeout.is_max() ? Time::Max() :
          clock_->GetMonotonicTime() + timeout);
}

void EvaluationContext::ValueChanged(BaseVariable* var) {
  DLOG(INFO) << "ValueChanged() called for variable " << var->GetName();
  OnValueChangedOrTimeout();
}

void EvaluationContext::OnTimeout() {
  DLOG(INFO) << "OnTimeout() called due to "
             << (timeout_marks_expiration_ ? "expiration" : "poll interval");
  timeout_event_ = MessageLoop::kTaskIdNull;
  is_expired_ = timeout_marks_expiration_;
  OnValueChangedOrTimeout();
}

void EvaluationContext::OnValueChangedOrTimeout() {
  // Copy the callback handle locally, allowing it to be reassigned.
  unique_ptr<Closure> callback = RemoveObserversAndTimeout();

  if (callback.get())
    callback->Run();
}

bool EvaluationContext::IsWallclockTimeGreaterThan(Time timestamp) {
  return IsTimeGreaterThanHelper(timestamp, evaluation_start_wallclock_,
                                 &reevaluation_time_wallclock_);
}

bool EvaluationContext::IsMonotonicTimeGreaterThan(Time timestamp) {
  return IsTimeGreaterThanHelper(timestamp, evaluation_start_monotonic_,
                                 &reevaluation_time_monotonic_);
}

void EvaluationContext::ResetEvaluation() {
  evaluation_start_wallclock_ = clock_->GetWallclockTime();
  evaluation_start_monotonic_ = clock_->GetMonotonicTime();
  reevaluation_time_wallclock_ = Time::Max();
  reevaluation_time_monotonic_ = Time::Max();
  evaluation_monotonic_deadline_ = MonotonicDeadline(evaluation_timeout_);

  // Remove the cached values of non-const variables
  for (auto it = value_cache_.begin(); it != value_cache_.end(); ) {
    if (it->first->GetMode() == kVariableModeConst) {
      ++it;
    } else {
      it = value_cache_.erase(it);
    }
  }
}

void EvaluationContext::ResetExpiration() {
  expiration_monotonic_deadline_ = MonotonicDeadline(expiration_timeout_);
  is_expired_ = false;
}

bool EvaluationContext::RunOnValueChangeOrTimeout(Closure callback) {
  // Check that the method was not called more than once.
  if (callback_.get()) {
    LOG(ERROR) << "RunOnValueChangeOrTimeout called more than once.";
    return false;
  }

  // Check that the context did not yet expire.
  if (is_expired()) {
    LOG(ERROR) << "RunOnValueChangeOrTimeout called on an expired context.";
    return false;
  }

  // Handle reevaluation due to a Is{Wallclock,Monotonic}TimeGreaterThan(). We
  // choose the smaller of the differences between evaluation start time and
  // reevaluation time among the wallclock and monotonic scales.
  TimeDelta timeout = std::min(
      GetTimeout(evaluation_start_wallclock_, reevaluation_time_wallclock_),
      GetTimeout(evaluation_start_monotonic_, reevaluation_time_monotonic_));

  // Handle reevaluation due to async or poll variables.
  bool waiting_for_value_change = false;
  for (auto& it : value_cache_) {
    switch (it.first->GetMode()) {
      case kVariableModeAsync:
        DLOG(INFO) << "Waiting for value on " << it.first->GetName();
        it.first->AddObserver(this);
        waiting_for_value_change = true;
        break;
      case kVariableModePoll:
        timeout = std::min(timeout, it.first->GetPollInterval());
        break;
      case kVariableModeConst:
        // Ignored.
        break;
    }
  }

  // Check if the re-evaluation is actually being scheduled. If there are no
  // events waited for, this function should return false.
  if (!waiting_for_value_change && timeout.is_max())
    return false;

  // Ensure that we take into account the expiration timeout.
  TimeDelta expiration = RemainingTime(expiration_monotonic_deadline_);
  timeout_marks_expiration_ = expiration < timeout;
  if (timeout_marks_expiration_)
    timeout = expiration;

  // Store the reevaluation callback.
  callback_.reset(new Closure(callback));

  // Schedule a timeout event, if one is set.
  if (!timeout.is_max()) {
    DLOG(INFO) << "Waiting for timeout in "
               << chromeos_update_engine::utils::FormatTimeDelta(timeout);
    timeout_event_ = MessageLoop::current()->PostDelayedTask(
        FROM_HERE,
        base::Bind(&EvaluationContext::OnTimeout,
                   weak_ptr_factory_.GetWeakPtr()),
        timeout);
  }

  return true;
}

string EvaluationContext::DumpContext() const {
  base::DictionaryValue* variables = new base::DictionaryValue();
  for (auto& it : value_cache_) {
    variables->SetString(it.first->GetName(), it.second.ToString());
  }

  base::DictionaryValue value;
  value.Set("variables", variables);  // Adopts |variables|.
  value.SetString(
      "evaluation_start_wallclock",
      chromeos_update_engine::utils::ToString(evaluation_start_wallclock_));
  value.SetString(
      "evaluation_start_monotonic",
      chromeos_update_engine::utils::ToString(evaluation_start_monotonic_));

  string json_str;
  base::JSONWriter::WriteWithOptions(
      value, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json_str);
  base::TrimWhitespaceASCII(json_str, base::TRIM_TRAILING, &json_str);

  return json_str;
}

}  // namespace chromeos_update_manager