/*
 * Copyright (C) 2021, 2022 Igalia S.L.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include "SVGBoundingBoxComputation.h"

#if ENABLE(LAYER_BASED_SVG_ENGINE)
#include "RenderChildIterator.h"
#include "RenderSVGContainer.h"
#include "RenderSVGForeignObject.h"
#include "RenderSVGImage.h"
#include "RenderSVGInline.h"
#include "RenderSVGResourceClipper.h"
#include "RenderSVGResourceFilter.h"
#include "RenderSVGResourceMarker.h"
#include "RenderSVGResourceMasker.h"
#include "RenderSVGRoot.h"
#include "RenderSVGShape.h"
#include "RenderSVGText.h"
#include "SVGResources.h"
#include "SVGResourcesCache.h"

namespace WebCore {

SVGBoundingBoxComputation::SVGBoundingBoxComputation(const RenderLayerModelObject& renderer)
    : m_renderer(renderer)
{
}

FloatRect SVGBoundingBoxComputation::computeDecoratedBoundingBox(const SVGBoundingBoxComputation::DecorationOptions& options, bool* boundingBoxValid) const
{
    // SVG2: Bounding boxes algorithm (https://svgwg.org/svg2-draft/coords.html#BoundingBoxes)

    // The following algorithm defines how to compute a bounding box for a given element. The inputs to the algorithm are:
    // - element, the element we are computing a bounding box for;
    // - space, a coordinate space in which the bounding box will be computed;
    // - fill, a boolean indicating whether the bounding box includes the geometry of the element and its descendants;
    // - stroke, a boolean indicating whether the bounding box includes the stroke of the element and its descendants;
    // - markers, a boolean indicating whether the bounding box includes the markers of the element and its descendants; and
    // - clipped, a boolean indicating whether the bounding box is affected by any clipping paths applied to the element and its descendants.

    // The algorithm to compute the bounding box is as follows, depending on the type of element:
    // - a shape (RenderSVGShape)
    // - a text content element (RenderSVGText or RenderSVGInline)
    // - an "a" element within a text content element (-> creates RenderSVGInline)
    if (is<RenderSVGShape>(m_renderer) || is<RenderSVGText>(m_renderer) || is<RenderSVGInline>(m_renderer))
        return handleShapeOrTextOrInline(options, boundingBoxValid);

    // - a container element (RenderSVGRoot / RenderSVGContainer)
    // - "use" (RenderSVGTransformableContainer)
    if (is<RenderSVGRoot>(m_renderer) || is<RenderSVGContainer>(m_renderer))
        return handleRootOrContainer(options, boundingBoxValid);

    // - "foreignObject"
    // - "image"
    // FIXME: [LBSE] Upstream new RenderSVGImage implementation
    // if (is<RenderSVGForeignObject>(m_renderer) || is<RenderSVGImage>(m_renderer))
    if (is<RenderSVGForeignObject>(m_renderer))
        return handleForeignObjectOrImage(options, boundingBoxValid);

    ASSERT_NOT_REACHED();
    return FloatRect();
}

FloatRect SVGBoundingBoxComputation::handleShapeOrTextOrInline(const SVGBoundingBoxComputation::DecorationOptions& options, bool* boundingBoxValid) const
{
    // 1. Let box be a rectangle initialized to (0, 0, 0, 0).
    FloatRect box;

    // 2. Let fill-shape be the equivalent path of element if it is a shape, or a shape that includes each of the
    //    glyph cells corresponding to the text within the elements otherwise.
    // 3. If fill is true, then set box to the tightest rectangle in the coordinate system space that contains fill-shape.
    //
    // Note: The values of the fill, fill-opacity and fill-rule properties do not affect fill-shape.
    if (options.contains(DecorationOption::IncludeFillShape))
        box = m_renderer.objectBoundingBox();

    // 4. If stroke is true and the element's stroke is anything other than none, then set box to be the union of box
    //    and the tightest rectangle in coordinate system space that contains the stroke shape of the element, with the
    //    assumption that the element has no dash pattern.
    //
    // Note: The values of the stroke-opacity, stroke-dasharray and stroke-dashoffset do not affect the calculation of the stroke shape.
    if (options.contains(DecorationOption::IncludeStrokeShape))
        box.unite(m_renderer.strokeBoundingBox());

    // 5. If markers is true, then for each marker marker rendered on the element:
    // - For each descendant graphics element child of the "marker" element that defines marker's content:
    //   - If child has an ancestor element within the "marker" that is 'display: none', has a failing conditional processing attribute,
    //     or is not an "a", "g", "svg" or "switch" element, then continue to the next descendant graphics element.
    //   - Otherwise, set box to be the union of box and the result of invoking the algorithm to compute a bounding box with child as
    //     the element, space as the target coordinate space, true for fill, stroke and markers, and clipped for clipped.
    if (options.contains(DecorationOption::IncludeMarkers) && is<RenderSVGShape>(m_renderer)) {
        DecorationOptions optionsForMarker = { DecorationOption::IncludeFillShape, DecorationOption::IncludeStrokeShape, DecorationOption::IncludeMarkers };
        if (options.contains(DecorationOption::IncludeClippers))
            optionsForMarker.add(DecorationOption::IncludeClippers);
        box.unite(downcast<RenderSVGShape>(m_renderer).computeMarkerBoundingBox(options));
    }

    // 6. If clipped is true and the value of clip-path on element is not none, then set box to be the tightest rectangle
    //    in coordinate system space that contains the intersection of box and the clipping path.
    adjustBoxForClippingAndEffects(options, box);

    // 7. Return box.
    if (boundingBoxValid)
        *boundingBoxValid = true;
    return box;
}

FloatRect SVGBoundingBoxComputation::handleRootOrContainer(const SVGBoundingBoxComputation::DecorationOptions& options, bool* boundingBoxValid) const
{
    auto transformationMatrixFromChild = [] (const RenderLayerModelObject& child) -> std::optional<TransformationMatrix> {
        if (!child.hasTransform())
            return std::nullopt;

        auto* container = child.parent();
        ASSERT(container);

        bool containerSkipped = false;
        ASSERT(container == child.container(nullptr, containerSkipped));
        ASSERT_UNUSED(containerSkipped, !containerSkipped);

        TransformationMatrix layerTransform;
        child.getTransformFromContainer(container, LayoutSize(), layerTransform);
        return layerTransform.isIdentity() ? std::nullopt : std::make_optional(WTFMove(layerTransform));
    };

    auto uniteBoundingBoxRespectingValidity = [] (bool& boxValid, FloatRect& box, const RenderLayerModelObject& child, const FloatRect& childBoundingBox) {
        bool isBoundingBoxValid = is<RenderSVGContainer>(child) ? downcast<RenderSVGContainer>(child).isObjectBoundingBoxValid() : true;
        if (!isBoundingBoxValid)
            return;

        if (boxValid) {
            box.uniteEvenIfEmpty(childBoundingBox);
            return;
        }

        box = childBoundingBox;
        boxValid = true;
    };

    // 1. Let box be a rectangle initialized to (0, 0, 0, 0).
    FloatRect box;
    bool boxValid = false;

    // 2. Let parent be the container element if it is one, or the root of the "use" element's shadow tree otherwise.

    // 3. For each descendant graphics element child of parent:
    //    - If child is not rendered then continue to the next descendant graphics element.
    //    - Otherwise, set box to be the union of box and the result of invoking the algorithm to compute a bounding box with child
    //      as the element and the same values for space, fill, stroke, markers and clipped as the corresponding algorithm input values.
    for (auto& child : childrenOfType<RenderLayerModelObject>(m_renderer)) {
        // FIXME: [LBSE] Upstream new RenderSVGHiddenContainer implementation
        // if (is<RenderSVGHiddenContainer>(child) || (is<RenderSVGShape>(child) && downcast<RenderSVGShape>(child).isRenderingDisabled()))
        if (is<RenderSVGShape>(child) && downcast<RenderSVGShape>(child).isRenderingDisabled())
            continue;

        SVGBoundingBoxComputation childBoundingBoxComputation(child);
        auto childBox = childBoundingBoxComputation.computeDecoratedBoundingBox(options);
        if (options.contains(DecorationOption::OverrideBoxWithFilterBoxForChildren) && is<RenderSVGContainer>(child))
            childBoundingBoxComputation.adjustBoxForClippingAndEffects({ DecorationOption::OverrideBoxWithFilterBox }, childBox);

        if (!options.contains(DecorationOption::IgnoreTransformations)) {
            if (auto layerTransform = transformationMatrixFromChild(child))
                childBox = layerTransform->mapRect(childBox);
        }

        if (options == objectBoundingBoxDecoration)
            uniteBoundingBoxRespectingValidity(boxValid, box, child, childBox);
        else
            box.unite(childBox);
    }

    // 4. If clipped is true:
    //    - If the value of clip-path on element is not none, then set box to be the tightest rectangle in coordinate system space that
    //      contains the intersection of box and the clipping path.
    //    - If the overflow property applies to the element and does not have a value of visible, then set box to be the tightest rectangle
    //      in coordinate system space that contains the intersection of box and the element's overflow bounds.
    //    - If the clip property applies to the element and does not have a value of auto, then set box to be the tightest rectangle in coordinate
    //      system space that contains the intersection of box and the rectangle specified by clip. (TODO!)
    adjustBoxForClippingAndEffects(options, box, { DecorationOption::OverrideBoxWithFilterBox });

    if (options.contains(DecorationOption::IncludeClippers) && m_renderer.hasNonVisibleOverflow()) {
        ASSERT(m_renderer.hasLayer());

        // FIXME: [LBSE] Upstream new RenderSVGViewportContainer / RenderSVGResourceMarker implementation
        // ASSERT(is<RenderSVGViewportContainer>(m_renderer) || is<RenderSVGResourceMarker>(m_renderer) || is<RenderSVGRoot>(m_renderer));
        ASSERT(is<RenderSVGRoot>(m_renderer));

        LayoutRect overflowClipRect;
        if (is<RenderSVGModelObject>(m_renderer))
            overflowClipRect = downcast<RenderSVGModelObject>(m_renderer).overflowClipRect(LayoutPoint());
        else if (is<RenderBox>(m_renderer))
            overflowClipRect = downcast<RenderBox>(m_renderer).overflowClipRect(LayoutPoint());
        else {
            ASSERT_NOT_REACHED();
            return FloatRect();
        }

        box.intersect(overflowClipRect);
    }

    // 5. Return box.
    if (boundingBoxValid)
        *boundingBoxValid = boxValid;
    return box;
}

FloatRect SVGBoundingBoxComputation::handleForeignObjectOrImage(const SVGBoundingBoxComputation::DecorationOptions& options, bool* boundingBoxValid) const
{
    // 1. Let box be the tightest rectangle in coordinate space space that contains the positioning rectangle
    //    defined by the "x", "y", "width" and "height" geometric properties of the element.
    //
    // Note: The fill, stroke and markers input arguments to this algorithm do not affect the bounding box returned for these elements.
    auto box = m_renderer.objectBoundingBox();

    // 2. If clipped is true and the value of clip-path on element is not none, then set box to be the tightest rectangle
    //    in coordinate system space that contains the intersection of box and the clipping path.
    adjustBoxForClippingAndEffects(options, box);

    // 3. Return box.
    if (boundingBoxValid)
        *boundingBoxValid = true;
    return box;
}

void SVGBoundingBoxComputation::adjustBoxForClippingAndEffects(const SVGBoundingBoxComputation::DecorationOptions& options, FloatRect& box, const SVGBoundingBoxComputation::DecorationOptions& optionsToCheckForFilters) const
{
    bool includeFilter = false;
    for (auto filterOption : optionsToCheckForFilters) {
        if (options.contains(filterOption)) {
            includeFilter = true;
            break;
        }
    }

    bool includeClipper = options.contains(DecorationOption::IncludeClippers);
    bool includeMasker = options.contains(DecorationOption::IncludeMaskers);

    if (includeFilter || includeClipper || includeMasker) {
        if (auto* resources = SVGResourcesCache::cachedResourcesForRenderer(m_renderer)) {
            if (includeFilter) {
                if (auto* filter = resources->filter())
                    box = filter->resourceBoundingBox(m_renderer);
            }

            if (includeClipper) {
                if (auto* clipper = resources->clipper())
                    box.intersect(clipper->resourceBoundingBox(m_renderer));
            }

            if (includeMasker) {
                if (auto* masker = resources->masker())
                    box.intersect(masker->resourceBoundingBox(m_renderer));
            }
        }
    }

    if (options.contains(DecorationOption::IncludeOutline))
        box.inflate(m_renderer.outlineStyleForRepaint().outlineSize());
}

}

#endif
