// Copyright (c) 2011 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 "ui/views/focus/focus_search.h"

#include "base/logging.h"
#include "ui/views/bubble/bubble_dialog_delegate.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/view.h"
#include "ui/views/view_properties.h"
#include "ui/views/widget/widget.h"

namespace views {

FocusSearch::FocusSearch(View* root, bool cycle, bool accessibility_mode)
    : root_(root),
      cycle_(cycle),
      accessibility_mode_(accessibility_mode) {
#if defined(OS_MACOSX)
  // On Mac, only the keyboard accessibility mode defined in FocusManager is
  // used. No special accessibility mode should be applicable for a
  // FocusTraversable.
  accessibility_mode_ = false;
#endif
}

View* FocusSearch::FindNextFocusableView(
    View* starting_view,
    FocusSearch::SearchDirection search_direction,
    FocusSearch::TraversalDirection traversal_direction,
    FocusSearch::StartingViewPolicy check_starting_view,
    FocusSearch::AnchoredDialogPolicy can_go_into_anchored_dialog,
    FocusTraversable** focus_traversable,
    View** focus_traversable_view) {
  *focus_traversable = nullptr;
  *focus_traversable_view = nullptr;

  if (!root_->has_children()) {
    NOTREACHED();
    // Nothing to focus on here.
    return nullptr;
  }

  View* initial_starting_view = starting_view;
  int starting_view_group = -1;
  if (starting_view)
    starting_view_group = starting_view->GetGroup();

  if (!starting_view) {
    // Default to the first/last child
    starting_view = search_direction == SearchDirection::kBackwards
                        ? root_->child_at(root_->child_count() - 1)
                        : root_->child_at(0);
    // If there was no starting view, then the one we select is a potential
    // focus candidate.
    check_starting_view = StartingViewPolicy::kCheckStartingView;
  } else {
    // The starting view should be a direct or indirect child of the root.
    DCHECK(Contains(root_, starting_view));
  }

  View* v = nullptr;
  if (search_direction == SearchDirection::kForwards) {
    v = FindNextFocusableViewImpl(
        starting_view, check_starting_view, true,
        (traversal_direction == TraversalDirection::kDown), starting_view_group,
        focus_traversable, focus_traversable_view);
  } else {
    // If the starting view is focusable, we don't want to go down, as we are
    // traversing the view hierarchy tree bottom-up.
    bool can_go_down = (traversal_direction == TraversalDirection::kDown) &&
                       !IsFocusable(starting_view);
    v = FindPreviousFocusableViewImpl(starting_view, check_starting_view, true,
                                      can_go_down, can_go_into_anchored_dialog,
                                      starting_view_group, focus_traversable,
                                      focus_traversable_view);
  }

  // Don't set the focus to something outside of this view hierarchy.
  if (v && v != root_ && !Contains(root_, v))
    v = nullptr;

  // If |cycle_| is true, prefer to keep cycling rather than returning nullptr.
  if (cycle_ && !v && initial_starting_view) {
    v = FindNextFocusableView(nullptr, search_direction, traversal_direction,
                              check_starting_view, can_go_into_anchored_dialog,
                              focus_traversable, focus_traversable_view);
    DCHECK(IsFocusable(v));
    return v;
  }

  // Doing some sanity checks.
  if (v) {
    DCHECK(IsFocusable(v));
    return v;
  }
  if (*focus_traversable) {
    DCHECK(*focus_traversable_view);
    return nullptr;
  }
  // Nothing found.
  return nullptr;
}

bool FocusSearch::IsViewFocusableCandidate(View* v, int skip_group_id) {
  return IsFocusable(v) &&
      (v->IsGroupFocusTraversable() || skip_group_id == -1 ||
       v->GetGroup() != skip_group_id);
}

bool FocusSearch::IsFocusable(View* v) {
  DCHECK(root_);
  // Sanity Check. Currently the FocusManager keyboard accessibility mode is
  // only used on Mac, for which |accessibility_mode_| is false.
  DCHECK(!(accessibility_mode_ &&
           root_->GetWidget()->GetFocusManager()->keyboard_accessible()));
  if (accessibility_mode_ ||
      root_->GetWidget()->GetFocusManager()->keyboard_accessible())
    return v && v->IsAccessibilityFocusable();
  return v && v->IsFocusable();
}

View* FocusSearch::FindSelectedViewForGroup(View* view) {
  if (view->IsGroupFocusTraversable() ||
      view->GetGroup() == -1)  // No group for that view.
    return view;

  View* selected_view = view->GetSelectedViewForGroup(view->GetGroup());
  if (selected_view)
    return selected_view;

  // No view selected for that group, default to the specified view.
  return view;
}

View* FocusSearch::GetParent(View* v) {
  return Contains(root_, v) ? v->parent() : nullptr;
}

bool FocusSearch::Contains(View* root, const View* v) {
  return root->Contains(v);
}

// Strategy for finding the next focusable view:
// - keep going down the first child, stop when you find a focusable view or
//   a focus traversable view (in that case return it) or when you reach a view
//   with no children.
// - go to the right sibling and start the search from there (by invoking
//   FindNextFocusableViewImpl on that view).
// - if the view has no right sibling, go up the parents until you find a parent
//   with a right sibling and start the search from there.
View* FocusSearch::FindNextFocusableViewImpl(
    View* starting_view,
    FocusSearch::StartingViewPolicy check_starting_view,
    bool can_go_up,
    bool can_go_down,
    int skip_group_id,
    FocusTraversable** focus_traversable,
    View** focus_traversable_view) {
  if (check_starting_view == StartingViewPolicy::kCheckStartingView) {
    if (IsViewFocusableCandidate(starting_view, skip_group_id)) {
      View* v = FindSelectedViewForGroup(starting_view);
      // The selected view might not be focusable (if it is disabled for
      // example).
      if (IsFocusable(v))
        return v;
    }

    *focus_traversable = starting_view->GetFocusTraversable();
    if (*focus_traversable) {
      *focus_traversable_view = starting_view;
      return nullptr;
    }
  }

  // First let's try the left child.
  if (can_go_down) {
    if (starting_view->has_children()) {
      View* v = FindNextFocusableViewImpl(
          starting_view->child_at(0), StartingViewPolicy::kCheckStartingView,
          false, true, skip_group_id, focus_traversable,
          focus_traversable_view);
      if (v || *focus_traversable)
        return v;
    } else {
      // Check to see if we should navigate into a dialog anchored at this view.
      BubbleDialogDelegateView* bubble =
          starting_view->GetProperty(kAnchoredDialogKey);
      if (bubble) {
        *focus_traversable = bubble->GetWidget()->GetFocusTraversable();
        *focus_traversable_view = starting_view;
        return nullptr;
      }
    }
  }

  // Then try the right sibling.
  View* sibling = starting_view->GetNextFocusableView();
  if (sibling) {
    View* v = FindNextFocusableViewImpl(
        sibling, FocusSearch::StartingViewPolicy::kCheckStartingView, false,
        true, skip_group_id, focus_traversable, focus_traversable_view);
    if (v || *focus_traversable)
      return v;
  }

  // Then go up to the parent sibling.
  if (can_go_up) {
    View* parent = GetParent(starting_view);
    while (parent && parent != root_) {
      BubbleDialogDelegateView* bubble =
          parent->GetProperty(kAnchoredDialogKey);
      if (bubble) {
        *focus_traversable = bubble->GetWidget()->GetFocusTraversable();
        *focus_traversable_view = starting_view;
        return nullptr;
      }

      sibling = parent->GetNextFocusableView();
      if (sibling) {
        return FindNextFocusableViewImpl(
            sibling, StartingViewPolicy::kCheckStartingView, true, true,
            skip_group_id, focus_traversable, focus_traversable_view);
      }
      parent = GetParent(parent);
    }
  }

  // We found nothing.
  return nullptr;
}

// Strategy for finding the previous focusable view:
// - keep going down on the right until you reach a view with no children, if it
//   it is a good candidate return it.
// - start the search on the left sibling.
// - if there are no left sibling, start the search on the parent (without going
//   down).
View* FocusSearch::FindPreviousFocusableViewImpl(
    View* starting_view,
    FocusSearch::StartingViewPolicy check_starting_view,
    bool can_go_up,
    bool can_go_down,
    FocusSearch::AnchoredDialogPolicy can_go_into_anchored_dialog,
    int skip_group_id,
    FocusTraversable** focus_traversable,
    View** focus_traversable_view) {
  // Normally when we navigate to a FocusTraversableParent, can_go_down is
  // false so we don't navigate back in. However, if we just navigated out
  // of an anchored dialog, allow going down in order to navigate into
  // children of |starting_view| next.
  if (starting_view->GetProperty(kAnchoredDialogKey) &&
      can_go_into_anchored_dialog ==
          AnchoredDialogPolicy::kSkipAnchoredDialog &&
      !can_go_down) {
    can_go_down = true;
  }

  // Let's go down and right as much as we can.
  if (can_go_down) {
    // Before we go into the direct children, we have to check if this view has
    // a FocusTraversable.
    *focus_traversable = starting_view->GetFocusTraversable();
    if (*focus_traversable) {
      *focus_traversable_view = starting_view;
      return nullptr;
    }

    // Check to see if we should navigate into a dialog anchored at this view.
    if (can_go_into_anchored_dialog ==
        AnchoredDialogPolicy::kCanGoIntoAnchoredDialog) {
      BubbleDialogDelegateView* bubble =
          starting_view->GetProperty(kAnchoredDialogKey);
      if (bubble) {
        *focus_traversable = bubble->GetWidget()->GetFocusTraversable();
        *focus_traversable_view = starting_view;
        return nullptr;
      }
    }

    can_go_into_anchored_dialog =
        AnchoredDialogPolicy::kCanGoIntoAnchoredDialog;
    if (starting_view->has_children()) {
      View* view =
          starting_view->child_at(starting_view->child_count() - 1);
      View* v = FindPreviousFocusableViewImpl(
          view, StartingViewPolicy::kCheckStartingView, false, true,
          can_go_into_anchored_dialog, skip_group_id, focus_traversable,
          focus_traversable_view);
      if (v || *focus_traversable)
        return v;
    }
  }

  // Then look at this view. Here, we do not need to see if the view has
  // a FocusTraversable, since we do not want to go down any more.
  if (check_starting_view == StartingViewPolicy::kCheckStartingView &&
      IsViewFocusableCandidate(starting_view, skip_group_id)) {
    View* v = FindSelectedViewForGroup(starting_view);
    // The selected view might not be focusable (if it is disabled for
    // example).
    if (IsFocusable(v))
      return v;
  }

  // Then try the left sibling.
  View* sibling = starting_view->GetPreviousFocusableView();
  if (sibling) {
    return FindPreviousFocusableViewImpl(
        sibling, StartingViewPolicy::kCheckStartingView, can_go_up, true,
        can_go_into_anchored_dialog, skip_group_id, focus_traversable,
        focus_traversable_view);
  }

  // Then go up the parent.
  if (can_go_up) {
    View* parent = GetParent(starting_view);
    if (parent)
      return FindPreviousFocusableViewImpl(
          parent, StartingViewPolicy::kCheckStartingView, true, false,
          can_go_into_anchored_dialog, skip_group_id, focus_traversable,
          focus_traversable_view);
  }

  // We found nothing.
  return nullptr;
}

}  // namespace views
