/*
 * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 */

#include "third_party/blink/renderer/core/html/html_progress_element.h"

#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h"
#include "third_party/blink/renderer/core/html/shadow/progress_shadow_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/layout/layout_progress.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"

namespace blink {

const double HTMLProgressElement::kIndeterminatePosition = -1;
const double HTMLProgressElement::kInvalidPosition = -2;

HTMLProgressElement::HTMLProgressElement(Document& document)
    : HTMLElement(html_names::kProgressTag, document), value_(nullptr) {
  UseCounter::Count(document, WebFeature::kProgressElement);
  SetHasCustomStyleCallbacks();
  EnsureUserAgentShadowRoot();
}

HTMLProgressElement::~HTMLProgressElement() = default;

LayoutObject* HTMLProgressElement::CreateLayoutObject(
    const ComputedStyle& style) {
  if (style.IsVerticalWritingMode()) {
    UseCounter::Count(GetDocument(), WebFeature::kVerticalFormControls);
  }
  if (!style.HasEffectiveAppearance()) {
    UseCounter::Count(GetDocument(),
                      WebFeature::kProgressElementWithNoneAppearance);
    return LayoutObject::CreateObject(this, style);
  }
  UseCounter::Count(GetDocument(),
                    WebFeature::kProgressElementWithProgressBarAppearance);
  return MakeGarbageCollected<LayoutProgress>(*this);
}

LayoutProgress* HTMLProgressElement::GetLayoutProgress() const {
  return DynamicTo<LayoutProgress>(GetLayoutObject());
}

void HTMLProgressElement::DidRecalcStyle(const StyleRecalcChange change) {
  HTMLElement::DidRecalcStyle(change);
  const ComputedStyle* style = GetComputedStyle();
  if (style) {
    bool is_horizontal = style->IsHorizontalWritingMode();
    bool is_ltr = style->IsLeftToRightDirection();
    if (is_horizontal && is_ltr) {
      UseCounter::Count(GetDocument(),
                        WebFeature::kProgressElementHorizontalLtr);
    } else if (is_horizontal && !is_ltr) {
      UseCounter::Count(GetDocument(),
                        WebFeature::kProgressElementHorizontalRtl);
    } else if (is_ltr) {
      UseCounter::Count(GetDocument(), WebFeature::kProgressElementVerticalLtr);
    } else {
      UseCounter::Count(GetDocument(), WebFeature::kProgressElementVerticalRtl);
    }
  }
}

void HTMLProgressElement::ParseAttribute(
    const AttributeModificationParams& params) {
  if (params.name == html_names::kValueAttr) {
    if (params.old_value.IsNull() != params.new_value.IsNull())
      PseudoStateChanged(CSSSelector::kPseudoIndeterminate);
    DidElementStateChange();
  } else if (params.name == html_names::kMaxAttr) {
    DidElementStateChange();
  } else {
    HTMLElement::ParseAttribute(params);
  }
}

void HTMLProgressElement::AttachLayoutTree(AttachContext& context) {
  HTMLElement::AttachLayoutTree(context);
  if (LayoutProgress* layout_progress = GetLayoutProgress())
    layout_progress->UpdateFromElement();
}

double HTMLProgressElement::value() const {
  double value =
      ParseHTMLFloatingPointNumber(getAttribute(html_names::kValueAttr), 0);
  // Otherwise, if the parsed value was greater than or equal to the maximum
  // value, then the current value of the progress bar is the maximum value
  // of the progress bar. Otherwise, if parsing the value attribute's value
  // resulted in an error, or a number less than or equal to zero, then the
  // current value of the progress bar is zero.
  return !std::isfinite(value) || value < 0 ? 0 : std::min(value, max());
}

void HTMLProgressElement::setValue(double value) {
  SetFloatingPointAttribute(html_names::kValueAttr, std::max(value, 0.));
}

double HTMLProgressElement::max() const {
  double max =
      ParseHTMLFloatingPointNumber(getAttribute(html_names::kMaxAttr), 1);
  // Otherwise, if the element has no max attribute, or if it has one but
  // parsing it resulted in an error, or if the parsed value was less than or
  // equal to zero, then the maximum value of the progress bar is 1.0.
  return !std::isfinite(max) || max <= 0 ? 1 : max;
}

void HTMLProgressElement::setMax(double max) {
  if (max <= 0) {
    return;
  }
  SetFloatingPointAttribute(html_names::kMaxAttr, max);
}

double HTMLProgressElement::position() const {
  if (!IsDeterminate())
    return HTMLProgressElement::kIndeterminatePosition;
  return value() / max();
}

bool HTMLProgressElement::IsDeterminate() const {
  return FastHasAttribute(html_names::kValueAttr);
}

void HTMLProgressElement::DidElementStateChange() {
  SetInlineSizePercentage(position() * 100);
  if (LayoutProgress* layout_progress = GetLayoutProgress())
    layout_progress->UpdateFromElement();
}

void HTMLProgressElement::DidAddUserAgentShadowRoot(ShadowRoot& root) {
  DCHECK(!value_);

  auto* inner = MakeGarbageCollected<ProgressShadowElement>(GetDocument());
  inner->SetShadowPseudoId(AtomicString("-webkit-progress-inner-element"));
  root.AppendChild(inner);

  auto* bar = MakeGarbageCollected<ProgressShadowElement>(GetDocument());
  bar->SetShadowPseudoId(AtomicString("-webkit-progress-bar"));
  value_ = MakeGarbageCollected<ProgressShadowElement>(GetDocument());
  value_->SetShadowPseudoId(AtomicString("-webkit-progress-value"));
  SetInlineSizePercentage(HTMLProgressElement::kIndeterminatePosition * 100);
  bar->AppendChild(value_);

  inner->AppendChild(bar);
}

bool HTMLProgressElement::ShouldAppearIndeterminate() const {
  return !IsDeterminate();
}

void HTMLProgressElement::Trace(Visitor* visitor) const {
  visitor->Trace(value_);
  HTMLElement::Trace(visitor);
}

void HTMLProgressElement::SetInlineSizePercentage(double position) const {
  value_->SetInlineStyleProperty(CSSPropertyID::kInlineSize, position,
                                 CSSPrimitiveValue::UnitType::kPercentage);
  value_->SetInlineStyleProperty(CSSPropertyID::kBlockSize, 100,
                                 CSSPrimitiveValue::UnitType::kPercentage);
}

bool HTMLProgressElement::SupportsBaseAppearanceInternal(
    BaseAppearanceValue value) const {
  if (!RuntimeEnabledFeatures::AppearanceBaseEnabled()) {
    return false;
  }
  return value == Element::BaseAppearanceValue::kBase;
}

}  // namespace blink
