/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/dom/HTMLOptionElement.h"

#include "BindContext.h"
#include "HTMLOptGroupElement.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/HTMLOptionElementBinding.h"
#include "mozilla/dom/HTMLSelectElement.h"
#include "nsGkAtoms.h"
#include "nsIFormControl.h"
#include "nsStyleConsts.h"

// Notify/query select frame for selected state
#include "mozilla/dom/Document.h"
#include "nsCOMPtr.h"
#include "nsContentCreatorFunctions.h"
#include "nsNodeInfoManager.h"
#include "nsTextNode.h"

/**
 * Implementation of &lt;option&gt;
 */

NS_IMPL_NS_NEW_HTML_ELEMENT(Option)

namespace mozilla::dom {

HTMLOptionElement::HTMLOptionElement(
    already_AddRefed<mozilla::dom::NodeInfo> aNodeInfo)
    : nsGenericHTMLElement(std::move(aNodeInfo)) {
  // We start off enabled
  AddStatesSilently(ElementState::ENABLED);
}

HTMLOptionElement::~HTMLOptionElement() = default;

NS_IMPL_ELEMENT_CLONE(HTMLOptionElement)

mozilla::dom::Element* HTMLOptionElement::GetFormForBindings() {
  HTMLFormElement* form = GetFormInternal();
  return RetargetReferenceTargetForBindings(form);
}

mozilla::dom::HTMLFormElement* HTMLOptionElement::GetFormInternal() {
  HTMLSelectElement* selectControl = GetSelect();
  return selectControl ? selectControl->GetFormInternal() : nullptr;
}

void HTMLOptionElement::SetSelectedInternal(bool aValue, bool aNotify) {
  mSelectedChanged = true;
  SetStates(ElementState::CHECKED, aValue, aNotify);
}

void HTMLOptionElement::OptGroupDisabledChanged(bool aNotify) {
  UpdateDisabledState(aNotify);
}

void HTMLOptionElement::UpdateDisabledState(bool aNotify) {
  bool isDisabled = HasAttr(nsGkAtoms::disabled);

  if (!isDisabled) {
    // https://html.spec.whatwg.org/#concept-option-disabled
    // Walk ancestors looking for a disabled optgroup, stopping at boundary
    // elements. Wrapper elements (div, span, etc.) are transparent.
    for (nsINode* ancestor = GetParent(); ancestor;
         ancestor = ancestor->GetParentNode()) {
      if (IsOptionListBoundary(*ancestor)) {
        break;
      }
      if (auto* optgroup = HTMLOptGroupElement::FromNode(ancestor)) {
        isDisabled = optgroup->IsDisabled();
        break;
      }
    }
  }

  ElementState disabledStates;
  if (isDisabled) {
    disabledStates |= ElementState::DISABLED;
  } else {
    disabledStates |= ElementState::ENABLED;
  }

  ElementState oldDisabledStates = State() & ElementState::DISABLED_STATES;
  ElementState changedStates = disabledStates ^ oldDisabledStates;

  if (!changedStates.IsEmpty()) {
    ToggleStates(changedStates, aNotify);
  }
}

void HTMLOptionElement::SetSelected(bool aValue) {
  // Note: The select content obj maintains all the PresState
  // so defer to it to get the answer
  if (HTMLSelectElement* select = GetSelect()) {
    int32_t index = Index();
    HTMLSelectElement::OptionFlags mask{
        HTMLSelectElement::OptionFlag::SetDisabled,
        HTMLSelectElement::OptionFlag::Notify};
    if (aValue) {
      mask += HTMLSelectElement::OptionFlag::IsSelected;
    }

    // This should end up calling SetSelectedInternal
    select->SetOptionsSelectedByIndex(index, index, mask);
  } else {
    SetSelectedInternal(aValue, true);
  }
}

int32_t HTMLOptionElement::Index() {
  static int32_t defaultIndex = 0;

  // Only select elements can contain a list of options.
  HTMLSelectElement* selectElement = GetSelect();
  if (!selectElement) {
    return defaultIndex;
  }

  HTMLOptionsCollection* options = selectElement->GetOptions();
  if (!options) {
    return defaultIndex;
  }

  int32_t index = defaultIndex;
  MOZ_ALWAYS_SUCCEEDS(options->GetOptionIndex(this, 0, true, &index));
  return index;
}

nsChangeHint HTMLOptionElement::GetAttributeChangeHint(
    const nsAtom* aAttribute, AttrModType aModType) const {
  nsChangeHint retval =
      nsGenericHTMLElement::GetAttributeChangeHint(aAttribute, aModType);

  if (aAttribute == nsGkAtoms::label) {
    retval |= nsChangeHint_ReconstructFrame;
  } else if (aAttribute == nsGkAtoms::text) {
    retval |= NS_STYLE_HINT_REFLOW;
  }
  return retval;
}

void HTMLOptionElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName,
                                      const nsAttrValue* aValue, bool aNotify) {
  nsGenericHTMLElement::BeforeSetAttr(aNamespaceID, aName, aValue, aNotify);

  if (aNamespaceID != kNameSpaceID_None || aName != nsGkAtoms::selected ||
      mSelectedChanged) {
    return;
  }

  // We just changed out selected state (since we look at the "selected"
  // attribute when mSelectedChanged is false).  Let's tell our select about
  // it.
  HTMLSelectElement* select = GetSelect();
  if (!select) {
    // If option is a child of select, SetOptionsSelectedByIndex will set the
    // selected state if needed.
    // Keep in sync with Element::SetNoNameSpaceAttrOnNewlyCreatedElement!
    SetStates(ElementState::CHECKED, !!aValue, aNotify);
    return;
  }

  NS_ASSERTION(!mSelectedChanged, "Shouldn't be here");

  int32_t index = Index();
  HTMLSelectElement::OptionFlags mask =
      HTMLSelectElement::OptionFlag::SetDisabled;
  if (aValue) {
    mask += HTMLSelectElement::OptionFlag::IsSelected;
  }

  if (aNotify) {
    mask += HTMLSelectElement::OptionFlag::Notify;
  }

  // This can end up calling SetSelectedInternal if our selected state needs to
  // change, which we will allow to take effect so that parts of
  // SetOptionsSelectedByIndex that might depend on it working don't get
  // confused.
  select->SetOptionsSelectedByIndex(index, index, mask);

  // the selected state might have been changed by SetOptionsSelectedByIndex,
  // possibly more than once; make sure our mSelectedChanged state is set back
  // correctly.
  mSelectedChanged = false;
}

void HTMLOptionElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
                                     const nsAttrValue* aValue,
                                     const nsAttrValue* aOldValue,
                                     nsIPrincipal* aSubjectPrincipal,
                                     bool aNotify) {
  if (aNameSpaceID == kNameSpaceID_None) {
    if (aName == nsGkAtoms::disabled) {
      UpdateDisabledState(aNotify);
    }

    if (aName == nsGkAtoms::value && Selected()) {
      // Since this option is selected, changing value may have changed missing
      // validity state of the select element
      if (HTMLSelectElement* select = GetSelect()) {
        select->UpdateValueMissingValidityState();
      }
    }

    if (aName == nsGkAtoms::selected) {
      SetStates(ElementState::DEFAULT, !!aValue, aNotify);
    }
  }

  return nsGenericHTMLElement::AfterSetAttr(
      aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
}

void HTMLOptionElement::GetText(nsAString& aText) {
  nsAutoString text;

  nsIContent* child = nsINode::GetFirstChild();
  while (child) {
    if (Text* textChild = child->GetAsText()) {
      textChild->AppendTextTo(text);
    }
    if (child->IsHTMLElement(nsGkAtoms::script) ||
        child->IsSVGElement(nsGkAtoms::script)) {
      child = child->GetNextNonChildNode(this);
    } else {
      child = child->GetNextNode(this);
    }
  }

  // XXX No CompressWhitespace for nsAString.  Sad.
  text.CompressWhitespace(true, true);
  aText = std::move(text);
}

void HTMLOptionElement::SetText(const nsAString& aText, ErrorResult& aRv) {
  aRv = nsContentUtils::SetNodeTextContent(this, aText, false);
}

nsresult HTMLOptionElement::BindToTree(BindContext& aContext,
                                       nsINode& aParent) {
  nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
  NS_ENSURE_SUCCESS(rv, rv);

  // Our new parent might change :disabled/:enabled state.
  UpdateDisabledState(false);

  // https://html.spec.whatwg.org/#the-option-element
  // The option HTML element insertion steps, given insertedOption, are to run
  // update an option's nearest ancestor select given insertedOption.
  UpdateNearestAncestorSelect();

  // The option HTML element post-connection steps, given insertedOption:
  // 1. If insertedOption's cached nearest ancestor select element is not null
  // and insertedOption is selected, then update select's descendant
  // selectedcontent elements given insertedOption's cached nearest ancestor
  // select element.

  // NOTE: Post-connection steps only run when connecting to a composed doc,
  // unlike insertion steps above which run for any tree insertion.
  if (aContext.InComposedDoc() && mCachedNearestAncestorSelect && Selected()) {
    mCachedNearestAncestorSelect->ScheduleSelectedContentUpdateScriptRunner();
  }

  return NS_OK;
}

void HTMLOptionElement::UnbindFromTree(UnbindContext& aContext) {
  // https://html.spec.whatwg.org/#the-option-element
  // The option HTML element removing steps, given removedOption and oldParent:
  //
  // 1. Let select be removedOption's cached nearest ancestor select element.
  RefPtr<HTMLSelectElement> oldSelect = mCachedNearestAncestorSelect;

  nsGenericHTMLElement::UnbindFromTree(aContext);

  // 3. Run update an option's nearest ancestor select given removedOption.
  UpdateNearestAncestorSelect();

  // 2. If removedOption is selected and select is not null and select has at
  //    least one selectedcontent element descendant, then queue a microtask to
  //    update a select's descendant selectedcontent elements given select.
  // NOTE: omitting the "has selectedcontent descendant" check for now.
  if (oldSelect && oldSelect != mCachedNearestAncestorSelect && Selected()) {
    oldSelect->ScheduleSelectedContentUpdate();
  }

  UpdateDisabledState(false);
}

// https://html.spec.whatwg.org/#concept-option-nearest-ancestor-select
HTMLSelectElement* HTMLOptionElement::ComputeNearestAncestorSelect() const {
  HTMLOptGroupElement* ancestorOptgroup = nullptr;
  // 1-2. For each ancestor of option's ancestors, in reverse tree order:
  for (nsINode* ancestor : Ancestors(*this)) {
    // 2.1. If ancestor is a datalist, hr, or option element, return null.
    if (ancestor->IsAnyOfHTMLElements(nsGkAtoms::datalist, nsGkAtoms::hr,
                                      nsGkAtoms::option)) {
      return nullptr;
    }
    // 2.2. If ancestor is an optgroup element:
    if (auto* optgroup = HTMLOptGroupElement::FromNode(ancestor)) {
      // 2.2.1. If ancestorOptgroup is not null, return null.
      if (ancestorOptgroup) {
        return nullptr;
      }
      // 2.2.2. Set ancestorOptgroup to ancestor.
      ancestorOptgroup = optgroup;
      continue;
    }
    // 2.3. If ancestor is a select element, return ancestor.
    if (auto* select = HTMLSelectElement::FromNode(ancestor)) {
      return select;
    }
  }
  // 3. Return null.
  return nullptr;
}

// https://html.spec.whatwg.org/#update-an-options-nearest-ancestor-select
void HTMLOptionElement::UpdateNearestAncestorSelect() {
  // 1. Let oldSelect be option's cached nearest ancestor select element.
  // 2. Let newSelect be option's option element nearest ancestor select.
  // 3. Set option's cached nearest ancestor select element to newSelect.
  mCachedNearestAncestorSelect = ComputeNearestAncestorSelect();
  // 4. If oldSelect is not newSelect:
  //    4.1/4.2: Run the selectedness setting algorithm on old/new select.
  //    NOTE: Deferred to HTMLSelectElement's mutation observer callbacks
  //    (ContentAppendedOrInserted / ContentWillBeRemoved) which run once
  //    per DOM mutation.
}

// Returns this option's nearest ancestor select element (cached), or null.
HTMLSelectElement* HTMLOptionElement::GetSelect() const {
  return mCachedNearestAncestorSelect;
}

already_AddRefed<HTMLOptionElement> HTMLOptionElement::Option(
    const GlobalObject& aGlobal, const nsAString& aText,
    const Optional<nsAString>& aValue, bool aDefaultSelected, bool aSelected,
    ErrorResult& aError) {
  nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports());
  Document* doc;
  if (!win || !(doc = win->GetExtantDoc())) {
    aError.Throw(NS_ERROR_FAILURE);
    return nullptr;
  }

  RefPtr<mozilla::dom::NodeInfo> nodeInfo = doc->NodeInfoManager()->GetNodeInfo(
      nsGkAtoms::option, nullptr, kNameSpaceID_XHTML, ELEMENT_NODE);

  auto* nim = nodeInfo->NodeInfoManager();
  RefPtr<HTMLOptionElement> option =
      new (nim) HTMLOptionElement(nodeInfo.forget());

  if (!aText.IsEmpty()) {
    // Create a new text node and append it to the option
    RefPtr<nsTextNode> textContent = new (option->NodeInfo()->NodeInfoManager())
        nsTextNode(option->NodeInfo()->NodeInfoManager());

    textContent->SetText(aText, false);

    option->AppendChildTo(textContent, false, aError);
    if (aError.Failed()) {
      return nullptr;
    }
  }

  if (aValue.WasPassed()) {
    // Set the value attribute for this element. We're calling SetAttr
    // directly because we want to pass aNotify == false.
    aError = option->SetAttr(kNameSpaceID_None, nsGkAtoms::value,
                             aValue.Value(), false);
    if (aError.Failed()) {
      return nullptr;
    }
  }

  if (aDefaultSelected) {
    // We're calling SetAttr directly because we want to pass
    // aNotify == false.
    aError =
        option->SetAttr(kNameSpaceID_None, nsGkAtoms::selected, u""_ns, false);
    if (aError.Failed()) {
      return nullptr;
    }
  }

  option->SetSelected(aSelected);
  option->SetSelectedChanged(false);

  return option.forget();
}

nsresult HTMLOptionElement::CopyInnerTo(Element* aDest) {
  nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest);
  NS_ENSURE_SUCCESS(rv, rv);

  if (aDest->OwnerDoc()->IsStaticDocument()) {
    static_cast<HTMLOptionElement*>(aDest)->SetSelected(Selected());
  }
  return NS_OK;
}

JSObject* HTMLOptionElement::WrapNode(JSContext* aCx,
                                      JS::Handle<JSObject*> aGivenProto) {
  return HTMLOptionElement_Binding::Wrap(aCx, this, aGivenProto);
}

}  // namespace mozilla::dom
