/*
 * Copyright (C) 2020 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "TableFormattingContext.h"

#if ENABLE(LAYOUT_FORMATTING_CONTEXT)

#include "LayoutBox.h"
#include "LayoutBoxGeometry.h"
#include "TableFormattingGeometry.h"

namespace WebCore {
namespace Layout {

// https://www.w3.org/TR/css-tables-3/#table-layout-algorithm
TableFormattingContext::TableLayout::TableLayout(const TableFormattingContext& formattingContext, const TableGrid& grid)
    : m_formattingContext(formattingContext)
    , m_grid(grid)
{
}

struct ColumnSpan {
    static size_t hasSpan(const TableGrid::Slot& slot) { return slot.hasColumnSpan(); }
    static size_t isSpanned(const TableGrid::Slot& slot) { return slot.isColumnSpanned(); }

    static size_t spanCount(const TableGrid::Cell& cell) { return cell.columnSpan(); }
    static size_t startSpan(const TableGrid::Cell& cell) { return cell.startColumn(); }
    static size_t endSpan(const TableGrid::Cell& cell) { return cell.endColumn(); }

    static size_t index(size_t columnIndex, size_t /*rowIndex*/) { return columnIndex; }
    static size_t size(const TableGrid& grid) { return grid.columns().size(); }

    static LayoutUnit spacing(const TableGrid& grid) { return grid.horizontalSpacing(); }
};

struct RowSpan {
    static size_t hasSpan(const TableGrid::Slot& slot) { return slot.hasRowSpan(); }
    static size_t isSpanned(const TableGrid::Slot& slot) { return slot.isRowSpanned(); }

    static size_t spanCount(const TableGrid::Cell& cell) { return cell.rowSpan(); }
    static size_t startSpan(const TableGrid::Cell& cell) { return cell.startRow(); }
    static size_t endSpan(const TableGrid::Cell& cell) { return cell.endRow(); }

    static size_t index(size_t /*columnIndex*/, size_t rowIndex) { return rowIndex; }
    static size_t size(const TableGrid& grid) { return grid.rows().size(); }

    static LayoutUnit spacing(const TableGrid& grid) { return grid.verticalSpacing(); }
};

struct GridSpace {
    bool isEmpty() const { return !preferredSize; }

    enum class Type {
        Percent,
        Fixed,
        Relative,
        Auto
    };
    Type type { Type::Auto };
    // Preferred width/height for column/row we start the distribution with (usually the minimum content width).
    float preferredSize { 0 };
    // The base space value for the distribution computation e.g [ 9 ] [ 1 ] values for 2 columns in the table means that if
    // we've go 100px flexible space to distribute, 90px and 10px go to the columns respectively.
    float flexBase { 0 };
    float minimumSize { 0 };
};

inline static GridSpace& operator-(GridSpace& a, const GridSpace& b)
{
    a.preferredSize = std::max(0.0f, a.preferredSize - b.preferredSize);
    a.flexBase = std::max(0.0f, a.flexBase - b.flexBase);
    return a;
}

inline static GridSpace& operator+=(GridSpace& a, const GridSpace& b)
{
    a.preferredSize += b.preferredSize;
    a.flexBase += b.flexBase;
    return a;
}

inline static GridSpace& operator-=(GridSpace& a, const GridSpace& b)
{
    return a - b;
}

inline static GridSpace& operator/(GridSpace& a, unsigned value)
{
    a.preferredSize /= value;
    a.flexBase /= value;
    return a;
}

