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

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

#include "ash/sticky_keys/sticky_keys_overlay.h"
#include "base/basictypes.h"
#include "base/debug/stack_trace.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tracker.h"
#include "ui/aura/window_tree_host.h"
#include "ui/events/event.h"
#include "ui/events/event_processor.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:
  void DispatchEvent(ui::Event* event, aura::Window* target);

  DISALLOW_COPY_AND_ASSIGN(StickyKeysHandlerDelegateImpl);
};

StickyKeysHandlerDelegateImpl::StickyKeysHandlerDelegateImpl() {
}

StickyKeysHandlerDelegateImpl::~StickyKeysHandlerDelegateImpl() {
}

void StickyKeysHandlerDelegateImpl::DispatchKeyEvent(ui::KeyEvent* event,
                                                     aura::Window* target) {
  DispatchEvent(event, target);
}

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()) {
    aura::Window* source = static_cast<aura::Window*>(event->target());
    ui::MouseWheelEvent new_event(*static_cast<ui::MouseWheelEvent*>(event),
                                  source,
                                  source->GetRootWindow());
    // Transform the location back to host coordinates before dispatching.
    new_event.UpdateForRootTransform(source->GetHost()->GetRootTransform());
    DispatchEvent(&new_event, target);
  } else {
    aura::Window* source = static_cast<aura::Window*>(event->target());
    ui::MouseEvent new_event(*event, source, source->GetRootWindow());
    // Transform the location back to host coordinates before dispatching.
    new_event.UpdateForRootTransform(source->GetHost()->GetRootTransform());
    DispatchEvent(&new_event, target);
  }
}

void StickyKeysHandlerDelegateImpl::DispatchScrollEvent(
    ui::ScrollEvent* event,
    aura::Window* target)  {
  DispatchEvent(event, target);
}

void StickyKeysHandlerDelegateImpl::DispatchEvent(ui::Event* event,
                                                  aura::Window* target) {
  DCHECK(target);
  ui::EventDispatchDetails details =
      target->GetHost()->event_processor()->OnEventFromSource(event);
  if (details.dispatcher_destroyed)
    return;
}

}  // namespace

///////////////////////////////////////////////////////////////////////////////
//  StickyKeys
StickyKeysController::StickyKeysController()
    : enabled_(false),
      mod3_enabled_(false),
      altgr_enabled_(false) {
}

StickyKeysController::~StickyKeysController() {
}

void StickyKeysController::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()));
      altgr_sticky_key_.reset(
          new StickyKeysHandler(ui::EF_ALTGR_DOWN,
                                new StickyKeysHandlerDelegateImpl()));
      ctrl_sticky_key_.reset(
          new StickyKeysHandler(ui::EF_CONTROL_DOWN,
                                new StickyKeysHandlerDelegateImpl()));
      mod3_sticky_key_.reset(
          new StickyKeysHandler(ui::EF_MOD3_DOWN,
                                new StickyKeysHandlerDelegateImpl()));

      overlay_.reset(new StickyKeysOverlay());
      overlay_->SetModifierVisible(ui::EF_ALTGR_DOWN, altgr_enabled_);
      overlay_->SetModifierVisible(ui::EF_MOD3_DOWN, mod3_enabled_);
    } else if (overlay_) {
      overlay_->Show(false);
    }
  }
}

void StickyKeysController::SetModifiersEnabled(bool mod3_enabled,
                                               bool altgr_enabled) {
  mod3_enabled_ = mod3_enabled;
  altgr_enabled_ = altgr_enabled;
  if (overlay_) {
    overlay_->SetModifierVisible(ui::EF_ALTGR_DOWN, altgr_enabled_);
    overlay_->SetModifierVisible(ui::EF_MOD3_DOWN, mod3_enabled_);
  }
}

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

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

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

void StickyKeysController::OnKeyEvent(ui::KeyEvent* event) {
  // Do not consume a translated key event which is generated by an IME.
  if (event->IsTranslated())
    return;

  if (enabled_) {
    if (HandleKeyEvent(event))
      event->StopPropagation();
    UpdateOverlay();
  }
}

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

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

