// 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 "ash/wm/gestures/bezel_gesture_handler.h"

#include "ash/accelerators/accelerator_controller.h"
#include "ash/accelerators/accelerator_table.h"
#include "ash/ash_switches.h"
#include "ash/launcher/launcher.h"
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/system/brightness/brightness_control_delegate.h"
#include "ash/system/tray/system_tray_delegate.h"
#include "ash/volume_control_delegate.h"
#include "base/command_line.h"
#include "ui/aura/root_window.h"
#include "ui/base/events/event.h"
#include "ui/base/ui_base_switches.h"
#include "ui/gfx/screen.h"

namespace {

// Device bezel operation constants (volume/brightness slider).

// This is the minimal brightness value allowed for the display.
const double kMinBrightnessPercent = 5.0;
// For device operation, the finger is not allowed to enter the screen more
// then this fraction of the size of the screen.
const double kAllowableScreenOverlapForDeviceCommand = 0.0005;

// TODO(skuhne): The noise reduction can be removed when / if we are adding a
// more general reduction.
// To avoid unwanted noise activation, the first 'n' events are being ignored
// for bezel device gestures.
const int kIgnoreFirstBezelDeviceEvents = 10;
// Within these 'n' huge coordinate changes are not allowed. The threshold is
// given in fraction of screen resolution changes.
const double kBezelNoiseDeltaFilter = 0.1;
// To avoid the most frequent noise (extreme locations) the bezel percent
// sliders will not cover the entire screen. We scale therefore the percent
// value by this many percent for minima and maxima extension.
// (Range extends to -kMinMaxInsetPercent .. 100 + kMinMaxInsetPercent).
const double kMinMaxInsetPercent = 5.0;
// To make it possible to reach minimas and maximas easily a range extension
// of -kMinMaxCutOffPercent .. 100 + kMinMaxCutOffPercent will be clamped to
// 0..100%. Everything beyond that will be ignored.
const double kMinMaxCutOffPercent = 2.0;

}  // namespace