template <typename SpanType>
static Vector<LayoutUnit> distributeAvailableSpace(const TableGrid& grid, LayoutUnit availableSpace, const Function<GridSpace(const TableGrid::Slot&, size_t)>& slotSpace)
{
    auto& columns = grid.columns();
    auto& rows = grid.rows();
    Vector<std::optional<GridSpace>> resolvedItems(SpanType::size(grid));
    auto collectNonSpanningCells = [&] {
        // 1. Collect the non-spanning spaces first. They are used for the final distribution as well as for distributing the spanning space.
        for (size_t columnIndex = 0; columnIndex < columns.size(); ++columnIndex) {
            for (size_t rowIndex = 0; rowIndex < rows.size(); ++rowIndex) {
                auto& slot = *grid.slot({ columnIndex, rowIndex });
                if (SpanType::hasSpan(slot) || SpanType::isSpanned(slot))
                    continue;
                auto index = SpanType::index(columnIndex, rowIndex);
                auto currentSpace = slotSpace(slot, index);
                if (!resolvedItems[index]) {
                    resolvedItems[index] = currentSpace;
                    continue;
                }
                auto& resolvedItem = resolvedItems[index];
                // FIXME: Add support for mixed column/row types e.g. first row first column is fixed, while second row first column is percent.
                ASSERT(resolvedItem->type == currentSpace.type);
                resolvedItem->preferredSize = std::max(resolvedItem->preferredSize, currentSpace.preferredSize);
                resolvedItem->flexBase = std::max(resolvedItem->flexBase, currentSpace.flexBase);
            }
        }
    };
    collectNonSpanningCells();

    auto collectAndDistributeSpanningCells = [&] {
        // 2. Collect the spanning cells.
        struct SpanningCell {
            SlotPosition position;
            GridSpace unresolvedSpace;
        };
        Vector<SpanningCell> spanningCells;
        for (size_t rowIndex = 0; rowIndex < rows.size(); ++rowIndex) {
            for (size_t columnIndex = 0; columnIndex < columns.size(); ++columnIndex) {
                auto& slot = *grid.slot({ columnIndex, rowIndex });
                if (SpanType::hasSpan(slot))
                    spanningCells.append({ { columnIndex, rowIndex }, slotSpace(slot, SpanType::index(columnIndex, rowIndex)) });
            }
        }
        // We need these spanning cells in the order of the number of columns/rows they span so that
        // we can resolve overlapping spans starting with the shorter ones e.g.
        // <td colspan=4>#a</td><td>#b</td>
        // <td colspan=2>#c</td><td colspan=3>#d</td>
        std::sort(spanningCells.begin(), spanningCells.end(), [&] (auto& a, auto& b) {
            return SpanType::spanCount(grid.slot(a.position)->cell()) < SpanType::spanCount(grid.slot(b.position)->cell());
        });

        // 3. Distribute the spanning cells' mimimum space across the columns/rows using the non-spanning spaces.
        // e.g. [ 1 ][ 5 ][ 1 ]
        //      [    9   ][ 1 ]
        // The initial widths are: [ 2 ][ 7 ][ 1 ]
        for (auto spanningCell : spanningCells) {
            auto& cell = grid.slot(spanningCell.position)->cell();
            auto unresolvedSpanningSpace = spanningCell.unresolvedSpace;
            if (!resolvedItems[SpanType::startSpan(cell)] || !resolvedItems[SpanType::endSpan(cell) - 1]) {
                // <td colspan=4>#a</td><td>#b</td>
                // <td colspan=2>#c</td><td colspan=3>#d</td>
                // Unresolved columns are: 1 2 3 4
                // 1. Take colspan=2 (shortest span) and resolve column 1 and 2
                // 2. Take colspan=3 and resolve column 3 and 4 (5 is resolved because it already has a non-spanning cell).
                // 3. colspan=4 needs no resolving because all the spanned columns (1 2 3 4) have already been resolved.
                auto unresolvedColumnCount = cell.columnSpan();
                for (auto spanIndex = SpanType::startSpan(cell); spanIndex < SpanType::endSpan(cell); ++spanIndex) {
                    if (!resolvedItems[spanIndex])
                        continue;
                    ASSERT(unresolvedColumnCount);
                    --unresolvedColumnCount;
                    unresolvedSpanningSpace -= *resolvedItems[spanIndex];
                }
                ASSERT(unresolvedColumnCount);
                auto equalSpaceForSpannedColumns = unresolvedSpanningSpace / unresolvedColumnCount;
                for (auto spanIndex = SpanType::startSpan(cell); spanIndex < SpanType::endSpan(cell); ++spanIndex) {
                    if (resolvedItems[spanIndex])
                        continue;
                    resolvedItems[spanIndex] = equalSpaceForSpannedColumns;
                }
            } else {
                // 1. Collect the non-spanning resolved spaces.
                // 2. Distribute the extra space among the spanned columns/rows based on the resolved space values.
                // e.g. spanning width: [   9   ]. Resolved widths for the spanned columns: [ 1 ] [ 2 ]
                // New resolved widths: [ 3 ] [ 6 ].
                auto resolvedSpanningSpace = GridSpace { };
                for (auto spanIndex = SpanType::startSpan(cell); spanIndex < SpanType::endSpan(cell); ++spanIndex)
                    resolvedSpanningSpace += *resolvedItems[spanIndex];
                if (resolvedSpanningSpace.preferredSize >= unresolvedSpanningSpace.preferredSize) {
                    // The spanning cell fits the spanned columns/rows just fine. Nothing to distribute.
                    continue;
                }
                auto spacing = SpanType::spacing(grid) * (SpanType::spanCount(cell) - 1);
                auto spaceToDistribute = unresolvedSpanningSpace - GridSpace { { }, spacing, spacing } - resolvedSpanningSpace;
                if (!spaceToDistribute.isEmpty()) {
                    auto columnsFlexBase = spaceToDistribute.flexBase / resolvedSpanningSpace.flexBase;
                    for (auto spanIndex = SpanType::startSpan(cell); spanIndex < SpanType::endSpan(cell); ++spanIndex)
                        *resolvedItems[spanIndex] += GridSpace { resolvedItems[spanIndex]->type, resolvedItems[spanIndex]->preferredSize * columnsFlexBase, resolvedItems[spanIndex]->flexBase * columnsFlexBase };
                }
            }
        }
    };
    collectAndDistributeSpanningCells();

    Vector<LayoutUnit> distributedSpaces(resolvedItems.size());
    auto spaceToDistribute = 0.f;
    auto computeSpaceToDistribute = [&] {
        // 4. Distribute the extra space using the final resolved sizes.
        spaceToDistribute = availableSpace - (resolvedItems.size() + 1) * SpanType::spacing(grid);
        auto adjustabledSpace = GridSpace { };
        for (auto& resolvedItem : resolvedItems) {
            ASSERT(resolvedItem);
            adjustabledSpace += *resolvedItem;
        }
        spaceToDistribute -= adjustabledSpace.preferredSize;
    };
    computeSpaceToDistribute();

    // Let's start with the preferred size for each column.
    for (size_t columnIndex = 0; columnIndex < resolvedItems.size(); ++columnIndex)
        distributedSpaces[columnIndex] = LayoutUnit { resolvedItems[columnIndex]->preferredSize };

    auto distributeSpace = [&] {
        if (!spaceToDistribute)
            return;
        // Setup the priority lists. We use these when expanding/shrinking slots.
        Vector<size_t> autoColumnIndexes;
        Vector<size_t> relativeColumnIndexes;
        Vector<size_t> fixedColumnIndexes;
        Vector<size_t> percentColumnIndexes;

        for (size_t columnIndex = 0; columnIndex < resolvedItems.size(); ++columnIndex) {
            switch (resolvedItems[columnIndex]->type) {
            case GridSpace::Type::Percent:
                percentColumnIndexes.append(columnIndex);
                break;
            case GridSpace::Type::Fixed:
                fixedColumnIndexes.append(columnIndex);
                break;
            case GridSpace::Type::Relative:
                relativeColumnIndexes.append(columnIndex);
                break;
            case GridSpace::Type::Auto:
                autoColumnIndexes.append(columnIndex);
                break;
            default:
                ASSERT_NOT_REACHED();
                break;
            }
        }

        if (spaceToDistribute > 0) {
            // Each column can get some extra space.
            auto hasSpaceToDistribute = [&] {
                ASSERT(spaceToDistribute > -LayoutUnit::epsilon());
                return spaceToDistribute > LayoutUnit::epsilon();
            };

            auto expandSpace = [&](const auto& columnIndexes) {
                auto adjustabledSpace = GridSpace { };
                for (auto& columnIndex : columnIndexes)
                    adjustabledSpace += *resolvedItems[columnIndex];

                auto columnsFlexBase = adjustabledSpace.flexBase ? spaceToDistribute / adjustabledSpace.flexBase : 0.f;
                for (auto& columnIndex : columnIndexes) {
                    auto extraSpace = columnsFlexBase * resolvedItems[columnIndex]->flexBase;
                    distributedSpaces[columnIndex] += LayoutUnit { extraSpace };
                    spaceToDistribute -= extraSpace;
                    if (!hasSpaceToDistribute())
                        return;
                }
            };
            // We distribute the extra space among columns in the priority order as follows:
            expandSpace(fixedColumnIndexes);
            if (hasSpaceToDistribute())
                expandSpace(percentColumnIndexes);
            if (hasSpaceToDistribute())
                expandSpace(relativeColumnIndexes);
            if (hasSpaceToDistribute())
                expandSpace(autoColumnIndexes);
            ASSERT(!hasSpaceToDistribute());
            return;
        }
        // Can't accommodate the preferred width. Let's use the priority list to shrink columns.
        auto spaceNeeded = -spaceToDistribute;

        auto needsMoreSpace = [&] {
            ASSERT(spaceNeeded > -LayoutUnit::epsilon());
            return spaceNeeded > LayoutUnit::epsilon();
        };

        auto shrinkSpace = [&](const auto& columnIndexes) {
            auto adjustabledSpace = GridSpace { };
            for (auto& columnIndex : columnIndexes)
                adjustabledSpace += *resolvedItems[columnIndex];

            auto columnsFlexBase = adjustabledSpace.flexBase ? spaceNeeded / adjustabledSpace.flexBase : 0.f;
            for (auto& columnIndex : columnIndexes) {
                auto& resolvedItem = *resolvedItems[columnIndex];
                auto spaceToRemove = std::min(resolvedItem.preferredSize - resolvedItem.minimumSize, columnsFlexBase * resolvedItem.flexBase);
                spaceToRemove = std::min(spaceToRemove, spaceNeeded);
                distributedSpaces[columnIndex] -= spaceToRemove;
                spaceNeeded -= spaceToRemove;
                if (!needsMoreSpace())
                    return;
            }
        };
        shrinkSpace(autoColumnIndexes);
        if (needsMoreSpace())
            shrinkSpace(relativeColumnIndexes);
        if (needsMoreSpace())
            shrinkSpace(fixedColumnIndexes);
        if (needsMoreSpace())
            shrinkSpace(percentColumnIndexes);
        ASSERT(!needsMoreSpace());
    };
    distributeSpace();

    return distributedSpaces;
}

