// 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 "content/browser/geolocation/geolocation_dispatcher_host.h"

#include <utility>

#include "base/bind.h"
#include "base/metrics/histogram.h"
#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_message_filter.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/geoposition.h"
#include "content/common/geolocation_messages.h"

namespace content {
namespace {

// Geoposition error codes for reporting in UMA.
enum GeopositionErrorCode {
  // NOTE: Do not renumber these as that would confuse interpretation of
  // previously logged data. When making changes, also update the enum list
  // in tools/metrics/histograms/histograms.xml to keep it in sync.

  // There was no error.
  GEOPOSITION_ERROR_CODE_NONE = 0,

  // User denied use of geolocation.
  GEOPOSITION_ERROR_CODE_PERMISSION_DENIED = 1,

  // Geoposition could not be determined.
  GEOPOSITION_ERROR_CODE_POSITION_UNAVAILABLE = 2,

  // Timeout.
  GEOPOSITION_ERROR_CODE_TIMEOUT = 3,

  // NOTE: Add entries only immediately above this line.
  GEOPOSITION_ERROR_CODE_COUNT = 4
};

void RecordGeopositionErrorCode(Geoposition::ErrorCode error_code) {
  GeopositionErrorCode code = GEOPOSITION_ERROR_CODE_NONE;
  switch (error_code) {
    case Geoposition::ERROR_CODE_NONE:
      code = GEOPOSITION_ERROR_CODE_NONE;
      break;
    case Geoposition::ERROR_CODE_PERMISSION_DENIED:
      code = GEOPOSITION_ERROR_CODE_PERMISSION_DENIED;
      break;
    case Geoposition::ERROR_CODE_POSITION_UNAVAILABLE:
      code = GEOPOSITION_ERROR_CODE_POSITION_UNAVAILABLE;
      break;
    case Geoposition::ERROR_CODE_TIMEOUT:
      code = GEOPOSITION_ERROR_CODE_TIMEOUT;
      break;
  }
  UMA_HISTOGRAM_ENUMERATION("Geolocation.LocationUpdate.ErrorCode",
                            code,
                            GEOPOSITION_ERROR_CODE_COUNT);
}

}  // namespace

GeolocationDispatcherHost::PendingPermission::PendingPermission(
    int render_frame_id,
    int render_process_id,
    int bridge_id)
    : render_frame_id(render_frame_id),
      render_process_id(render_process_id),
      bridge_id(bridge_id) {
}

GeolocationDispatcherHost::PendingPermission::~PendingPermission() {
}

GeolocationDispatcherHost::GeolocationDispatcherHost(
    WebContents* web_contents)
    : WebContentsObserver(web_contents),
      paused_(false),
      weak_factory_(this) {
  // This is initialized by WebContentsImpl. Do not add any non-trivial
  // initialization here, defer to OnStartUpdating which is triggered whenever
  // a javascript geolocation object is actually initialized.
}

GeolocationDispatcherHost::~GeolocationDispatcherHost() {
}

void GeolocationDispatcherHost::RenderFrameDeleted(
    RenderFrameHost* render_frame_host) {
  OnStopUpdating(render_frame_host);
}

void GeolocationDispatcherHost::RenderViewHostChanged(
    RenderViewHost* old_host,
    RenderViewHost* new_host) {
  updating_frames_.clear();
  paused_ = false;
  geolocation_subscription_.reset();
}

bool GeolocationDispatcherHost::OnMessageReceived(
    const IPC::Message& msg, RenderFrameHost* render_frame_host) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP_WITH_PARAM(GeolocationDispatcherHost, msg,
                                   render_frame_host)
    IPC_MESSAGE_HANDLER(GeolocationHostMsg_RequestPermission,
                        OnRequestPermission)
    IPC_MESSAGE_HANDLER(GeolocationHostMsg_CancelPermissionRequest,
                        OnCancelPermissionRequest)
    IPC_MESSAGE_HANDLER(GeolocationHostMsg_StartUpdating, OnStartUpdating)
    IPC_MESSAGE_HANDLER(GeolocationHostMsg_StopUpdating, OnStopUpdating)
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}

void GeolocationDispatcherHost::OnLocationUpdate(
    const Geoposition& geoposition) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));

  RecordGeopositionErrorCode(geoposition.error_code);
  if (paused_)
    return;

  for (std::map<RenderFrameHost*, bool>::iterator i = updating_frames_.begin();
       i != updating_frames_.end(); ++i) {
    i->first->Send(new GeolocationMsg_PositionUpdated(
        i->first->GetRoutingID(), geoposition));
  }
}

