/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "RubyUtils.h"
#include "nsRubyFrame.h"
#include "nsRubyBaseFrame.h"
#include "nsRubyTextFrame.h"
#include "nsRubyBaseContainerFrame.h"
#include "nsRubyTextContainerFrame.h"

using namespace mozilla;

NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(ReservedISize, nscoord)

/* static */ void
RubyUtils::SetReservedISize(nsIFrame* aFrame, nscoord aISize)
{
  MOZ_ASSERT(IsExpandableRubyBox(aFrame));
  aFrame->SetProperty(ReservedISize(), aISize);
}

/* static */ void
RubyUtils::ClearReservedISize(nsIFrame* aFrame)
{
  MOZ_ASSERT(IsExpandableRubyBox(aFrame));
  aFrame->RemoveProperty(ReservedISize());
}

/* static */ nscoord
RubyUtils::GetReservedISize(nsIFrame* aFrame)
{
  MOZ_ASSERT(IsExpandableRubyBox(aFrame));
  return aFrame->GetProperty(ReservedISize());
}

AutoRubyTextContainerArray::AutoRubyTextContainerArray(
  nsRubyBaseContainerFrame* aBaseContainer)
{
  for (nsIFrame* frame = aBaseContainer->GetNextSibling();
       frame && frame->GetType() == nsGkAtoms::rubyTextContainerFrame;
       frame = frame->GetNextSibling()) {
    AppendElement(static_cast<nsRubyTextContainerFrame*>(frame));
  }
}

nsIFrame*
RubyColumn::Iterator::operator*() const
{
  nsIFrame* frame;
  if (mIndex == -1) {
    frame = mColumn.mBaseFrame;
  } else {
    frame = mColumn.mTextFrames[mIndex];
  }
  MOZ_ASSERT(frame, "Frame here cannot be null");
  return frame;
}

void
RubyColumn::Iterator::SkipUntilExistingFrame()
{
  if (mIndex == -1) {
    if (mColumn.mBaseFrame) {
      return;
    }
    ++mIndex;
  }
  int32_t numTextFrames = mColumn.mTextFrames.Length();
  for (; mIndex < numTextFrames; ++mIndex) {
    if (mColumn.mTextFrames[mIndex]) {
      break;
    }
  }
}

RubySegmentEnumerator::RubySegmentEnumerator(nsRubyFrame* aRubyFrame)
{
  nsIFrame* frame = aRubyFrame->PrincipalChildList().FirstChild();
  MOZ_ASSERT(!frame ||
             frame->GetType() == nsGkAtoms::rubyBaseContainerFrame);
  mBaseContainer = static_cast<nsRubyBaseContainerFrame*>(frame);
}

void
RubySegmentEnumerator::Next()
{
  MOZ_ASSERT(mBaseContainer);
  nsIFrame* frame = mBaseContainer->GetNextSibling();
  while (frame && frame->GetType() != nsGkAtoms::rubyBaseContainerFrame) {
    frame = frame->GetNextSibling();
  }
  mBaseContainer = static_cast<nsRubyBaseContainerFrame*>(frame);
}

RubyColumnEnumerator::RubyColumnEnumerator(
  nsRubyBaseContainerFrame* aBaseContainer,
  const AutoRubyTextContainerArray& aTextContainers)
  : mAtIntraLevelWhitespace(false)
{
  const uint32_t rtcCount = aTextContainers.Length();
  mFrames.SetCapacity(rtcCount + 1);

  nsIFrame* rbFrame = aBaseContainer->PrincipalChildList().FirstChild();
  MOZ_ASSERT(!rbFrame || rbFrame->GetType() == nsGkAtoms::rubyBaseFrame);
  mFrames.AppendElement(static_cast<nsRubyContentFrame*>(rbFrame));
  for (uint32_t i = 0; i < rtcCount; i++) {
    nsRubyTextContainerFrame* container = aTextContainers[i];
    // If the container is for span, leave a nullptr here.
    // Spans do not take part in pairing.
    nsIFrame* rtFrame = !container->IsSpanContainer() ?
      container->PrincipalChildList().FirstChild() : nullptr;
    MOZ_ASSERT(!rtFrame || rtFrame->GetType() == nsGkAtoms::rubyTextFrame);
    mFrames.AppendElement(static_cast<nsRubyContentFrame*>(rtFrame));
  }

  // We have to init mAtIntraLevelWhitespace to be correct for the
  // first column. There are two ways we could end up with intra-level
  // whitespace in our first colum:
  // 1. The current segment itself is an inter-segment whitespace;
  // 2. If our ruby segment is split across multiple lines, and some
  //    intra-level whitespace happens to fall right after a line-break.
  //    Each line will get its own nsRubyBaseContainerFrame, and the
  //    container right after the line-break will end up with its first
  //    column containing that intra-level whitespace.
  for (uint32_t i = 0, iend = mFrames.Length(); i < iend; i++) {
    nsRubyContentFrame* frame = mFrames[i];
    if (frame && frame->IsIntraLevelWhitespace()) {
      mAtIntraLevelWhitespace = true;
      break;
    }
  }
}

