// Copyright 2013 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/sticky_keys.h"

#if defined(USE_X11)
#include <X11/extensions/XInput2.h>
#include <X11/Xlib.h>
#undef RootWindow
#endif

#include "base/basictypes.h"
#include "base/debug/stack_trace.h"
#include "ui/aura/root_window.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tracker.h"
#include "ui/events/event.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"

namespace ash {

namespace {

// Returns true if the type of mouse event should be modified by sticky keys.
bool ShouldModifyMouseEvent(ui::MouseEvent* event) {
  ui::EventType type = event->type();
  return type == ui::ET_MOUSE_PRESSED || type == ui::ET_MOUSE_RELEASED ||
         type == ui::ET_MOUSEWHEEL;
}

// An implementation of StickyKeysHandler::StickyKeysHandlerDelegate.
class StickyKeysHandlerDelegateImpl :
    public StickyKeysHandler::StickyKeysHandlerDelegate {
 public:
  StickyKeysHandlerDelegateImpl();
  virtual ~StickyKeysHandlerDelegateImpl();

  // StickyKeysHandlerDelegate overrides.
  virtual void DispatchKeyEvent(ui::KeyEvent* event,
                                aura::Window* target) OVERRIDE;

  virtual void DispatchMouseEvent(ui::MouseEvent* event,
                                  aura::Window* target) OVERRIDE;

  virtual void DispatchScrollEvent(ui::ScrollEvent* event,
                                   aura::Window* target) OVERRIDE;
 private:
  DISALLOW_COPY_AND_ASSIGN(StickyKeysHandlerDelegateImpl);
};

StickyKeysHandlerDelegateImpl::StickyKeysHandlerDelegateImpl() {
}

StickyKeysHandlerDelegateImpl::~StickyKeysHandlerDelegateImpl() {
}

void StickyKeysHandlerDelegateImpl::DispatchKeyEvent(ui::KeyEvent* event,
                                                     aura::Window* target) {
  DCHECK(target);
  target->GetDispatcher()->AsRootWindowHostDelegate()->OnHostKeyEvent(event);
}

void StickyKeysHandlerDelegateImpl::DispatchMouseEvent(ui::MouseEvent* event,
                                                       aura::Window* target) {
  DCHECK(target);
  // We need to send a new, untransformed mouse event to the host.
  if (event->IsMouseWheelEvent()) {
    ui::MouseWheelEvent new_event(*static_cast<ui::MouseWheelEvent*>(event));
    target->GetDispatcher()->AsRootWindowHostDelegate()
        ->OnHostMouseEvent(&new_event);
  } else {
    ui::MouseEvent new_event(*event, target, target->GetRootWindow());
    target->GetDispatcher()->AsRootWindowHostDelegate()
        ->OnHostMouseEvent(&new_event);
  }
}

void StickyKeysHandlerDelegateImpl::DispatchScrollEvent(
    ui::ScrollEvent* event,
    aura::Window* target)  {
  DCHECK(target);
  target->GetDispatcher()->AsRootWindowHostDelegate()
      ->OnHostScrollEvent(event);
}

}  // namespace

///////////////////////////////////////////////////////////////////////////////
//  StickyKeys
StickyKeys::StickyKeys()
    : enabled_(false),
      shift_sticky_key_(
          new StickyKeysHandler(ui::EF_SHIFT_DOWN,
                              new StickyKeysHandlerDelegateImpl())),
      alt_sticky_key_(
          new StickyKeysHandler(ui::EF_ALT_DOWN,
                                new StickyKeysHandlerDelegateImpl())),
      ctrl_sticky_key_(
          new StickyKeysHandler(ui::EF_CONTROL_DOWN,
                                new StickyKeysHandlerDelegateImpl())) {
}

StickyKeys::~StickyKeys() {
}

void StickyKeys::Enable(bool enabled) {
  if (enabled_ != enabled) {
    enabled_ = enabled;

    // Reset key handlers when activating sticky keys to ensure all
    // the handlers' states are reset.
    if (enabled_) {
      shift_sticky_key_.reset(
          new StickyKeysHandler(ui::EF_SHIFT_DOWN,
                              new StickyKeysHandlerDelegateImpl()));
      alt_sticky_key_.reset(
          new StickyKeysHandler(ui::EF_ALT_DOWN,
                                new StickyKeysHandlerDelegateImpl()));
      ctrl_sticky_key_.reset(
          new StickyKeysHandler(ui::EF_CONTROL_DOWN,
                                new StickyKeysHandlerDelegateImpl()));
    }
  }
}

bool StickyKeys::HandleKeyEvent(ui::KeyEvent* event) {
  return shift_sticky_key_->HandleKeyEvent(event) ||
      alt_sticky_key_->HandleKeyEvent(event) ||
      ctrl_sticky_key_->HandleKeyEvent(event);
  return ctrl_sticky_key_->HandleKeyEvent(event);
}

bool StickyKeys::HandleMouseEvent(ui::MouseEvent* event) {
  return shift_sticky_key_->HandleMouseEvent(event) ||
      alt_sticky_key_->HandleMouseEvent(event) ||
      ctrl_sticky_key_->HandleMouseEvent(event);
}

bool StickyKeys::HandleScrollEvent(ui::ScrollEvent* event) {
  return shift_sticky_key_->HandleScrollEvent(event) ||
      alt_sticky_key_->HandleScrollEvent(event) ||
      ctrl_sticky_key_->HandleScrollEvent(event);
}

void StickyKeys::OnKeyEvent(ui::KeyEvent* event) {
  // Do not consume a translated key event which is generated by an IME.
  if (event->type() == ui::ET_TRANSLATED_KEY_PRESS ||
      event->type() == ui::ET_TRANSLATED_KEY_RELEASE) {
    return;
  }

  if (enabled_ && HandleKeyEvent(event))
    event->StopPropagation();
}

void StickyKeys::OnMouseEvent(ui::MouseEvent* event) {
  if (enabled_ && HandleMouseEvent(event))
    event->StopPropagation();
}

void StickyKeys::OnScrollEvent(ui::ScrollEvent* event) {
  if (enabled_ && HandleScrollEvent(event))
    event->StopPropagation();
}

///////////////////////////////////////////////////////////////////////////////
//  StickyKeysHandler
StickyKeysHandler::StickyKeysHandler(ui::EventFlags target_modifier_flag,
                                     StickyKeysHandlerDelegate* delegate)
    : modifier_flag_(target_modifier_flag),
      current_state_(DISABLED),
      event_from_myself_(false),
      preparing_to_enable_(false),
      scroll_delta_(0),
      delegate_(delegate) {
}

StickyKeysHandler::~StickyKeysHandler() {
}

StickyKeysHandler::StickyKeysHandlerDelegate::StickyKeysHandlerDelegate() {
}

StickyKeysHandler::StickyKeysHandlerDelegate::~StickyKeysHandlerDelegate() {
}

bool StickyKeysHandler::HandleKeyEvent(ui::KeyEvent* event) {
  if (event_from_myself_)
    return false;  // Do not handle self-generated key event.
  switch (current_state_) {
    case DISABLED:
      return HandleDisabledState(event);
    case ENABLED:
      return HandleEnabledState(event);
    case LOCKED:
      return HandleLockedState(event);
  }
  NOTREACHED();
  return false;
}

bool StickyKeysHandler::HandleMouseEvent(ui::MouseEvent* event) {
  preparing_to_enable_ = false;
  if (event_from_myself_ || current_state_ == DISABLED
      || !ShouldModifyMouseEvent(event)) {
    return false;
  }
  DCHECK(current_state_ == ENABLED || current_state_ == LOCKED);

  AppendModifier(event);
  // Only disable on the mouse released event in normal, non-locked mode.
  if (current_state_ == ENABLED && event->type() != ui::ET_MOUSE_PRESSED) {
    current_state_ = DISABLED;
    DispatchEventAndReleaseModifier(event);
    return true;
  }

  return false;
}

bool StickyKeysHandler::HandleScrollEvent(ui::ScrollEvent* event) {
  preparing_to_enable_ = false;
  if (event_from_myself_ || current_state_ == DISABLED)
    return false;
  DCHECK(current_state_ == ENABLED || current_state_ == LOCKED);

  // We detect a direction change if the current |scroll_delta_| is assigned
  // and the offset of the current scroll event has the opposing sign.
  bool direction_changed = false;
  if (current_state_ == ENABLED && event->type() == ui::ET_SCROLL) {
    int offset = event->y_offset();
    if (scroll_delta_)
      direction_changed = offset * scroll_delta_ <= 0;
    scroll_delta_ = offset;
  }

  if (!direction_changed)
    AppendModifier(event);

  // We want to modify all the scroll events in the scroll sequence, which ends
  // with a fling start event. We also stop when the scroll sequence changes
  // direction.
  if (current_state_ == ENABLED &&
      (event->type() == ui::ET_SCROLL_FLING_START || direction_changed)) {
    current_state_ = DISABLED;
    scroll_delta_ = 0;
    DispatchEventAndReleaseModifier(event);
    return true;
  }

  return false;
}

StickyKeysHandler::KeyEventType
    StickyKeysHandler::TranslateKeyEvent(ui::KeyEvent* event) {
  bool is_target_key = false;
  if (event->key_code() == ui::VKEY_SHIFT ||
      event->key_code() == ui::VKEY_LSHIFT ||
      event->key_code() == ui::VKEY_RSHIFT) {
    is_target_key = (modifier_flag_ == ui::EF_SHIFT_DOWN);
  } else if (event->key_code() == ui::VKEY_CONTROL ||
      event->key_code() == ui::VKEY_LCONTROL ||
      event->key_code() == ui::VKEY_RCONTROL) {
    is_target_key = (modifier_flag_ == ui::EF_CONTROL_DOWN);
  } else if (event->key_code() == ui::VKEY_MENU ||
      event->key_code() == ui::VKEY_LMENU ||
      event->key_code() == ui::VKEY_RMENU) {
    is_target_key = (modifier_flag_ == ui::EF_ALT_DOWN);
  } else {
    return event->type() == ui::ET_KEY_PRESSED ?
        NORMAL_KEY_DOWN : NORMAL_KEY_UP;
  }

  if (is_target_key) {
    return event->type() == ui::ET_KEY_PRESSED ?
        TARGET_MODIFIER_DOWN : TARGET_MODIFIER_UP;
  }
  return event->type() == ui::ET_KEY_PRESSED ?
      OTHER_MODIFIER_DOWN : OTHER_MODIFIER_UP;
}

bool StickyKeysHandler::HandleDisabledState(ui::KeyEvent* event) {
  switch (TranslateKeyEvent(event)) {
    case TARGET_MODIFIER_UP:
      if (preparing_to_enable_) {
        preparing_to_enable_ = false;
        scroll_delta_ = 0;
        current_state_ = ENABLED;
        modifier_up_event_.reset(new ui::KeyEvent(*event));
        return true;
      }
      return false;
    case TARGET_MODIFIER_DOWN:
      preparing_to_enable_ = true;
      return false;
    case NORMAL_KEY_DOWN:
      preparing_to_enable_ = false;
      return false;
    case NORMAL_KEY_UP:
    case OTHER_MODIFIER_DOWN:
    case OTHER_MODIFIER_UP:
      return false;
  }
  NOTREACHED();
  return false;
}

bool StickyKeysHandler::HandleEnabledState(ui::KeyEvent* event) {
  switch (TranslateKeyEvent(event)) {
    case NORMAL_KEY_UP:
    case TARGET_MODIFIER_DOWN:
      return true;
    case TARGET_MODIFIER_UP:
      current_state_ = LOCKED;
      modifier_up_event_.reset();
      return true;
    case NORMAL_KEY_DOWN: {
      current_state_ = DISABLED;
      AppendModifier(event);
      DispatchEventAndReleaseModifier(event);
      return true;
    }
    case OTHER_MODIFIER_DOWN:
    case OTHER_MODIFIER_UP:
      return false;
  }
  NOTREACHED();
  return false;
}

bool StickyKeysHandler::HandleLockedState(ui::KeyEvent* event) {
  switch (TranslateKeyEvent(event)) {
    case TARGET_MODIFIER_DOWN:
      return true;
    case TARGET_MODIFIER_UP:
      current_state_ = DISABLED;
      return false;
    case NORMAL_KEY_DOWN:
    case NORMAL_KEY_UP:
      AppendModifier(event);
      return false;
    case OTHER_MODIFIER_DOWN:
    case OTHER_MODIFIER_UP:
      return false;
  }
  NOTREACHED();
  return false;
}

void StickyKeysHandler::DispatchEventAndReleaseModifier(ui::Event* event) {
  DCHECK(event->IsKeyEvent() ||
         event->IsMouseEvent() ||
         event->IsScrollEvent());
  DCHECK(modifier_up_event_.get());
  aura::Window* target = static_cast<aura::Window*>(event->target());
  DCHECK(target);
  aura::Window* root_window = target->GetRootWindow();
  DCHECK(root_window);

  aura::WindowTracker window_tracker;
  window_tracker.Add(target);

  event_from_myself_ = true;
  if (event->IsKeyEvent()) {
    delegate_->DispatchKeyEvent(static_cast<ui::KeyEvent*>(event), target);
  } else if (event->IsMouseEvent()) {
    delegate_->DispatchMouseEvent(static_cast<ui::MouseEvent*>(event), target);
  } else {
    delegate_->DispatchScrollEvent(
        static_cast<ui::ScrollEvent*>(event), target);
  }

  // The action triggered above may have destroyed the event target, in which
  // case we will dispatch the modifier up event to the root window instead.
  aura::Window* modifier_up_target =
      window_tracker.Contains(target) ? target : root_window;
  delegate_->DispatchKeyEvent(modifier_up_event_.get(), modifier_up_target);
  event_from_myself_ = false;
}

void StickyKeysHandler::AppendNativeEventMask(unsigned int* state) {
  unsigned int& state_ref = *state;
  switch (modifier_flag_) {
    case ui::EF_CONTROL_DOWN:
      state_ref |= ControlMask;
      break;
    case ui::EF_ALT_DOWN:
      state_ref |= Mod1Mask;
      break;
    case ui::EF_SHIFT_DOWN:
      state_ref |= ShiftMask;
      break;
    default:
      NOTREACHED();
  }
}

void StickyKeysHandler::AppendModifier(ui::KeyEvent* event) {
#if defined(USE_X11)
  XEvent* xev = event->native_event();
  if (xev) {
    XKeyEvent* xkey = &(xev->xkey);
    AppendNativeEventMask(&xkey->state);
  }
#elif defined(USE_OZONE)
  NOTIMPLEMENTED() << "Modifier key is not handled";
#endif
  event->set_flags(event->flags() | modifier_flag_);
  event->set_character(ui::GetCharacterFromKeyCode(event->key_code(),
                                                   event->flags()));
  event->NormalizeFlags();
}

void StickyKeysHandler::AppendModifier(ui::MouseEvent* event) {
#if defined(USE_X11)
  XEvent* xev = event->native_event();
  if (xev) {
    XButtonEvent* xkey = &(xev->xbutton);
    AppendNativeEventMask(&xkey->state);
  }
#elif defined(USE_OZONE)
  NOTIMPLEMENTED() << "Modifier key is not handled";
#endif
  event->set_flags(event->flags() | modifier_flag_);
}

void StickyKeysHandler::AppendModifier(ui::ScrollEvent* event) {
#if defined(USE_X11)
  XEvent* xev = event->native_event();
  if (xev) {
    XIDeviceEvent* xievent =
        static_cast<XIDeviceEvent*>(xev->xcookie.data);
    if (xievent) {
      AppendNativeEventMask(reinterpret_cast<unsigned int*>(
          &xievent->mods.effective));
    }
  }
#elif defined(USE_OZONE)
  NOTIMPLEMENTED() << "Modifier key is not handled";
#endif
  event->set_flags(event->flags() | modifier_flag_);
}

}  // namespace ash