/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
// vim:cindent:ts=2:et:sw=2:
/* 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/. */

/*
 * Web-compatible algorithms that determine column and table widths,
 * used for CSS2's 'table-layout: auto'.
 */

#include "BasicTableLayoutStrategy.h"

#include <algorithm>

#include "nsTableFrame.h"
#include "nsTableColFrame.h"
#include "nsTableCellFrame.h"
#include "nsLayoutUtils.h"
#include "nsGkAtoms.h"
#include "SpanningCellSorter.h"
#include "nsIContent.h"

using namespace mozilla;
using namespace mozilla::layout;

#undef DEBUG_TABLE_STRATEGY

BasicTableLayoutStrategy::BasicTableLayoutStrategy(nsTableFrame* aTableFrame)
    : nsITableLayoutStrategy(nsITableLayoutStrategy::Auto),
      mTableFrame(aTableFrame) {
  MarkIntrinsicISizesDirty();
}

/* virtual */
BasicTableLayoutStrategy::~BasicTableLayoutStrategy() = default;

/* virtual */
nscoord BasicTableLayoutStrategy::GetMinISize(gfxContext* aRenderingContext) {
  if (mMinISize == NS_INTRINSIC_ISIZE_UNKNOWN) {
    ComputeIntrinsicISizes(aRenderingContext);
  }
  return mMinISize;
}

/* virtual */
nscoord BasicTableLayoutStrategy::GetPrefISize(gfxContext* aRenderingContext,
                                               bool aComputingSize) {
  NS_ASSERTION((mPrefISize == NS_INTRINSIC_ISIZE_UNKNOWN) ==
                   (mPrefISizePctExpand == NS_INTRINSIC_ISIZE_UNKNOWN),
               "dirtyness out of sync");
  if (mPrefISize == NS_INTRINSIC_ISIZE_UNKNOWN) {
    ComputeIntrinsicISizes(aRenderingContext);
  }
  return aComputingSize ? mPrefISizePctExpand : mPrefISize;
}

struct CellISizeInfo {
  CellISizeInfo(nscoord aMinCoord, nscoord aPrefCoord, float aPrefPercent,
                bool aHasSpecifiedISize)
      : hasSpecifiedISize(aHasSpecifiedISize),
        minCoord(aMinCoord),
        prefCoord(aPrefCoord),
        prefPercent(aPrefPercent) {}

  bool hasSpecifiedISize;
  nscoord minCoord;
  nscoord prefCoord;
  float prefPercent;
};