namespace ash {
namespace internal {

BezelGestureHandler::BezelGestureHandler()
    : overlap_percent_(5),
      start_location_(BEZEL_START_UNSET),
      orientation_(SCROLL_ORIENTATION_UNSET),
      is_scrubbing_(false),
      initiation_delay_events_(0) {
  enable_bezel_device_control_ =
      CommandLine::ForCurrentProcess()->HasSwitch(
                ::switches::kEnableBezelTouch);
}

BezelGestureHandler::~BezelGestureHandler() {
}

void BezelGestureHandler::ProcessGestureEvent(aura::Window* target,
                                              const ui::GestureEvent& event) {
  switch (event.type()) {
    case ui::ET_GESTURE_SCROLL_BEGIN:
      HandleBezelGestureStart(target, event);

      // TODO(sad|skuhne): Fix the bezel gestures for when the shelf is on the
      //                   left/right of the screen.
      if (start_location_ == BEZEL_START_BOTTOM)
        shelf_handler_.ProcessGestureEvent(event);
      break;
    case ui::ET_GESTURE_SCROLL_UPDATE:
      // Check if a valid start position has been set.
      if (start_location_ == BEZEL_START_UNSET)
        break;

      if (DetermineGestureOrientation(event))
        HandleBezelGestureUpdate(target, event);
      break;
    case ui::ET_GESTURE_SCROLL_END:
    case ui::ET_SCROLL_FLING_START:
      if (start_location_ == BEZEL_START_BOTTOM)
        shelf_handler_.ProcessGestureEvent(event);
      HandleBezelGestureEnd();
      break;
    default:
      break;
  }
}

bool BezelGestureHandler::HandleDeviceControl(
    const gfx::Rect& screen,
    const ui::GestureEvent& event) {
  // Get the slider position as value from the absolute position.
  // Note that the highest value is at the top.
  double percent = 100.0 - 100.0 * (event.y() - screen.y()) / screen.height();
  if (!DeNoiseBezelSliderPosition(&percent)) {
    // Note: Even though this particular event might be noise, the gesture
    // itself is still valid and should not get cancelled.
    return false;
  }
  ash::AcceleratorController* accelerator =
      ash::Shell::GetInstance()->accelerator_controller();
  if (start_location_ == BEZEL_START_LEFT) {
    ash::BrightnessControlDelegate* delegate =
        accelerator->brightness_control_delegate();
    if (delegate)
      delegate->SetBrightnessPercent(
          LimitBezelBrightnessFromSlider(percent), true);
  } else if (start_location_ == BEZEL_START_RIGHT) {
    Shell::GetInstance()->system_tray_delegate()->GetVolumeControlDelegate()->
        SetVolumePercent(percent);
  } else {
    // No further events are necessary.
    return true;
  }

  // More notifications can be send.
  return false;
}

bool BezelGestureHandler::HandleLauncherControl(const ui::GestureEvent& event) {
  CHECK_EQ(BEZEL_START_BOTTOM, start_location_);
  shelf_handler_.ProcessGestureEvent(event);
  return event.type() == ui::ET_GESTURE_SCROLL_END ||
         event.type() == ui::ET_SCROLL_FLING_START;
}

bool BezelGestureHandler::HandleApplicationControl(
    const ui::GestureEvent& event) {
  ash::AcceleratorController* accelerator =
      ash::Shell::GetInstance()->accelerator_controller();
  if (start_location_ == BEZEL_START_LEFT && event.details().scroll_x() > 0) {
    accelerator->PerformAction(CYCLE_BACKWARD_LINEAR, ui::Accelerator());
  } else if (start_location_ == BEZEL_START_RIGHT &&
      event.details().scroll_x() < 0) {
    accelerator->PerformAction(CYCLE_FORWARD_LINEAR, ui::Accelerator());
  } else {
    return false;
  }

  // No further notifications for this gesture.
  return true;
}

void BezelGestureHandler::HandleBezelGestureStart(
    aura::Window* target,
    const ui::GestureEvent& event) {
  gfx::Rect screen =
      Shell::GetScreen()->GetDisplayNearestWindow(target).bounds();
  int overlap_area = screen.width() * overlap_percent_ / 100;
  orientation_ = SCROLL_ORIENTATION_UNSET;

  if (event.x() <= screen.x() + overlap_area) {
    start_location_ = BEZEL_START_LEFT;
  } else if (event.x() >= screen.right() - overlap_area) {
    start_location_ = BEZEL_START_RIGHT;
  } else if (event.y() >= screen.bottom()) {
    start_location_ = BEZEL_START_BOTTOM;
  }
}

bool BezelGestureHandler::DetermineGestureOrientation(
    const ui::GestureEvent& event) {
  if (orientation_ == SCROLL_ORIENTATION_UNSET) {
    if (!event.details().scroll_x() && !event.details().scroll_y())
      return false;

    // For left and right the scroll angle needs to be much steeper to
    // be accepted for a 'device configuration' gesture.
    if (start_location_ == BEZEL_START_LEFT ||
        start_location_ == BEZEL_START_RIGHT) {
      orientation_ = abs(event.details().scroll_y()) >
                     abs(event.details().scroll_x()) * 3 ?
          SCROLL_ORIENTATION_VERTICAL : SCROLL_ORIENTATION_HORIZONTAL;
    } else {
      orientation_ = abs(event.details().scroll_y()) >
                     abs(event.details().scroll_x()) ?
          SCROLL_ORIENTATION_VERTICAL : SCROLL_ORIENTATION_HORIZONTAL;
    }

    // Reset the delay counter for noise event filtering.
    initiation_delay_events_ = 0;
  }
  return true;
}

void BezelGestureHandler::HandleBezelGestureUpdate(
    aura::Window* target,
    const ui::GestureEvent& event) {
  if (orientation_ == SCROLL_ORIENTATION_HORIZONTAL) {
    if (HandleApplicationControl(event))
      start_location_ = BEZEL_START_UNSET;
  } else {
    if (start_location_ == BEZEL_START_BOTTOM) {
      if (HandleLauncherControl(event))
        start_location_ = BEZEL_START_UNSET;
    } else {
      // Check if device gestures should be performed or not.
      if (!enable_bezel_device_control_) {
        start_location_ = BEZEL_START_UNSET;
        return;
      }
      gfx::Rect screen =
          Shell::GetScreen()->GetDisplayNearestWindow(target).bounds();
      // Limit the user gesture "mostly" to the off screen area and check for
      // noise invocation.
      if (!GestureInBezelArea(screen, event) ||
          BezelGestureMightBeNoise(screen, event))
        return;
      if (HandleDeviceControl(screen, event))
        start_location_ = BEZEL_START_UNSET;
    }
  }
}

void BezelGestureHandler::HandleBezelGestureEnd() {
  // All which is needed is to set the gesture start location to undefined.
  start_location_ = BEZEL_START_UNSET;
}

bool BezelGestureHandler::GestureInBezelArea(
    const gfx::Rect& screen,
    const ui::GestureEvent& event) {
  // Limit the gesture mostly to the off screen.
  double allowable_offset =
      screen.width() * kAllowableScreenOverlapForDeviceCommand;
  if ((start_location_ == BEZEL_START_LEFT &&
       event.x() > allowable_offset) ||
      (start_location_ == BEZEL_START_RIGHT &&
       event.x() < screen.width() - allowable_offset)) {
    start_location_ = BEZEL_START_UNSET;
    return false;
  }
  return true;
}

bool BezelGestureHandler::BezelGestureMightBeNoise(
    const gfx::Rect& screen,
    const ui::GestureEvent& event) {
  // The first events will not trigger an action.
  if (initiation_delay_events_++ < kIgnoreFirstBezelDeviceEvents) {
    // When the values are too far apart we ignore it since it might
    // be random noise.
    double delta_y = event.details().scroll_y();
    double span_y = screen.height();
    if (abs(delta_y / span_y) > kBezelNoiseDeltaFilter)
      start_location_ = BEZEL_START_UNSET;
    return true;
  }
  return false;
}

bool BezelGestureHandler::DeNoiseBezelSliderPosition(double* percent) {
  // The range gets passed as 0..100% and is extended to the range of
  // (-kMinMaxInsetPercent) .. (100 + kMinMaxInsetPercent). This way we can
  // cut off the extreme upper and lower values which are prone to noise.
  // It additionally adds a "security buffer" which can then be clamped to the
  // extremes to empower the user to get to these values (0% and 100%).
  *percent = *percent * (100.0 + 2 * kMinMaxInsetPercent) / 100 -
      kMinMaxInsetPercent;
  // Values which fall outside of the acceptable inner range area get ignored.
  if (*percent < -kMinMaxCutOffPercent ||
      *percent > 100.0 + kMinMaxCutOffPercent)
    return false;
  // Excessive values get then clamped to the 0..100% range.
  *percent = std::max(std::min(*percent, 100.0), 0.0);
  return true;
}

double BezelGestureHandler::LimitBezelBrightnessFromSlider(double percent) {
  // Turning off the display makes no sense, so we map the accessible range to
  // kMinimumBrightness .. 100%.
  percent = (percent + kMinBrightnessPercent) * 100.0 /
      (100.0 + kMinBrightnessPercent);
  // Clamp to avoid rounding issues.
  return std::min(percent, 100.0);
}

}  // namespace internal
}  // namespace ash
