// Copyright 2018 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 "components/autofill_assistant/browser/element_area.h"

#include <algorithm>

#include "base/bind.h"
#include "base/logging.h"
#include "base/single_thread_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "components/autofill_assistant/browser/script_executor_delegate.h"
#include "components/autofill_assistant/browser/web_controller.h"

namespace autofill_assistant {

namespace {
// Waiting period between two checks.
static constexpr base::TimeDelta kCheckDelay =
    base::TimeDelta::FromMilliseconds(100);
}  // namespace

ElementArea::ElementArea(ScriptExecutorDelegate* delegate)
    : delegate_(delegate), scheduled_update_(false), weak_ptr_factory_(this) {
  DCHECK(delegate_);
}

ElementArea::~ElementArea() = default;

void ElementArea::Clear() {
  rectangles_.clear();
  ReportUpdate();
}

void ElementArea::SetFromProto(const ElementAreaProto& proto) {
  rectangles_.clear();
  for (const auto& rectangle_proto : proto.rectangles()) {
    rectangles_.emplace_back();
    Rectangle& rectangle = rectangles_.back();
    for (const auto& element_proto : rectangle_proto.elements()) {
      rectangle.positions.emplace_back();
      ElementPosition& position = rectangle.positions.back();
      position.selector = Selector(element_proto);
    }
    rectangle.full_width = rectangle_proto.full_width();
  }
  ReportUpdate();

  if (rectangles_.empty())
    return;

  if (!scheduled_update_) {
    // Check once and schedule regular updates.
    scheduled_update_ = true;
    KeepUpdatingElementPositions();
  } else {
    // If regular updates are already scheduled, just force a check of position
    // right away and keep running the scheduled updates.
    UpdatePositions();
  }
}

void ElementArea::UpdatePositions() {
  if (rectangles_.empty())
    return;

  for (auto& rectangle : rectangles_) {
    for (auto& position : rectangle.positions) {
      // To avoid reporting partial rectangles, all element positions become
      // pending at the same time.
      position.pending_update = true;
    }
    for (auto& position : rectangle.positions) {
      delegate_->GetWebController()->GetElementPosition(
          position.selector,
          base::BindOnce(&ElementArea::OnGetElementPosition,
                         weak_ptr_factory_.GetWeakPtr(), position.selector));
    }
  }
}

bool ElementArea::IsEmpty() const {
  for (const auto& rectangle : rectangles_) {
    for (const auto& position : rectangle.positions) {
      if (!position.rect.empty()) {
        return false;
      }
    }
  }
  return true;
}

ElementArea::ElementPosition::ElementPosition() = default;
ElementArea::ElementPosition::ElementPosition(const ElementPosition& orig) =
    default;
ElementArea::ElementPosition::~ElementPosition() = default;

ElementArea::Rectangle::Rectangle() = default;
ElementArea::Rectangle::Rectangle(const Rectangle& orig) = default;
ElementArea::Rectangle::~Rectangle() = default;

bool ElementArea::Rectangle::IsPending() const {
  for (const auto& position : positions) {
    if (position.pending_update)
      return true;
  }
  return false;
}

bool ElementArea::Rectangle::FillRect(RectF* rect) const {
  bool has_first_rect = false;
  for (const auto& position : positions) {
    if (position.rect.empty())
      continue;

    if (!has_first_rect) {
      *rect = position.rect;
      has_first_rect = true;
      continue;
    }
    rect->top = std::min(rect->top, position.rect.top);
    rect->bottom = std::max(rect->bottom, position.rect.bottom);
    rect->left = std::min(rect->left, position.rect.left);
    rect->right = std::max(rect->right, position.rect.right);
  }
  if (!has_first_rect)
    return false;

  if (full_width) {
    rect->left = 0.0;
    rect->right = 1.0;
  }
  return true;
}

void ElementArea::KeepUpdatingElementPositions() {
  if (rectangles_.empty()) {
    scheduled_update_ = false;
    return;
  }

  UpdatePositions();
  base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&ElementArea::KeepUpdatingElementPositions,
                     weak_ptr_factory_.GetWeakPtr()),
      kCheckDelay);
}

void ElementArea::OnGetElementPosition(const Selector& selector,
                                       bool found,
                                       const RectF& rect) {
  // found == false, has all coordinates set to 0.0, which clears the area.
  bool updated = false;
  for (auto& rectangle : rectangles_) {
    for (auto& position : rectangle.positions) {
      if (selector == position.selector) {
        position.pending_update = false;
        position.rect = rect;
        updated = true;
      }
    }
  }
  if (updated) {
    ReportUpdate();
  }
  // If the set of elements has changed, the given selector will not be found in
  // rectangles_. This is fine.
}

void ElementArea::ReportUpdate() {
  if (!on_update_)
    return;

  for (const auto& rectangle : rectangles_) {
    if (rectangle.IsPending()) {
      // We don't have everything we need yet
      return;
    }
  }

  std::vector<RectF> areas;
  for (auto& rectangle : rectangles_) {
    RectF rect;
    if (rectangle.FillRect(&rect)) {
      areas.emplace_back(rect);
    }
  }
  on_update_.Run(areas);
}

}  // namespace autofill_assistant
