/*
 * 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 "TableWrapperBlockFormattingContext.h"

#include "BlockFormattingGeometry.h"
#include "BlockFormattingState.h"
#include "BlockMarginCollapse.h"
#include "LayoutBoxGeometry.h"
#include "LayoutChildIterator.h"
#include "LayoutContext.h"
#include "LayoutInitialContainingBlock.h"
#include "RenderObject.h"
#include "TableFormattingConstraints.h"
#include "TableFormattingContext.h"
#include "TableFormattingState.h"
#include "TableWrapperBlockFormattingQuirks.h"

namespace WebCore {
namespace Layout {

WTF_MAKE_ISO_ALLOCATED_IMPL(TableWrapperBlockFormattingContext);

TableWrapperBlockFormattingContext::TableWrapperBlockFormattingContext(const ElementBox& formattingContextRoot, BlockFormattingState& formattingState)
    : BlockFormattingContext(formattingContextRoot, formattingState)
    , m_tableWrapperFormattingQuirks(*this)
{
}

void TableWrapperBlockFormattingContext::layoutInFlowContent(const ConstraintsForInFlowContent& constraints)
{
    // The table generates a principal block container box called the table wrapper box that contains the table box itself and any caption boxes
    // (in document order). The table box is a block-level box that contains the table's internal table boxes.
    // The caption boxes are principal block-level boxes that retain their own content, padding, margin, and border areas, and are rendered
    // as normal block boxes inside the table wrapper box. Whether the caption boxes are placed before or after the table box is decided by
    // the 'caption-side' property, as described below.
    for (auto& child : childrenOfType<ElementBox>(root())) {
        if (child.isTableBox())
            layoutTableBox(child, constraints);
        else if (child.isTableCaption())
            ASSERT_NOT_IMPLEMENTED_YET();
        else
            ASSERT_NOT_REACHED();
    }
}

void TableWrapperBlockFormattingContext::layoutTableBox(const ElementBox& tableBox, const ConstraintsForInFlowContent& constraints)
{
    layoutState().ensureTableFormattingState(tableBox);

    computeBorderAndPaddingForTableBox(tableBox, constraints.horizontal());
    computeStaticVerticalPosition(tableBox, constraints.logicalTop());
    computeWidthAndMarginForTableBox(tableBox, constraints.horizontal());
    computeStaticHorizontalPosition(tableBox, constraints.horizontal());

    if (tableBox.hasChild()) {
        auto& formattingGeometry = this->formattingGeometry();
        auto constraints = ConstraintsForTableContent { formattingGeometry.constraintsForInFlowContent(tableBox), formattingGeometry.computedHeight(tableBox) };
        LayoutContext::createFormattingContext(tableBox, layoutState())->layoutInFlowContent(constraints);
    }

    computeHeightAndMarginForTableBox(tableBox, constraints);
}

void TableWrapperBlockFormattingContext::computeBorderAndPaddingForTableBox(const ElementBox& tableBox, const HorizontalConstraints& horizontalConstraints)
{
    ASSERT(tableBox.isTableBox());
    if (!tableBox.hasChild() || tableBox.style().borderCollapse() == BorderCollapse::Separate) {
        BlockFormattingContext::computeBorderAndPadding(tableBox, horizontalConstraints);
        return;
    }
    // UAs must compute an initial left and right border width for the table by examining
    // the first and last cells in the first row of the table.
    // The left border width of the table is half of the first cell's collapsed left border,
    // and the right border width of the table is half of the last cell's collapsed right border.
    // The top border width of the table is computed by examining all cells who collapse their top
    // borders with the top border of the table. The top border width of the table is equal to half of the
    // maximum collapsed top border. The bottom border width is computed by examining all cells whose bottom borders collapse
    // with the bottom of the table. The bottom border width is equal to half of the maximum collapsed bottom border.
    auto& grid = layoutState().formattingStateForTableFormattingContext(tableBox).tableGrid();
    auto tableBorder = formattingGeometry().computedBorder(tableBox);

    auto& firstColumnFirstRowBox = grid.slot({ 0 , 0 })->cell().box();
    auto leftBorder = std::max(tableBorder.horizontal.start, formattingGeometry().computedBorder(firstColumnFirstRowBox).horizontal.start);

    auto& lastColumnFirstRow = grid.slot({ grid.columns().size() - 1, 0 })->cell().box();
    auto rightBorder = std::max(tableBorder.horizontal.end, formattingGeometry().computedBorder(lastColumnFirstRow).horizontal.end);

    auto topBorder = tableBorder.vertical.before;
    auto bottomBorder = tableBorder.vertical.after;
    auto lastRowIndex = grid.rows().size() - 1;
    for (size_t columnIndex = 0; columnIndex < grid.columns().size(); ++columnIndex) {
        auto& boxInFirstRox = grid.slot({ columnIndex, 0 })->cell().box();
        auto& boxInLastRow = grid.slot({ columnIndex, lastRowIndex })->cell().box();

        topBorder = std::max(topBorder, formattingGeometry().computedBorder(boxInFirstRox).vertical.before);
        bottomBorder = std::max(bottomBorder, formattingGeometry().computedBorder(boxInLastRow).vertical.after);
    }

    topBorder = std::max(topBorder, formattingGeometry().computedBorder(*tableBox.firstChild()).vertical.before);
    for (auto& section : childrenOfType<ElementBox>(tableBox)) {
        auto horizontalBorder = formattingGeometry().computedBorder(section).horizontal;
        leftBorder = std::max(leftBorder, horizontalBorder.start);
        rightBorder = std::max(rightBorder, horizontalBorder.end);
    }
    bottomBorder = std::max(bottomBorder, formattingGeometry().computedBorder(*tableBox.lastChild()).vertical.after);

    auto& rows = grid.rows().list();
    topBorder = std::max(topBorder, formattingGeometry().computedBorder(rows.first().box()).vertical.before);
    for (auto& row : rows) {
        auto horizontalBorder = formattingGeometry().computedBorder(row.box()).horizontal;
        leftBorder = std::max(leftBorder, horizontalBorder.start);
        rightBorder = std::max(rightBorder, horizontalBorder.end);
    }
    bottomBorder = std::max(bottomBorder, formattingGeometry().computedBorder(rows.last().box()).vertical.after);

    auto collapsedBorder = BoxGeometry::Edges { { leftBorder, rightBorder }, { topBorder, bottomBorder } };
    grid.setCollapsedBorder(collapsedBorder);

    auto& boxGeometry = formattingState().boxGeometry(tableBox);
    boxGeometry.setBorder({ { collapsedBorder.horizontal.start / 2, collapsedBorder.horizontal.end / 2 }, { collapsedBorder.vertical.before / 2, collapsedBorder.vertical.after / 2 } });
    boxGeometry.setPadding(formattingGeometry().computedPadding(tableBox, horizontalConstraints.logicalWidth));
}

void TableWrapperBlockFormattingContext::computeWidthAndMarginForTableBox(const ElementBox& tableBox, const HorizontalConstraints& horizontalConstraints)
{
    ASSERT(tableBox.isTableBox());
    // This is a special table "fit-content size" behavior handling. Not in the spec though.
    // Table returns its final width as min/max. Use this final width value to computed horizontal margins etc.
    auto& formattingStateForTableBox = layoutState().formattingStateForTableFormattingContext(tableBox);
    auto intrinsicWidthConstraints = IntrinsicWidthConstraints { };
    if (auto precomputedIntrinsicWidthConstraints = formattingStateForTableBox.intrinsicWidthConstraints())
        intrinsicWidthConstraints = *precomputedIntrinsicWidthConstraints;
    else {
        if (tableBox.hasChild())
            intrinsicWidthConstraints = LayoutContext::createFormattingContext(tableBox, layoutState())->computedIntrinsicWidthConstraints();
        formattingStateForTableBox.setIntrinsicWidthConstraints(intrinsicWidthConstraints);
    }

    auto availableHorizontalSpace = horizontalConstraints.logicalWidth;
    // 1. The table generates a principal block container box called the table wrapper box that contains the table box itself and any caption boxes.
    // 2. The table wrapper box establishes a block formatting context, and the table box establishes a table formatting context.
    // 3. The computed values of properties 'position', 'float', 'margin-*', 'top', 'right', 'bottom', and 'left' on
    //    the table element are used on the table wrapper box and not the table box; all other values of non-inheritable
    //    properties are used on the table box and not the table wrapper box.
    // 4. In a block formatting context, each box's left outer edge touches the left edge of the containing block.
    //    This is true even in the presence of floats, unless the box establishes a new block formatting context (in which case the
    //    box itself may become narrower due to the floats)
    //
    // Now consider the following case:
    // <div style="display: block; width: 500px;">
    //     <div style="float: left; width: 100px;"></div>
    //         <div style="display: table; width: 10%;"></div>
    //     </div>
    // </div>
    // 1. We create a table wrapper box to wrap the "display: table" block level box (#1).
    // 2. The table wrapper box's width property is set to auto (#3).
    // 3. Since it establishes a new block formatting context, the available horizontal space gets shrunk by the float (#4)
    // 4. The table wrapper box's used width computes to 500px - 100px -> 400px;
    //
    // Now we are inside the BFC established by the table wrapper box and try to resolve the table's width -> %10.
    // According to the normal BFC rules, it should compute to 10% of the containing block's logical width: 400px -> 40px.
    // However in practice it computes to 50px (10% of 500px).
    // Similar setup with non-table content would resolve the inner block level box's width to 40px;
    // This needs clarification in the spec.
    auto horizontalConstraintForResolvingWidth = m_horizontalConstraintsIgnoringFloats.logicalWidth;
    auto& formattingGeometry = this->formattingGeometry();
    auto computedWidth = formattingGeometry.computedWidth(tableBox, horizontalConstraintForResolvingWidth);
    auto computedMaxWidth = formattingGeometry.computedMaxWidth(tableBox, horizontalConstraintForResolvingWidth);
    auto computedMinWidth = formattingGeometry.computedMinWidth(tableBox, horizontalConstraintForResolvingWidth);
    // Use the generic shrink-to-fit-width logic as the initial width for the table.
    auto usedWidth = std::min(std::max(intrinsicWidthConstraints.minimum, availableHorizontalSpace), intrinsicWidthConstraints.maximum);
    if (computedWidth || computedMinWidth || computedMaxWidth) {
        if (computedWidth) {
            // Normalize the computed width value first.
            if (computedMaxWidth && *computedWidth > *computedMaxWidth)
                computedWidth = computedMaxWidth;
            if (computedMinWidth && *computedWidth < *computedMinWidth)
                computedWidth = computedMinWidth;
            usedWidth = *computedWidth < intrinsicWidthConstraints.minimum ? intrinsicWidthConstraints.minimum : *computedWidth;
        }

        if (computedMaxWidth && *computedMaxWidth < usedWidth)
            usedWidth = intrinsicWidthConstraints.minimum;
        if (computedMinWidth && *computedMinWidth > usedWidth)
            usedWidth = *computedMinWidth;
    }

    auto contentWidthAndMargin = formattingGeometry.inFlowContentWidthAndMargin(tableBox, horizontalConstraints, OverriddenHorizontalValues { usedWidth, { } });

    auto& boxGeometry = formattingState().boxGeometry(tableBox);
    boxGeometry.setContentBoxWidth(contentWidthAndMargin.contentWidth);
    boxGeometry.setHorizontalMargin({ contentWidthAndMargin.usedMargin.start, contentWidthAndMargin.usedMargin.end });
}

void TableWrapperBlockFormattingContext::computeHeightAndMarginForTableBox(const ElementBox& tableBox, const ConstraintsForInFlowContent& constraints)
{
    ASSERT(tableBox.isTableBox());
    // Table is a special BFC content. Its height is mainly driven by the content. Computed height, min-height and max-height are all
    // already been taken into account during the TFC layout.
    auto overriddenTableHeight = [&]() -> std::optional<LayoutUnit> {
        if (layoutState().inQuirksMode())
            return formattingQuirks().overriddenTableHeight(tableBox);
        if (tableBox.hasInFlowOrFloatingChild())
            return formattingGeometry().contentHeightForFormattingContextRoot(tableBox);
        return { };
    }();

    auto heightAndMargin = formattingGeometry().inFlowContentHeightAndMargin(tableBox, constraints.horizontal(), { overriddenTableHeight });
    auto verticalMargin = marginCollapse().collapsedVerticalValues(tableBox, heightAndMargin.nonCollapsedMargin);
    // Cache the computed positive and negative margin value pair.
    formattingState().setUsedVerticalMargin(tableBox, verticalMargin);

    auto& boxGeometry = formattingState().boxGeometry(tableBox);
    boxGeometry.setTop(verticalPositionWithMargin(tableBox, verticalMargin, constraints.logicalTop()));
    boxGeometry.setContentBoxHeight(heightAndMargin.contentHeight);
    boxGeometry.setVerticalMargin({ marginBefore(verticalMargin), marginAfter(verticalMargin) });
    // Adjust the previous sibling's margin bottom now that this box's vertical margin is computed.
    updateMarginAfterForPreviousSibling(tableBox);
}

}
}

