//
// 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/real_shill_provider.h"

#include <string>

#include <base/logging.h>
#include <base/strings/stringprintf.h>
#include <brillo/type_name_undecorate.h>
#include <shill/dbus-constants.h>
#include <shill/dbus-proxies.h>

using org::chromium::flimflam::ManagerProxyInterface;
using org::chromium::flimflam::ServiceProxyInterface;
using std::string;

namespace chromeos_update_manager {

ConnectionType RealShillProvider::ParseConnectionType(const string& type_str) {
  if (type_str == shill::kTypeEthernet) {
    return ConnectionType::kEthernet;
  } else if (type_str == shill::kTypeWifi) {
    return ConnectionType::kWifi;
  } else if (type_str == shill::kTypeWimax) {
    return ConnectionType::kWimax;
  } else if (type_str == shill::kTypeBluetooth) {
    return ConnectionType::kBluetooth;
  } else if (type_str == shill::kTypeCellular) {
    return ConnectionType::kCellular;
  }
  return ConnectionType::kUnknown;
}

ConnectionTethering RealShillProvider::ParseConnectionTethering(
    const string& tethering_str) {
  if (tethering_str == shill::kTetheringNotDetectedState) {
    return ConnectionTethering::kNotDetected;
  } else if (tethering_str == shill::kTetheringSuspectedState) {
    return ConnectionTethering::kSuspected;
  } else if (tethering_str == shill::kTetheringConfirmedState) {
    return ConnectionTethering::kConfirmed;
  }
  return ConnectionTethering::kUnknown;
}

bool RealShillProvider::Init() {
  ManagerProxyInterface* manager_proxy = shill_proxy_->GetManagerProxy();
  if (!manager_proxy)
    return false;

  // Subscribe to the manager's PropertyChanged signal.
  manager_proxy->RegisterPropertyChangedSignalHandler(
      base::Bind(&RealShillProvider::OnManagerPropertyChanged,
                 base::Unretained(this)),
      base::Bind(&RealShillProvider::OnSignalConnected,
                 base::Unretained(this)));

  // Attempt to read initial connection status. Even if this fails because shill
  // is not responding (e.g. it is down) we'll be notified via "PropertyChanged"
  // signal as soon as it comes up, so this is not a critical step.
  brillo::VariantDictionary properties;
  brillo::ErrorPtr error;
  if (!manager_proxy->GetProperties(&properties, &error))
    return true;

  const auto& prop_default_service =
      properties.find(shill::kDefaultServiceProperty);
  if (prop_default_service != properties.end()) {
    OnManagerPropertyChanged(prop_default_service->first,
                             prop_default_service->second);
  }

  return true;
}

void RealShillProvider::OnManagerPropertyChanged(const string& name,
                                                 const brillo::Any& value) {
  if (name == shill::kDefaultServiceProperty) {
    dbus::ObjectPath service_path = value.TryGet<dbus::ObjectPath>();
    if (!service_path.IsValid()) {
      LOG(WARNING) << "Got an invalid DefaultService path. The property value "
                      "contains a "
                   << value.GetUndecoratedTypeName()
                   << ", read as the object path: '" << service_path.value()
                   << "'";
    }
    ProcessDefaultService(service_path);
  }
}

void RealShillProvider::OnSignalConnected(const string& interface_name,
                                          const string& signal_name,
                                          bool successful) {
  if (!successful) {
    LOG(ERROR) << "Couldn't connect to the signal " << interface_name << "."
               << signal_name;
  }
}

bool RealShillProvider::ProcessDefaultService(
    const dbus::ObjectPath& default_service_path) {
  // We assume that if the service path didn't change, then the connection
  // type and the tethering status of it also didn't change.
  if (default_service_path_ == default_service_path)
    return true;

  // Update the connection status.
  default_service_path_ = default_service_path;
  bool is_connected = (default_service_path_.IsValid() &&
                       default_service_path_.value() != "/");
  var_is_connected_.SetValue(is_connected);
  var_conn_last_changed_.SetValue(clock_->GetWallclockTime());

  if (!is_connected) {
    var_conn_type_.UnsetValue();
    var_conn_tethering_.UnsetValue();
    return true;
  }

  // We create and dispose the ServiceProxyInterface on every request.
  std::unique_ptr<ServiceProxyInterface> service =
      shill_proxy_->GetServiceForPath(default_service_path_);

  // Get the connection properties synchronously.
  brillo::VariantDictionary properties;
  brillo::ErrorPtr error;
  if (!service->GetProperties(&properties, &error)) {
    var_conn_type_.UnsetValue();
    var_conn_tethering_.UnsetValue();
    return false;
  }

  // Get the connection tethering mode.
  const auto& prop_tethering = properties.find(shill::kTetheringProperty);
  if (prop_tethering == properties.end()) {
    // Remove the value if not present on the service. This most likely means an
    // error in shill and the policy will handle it, but we will print a log
    // message as well for accessing an unused variable.
    var_conn_tethering_.UnsetValue();
    LOG(ERROR) << "Could not find connection type (service: "
               << default_service_path_.value() << ")";
  } else {
    // If the property doesn't contain a string value, the empty string will
    // become kUnknown.
    var_conn_tethering_.SetValue(
        ParseConnectionTethering(prop_tethering->second.TryGet<string>()));
  }

  // Get the connection type.
  const auto& prop_type = properties.find(shill::kTypeProperty);
  if (prop_type == properties.end()) {
    var_conn_type_.UnsetValue();
    LOG(ERROR) << "Could not find connection tethering mode (service: "
               << default_service_path_.value() << ")";
  } else {
    string type_str = prop_type->second.TryGet<string>();
    if (type_str == shill::kTypeVPN) {
      const auto& prop_physical =
          properties.find(shill::kPhysicalTechnologyProperty);
      if (prop_physical == properties.end()) {
        LOG(ERROR) << "No PhysicalTechnology property found for a VPN"
                   << " connection (service: " << default_service_path_.value()
                   << "). Using default kUnknown value.";
        var_conn_type_.SetValue(ConnectionType::kUnknown);
      } else {
        var_conn_type_.SetValue(
            ParseConnectionType(prop_physical->second.TryGet<string>()));
      }
    } else {
      var_conn_type_.SetValue(ParseConnectionType(type_str));
    }
  }

  return true;
}

}  // namespace chromeos_update_manager