void StickyKeysController::UpdateOverlay() {
  overlay_->SetModifierKeyState(
      ui::EF_SHIFT_DOWN, shift_sticky_key_->current_state());
  overlay_->SetModifierKeyState(
      ui::EF_CONTROL_DOWN, ctrl_sticky_key_->current_state());
  overlay_->SetModifierKeyState(
      ui::EF_ALT_DOWN, alt_sticky_key_->current_state());
  overlay_->SetModifierKeyState(
      ui::EF_ALTGR_DOWN, altgr_sticky_key_->current_state());
  overlay_->SetModifierKeyState(
      ui::EF_MOD3_DOWN, mod3_sticky_key_->current_state());

  bool key_in_use =
      shift_sticky_key_->current_state() != STICKY_KEY_STATE_DISABLED ||
      alt_sticky_key_->current_state() != STICKY_KEY_STATE_DISABLED ||
      altgr_sticky_key_->current_state() != STICKY_KEY_STATE_DISABLED ||
      ctrl_sticky_key_->current_state() != STICKY_KEY_STATE_DISABLED ||
      mod3_sticky_key_->current_state() != STICKY_KEY_STATE_DISABLED;

  overlay_->Show(enabled_ && key_in_use);
}

StickyKeysOverlay* StickyKeysController::GetOverlayForTest() {
  return overlay_.get();
}

///////////////////////////////////////////////////////////////////////////////
//  StickyKeysHandler
StickyKeysHandler::StickyKeysHandler(ui::EventFlags modifier_flag,
                                     StickyKeysHandlerDelegate* delegate)
    : modifier_flag_(modifier_flag),
      current_state_(STICKY_KEY_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 STICKY_KEY_STATE_DISABLED:
      return HandleDisabledState(event);
    case STICKY_KEY_STATE_ENABLED:
      return HandleEnabledState(event);
    case STICKY_KEY_STATE_LOCKED:
      return HandleLockedState(event);
  }
  NOTREACHED();
  return false;
}

bool StickyKeysHandler::HandleMouseEvent(ui::MouseEvent* event) {
  if (ShouldModifyMouseEvent(event))
    preparing_to_enable_ = false;

  if (event_from_myself_ || current_state_ == STICKY_KEY_STATE_DISABLED
      || !ShouldModifyMouseEvent(event)) {
    return false;
  }
  DCHECK(current_state_ == STICKY_KEY_STATE_ENABLED ||
         current_state_ == STICKY_KEY_STATE_LOCKED);

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

  return false;
}

bool StickyKeysHandler::HandleScrollEvent(ui::ScrollEvent* event) {
  preparing_to_enable_ = false;
  if (event_from_myself_ || current_state_ == STICKY_KEY_STATE_DISABLED)
    return false;
  DCHECK(current_state_ == STICKY_KEY_STATE_ENABLED ||
         current_state_ == STICKY_KEY_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_ == STICKY_KEY_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_ == STICKY_KEY_STATE_ENABLED &&
      (event->type() == ui::ET_SCROLL_FLING_START || direction_changed)) {
    current_state_ = STICKY_KEY_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 if (event->key_code() == ui::VKEY_ALTGR) {
    is_target_key = (modifier_flag_ == ui::EF_ALTGR_DOWN);
  } else if (event->key_code() == ui::VKEY_OEM_8) {
    is_target_key = (modifier_flag_ == ui::EF_MOD3_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_ = STICKY_KEY_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_ = STICKY_KEY_STATE_LOCKED;
      modifier_up_event_.reset();
      return true;
    case NORMAL_KEY_DOWN: {
      current_state_ = STICKY_KEY_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_ = STICKY_KEY_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) {
#if defined(USE_X11)
  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_ALTGR_DOWN:
      state_ref |= Mod5Mask;
      break;
    case ui::EF_SHIFT_DOWN:
      state_ref |= ShiftMask;
      break;
    case ui::EF_MOD3_DOWN:
      state_ref |= Mod3Mask;
      break;
    default:
      NOTREACHED();
  }
#endif
}

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)
  // The native mouse event can either be a classic X button event or an
  // XInput2 button event.
  XEvent* xev = event->native_event();
  if (xev) {
    switch (xev->type) {
      case ButtonPress:
      case ButtonRelease: {
        XButtonEvent* xkey = &(xev->xbutton);
        AppendNativeEventMask(&xkey->state);
        break;
      }
      case GenericEvent: {
        XIDeviceEvent* xievent =
            static_cast<XIDeviceEvent*>(xev->xcookie.data);
        CHECK(xievent->evtype == XI_ButtonPress ||
              xievent->evtype == XI_ButtonRelease);
        AppendNativeEventMask(
            reinterpret_cast<unsigned int*>(&xievent->mods.effective));
        break;
      }
      default:
        NOTREACHED();
    }
  }
#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