void GeolocationDispatcherHost::OnRequestPermission(
    RenderFrameHost* render_frame_host,
    int bridge_id,
    const GURL& requesting_frame,
    bool user_gesture) {
  int render_process_id = render_frame_host->GetProcess()->GetID();
  int render_frame_id = render_frame_host->GetRoutingID();

  PendingPermission pending_permission(
      render_frame_id, render_process_id, bridge_id);
  pending_permissions_.push_back(pending_permission);

  GetContentClient()->browser()->RequestGeolocationPermission(
      web_contents(),
      bridge_id,
      requesting_frame,
      user_gesture,
      base::Bind(&GeolocationDispatcherHost::SendGeolocationPermissionResponse,
                 weak_factory_.GetWeakPtr(),
                 render_process_id, render_frame_id, bridge_id),
      &pending_permissions_.back().cancel);
}

void GeolocationDispatcherHost::OnCancelPermissionRequest(
    RenderFrameHost* render_frame_host,
    int bridge_id,
    const GURL& requesting_frame) {
  int render_process_id = render_frame_host->GetProcess()->GetID();
  int render_frame_id = render_frame_host->GetRoutingID();
  for (size_t i = 0; i < pending_permissions_.size(); ++i) {
    if (pending_permissions_[i].render_process_id == render_process_id &&
        pending_permissions_[i].render_frame_id == render_frame_id &&
        pending_permissions_[i].bridge_id == bridge_id) {
      if (!pending_permissions_[i].cancel.is_null())
        pending_permissions_[i].cancel.Run();
      pending_permissions_.erase(pending_permissions_.begin() + i);
      return;
    }
  }
}

void GeolocationDispatcherHost::OnStartUpdating(
    RenderFrameHost* render_frame_host,
    const GURL& requesting_frame,
    bool enable_high_accuracy) {
  // StartUpdating() can be invoked as a result of high-accuracy mode
  // being enabled / disabled. No need to record the dispatcher again.
  UMA_HISTOGRAM_BOOLEAN(
      "Geolocation.GeolocationDispatcherHostImpl.EnableHighAccuracy",
      enable_high_accuracy);

  updating_frames_[render_frame_host] = enable_high_accuracy;
  RefreshGeolocationOptions();
}

void GeolocationDispatcherHost::OnStopUpdating(
    RenderFrameHost* render_frame_host) {
  updating_frames_.erase(render_frame_host);
  RefreshGeolocationOptions();
}

void GeolocationDispatcherHost::PauseOrResume(bool should_pause) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  paused_ = should_pause;
  RefreshGeolocationOptions();
}

void GeolocationDispatcherHost::RefreshGeolocationOptions() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));

  if (updating_frames_.empty() || paused_) {
    geolocation_subscription_.reset();
    return;
  }

  bool high_accuracy = false;
  for (std::map<RenderFrameHost*, bool>::iterator i =
            updating_frames_.begin(); i != updating_frames_.end(); ++i) {
    if (i->second) {
      high_accuracy = true;
      break;
    }
  }
  geolocation_subscription_ = GeolocationProvider::GetInstance()->
      AddLocationUpdateCallback(
          base::Bind(&GeolocationDispatcherHost::OnLocationUpdate,
                      base::Unretained(this)),
          high_accuracy);
}

void GeolocationDispatcherHost::SendGeolocationPermissionResponse(
    int render_process_id,
    int render_frame_id,
    int bridge_id,
    bool allowed) {
  for (size_t i = 0; i < pending_permissions_.size(); ++i) {
    if (pending_permissions_[i].render_process_id == render_process_id &&
        pending_permissions_[i].render_frame_id == render_frame_id &&
        pending_permissions_[i].bridge_id == bridge_id) {
      RenderFrameHost* render_frame_host =
          RenderFrameHost::FromID(render_process_id, render_frame_id);
      if (render_frame_host) {
        render_frame_host->Send(new GeolocationMsg_PermissionSet(
            render_frame_id, bridge_id, allowed));
      }

      if (allowed) {
        GeolocationProviderImpl::GetInstance()->
            UserDidOptIntoLocationServices();
      }

      pending_permissions_.erase(pending_permissions_.begin() + i);
      return;
    }
  }

  NOTREACHED();
}

}  // namespace content