// A helper for ComputeColumnIntrinsicISizes(), used for both column and cell
// intrinsic inline size calculations. The parts needed only for cells are
// skipped when aIsCell is false.
static CellISizeInfo GetISizeInfo(gfxContext* aRenderingContext,
                                  nsIFrame* aFrame, WritingMode aWM,
                                  bool aIsCell) {
  MOZ_ASSERT(aFrame->GetWritingMode() == aWM,
             "The caller is expected to pass aFrame's writing mode!");
  nscoord minCoord, prefCoord;
  const nsStylePosition* stylePos = aFrame->StylePosition();
  const auto positionProperty = aFrame->StyleDisplay()->mPosition;
  bool isQuirks =
      aFrame->PresContext()->CompatibilityMode() == eCompatibility_NavQuirks;
  nscoord boxSizingToBorderEdge = 0;
  if (aIsCell) {
    // If aFrame is a container for font size inflation, then shrink
    // wrapping inside of it should not apply font size inflation.
    AutoMaybeDisableFontInflation an(aFrame);

    // Resolve the cell's block size 'cellBSize' as a percentage basis, in case
    // it impacts its children's inline-size contributions (e.g. via percentage
    // block size + aspect-ratio). However, this behavior might not be
    // web-compatible (Bug 1461852).
    //
    // Note that if the cell *itself* has a percentage-based block size, we
    // treat it as unresolvable here by using an unconstrained cbBSize. It will
    // be resolved during the "special bsize reflow" pass if the table has a
    // specified block size. See nsTableFrame::Reflow() and
    // ReflowInput::Flags::mSpecialBSizeReflow.
    const nscoord cbBSize = NS_UNCONSTRAINEDSIZE;
    const nscoord contentEdgeToBoxSizingBSize =
        stylePos->mBoxSizing == StyleBoxSizing::Border
            ? aFrame->IntrinsicBSizeOffsets().BorderPadding()
            : 0;
    const nscoord cellBSize = nsIFrame::ComputeBSizeValueAsPercentageBasis(
        *stylePos->BSize(aWM, positionProperty),
        *stylePos->MinBSize(aWM, positionProperty),
        *stylePos->MaxBSize(aWM, positionProperty), cbBSize,
        contentEdgeToBoxSizingBSize);

    const IntrinsicSizeInput input(
        aRenderingContext, Nothing(),
        Some(LogicalSize(aWM, NS_UNCONSTRAINEDSIZE, cellBSize)));
    minCoord = aFrame->GetMinISize(input);
    prefCoord = aFrame->GetPrefISize(input);
    // Until almost the end of this function, minCoord and prefCoord
    // represent the box-sizing based isize values (which mean they
    // should include inline padding and border width when
    // box-sizing is set to border-box).
    // Note that this function returns border-box isize, we add the
    // outer edges near the end of this function.

    // XXX Should we ignore percentage padding?
    nsIFrame::IntrinsicSizeOffsetData offsets = aFrame->IntrinsicISizeOffsets();
    if (stylePos->mBoxSizing == StyleBoxSizing::Content) {
      boxSizingToBorderEdge = offsets.padding + offsets.border;
    } else {
      // StyleBoxSizing::Border
      minCoord += offsets.padding + offsets.border;
      prefCoord += offsets.padding + offsets.border;
    }
  } else {
    minCoord = 0;
    prefCoord = 0;
  }
  float prefPercent = 0.0f;
  bool hasSpecifiedISize = false;

  const auto iSize = stylePos->ISize(aWM, positionProperty);
  // NOTE: We're ignoring calc() units with both lengths and percentages here,
  // for lack of a sensible idea for what to do with them.  This means calc()
  // with percentages is basically handled like 'auto' for table cells and
  // columns.
  if (iSize->ConvertsToLength()) {
    hasSpecifiedISize = true;
    nscoord c = iSize->ToLength();
    // Quirk: A cell with "nowrap" set and a coord value for the
    // isize which is bigger than the intrinsic minimum isize uses
    // that coord value as the minimum isize.
    // This is kept up-to-date with dynamic changes to nowrap by code in
    // nsTableCellFrame::AttributeChanged
    if (aIsCell && c > minCoord && isQuirks &&
        aFrame->GetContent()->AsElement()->HasAttr(nsGkAtoms::nowrap)) {
      minCoord = c;
    }
    prefCoord = std::max(c, minCoord);
  } else if (iSize->ConvertsToPercentage()) {
    prefPercent = iSize->ToPercentage();
  } else if (aIsCell) {
    switch (iSize->tag) {
      case StyleSize::Tag::MaxContent:
        // 'inline-size' only affects pref isize, not min
        // isize, so don't change anything
        break;
      case StyleSize::Tag::MinContent:
        prefCoord = minCoord;
        break;
      case StyleSize::Tag::MozAvailable:
      case StyleSize::Tag::WebkitFillAvailable:
      case StyleSize::Tag::Stretch:
      case StyleSize::Tag::FitContent:
      case StyleSize::Tag::FitContentFunction:
        // TODO: Bug 1708310: Make sure fit-content() work properly in table.
      case StyleSize::Tag::Auto:
      case StyleSize::Tag::LengthPercentage:
      case StyleSize::Tag::AnchorSizeFunction:
      case StyleSize::Tag::AnchorContainingCalcFunction:
        break;
    }
  }

  auto maxISize = stylePos->MaxISize(aWM, positionProperty);
  if (nsIFrame::ToExtremumLength(*maxISize)) {
    if (!aIsCell || maxISize->BehavesLikeStretchOnInlineAxis()) {
      maxISize = AnchorResolvedMaxSizeHelper::None();
    } else if (maxISize->IsFitContent() || maxISize->IsFitContentFunction()) {
      // TODO: Bug 1708310: Make sure fit-content() work properly in table.
      // for 'max-inline-size', '-moz-fit-content' is like 'max-content'
      maxISize = AnchorResolvedMaxSizeHelper::MaxContent();
    }
  }
  // XXX To really implement 'max-inline-size' well, we'd need to store
  // it separately on the columns.
  const LogicalSize zeroSize(aWM);
  if (maxISize->ConvertsToLength() || nsIFrame::ToExtremumLength(*maxISize)) {
    nscoord c = aFrame
                    ->ComputeISizeValue(aRenderingContext, aWM, zeroSize,
                                        zeroSize, 0, *maxISize,
                                        *stylePos->BSize(aWM, positionProperty),
                                        aFrame->GetAspectRatio())
                    .mISize;
    minCoord = std::min(c, minCoord);
    prefCoord = std::min(c, prefCoord);
  } else if (maxISize->ConvertsToPercentage()) {
    float p = maxISize->ToPercentage();
    if (p < prefPercent) {
      prefPercent = p;
    }
  }

  auto minISize = stylePos->MinISize(aWM, positionProperty);
  if (nsIFrame::ToExtremumLength(*maxISize)) {
    if (!aIsCell || minISize->BehavesLikeStretchOnInlineAxis()) {
      minISize = AnchorResolvedSizeHelper::Zero();
    } else if (minISize->IsFitContent() || minISize->IsFitContentFunction()) {
      // TODO: Bug 1708310: Make sure fit-content() work properly in table.
      // for 'min-inline-size', '-moz-fit-content' is like 'min-content'
      minISize = AnchorResolvedSizeHelper::MinContent();
    }
  }

  if (minISize->ConvertsToLength() || nsIFrame::ToExtremumLength(*minISize)) {
    nscoord c = aFrame
                    ->ComputeISizeValue(aRenderingContext, aWM, zeroSize,
                                        zeroSize, 0, *minISize,
                                        *stylePos->BSize(aWM, positionProperty),
                                        aFrame->GetAspectRatio())
                    .mISize;
    minCoord = std::max(c, minCoord);
    prefCoord = std::max(c, prefCoord);
  } else if (minISize->ConvertsToPercentage()) {
    float p = minISize->ToPercentage();
    if (p > prefPercent) {
      prefPercent = p;
    }
  }

  // XXX Should col frame have border/padding considered?
  if (aIsCell) {
    minCoord += boxSizingToBorderEdge;
    prefCoord = NSCoordSaturatingAdd(prefCoord, boxSizingToBorderEdge);
  }

  return CellISizeInfo(minCoord, prefCoord, prefPercent, hasSpecifiedISize);
}

static inline CellISizeInfo GetCellISizeInfo(gfxContext* aRenderingContext,
                                             nsTableCellFrame* aCellFrame,
                                             WritingMode aWM) {
  return GetISizeInfo(aRenderingContext, aCellFrame, aWM, true);
}