TableFormattingContext::TableLayout::DistributedSpaces TableFormattingContext::TableLayout::distributedHorizontalSpace(LayoutUnit availableHorizontalSpace)
{
    auto hasEnoughAvailableSpaceForMaximumWidth = availableHorizontalSpace >= m_grid.widthConstraints()->maximum;
    return distributeAvailableSpace<ColumnSpan>(m_grid, availableHorizontalSpace, [&] (const TableGrid::Slot& slot, size_t columnIndex) {
        auto& column = m_grid.columns().list()[columnIndex];
        auto columnWidth = std::optional<float> { };
        auto type = GridSpace::Type::Auto;

        auto& computedLogicalWidth = column.computedLogicalWidth();
        switch (computedLogicalWidth.type()) {
        case LengthType::Fixed:
            columnWidth = computedLogicalWidth.value();
            type = GridSpace::Type::Fixed;
            break;
        case LengthType::Percent:
            columnWidth = computedLogicalWidth.value() * availableHorizontalSpace / 100.0f;
            type = GridSpace::Type::Percent;
            break;
        case LengthType::Relative:
            ASSERT_NOT_IMPLEMENTED_YET();
            break;
        default:
            break;
        }

        float minimumContentWidth = slot.widthConstraints().minimum;
        float maximumContentWidth = slot.widthConstraints().maximum;
        auto preferredWidth = std::max(minimumContentWidth, columnWidth.value_or(maximumContentWidth));
        if (!hasEnoughAvailableSpaceForMaximumWidth) {
            // maximum width > available horizontal space
            // preferred width: it's always at least as wide as the minimum content width and if column fixed width is set then the greater of the two.
            // base value for adjustable space distribution for a cell: preferred width - minimum content width
            // total adjustable horizontal space: available horizontal space - preferred width(s)
            // column extra width: total adjustable space / base value for distribution for all columns * base value for distribution for this column
            // column final width: preferred width + extra width
            // e.g
            // <table style="width: 120px">
            //   <td style="width: 80px;">some content</td>
            //   <td style="width: 20px;">some content></td>
            // </table>
            // maximum width (160px) > available horizontal space (120)
            // "some content" -> minimum content width is 50px 
            // preferred widths: 80px and 50px
            // base values for distribution: 30px and 0px
            // total adjustable space: 120px - 80px - 50px = -10px <- Note the negative available space with over-constrained values.
            // columns extra widths: -10px / (30px + 0px) * 30px = -10px and -10px / (30px + 0px) * 0px = 0px
            // columns final widths: 70px and 50px (first column is narrower than its preferred width and the second is as wide as the minimum content width)
            return GridSpace { type, preferredWidth, preferredWidth - minimumContentWidth, minimumContentWidth };
        }

        // maximum width <= available horizontal space
        // preferred width: it's usually the maximum content width but at least as wide as the minimum content width (or the column fixed width)
        // base value for adjustable space distribution for a cell: preferred width (again usually the maximum content width)
        // total adjustable horizontal space: available horizontal space - preferred width(s)
        // column extra width: total adjustable space / base value for distribution for all columns * base value for distribution for this column
        // column final width: preferred width + extra width
        // e.g
        // <table style="width: 2600px">
        //   <td style="width: 40px;"><div style="width: 200px;"></div></td>
        //   <td style="width: 300px;"><div style="width: 100px;"></div></td>
        // </table>
        // maximum width (500px) < available horizontal space (2600px)
        // preferred widths: 200px and 300px
        // base values for distribution: 200px and 300px
        // total adjustable space: 2600px - 500px = 2100px
        // columns extra widths: 2100px / 500px * 200px = 840px and 2100px / 500px * 300px = 1260px
        // columns final widths: 1040px and 1560px
        return GridSpace { type, preferredWidth, preferredWidth, minimumContentWidth };
    });
}

