// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/modules/media_controls/elements/media_control_slider_element.h"

#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/html_div_element.h"
#include "third_party/blink/renderer/core/html/shadow/shadow_element_names.h"
#include "third_party/blink/renderer/core/input_type_names.h"
#include "third_party/blink/renderer/core/layout/geometry/logical_rect.h"
#include "third_party/blink/renderer/core/layout/layout_box.h"
#include "third_party/blink/renderer/core/layout/layout_box_model_object.h"
#include "third_party/blink/renderer/core/resize_observer/resize_observer.h"
#include "third_party/blink/renderer/core/resize_observer/resize_observer_entry.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_elements_helper.h"
#include "third_party/blink/renderer/modules/media_controls/media_controls_impl.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"

namespace blink {

namespace {

void SetSegmentDivFraction(HTMLDivElement* segment,
                           double fraction,
                           int width,
                           float zoom_factor) {
  const int segment_width =
      ClampTo<int>(floor((fraction * width) / zoom_factor));

  // Get the current width for the segment. If the box is not present
  // then it will be a nullptr so we should assume zero.
  const LayoutBox* box = segment->GetLayoutBox();
  const int current_width = box ? box->OffsetWidth().Round() : 0;

  // If the width has not changed then do not update the segment.
  if (segment_width == current_width) {
    return;
  }

  StringBuilder builder;
  builder.Append("width: ");
  builder.AppendNumber(segment_width);
  builder.Append("px;");
  segment->setAttribute(html_names::kStyleAttr, builder.ToAtomicString());
}

}  // namespace.

class MediaControlSliderElement::MediaControlSliderElementResizeObserverDelegate
    final : public ResizeObserver::Delegate {
 public:
  explicit MediaControlSliderElementResizeObserverDelegate(
      MediaControlSliderElement* element)
      : element_(element) {
    DCHECK(element);
  }
  ~MediaControlSliderElementResizeObserverDelegate() override = default;

  void OnResize(
      const HeapVector<Member<ResizeObserverEntry>>& entries) override {
    DCHECK_EQ(1u, entries.size());
    DCHECK_EQ(entries[0]->target(), element_);
    element_->NotifyElementSizeChanged();
  }

  void Trace(Visitor* visitor) const override {
    visitor->Trace(element_);
    ResizeObserver::Delegate::Trace(visitor);
  }

 private:
  Member<MediaControlSliderElement> element_;
};

MediaControlSliderElement::MediaControlSliderElement(
    MediaControlsImpl& media_controls)
    : MediaControlInputElement(media_controls),
      segment_highlight_before_(nullptr),
      segment_highlight_after_(nullptr),
      resize_observer_(ResizeObserver::Create(
          GetDocument().domWindow(),
          MakeGarbageCollected<MediaControlSliderElementResizeObserverDelegate>(
              this))) {
  setType(input_type_names::kRange);
  setAttribute(html_names::kStepAttr, AtomicString("any"));
  OnControlsShown();
}

Element& MediaControlSliderElement::GetTrackElement() {
  // The timeline element has a shadow root with the following
  // structure:
  //
  // #shadow-root
  //   - div
  //     - div::-webkit-slider-runnable-track#track
  Element* track =
      GetShadowRoot()->getElementById(shadow_element_names::kIdSliderTrack);
  DCHECK(track);
  return *track;
}

void MediaControlSliderElement::SetupBarSegments() {
  DCHECK((segment_highlight_after_ && segment_highlight_before_) ||
         (!segment_highlight_after_ && !segment_highlight_before_));

  if (segment_highlight_after_ || segment_highlight_before_)
    return;

  Element& track = GetTrackElement();
  track.SetShadowPseudoId(
      shadow_element_names::kPseudoMediaControlsSegmentedTrack);

  // Add the following structure to #track.
  //
  // div::internal-track-segment-background (container)
  //   - div::internal-track-segment-highlight-before (blue highlight)
  //   - div::internal-track-segment-highlight-after (dark gray highlight)
  HTMLDivElement* background = MediaControlElementsHelper::CreateDiv(
      AtomicString("-internal-track-segment-background"), &track);
  segment_highlight_before_ = MediaControlElementsHelper::CreateDiv(
      AtomicString("-internal-track-segment-highlight-before"), background);
  segment_highlight_after_ = MediaControlElementsHelper::CreateDiv(
      AtomicString("-internal-track-segment-highlight-after"), background);
}

void MediaControlSliderElement::SetBeforeSegmentFraction(double fraction) {
  DCHECK(segment_highlight_before_);
  before_segment_fraction_ = fraction;
  SetSegmentDivFraction(segment_highlight_before_, before_segment_fraction_,
                        TrackWidth(), ZoomFactor());
}

void MediaControlSliderElement::SetAfterSegmentFraction(double fraction) {
  DCHECK(segment_highlight_after_);
  after_segment_fraction_ = fraction;
  SetSegmentDivFraction(segment_highlight_after_, after_segment_fraction_,
                        TrackWidth(), ZoomFactor());
}

int MediaControlSliderElement::TrackWidth() {
  LayoutBoxModelObject* box = GetTrackElement().GetLayoutBoxModelObject();
  return box ? box->OffsetWidth().Round() : 0;
}

float MediaControlSliderElement::ZoomFactor() const {
  const LocalFrame* frame = GetDocument().GetFrame();
  return frame ? frame->LayoutZoomFactor() : 1;
}

void MediaControlSliderElement::NotifyElementSizeChanged() {
  SetSegmentDivFraction(segment_highlight_before_, before_segment_fraction_,
                        TrackWidth(), ZoomFactor());
  SetSegmentDivFraction(segment_highlight_after_, after_segment_fraction_,
                        TrackWidth(), ZoomFactor());
}

void MediaControlSliderElement::Trace(Visitor* visitor) const {
  visitor->Trace(segment_highlight_before_);
  visitor->Trace(segment_highlight_after_);
  visitor->Trace(resize_observer_);
  MediaControlInputElement::Trace(visitor);
}

void MediaControlSliderElement::OnControlsShown() {
  resize_observer_->observe(this);
}

void MediaControlSliderElement::OnControlsHidden() {
  resize_observer_->disconnect();
}

}  // namespace blink
