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

namespace css = mozilla::css;

#undef  DEBUG_TABLE_STRATEGY 

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

/* virtual */
BasicTableLayoutStrategy::~BasicTableLayoutStrategy()
{
}

/* virtual */ nscoord
BasicTableLayoutStrategy::GetMinISize(nsRenderingContext* aRenderingContext)
{
    DISPLAY_MIN_WIDTH(mTableFrame, mMinISize);
    if (mMinISize == NS_INTRINSIC_WIDTH_UNKNOWN) {
        ComputeIntrinsicISizes(aRenderingContext);
    }
    return mMinISize;
}

/* virtual */ nscoord
BasicTableLayoutStrategy::GetPrefISize(nsRenderingContext* aRenderingContext,
                                       bool aComputingSize)
{
    DISPLAY_PREF_WIDTH(mTableFrame, mPrefISize);
    NS_ASSERTION((mPrefISize == NS_INTRINSIC_WIDTH_UNKNOWN) ==
                 (mPrefISizePctExpand == NS_INTRINSIC_WIDTH_UNKNOWN),
                 "dirtyness out of sync");
    if (mPrefISize == NS_INTRINSIC_WIDTH_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;
};

// Used for both column and cell calculations.  The parts needed only
// for cells are skipped when aIsCell is false.
static CellISizeInfo
GetISizeInfo(nsRenderingContext *aRenderingContext,
             nsIFrame *aFrame, WritingMode aWM, bool aIsCell)
{
    nscoord minCoord, prefCoord;
    const nsStylePosition *stylePos = aFrame->StylePosition();
    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);

        minCoord = aFrame->GetMinISize(aRenderingContext);
        prefCoord = aFrame->GetPrefISize(aRenderingContext);
        // 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::IntrinsicISizeOffsetData offsets =
            aFrame->IntrinsicISizeOffsets();

        // In quirks mode, table cell isize should be content-box,
        // but bsize should be border box.
        // Because of this historic anomaly, we do not use quirk.css.
        // (We can't specify one value of box-sizing for isize and another
        // for bsize).
        // For this reason, we also do not use box-sizing for just one of
        // them, as this may be confusing.
        if (isQuirks) {
            boxSizingToBorderEdge = offsets.hPadding + offsets.hBorder;
        }
        else {
            switch (stylePos->mBoxSizing) {
                case StyleBoxSizing::Content:
                    boxSizingToBorderEdge = offsets.hPadding + offsets.hBorder;
                    break;
                case StyleBoxSizing::Padding:
                    minCoord += offsets.hPadding;
                    prefCoord += offsets.hPadding;
                    boxSizingToBorderEdge = offsets.hBorder;
                    break;
                case StyleBoxSizing::Border:
                    minCoord += offsets.hPadding + offsets.hBorder;
                    prefCoord += offsets.hPadding + offsets.hBorder;
                    break;
            }
        }
    } else {
        minCoord = 0;
        prefCoord = 0;
    }
    float prefPercent = 0.0f;
    bool hasSpecifiedISize = false;

    const nsStyleCoord& iSize = stylePos->ISize(aWM);
    nsStyleUnit unit = iSize.GetUnit();
    // NOTE: We're ignoring calc() units with 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;
        // Note: since ComputeISizeValue was designed to return content-box
        // isize, it will (in some cases) subtract the box-sizing edges.
        // We prevent this unwanted behavior by calling it with
        // aContentEdgeToBoxSizing and aBoxSizingToMarginEdge set to 0.
        nscoord c = nsLayoutUtils::ComputeISizeValue(aRenderingContext,
                                                     aFrame, 0, 0, 0, iSize);
        // 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()->HasAttr(kNameSpaceID_None,
                                          nsGkAtoms::nowrap)) {
            minCoord = c;
        }
        prefCoord = std::max(c, minCoord);
    } else if (unit == eStyleUnit_Percent) {
        prefPercent = iSize.GetPercentValue();
    } else if (unit == eStyleUnit_Enumerated && aIsCell) {
        switch (iSize.GetIntValue()) {
            case NS_STYLE_WIDTH_MAX_CONTENT:
                // 'inline-size' only affects pref isize, not min
                // isize, so don't change anything
                break;
            case NS_STYLE_WIDTH_MIN_CONTENT:
                prefCoord = minCoord;
                break;
            case NS_STYLE_WIDTH_FIT_CONTENT:
            case NS_STYLE_WIDTH_AVAILABLE:
                // act just like 'inline-size: auto'
                break;
            default:
                NS_NOTREACHED("unexpected enumerated value");
        }
    }

    nsStyleCoord maxISize(stylePos->MaxISize(aWM));
    if (maxISize.GetUnit() == eStyleUnit_Enumerated) {
        if (!aIsCell || maxISize.GetIntValue() == NS_STYLE_WIDTH_AVAILABLE) {
            maxISize.SetNoneValue();
        } else if (maxISize.GetIntValue() == NS_STYLE_WIDTH_FIT_CONTENT) {
            // for 'max-inline-size', '-moz-fit-content' is like
            // '-moz-max-content'
            maxISize.SetIntValue(NS_STYLE_WIDTH_MAX_CONTENT,
                                 eStyleUnit_Enumerated);
        }
    }
    unit = maxISize.GetUnit();
    // XXX To really implement 'max-inline-size' well, we'd need to store
    // it separately on the columns.
    if (maxISize.ConvertsToLength() || unit == eStyleUnit_Enumerated) {
        nscoord c =
            nsLayoutUtils::ComputeISizeValue(aRenderingContext, aFrame,
                                             0, 0, 0, maxISize);
        minCoord = std::min(c, minCoord);
        prefCoord = std::min(c, prefCoord);
    } else if (unit == eStyleUnit_Percent) {
        float p = stylePos->MaxISize(aWM).GetPercentValue();
        if (p < prefPercent) {
            prefPercent = p;
        }
    }
    // treat calc() with percentages on max-inline-size just like 'none'.

    nsStyleCoord minISize(stylePos->MinISize(aWM));
    if (minISize.GetUnit() == eStyleUnit_Enumerated) {
        if (!aIsCell || minISize.GetIntValue() == NS_STYLE_WIDTH_AVAILABLE) {
            minISize.SetCoordValue(0);
        } else if (minISize.GetIntValue() == NS_STYLE_WIDTH_FIT_CONTENT) {
            // for 'min-inline-size', '-moz-fit-content' is like
            // '-moz-min-content'
            minISize.SetIntValue(NS_STYLE_WIDTH_MIN_CONTENT,
                                 eStyleUnit_Enumerated);
        }
    }
    unit = minISize.GetUnit();
    if (minISize.ConvertsToLength() || unit == eStyleUnit_Enumerated) {
        nscoord c =
            nsLayoutUtils::ComputeISizeValue(aRenderingContext, aFrame,
                                             0, 0, 0, minISize);
        minCoord = std::max(c, minCoord);
        prefCoord = std::max(c, prefCoord);
    } else if (unit == eStyleUnit_Percent) {
        float p = stylePos->MinISize(aWM).GetPercentValue();
        if (p > prefPercent) {
            prefPercent = p;
        }
    }
    // treat calc() with percentages on min-inline-size just like '0'.

    // 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(nsRenderingContext *aRenderingContext,
                 nsTableCellFrame *aCellFrame, WritingMode aWM)
{
    return GetISizeInfo(aRenderingContext, aCellFrame, aWM, true);
}