static inline CellISizeInfo GetColISizeInfo(gfxContext* aRenderingContext,
                                            nsIFrame* aFrame, WritingMode aWM) {
  return GetISizeInfo(aRenderingContext, aFrame, aWM, false);
}

/**
 * The algorithm in this function, in addition to meeting the
 * requirements of Web-compatibility, is also invariant under reordering
 * of the rows within a table (something that most, but not all, other
 * browsers are).
 */
void BasicTableLayoutStrategy::ComputeColumnIntrinsicISizes(
    gfxContext* aRenderingContext) {
  nsTableFrame* tableFrame = mTableFrame;
  nsTableCellMap* cellMap = tableFrame->GetCellMap();
  WritingMode wm = tableFrame->GetWritingMode();

  mozilla::AutoStackArena arena;
  SpanningCellSorter spanningCells;

  // Loop over the columns to consider the columns and cells *without*
  // a colspan.
  int32_t col, col_end;
  for (col = 0, col_end = cellMap->GetColCount(); col < col_end; ++col) {
    nsTableColFrame* colFrame = tableFrame->GetColFrame(col);
    if (!colFrame) {
      NS_ERROR("column frames out of sync with cell map");
      continue;
    }
    colFrame->ResetIntrinsics();
    colFrame->ResetSpanIntrinsics();

    // Consider the isizes on the column.
    CellISizeInfo colInfo = GetColISizeInfo(aRenderingContext, colFrame, wm);
    colFrame->AddCoords(colInfo.minCoord, colInfo.prefCoord,
                        colInfo.hasSpecifiedISize);
    colFrame->AddPrefPercent(colInfo.prefPercent);

    // Consider the isizes on the column-group.  Note that we follow
    // what the HTML spec says here, and make the isize apply to
    // each column in the group, not the group as a whole.

    // If column has isize, column-group doesn't override isize.
    if (colInfo.minCoord == 0 && colInfo.prefCoord == 0 &&
        colInfo.prefPercent == 0.0f) {
      NS_ASSERTION(colFrame->GetParent()->IsTableColGroupFrame(),
                   "expected a column-group");
      colInfo = GetColISizeInfo(aRenderingContext, colFrame->GetParent(), wm);
      colFrame->AddCoords(colInfo.minCoord, colInfo.prefCoord,
                          colInfo.hasSpecifiedISize);
      colFrame->AddPrefPercent(colInfo.prefPercent);
    }

    // Consider the contents of and the isizes on the cells without
    // colspans.
    nsCellMapColumnIterator columnIter(cellMap, col);
    int32_t row, colSpan;
    nsTableCellFrame* cellFrame;
    while ((cellFrame = columnIter.GetNextFrame(&row, &colSpan))) {
      if (colSpan > 1) {
        spanningCells.AddCell(colSpan, row, col);
        continue;
      }

      CellISizeInfo info = GetCellISizeInfo(aRenderingContext, cellFrame, wm);

      colFrame->AddCoords(info.minCoord, info.prefCoord,
                          info.hasSpecifiedISize);
      colFrame->AddPrefPercent(info.prefPercent);
    }
#ifdef DEBUG_dbaron_off
    printf("table %p col %d nonspan: min=%d pref=%d spec=%d pct=%f\n",
           mTableFrame, col, colFrame->GetMinCoord(), colFrame->GetPrefCoord(),
           colFrame->GetHasSpecifiedCoord(), colFrame->GetPrefPercent());
#endif
  }
#ifdef DEBUG_TABLE_STRATEGY
  printf("ComputeColumnIntrinsicISizes single\n");
  mTableFrame->Dump(false, true, false);
#endif

  // Consider the cells with a colspan that we saved in the loop above
  // into the spanning cell sorter.  We consider these cells by seeing
  // if they require adding to the isizes resulting only from cells
  // with a smaller colspan, and therefore we must process them sorted
  // in increasing order by colspan.  For each colspan group, we
  // accumulate new values to accumulate in the column frame's Span*
  // members.
  //
  // Considering things only relative to the isizes resulting from
  // cells with smaller colspans (rather than incrementally including
  // the results from spanning cells, or doing spanning and
  // non-spanning cells in a single pass) means that layout remains
  // row-order-invariant and (except for percentage isizes that add to
  // more than 100%) column-order invariant.
  //
  // Starting with smaller colspans makes it more likely that we
  // satisfy all the constraints given and don't distribute space to
  // columns where we don't need it.
  SpanningCellSorter::Item* item;
  int32_t colSpan;
  while ((item = spanningCells.GetNext(&colSpan))) {
    NS_ASSERTION(colSpan > 1,
                 "cell should not have been put in spanning cell sorter");
    do {
      int32_t row = item->row;
      col = item->col;
      CellData* cellData = cellMap->GetDataAt(row, col);
      NS_ASSERTION(cellData && cellData->IsOrig(),
                   "bogus result from spanning cell sorter");

      nsTableCellFrame* cellFrame = cellData->GetCellFrame();
      NS_ASSERTION(cellFrame, "bogus result from spanning cell sorter");

      CellISizeInfo info = GetCellISizeInfo(aRenderingContext, cellFrame, wm);

      if (info.prefPercent > 0.0f) {
        DistributePctISizeToColumns(info.prefPercent, col, colSpan);
      }
      DistributeISizeToColumns(info.minCoord, col, colSpan,
                               BtlsISizeType::MinISize, info.hasSpecifiedISize);
      DistributeISizeToColumns(info.prefCoord, col, colSpan,
                               BtlsISizeType::PrefISize,
                               info.hasSpecifiedISize);
    } while ((item = item->next));

    // Combine the results of the span analysis into the main results,
    // for each increment of colspan.

    for (col = 0, col_end = cellMap->GetColCount(); col < col_end; ++col) {
      nsTableColFrame* colFrame = tableFrame->GetColFrame(col);
      if (!colFrame) {
        NS_ERROR("column frames out of sync with cell map");
        continue;
      }

      colFrame->AccumulateSpanIntrinsics();
      colFrame->ResetSpanIntrinsics();

#ifdef DEBUG_dbaron_off
      printf("table %p col %d span %d: min=%d pref=%d spec=%d pct=%f\n",
             mTableFrame, col, colSpan, colFrame->GetMinCoord(),
             colFrame->GetPrefCoord(), colFrame->GetHasSpecifiedCoord(),
             colFrame->GetPrefPercent());
#endif
    }
  }

  // Prevent percentages from adding to more than 100% by (to be
  // compatible with other browsers) treating any percentages that would
  // increase the total percentage to more than 100% as the number that
  // would increase it to only 100% (which is 0% if we've already hit
  // 100%).  This means layout depends on the order of columns.
  float pct_used = 0.0f;
  for (col = 0, col_end = cellMap->GetColCount(); col < col_end; ++col) {
    nsTableColFrame* colFrame = tableFrame->GetColFrame(col);
    if (!colFrame) {
      NS_ERROR("column frames out of sync with cell map");
      continue;
    }

    colFrame->AdjustPrefPercent(&pct_used);
  }

#ifdef DEBUG_TABLE_STRATEGY
  printf("ComputeColumnIntrinsicISizes spanning\n");
  mTableFrame->Dump(false, true, false);
#endif
}