TableFormattingContext::TableLayout::DistributedSpaces TableFormattingContext::TableLayout::distributedVerticalSpace(std::optional<LayoutUnit> availableVerticalSpace)
{
    auto& rows = m_grid.rows();
    auto& columns = m_grid.columns();

    Vector<LayoutUnit> rowHeight(rows.size());
    auto tableUsedHeight = LayoutUnit { };
    // 2. Collect initial, baseline aligned row heights.
    for (size_t rowIndex = 0; rowIndex < rows.size(); ++rowIndex) {
        auto maximumColumnAscent = InlineLayoutUnit { };
        auto maximumColumnDescent = InlineLayoutUnit { };
        // Initial minimum height is the computed height if available <tr style="height: 100px"><td></td></tr>
        rowHeight[rowIndex] = formattingContext().formattingGeometry().computedHeight(rows.list()[rowIndex].box(), availableVerticalSpace).value_or(0_lu);
        for (size_t columnIndex = 0; columnIndex < columns.size(); ++columnIndex) {
            auto& slot = *m_grid.slot({ columnIndex, rowIndex });
            if (slot.isRowSpanned())
                continue;
            if (slot.hasRowSpan())
                continue;
            // The minimum height of a row (without spanning-related height distribution) is defined as the height of an hypothetical
            // linebox containing the cells originating in the row.
            auto& cell = slot.cell();
            auto& cellBox = cell.box();
            auto height = formattingContext().geometryForBox(cellBox).borderBoxHeight();
            if (cellBox.style().verticalAlign() == VerticalAlign::Baseline) {
                maximumColumnAscent = std::max(maximumColumnAscent, cell.baseline());
                maximumColumnDescent = std::max(maximumColumnDescent, height - cell.baseline());
                rowHeight[rowIndex] = std::max(rowHeight[rowIndex], LayoutUnit { maximumColumnAscent + maximumColumnDescent });
            } else
                rowHeight[rowIndex] = std::max(rowHeight[rowIndex], height);
        }
        tableUsedHeight += rowHeight[rowIndex];
    }
    // FIXME: Collect spanning row maximum heights.

    tableUsedHeight += (rows.size() + 1) * m_grid.verticalSpacing();
    auto availableSpace = std::max(availableVerticalSpace.value_or(0_lu), tableUsedHeight);
    // Distribute extra space if the table is supposed to be taller than the sum of the row heights.
    return distributeAvailableSpace<RowSpan>(m_grid, availableSpace, [&] (const TableGrid::Slot& slot, size_t rowIndex) {
        if (slot.hasRowSpan()) {
            auto borderBoxHeight = formattingContext().geometryForBox(slot.cell().box()).borderBoxHeight();
            return GridSpace { { }, borderBoxHeight, borderBoxHeight, borderBoxHeight };
        }
        auto& rows = m_grid.rows();
        auto computedRowHeight = formattingContext().formattingGeometry().computedHeight(rows.list()[rowIndex].box(), { });
        auto height = std::max<float>(rowHeight[rowIndex], computedRowHeight.value_or(0_lu));
        return GridSpace { { }, height, height, height };
    });
}

}
}

#endif