void
RubyColumnEnumerator::Next()
{
  bool advancingToIntraLevelWhitespace = false;
  for (uint32_t i = 0, iend = mFrames.Length(); i < iend; i++) {
    nsRubyContentFrame* frame = mFrames[i];
    // If we've got intra-level whitespace frames at some levels in the
    // current ruby column, we "faked" an anonymous box for all other
    // levels for this column. So when we advance off this column, we
    // don't advance any of the frames in those levels, because we're
    // just advancing across the "fake" frames.
    if (frame && (!mAtIntraLevelWhitespace ||
                  frame->IsIntraLevelWhitespace())) {
      nsIFrame* nextSibling = frame->GetNextSibling();
      MOZ_ASSERT(!nextSibling || nextSibling->GetType() == frame->GetType(),
                 "Frame type should be identical among a level");
      mFrames[i] = frame = static_cast<nsRubyContentFrame*>(nextSibling);
      if (!advancingToIntraLevelWhitespace &&
          frame && frame->IsIntraLevelWhitespace()) {
        advancingToIntraLevelWhitespace = true;
      }
    }
  }
  MOZ_ASSERT(!advancingToIntraLevelWhitespace || !mAtIntraLevelWhitespace,
             "Should never have adjacent intra-level whitespace columns");
  mAtIntraLevelWhitespace = advancingToIntraLevelWhitespace;
}

bool
RubyColumnEnumerator::AtEnd() const
{
  for (uint32_t i = 0, iend = mFrames.Length(); i < iend; i++) {
    if (mFrames[i]) {
      return false;
    }
  }
  return true;
}

nsRubyContentFrame*
RubyColumnEnumerator::GetFrameAtLevel(uint32_t aIndex) const
{
  // If the current ruby column is for intra-level whitespaces, we
  // return nullptr for any levels that do not have an actual intra-
  // level whitespace frame in this column.  This nullptr represents
  // an anonymous empty intra-level whitespace box.  (In this case,
  // it's important that we NOT return mFrames[aIndex], because it's
  // really part of the next column, not the current one.)
  nsRubyContentFrame* frame = mFrames[aIndex];
  return !mAtIntraLevelWhitespace ||
         (frame && frame->IsIntraLevelWhitespace()) ? frame : nullptr;
}

void
RubyColumnEnumerator::GetColumn(RubyColumn& aColumn) const
{
  nsRubyContentFrame* rbFrame = GetFrameAtLevel(0);
  MOZ_ASSERT(!rbFrame || rbFrame->GetType() == nsGkAtoms::rubyBaseFrame);
  aColumn.mBaseFrame = static_cast<nsRubyBaseFrame*>(rbFrame);
  aColumn.mTextFrames.ClearAndRetainStorage();
  for (uint32_t i = 1, iend = mFrames.Length(); i < iend; i++) {
    nsRubyContentFrame* rtFrame = GetFrameAtLevel(i);
    MOZ_ASSERT(!rtFrame || rtFrame->GetType() == nsGkAtoms::rubyTextFrame);
    aColumn.mTextFrames.AppendElement(static_cast<nsRubyTextFrame*>(rtFrame));
  }
  aColumn.mIsIntraLevelWhitespace = mAtIntraLevelWhitespace;
}