void BasicTableLayoutStrategy::ComputeIntrinsicISizes(
    gfxContext* aRenderingContext) {
  ComputeColumnIntrinsicISizes(aRenderingContext);

  nsTableCellMap* cellMap = mTableFrame->GetCellMap();
  nscoord min = 0, pref = 0, max_small_pct_pref = 0, nonpct_pref_total = 0;
  float pct_total = 0.0f;  // always from 0.0f - 1.0f
  int32_t colCount = cellMap->GetColCount();
  // add a total of (colcount + 1) lots of cellSpacingX for columns where a
  // cell originates
  nscoord add = mTableFrame->GetColSpacing(colCount);

  for (int32_t col = 0; col < colCount; ++col) {
    nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
    if (!colFrame) {
      NS_ERROR("column frames out of sync with cell map");
      continue;
    }
    if (mTableFrame->ColumnHasCellSpacingBefore(col)) {
      add += mTableFrame->GetColSpacing(col - 1);
    }
    min += colFrame->GetMinCoord();
    pref = NSCoordSaturatingAdd(pref, colFrame->GetPrefCoord());

    // Percentages are of the table, so we have to reverse them for
    // intrinsic isizes.
    float p = colFrame->GetPrefPercent();
    if (p > 0.0f) {
      nscoord colPref = colFrame->GetPrefCoord();
      nscoord new_small_pct_expand =
          (colPref == nscoord_MAX ? nscoord_MAX : nscoord(float(colPref) / p));
      if (new_small_pct_expand > max_small_pct_pref) {
        max_small_pct_pref = new_small_pct_expand;
      }
      pct_total += p;
    } else {
      nonpct_pref_total =
          NSCoordSaturatingAdd(nonpct_pref_total, colFrame->GetPrefCoord());
    }
  }

  nscoord pref_pct_expand = pref;

  // Account for small percentages expanding the preferred isize of
  // *other* columns.
  if (max_small_pct_pref > pref_pct_expand) {
    pref_pct_expand = max_small_pct_pref;
  }

  // Account for large percentages expanding the preferred isize of
  // themselves.  There's no need to iterate over the columns multiple
  // times, since when there is such a need, the small percentage
  // effect is bigger anyway.  (I think!)
  NS_ASSERTION(0.0f <= pct_total && pct_total <= 1.0f,
               "column percentage inline-sizes not adjusted down to 100%");
  if (pct_total == 1.0f) {
    if (nonpct_pref_total > 0) {
      pref_pct_expand = nscoord_MAX;
      // XXX Or should I use some smaller value?  (Test this using
      // nested tables!)
    }
  } else {
    nscoord large_pct_pref =
        (nonpct_pref_total == nscoord_MAX
             ? nscoord_MAX
             : nscoord(float(nonpct_pref_total) / (1.0f - pct_total)));
    if (large_pct_pref > pref_pct_expand) {
      pref_pct_expand = large_pct_pref;
    }
  }

  // border-spacing isn't part of the basis for percentages
  if (colCount > 0) {
    min += add;
    pref = NSCoordSaturatingAdd(pref, add);
    pref_pct_expand = NSCoordSaturatingAdd(pref_pct_expand, add);
  }

  mMinISize = min;
  mPrefISize = pref;
  mPrefISizePctExpand = pref_pct_expand;
}

/* virtual */
void BasicTableLayoutStrategy::MarkIntrinsicISizesDirty() {
  mMinISize = NS_INTRINSIC_ISIZE_UNKNOWN;
  mPrefISize = NS_INTRINSIC_ISIZE_UNKNOWN;
  mPrefISizePctExpand = NS_INTRINSIC_ISIZE_UNKNOWN;
  mLastCalcISize = nscoord_MIN;
}