static inline CellISizeInfo
GetColISizeInfo(nsRenderingContext *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(nsRenderingContext* 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()->GetType() ==
                             nsGkAtoms::tableColGroupFrame,
                         "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, 
                                     BTLS_MIN_ISIZE, info.hasSpecifiedISize);
            DistributeISizeToColumns(info.prefCoord, col, colSpan, 
                                     BTLS_PREF_ISIZE, 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(nsRenderingContext* 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_WIDTH_UNKNOWN;
    mPrefISize = NS_INTRINSIC_WIDTH_UNKNOWN;
    mPrefISizePctExpand = NS_INTRINSIC_WIDTH_UNKNOWN;
    mLastCalcISize = nscoord_MIN;
}

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

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

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

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

    DistributeISizeToColumns(iSize, 0, colCount, BTLS_FINAL_ISIZE, 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 != BTLS_FINAL_ISIZE ||
                 (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 == BTLS_FINAL_ISIZE) {
        // 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 += 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 += 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 != BTLS_FINAL_ISIZE && aISize <= guess_min) {
            // Return early -- we don't have any extra space to distribute.
            return;
        }
        NS_ASSERTION(!(aISizeType == BTLS_FINAL_ISIZE && 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 += 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 -= pref_minus_min;
                            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 += 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 -= col_iSize;
                            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 -= col_iSize;
                        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 += NSToCoordRound(pct * c);
                    basis.f -= pct;
                }
                break;
            case FLEX_ALL_LARGE:
                {
                    float c = float(space) / float(basis.c);
                    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 BTLS_MIN_ISIZE:
                {
                    // 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 BTLS_PREF_ISIZE:
                {
                    // 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 BTLS_FINAL_ISIZE:
                {
                    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");
}