/* virtual */
void BasicTableLayoutStrategy::ComputeColumnISizes(
    const ReflowInput& aReflowInput) {
  nscoord iSize = aReflowInput.ComputedISize();

  if (mLastCalcISize == iSize) {
    return;
  }
  mLastCalcISize = iSize;

  NS_ASSERTION((mMinISize == NS_INTRINSIC_ISIZE_UNKNOWN) ==
                   (mPrefISize == NS_INTRINSIC_ISIZE_UNKNOWN),
               "dirtyness out of sync");
  NS_ASSERTION((mMinISize == NS_INTRINSIC_ISIZE_UNKNOWN) ==
                   (mPrefISizePctExpand == NS_INTRINSIC_ISIZE_UNKNOWN),
               "dirtyness out of sync");
  // XXX Is this needed?
  if (mMinISize == NS_INTRINSIC_ISIZE_UNKNOWN) {
    ComputeIntrinsicISizes(aReflowInput.mRenderingContext);
  }

  nsTableCellMap* cellMap = mTableFrame->GetCellMap();
  int32_t colCount = cellMap->GetColCount();
  if (colCount <= 0) {
    return;  // nothing to do
  }

  DistributeISizeToColumns(iSize, 0, colCount, BtlsISizeType::FinalISize,
                           false);

#ifdef DEBUG_TABLE_STRATEGY
  printf("ComputeColumnISizes final\n");
  mTableFrame->Dump(false, true, false);
#endif
}

void BasicTableLayoutStrategy::DistributePctISizeToColumns(float aSpanPrefPct,
                                                           int32_t aFirstCol,
                                                           int32_t aColCount) {
  // First loop to determine:
  int32_t nonPctColCount = 0;  // number of spanned columns without % isize
  nscoord nonPctTotalPrefISize = 0;  // total pref isize of those columns
  // and to reduce aSpanPrefPct by columns that already have % isize

  int32_t scol, scol_end;
  nsTableCellMap* cellMap = mTableFrame->GetCellMap();
  for (scol = aFirstCol, scol_end = aFirstCol + aColCount; scol < scol_end;
       ++scol) {
    nsTableColFrame* scolFrame = mTableFrame->GetColFrame(scol);
    if (!scolFrame) {
      NS_ERROR("column frames out of sync with cell map");
      continue;
    }
    float scolPct = scolFrame->GetPrefPercent();
    if (scolPct == 0.0f) {
      nonPctTotalPrefISize += scolFrame->GetPrefCoord();
      if (cellMap->GetNumCellsOriginatingInCol(scol) > 0) {
        ++nonPctColCount;
      }
    } else {
      aSpanPrefPct -= scolPct;
    }
  }

  if (aSpanPrefPct <= 0.0f || nonPctColCount == 0) {
    // There's no %-isize on the colspan left over to distribute,
    // or there are no columns to which we could distribute %-isize
    return;
  }

  // Second loop, to distribute what remains of aSpanPrefPct
  // between the non-percent-isize spanned columns
  const bool spanHasNonPctPref = nonPctTotalPrefISize > 0;  // Loop invariant
  for (scol = aFirstCol, scol_end = aFirstCol + aColCount; scol < scol_end;
       ++scol) {
    nsTableColFrame* scolFrame = mTableFrame->GetColFrame(scol);
    if (!scolFrame) {
      NS_ERROR("column frames out of sync with cell map");
      continue;
    }

    if (scolFrame->GetPrefPercent() == 0.0f) {
      NS_ASSERTION((!spanHasNonPctPref || nonPctTotalPrefISize != 0) &&
                       nonPctColCount != 0,
                   "should not be zero if we haven't allocated "
                   "all pref percent");

      float allocatedPct;  // % isize to be given to this column
      if (spanHasNonPctPref) {
        // Group so we're multiplying by 1.0f when we need
        // to use up aSpanPrefPct.
        allocatedPct = aSpanPrefPct * (float(scolFrame->GetPrefCoord()) /
                                       float(nonPctTotalPrefISize));
      } else if (cellMap->GetNumCellsOriginatingInCol(scol) > 0) {
        // distribute equally when all pref isizes are 0
        allocatedPct = aSpanPrefPct / float(nonPctColCount);
      } else {
        allocatedPct = 0.0f;
      }
      // Allocate the percent
      scolFrame->AddSpanPrefPercent(allocatedPct);

      // To avoid accumulating rounding error from division,
      // subtract this column's values from the totals.
      aSpanPrefPct -= allocatedPct;
      nonPctTotalPrefISize -= scolFrame->GetPrefCoord();
      if (cellMap->GetNumCellsOriginatingInCol(scol) > 0) {
        --nonPctColCount;
      }

      if (!aSpanPrefPct) {
        // No more span-percent-isize to distribute --> we're done.
        NS_ASSERTION(
            spanHasNonPctPref ? nonPctTotalPrefISize == 0 : nonPctColCount == 0,
            "No more pct inline-size to distribute, "
            "but there are still cols that need some.");
        return;
      }
    }
  }
}

void BasicTableLayoutStrategy::DistributeISizeToColumns(
    nscoord aISize, int32_t aFirstCol, int32_t aColCount,
    BtlsISizeType aISizeType, bool aSpanHasSpecifiedISize) {
  NS_ASSERTION(
      aISizeType != BtlsISizeType::FinalISize ||
          (aFirstCol == 0 &&
           aColCount == mTableFrame->GetCellMap()->GetColCount()),
      "Computing final column isizes, but didn't get full column range");

  nscoord subtract = 0;
  // aISize initially includes border-spacing for the boundaries in between
  // each of the columns. We start at aFirstCol + 1 because the first
  // in-between boundary would be at the left edge of column aFirstCol + 1
  for (int32_t col = aFirstCol + 1; col < aFirstCol + aColCount; ++col) {
    if (mTableFrame->ColumnHasCellSpacingBefore(col)) {
      // border-spacing isn't part of the basis for percentages.
      subtract += mTableFrame->GetColSpacing(col - 1);
    }
  }
  if (aISizeType == BtlsISizeType::FinalISize) {
    // If we're computing final col-isize, then aISize initially includes
    // border spacing on the table's far istart + far iend edge, too.  Need
    // to subtract those out, too.
    subtract += (mTableFrame->GetColSpacing(-1) +
                 mTableFrame->GetColSpacing(aColCount));
  }
  aISize = NSCoordSaturatingSubtract(aISize, subtract, nscoord_MAX);

  /*
   * The goal of this function is to distribute |aISize| between the
   * columns by making an appropriate AddSpanCoords or SetFinalISize
   * call for each column.  (We call AddSpanCoords if we're
   * distributing a column-spanning cell's minimum or preferred isize
   * to its spanned columns.  We call SetFinalISize if we're
   * distributing a table's final isize to its columns.)
   *
   * The idea is to either assign one of the following sets of isizes
   * or a weighted average of two adjacent sets of isizes.  It is not
   * possible to assign values smaller than the smallest set of
   * isizes.  However, see below for handling the case of assigning
   * values larger than the largest set of isizes.  From smallest to
   * largest, these are:
   *
   * 1. [guess_min] Assign all columns their min isize.
   *
   * 2. [guess_min_pct] Assign all columns with percentage isizes
   * their percentage isize, and all other columns their min isize.
   *
   * 3. [guess_min_spec] Assign all columns with percentage isizes
   * their percentage isize, all columns with specified coordinate
   * isizes their pref isize (since it doesn't matter whether it's the
   * largest contributor to the pref isize that was the specified
   * contributor), and all other columns their min isize.
   *
   * 4. [guess_pref] Assign all columns with percentage isizes their
   * specified isize, and all other columns their pref isize.
   *
   * If |aISize| is *larger* than what we would assign in (4), then we
   * expand the columns:
   *
   *   a. if any columns without a specified coordinate isize or
   *   percent isize have nonzero pref isize, in proportion to pref
   *   isize [total_flex_pref]
   *
   *   b. otherwise, if any columns without a specified coordinate
   *   isize or percent isize, but with cells originating in them,
   *   have zero pref isize, equally between these
   *   [numNonSpecZeroISizeCols]
   *
   *   c. otherwise, if any columns without percent isize have nonzero
   *   pref isize, in proportion to pref isize [total_fixed_pref]
   *
   *   d. otherwise, if any columns have nonzero percentage isizes, in
   *   proportion to the percentage isizes [total_pct]
   *
   *   e. otherwise, equally.
   */

  // Loop #1 over the columns, to figure out the four values above so
  // we know which case we're dealing with.

  nscoord guess_min = 0, guess_min_pct = 0, guess_min_spec = 0, guess_pref = 0,
          total_flex_pref = 0, total_fixed_pref = 0;
  float total_pct = 0.0f;  // 0.0f to 1.0f
  int32_t numInfiniteISizeCols = 0;
  int32_t numNonSpecZeroISizeCols = 0;

  int32_t col;
  nsTableCellMap* cellMap = mTableFrame->GetCellMap();
  for (col = aFirstCol; col < aFirstCol + aColCount; ++col) {
    nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
    if (!colFrame) {
      NS_ERROR("column frames out of sync with cell map");
      continue;
    }
    nscoord min_iSize = colFrame->GetMinCoord();
    guess_min += min_iSize;
    if (colFrame->GetPrefPercent() != 0.0f) {
      float pct = colFrame->GetPrefPercent();
      total_pct += pct;
      nscoord val = nscoord(float(aISize) * pct);
      if (val < min_iSize) {
        val = min_iSize;
      }
      guess_min_pct = NSCoordSaturatingAdd(guess_min_pct, val);
      guess_pref = NSCoordSaturatingAdd(guess_pref, val);
    } else {
      nscoord pref_iSize = colFrame->GetPrefCoord();
      if (pref_iSize == nscoord_MAX) {
        ++numInfiniteISizeCols;
      }
      guess_pref = NSCoordSaturatingAdd(guess_pref, pref_iSize);
      guess_min_pct = NSCoordSaturatingAdd(guess_min_pct, min_iSize);
      if (colFrame->GetHasSpecifiedCoord()) {
        // we'll add on the rest of guess_min_spec outside the
        // loop
        nscoord delta = NSCoordSaturatingSubtract(pref_iSize, min_iSize, 0);
        guess_min_spec = NSCoordSaturatingAdd(guess_min_spec, delta);
        total_fixed_pref = NSCoordSaturatingAdd(total_fixed_pref, pref_iSize);
      } else if (pref_iSize == 0) {
        if (cellMap->GetNumCellsOriginatingInCol(col) > 0) {
          ++numNonSpecZeroISizeCols;
        }
      } else {
        total_flex_pref = NSCoordSaturatingAdd(total_flex_pref, pref_iSize);
      }
    }
  }
  guess_min_spec = NSCoordSaturatingAdd(guess_min_spec, guess_min_pct);

  // Determine what we're flexing:
  enum Loop2Type {
    FLEX_PCT_SMALL,        // between (1) and (2) above
    FLEX_FIXED_SMALL,      // between (2) and (3) above
    FLEX_FLEX_SMALL,       // between (3) and (4) above
    FLEX_FLEX_LARGE,       // greater than (4) above, case (a)
    FLEX_FLEX_LARGE_ZERO,  // greater than (4) above, case (b)
    FLEX_FIXED_LARGE,      // greater than (4) above, case (c)
    FLEX_PCT_LARGE,        // greater than (4) above, case (d)
    FLEX_ALL_LARGE         // greater than (4) above, case (e)
  };

  Loop2Type l2t;
  // These are constants (over columns) for each case's math.  We use
  // a pair of nscoords rather than a float so that we can subtract
  // each column's allocation so we avoid accumulating rounding error.
  nscoord space;  // the amount of extra isize to allocate
  union {
    nscoord c;
    float f;
  } basis;  // the sum of the statistic over columns to divide it
  if (aISize < guess_pref) {
    if (aISizeType != BtlsISizeType::FinalISize && aISize <= guess_min) {
      // Return early -- we don't have any extra space to distribute.
      return;
    }
    NS_ASSERTION(
        !(aISizeType == BtlsISizeType::FinalISize && aISize < guess_min),
        "Table inline-size is less than the sum of its columns' min "
        "inline-sizes");
    if (aISize < guess_min_pct) {
      l2t = FLEX_PCT_SMALL;
      space = aISize - guess_min;
      basis.c = guess_min_pct - guess_min;
    } else if (aISize < guess_min_spec) {
      l2t = FLEX_FIXED_SMALL;
      space = aISize - guess_min_pct;
      basis.c =
          NSCoordSaturatingSubtract(guess_min_spec, guess_min_pct, nscoord_MAX);
    } else {
      l2t = FLEX_FLEX_SMALL;
      space = aISize - guess_min_spec;
      basis.c =
          NSCoordSaturatingSubtract(guess_pref, guess_min_spec, nscoord_MAX);
    }
  } else {
    space = NSCoordSaturatingSubtract(aISize, guess_pref, nscoord_MAX);
    if (total_flex_pref > 0) {
      l2t = FLEX_FLEX_LARGE;
      basis.c = total_flex_pref;
    } else if (numNonSpecZeroISizeCols > 0) {
      l2t = FLEX_FLEX_LARGE_ZERO;
      basis.c = numNonSpecZeroISizeCols;
    } else if (total_fixed_pref > 0) {
      l2t = FLEX_FIXED_LARGE;
      basis.c = total_fixed_pref;
    } else if (total_pct > 0.0f) {
      l2t = FLEX_PCT_LARGE;
      basis.f = total_pct;
    } else {
      l2t = FLEX_ALL_LARGE;
      basis.c = aColCount;
    }
  }

#ifdef DEBUG_dbaron_off
  printf(
      "ComputeColumnISizes: %d columns in isize %d,\n"
      "  guesses=[%d,%d,%d,%d], totals=[%d,%d,%f],\n"
      "  l2t=%d, space=%d, basis.c=%d\n",
      aColCount, aISize, guess_min, guess_min_pct, guess_min_spec, guess_pref,
      total_flex_pref, total_fixed_pref, total_pct, l2t, space, basis.c);
#endif

  for (col = aFirstCol; col < aFirstCol + aColCount; ++col) {
    nsTableColFrame* colFrame = mTableFrame->GetColFrame(col);
    if (!colFrame) {
      NS_ERROR("column frames out of sync with cell map");
      continue;
    }
    nscoord col_iSize;

    float pct = colFrame->GetPrefPercent();
    if (pct != 0.0f) {
      col_iSize = nscoord(float(aISize) * pct);
      nscoord col_min = colFrame->GetMinCoord();
      if (col_iSize < col_min) {
        col_iSize = col_min;
      }
    } else {
      col_iSize = colFrame->GetPrefCoord();
    }

    nscoord col_iSize_before_adjust = col_iSize;

    switch (l2t) {
      case FLEX_PCT_SMALL:
        col_iSize = col_iSize_before_adjust = colFrame->GetMinCoord();
        if (pct != 0.0f) {
          nscoord pct_minus_min = nscoord(float(aISize) * pct) - col_iSize;
          if (pct_minus_min > 0) {
            float c = float(space) / float(basis.c);
            basis.c -= pct_minus_min;
            col_iSize = NSCoordSaturatingAdd(
                col_iSize, NSToCoordRound(float(pct_minus_min) * c));
          }
        }
        break;
      case FLEX_FIXED_SMALL:
        if (pct == 0.0f) {
          NS_ASSERTION(col_iSize == colFrame->GetPrefCoord(),
                       "wrong inline-size assigned");
          if (colFrame->GetHasSpecifiedCoord()) {
            nscoord col_min = colFrame->GetMinCoord();
            nscoord pref_minus_min = col_iSize - col_min;
            col_iSize = col_iSize_before_adjust = col_min;
            if (pref_minus_min != 0) {
              float c = float(space) / float(basis.c);
              basis.c = NSCoordSaturatingSubtract(basis.c, pref_minus_min,
                                                  nscoord_MAX);
              col_iSize = NSCoordSaturatingAdd(
                  col_iSize, NSToCoordRound(float(pref_minus_min) * c));
            }
          } else {
            col_iSize = col_iSize_before_adjust = colFrame->GetMinCoord();
          }
        }
        break;
      case FLEX_FLEX_SMALL:
        if (pct == 0.0f && !colFrame->GetHasSpecifiedCoord()) {
          NS_ASSERTION(col_iSize == colFrame->GetPrefCoord(),
                       "wrong inline-size assigned");
          nscoord col_min = colFrame->GetMinCoord();
          nscoord pref_minus_min =
              NSCoordSaturatingSubtract(col_iSize, col_min, 0);
          col_iSize = col_iSize_before_adjust = col_min;
          if (pref_minus_min != 0) {
            float c = float(space) / float(basis.c);
            // If we have infinite-isize cols, then the standard
            // adjustment to col_iSize using 'c' won't work,
            // because basis.c and pref_minus_min are both
            // nscoord_MAX and will cancel each other out in the
            // col_iSize adjustment (making us assign all the
            // space to the first inf-isize col).  To correct for
            // this, we'll also divide by numInfiniteISizeCols to
            // spread the space equally among the inf-isize cols.
            if (numInfiniteISizeCols) {
              if (colFrame->GetPrefCoord() == nscoord_MAX) {
                c = c / float(numInfiniteISizeCols);
                --numInfiniteISizeCols;
              } else {
                c = 0.0f;
              }
            }
            basis.c =
                NSCoordSaturatingSubtract(basis.c, pref_minus_min, nscoord_MAX);
            col_iSize = NSCoordSaturatingAdd(
                col_iSize, NSToCoordRound(float(pref_minus_min) * c));
          }
        }
        break;
      case FLEX_FLEX_LARGE:
        if (pct == 0.0f && !colFrame->GetHasSpecifiedCoord()) {
          NS_ASSERTION(col_iSize == colFrame->GetPrefCoord(),
                       "wrong inline-size assigned");
          if (col_iSize != 0) {
            if (space == nscoord_MAX) {
              basis.c -= col_iSize;
              col_iSize = nscoord_MAX;
            } else {
              float c = float(space) / float(basis.c);
              basis.c =
                  NSCoordSaturatingSubtract(basis.c, col_iSize, nscoord_MAX);
              col_iSize = NSCoordSaturatingAdd(
                  col_iSize, NSToCoordRound(float(col_iSize) * c));
            }
          }
        }
        break;
      case FLEX_FLEX_LARGE_ZERO:
        if (pct == 0.0f && !colFrame->GetHasSpecifiedCoord() &&
            cellMap->GetNumCellsOriginatingInCol(col) > 0) {
          NS_ASSERTION(col_iSize == 0 && colFrame->GetPrefCoord() == 0,
                       "Since we're in FLEX_FLEX_LARGE_ZERO case, "
                       "all auto-inline-size cols should have zero "
                       "pref inline-size.");
          float c = float(space) / float(basis.c);
          col_iSize += NSToCoordRound(c);
          --basis.c;
        }
        break;
      case FLEX_FIXED_LARGE:
        if (pct == 0.0f) {
          NS_ASSERTION(col_iSize == colFrame->GetPrefCoord(),
                       "wrong inline-size assigned");
          NS_ASSERTION(
              colFrame->GetHasSpecifiedCoord() || colFrame->GetPrefCoord() == 0,
              "wrong case");
          if (col_iSize != 0) {
            float c = float(space) / float(basis.c);
            basis.c =
                NSCoordSaturatingSubtract(basis.c, col_iSize, nscoord_MAX);
            col_iSize = NSCoordSaturatingAdd(
                col_iSize, NSToCoordRound(float(col_iSize) * c));
          }
        }
        break;
      case FLEX_PCT_LARGE:
        NS_ASSERTION(pct != 0.0f || colFrame->GetPrefCoord() == 0,
                     "wrong case");
        if (pct != 0.0f) {
          float c = float(space) / basis.f;
          col_iSize = NSCoordSaturatingAdd(col_iSize, NSToCoordRound(pct * c));
          basis.f -= pct;
        }
        break;
      case FLEX_ALL_LARGE: {
        float c = float(space) / float(basis.c);
        col_iSize = NSCoordSaturatingAdd(col_iSize, NSToCoordRound(c));
        --basis.c;
      } break;
    }

    // Only subtract from space if it's a real number.
    if (space != nscoord_MAX) {
      NS_ASSERTION(col_iSize != nscoord_MAX,
                   "How is col_iSize nscoord_MAX if space isn't?");
      NS_ASSERTION(
          col_iSize_before_adjust != nscoord_MAX,
          "How is col_iSize_before_adjust nscoord_MAX if space isn't?");
      space -= col_iSize - col_iSize_before_adjust;
    }

    NS_ASSERTION(col_iSize >= colFrame->GetMinCoord(),
                 "assigned inline-size smaller than min");

    // Apply the new isize
    switch (aISizeType) {
      case BtlsISizeType::MinISize: {
        // Note: AddSpanCoords requires both a min and pref isize.
        // For the pref isize, we'll just pass in our computed
        // min isize, because the real pref isize will be at least
        // as big
        colFrame->AddSpanCoords(col_iSize, col_iSize, aSpanHasSpecifiedISize);
      } break;
      case BtlsISizeType::PrefISize: {
        // Note: AddSpanCoords requires both a min and pref isize.
        // For the min isize, we'll just pass in 0, because
        // the real min isize will be at least 0
        colFrame->AddSpanCoords(0, col_iSize, aSpanHasSpecifiedISize);
      } break;
      case BtlsISizeType::FinalISize: {
        nscoord old_final = colFrame->GetFinalISize();
        colFrame->SetFinalISize(col_iSize);

        if (old_final != col_iSize) {
          mTableFrame->DidResizeColumns();
        }
      } break;
    }
  }
  NS_ASSERTION(
      (space == 0 || space == nscoord_MAX) &&
          ((l2t == FLEX_PCT_LARGE) ? (-0.001f < basis.f && basis.f < 0.001f)
                                   : (basis.c == 0 || basis.c == nscoord_MAX)),
      "didn't subtract all that we added");
}
