/* 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 https://mozilla.org/MPL/2.0/. */

//! The restyle damage is a hint that tells layout which kind of operations may
//! be needed in presence of incremental style changes.

use crate::computed_values::display::T as Display;
use crate::matching::{StyleChange, StyleDifference};
use crate::properties::ComputedValues;
use std::fmt;

bitflags! {
    /// Individual layout actions that may be necessary after restyling.
    pub struct ServoRestyleDamage: u8 {
        /// Repaint the node itself.
        ///
        /// Currently unused; need to decide how this propagates.
        const REPAINT = 0x01;

        /// The stacking-context-relative position of this node or its
        /// descendants has changed.
        ///
        /// Propagates both up and down the flow tree.
        const REPOSITION = 0x02;

        /// Recompute the overflow regions (bounding box of object and all descendants).
        ///
        /// Propagates down the flow tree because the computation is bottom-up.
        const STORE_OVERFLOW = 0x04;

        /// Recompute intrinsic inline_sizes (minimum and preferred).
        ///
        /// Propagates down the flow tree because the computation is.
        /// bottom-up.
        const BUBBLE_ISIZES = 0x08;

        /// Recompute actual inline-sizes and block-sizes, only taking
        /// out-of-flow children into account.
        ///
        /// Propagates up the flow tree because the computation is top-down.
        const REFLOW_OUT_OF_FLOW = 0x10;

        /// Recompute actual inline_sizes and block_sizes.
        ///
        /// Propagates up the flow tree because the computation is top-down.
        const REFLOW = 0x20;

        /// Re-resolve generated content.
        ///
        /// Propagates up the flow tree because the computation is inorder.
        const RESOLVE_GENERATED_CONTENT = 0x40;

        /// The entire flow needs to be reconstructed.
        const RECONSTRUCT_FLOW = 0x80;
    }
}

malloc_size_of_is_0!(ServoRestyleDamage);

impl ServoRestyleDamage {
    /// Compute the `StyleDifference` (including the appropriate restyle damage)
    /// for a given style change between `old` and `new`.
    pub fn compute_style_difference(old: &ComputedValues, new: &ComputedValues) -> StyleDifference {
        let damage = compute_damage(old, new);
        let change = if damage.is_empty() {
            StyleChange::Unchanged
        } else {
            // FIXME(emilio): Differentiate between reset and inherited
            // properties here, and set `reset_only` appropriately so the
            // optimization to skip the cascade in those cases applies.
            StyleChange::Changed { reset_only: false }
        };
        StyleDifference { damage, change }
    }

    /// Returns a bitmask that represents a flow that needs to be rebuilt and
    /// reflowed.
    ///
    /// FIXME(bholley): Do we ever actually need this? Shouldn't
    /// RECONSTRUCT_FLOW imply everything else?
    pub fn rebuild_and_reflow() -> ServoRestyleDamage {
        ServoRestyleDamage::REPAINT |
            ServoRestyleDamage::REPOSITION |
            ServoRestyleDamage::STORE_OVERFLOW |
            ServoRestyleDamage::BUBBLE_ISIZES |
            ServoRestyleDamage::REFLOW_OUT_OF_FLOW |
            ServoRestyleDamage::REFLOW |
            ServoRestyleDamage::RECONSTRUCT_FLOW
    }

    /// Returns a bitmask indicating that the frame needs to be reconstructed.
    pub fn reconstruct() -> ServoRestyleDamage {
        ServoRestyleDamage::RECONSTRUCT_FLOW
    }

    /// Supposing a flow has the given `position` property and this damage,
    /// returns the damage that we should add to the *parent* of this flow.
    pub fn damage_for_parent(self, child_is_absolutely_positioned: bool) -> ServoRestyleDamage {
        if child_is_absolutely_positioned {
            self & (ServoRestyleDamage::REPAINT |
                ServoRestyleDamage::REPOSITION |
                ServoRestyleDamage::STORE_OVERFLOW |
                ServoRestyleDamage::REFLOW_OUT_OF_FLOW |
                ServoRestyleDamage::RESOLVE_GENERATED_CONTENT)
        } else {
            self & (ServoRestyleDamage::REPAINT |
                ServoRestyleDamage::REPOSITION |
                ServoRestyleDamage::STORE_OVERFLOW |
                ServoRestyleDamage::REFLOW |
                ServoRestyleDamage::REFLOW_OUT_OF_FLOW |
                ServoRestyleDamage::RESOLVE_GENERATED_CONTENT)
        }
    }

    /// Supposing the *parent* of a flow with the given `position` property has
    /// this damage, returns the damage that we should add to this flow.
    pub fn damage_for_child(
        self,
        parent_is_absolutely_positioned: bool,
        child_is_absolutely_positioned: bool,
    ) -> ServoRestyleDamage {
        match (
            parent_is_absolutely_positioned,
            child_is_absolutely_positioned,
        ) {
            (false, true) => {
                // Absolute children are out-of-flow and therefore insulated from changes.
                //
                // FIXME(pcwalton): Au contraire, if the containing block dimensions change!
                self & (ServoRestyleDamage::REPAINT | ServoRestyleDamage::REPOSITION)
            },
            (true, false) => {
                // Changing the position of an absolutely-positioned block requires us to reflow
                // its kids.
                if self.contains(ServoRestyleDamage::REFLOW_OUT_OF_FLOW) {
                    self | ServoRestyleDamage::REFLOW
                } else {
                    self
                }
            },
            _ => {
                // TODO(pcwalton): Take floatedness into account.
                self & (ServoRestyleDamage::REPAINT |
                    ServoRestyleDamage::REPOSITION |
                    ServoRestyleDamage::REFLOW)
            },
        }
    }
}

impl Default for ServoRestyleDamage {
    fn default() -> Self {
        Self::empty()
    }
}

impl fmt::Display for ServoRestyleDamage {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        let mut first_elem = true;

        let to_iter = [
            (ServoRestyleDamage::REPAINT, "Repaint"),
            (ServoRestyleDamage::REPOSITION, "Reposition"),
            (ServoRestyleDamage::STORE_OVERFLOW, "StoreOverflow"),
            (ServoRestyleDamage::BUBBLE_ISIZES, "BubbleISizes"),
            (ServoRestyleDamage::REFLOW_OUT_OF_FLOW, "ReflowOutOfFlow"),
            (ServoRestyleDamage::REFLOW, "Reflow"),
            (
                ServoRestyleDamage::RESOLVE_GENERATED_CONTENT,
                "ResolveGeneratedContent",
            ),
            (ServoRestyleDamage::RECONSTRUCT_FLOW, "ReconstructFlow"),
        ];

        for &(damage, damage_str) in &to_iter {
            if self.contains(damage) {
                if !first_elem {
                    write!(f, " | ")?;
                }
                write!(f, "{}", damage_str)?;
                first_elem = false;
            }
        }

        if first_elem {
            write!(f, "NoDamage")?;
        }

        Ok(())
    }
}

fn compute_damage(old: &ComputedValues, new: &ComputedValues) -> ServoRestyleDamage {
    let mut damage = ServoRestyleDamage::empty();

    // This should check every CSS property, as enumerated in the fields of
    // https://doc.servo.org/style/properties/struct.ComputedValues.html

    // This uses short-circuiting boolean OR for its side effects and ignores the result.
    let _ = restyle_damage_rebuild_and_reflow!(
        old,
        new,
        damage,
        [
            ServoRestyleDamage::REPAINT,
            ServoRestyleDamage::REPOSITION,
            ServoRestyleDamage::STORE_OVERFLOW,
            ServoRestyleDamage::BUBBLE_ISIZES,
            ServoRestyleDamage::REFLOW_OUT_OF_FLOW,
            ServoRestyleDamage::REFLOW,
            ServoRestyleDamage::RECONSTRUCT_FLOW
        ]
    ) || (new.get_box().display == Display::Inline &&
        restyle_damage_rebuild_and_reflow_inline!(
            old,
            new,
            damage,
            [
                ServoRestyleDamage::REPAINT,
                ServoRestyleDamage::REPOSITION,
                ServoRestyleDamage::STORE_OVERFLOW,
                ServoRestyleDamage::BUBBLE_ISIZES,
                ServoRestyleDamage::REFLOW_OUT_OF_FLOW,
                ServoRestyleDamage::REFLOW,
                ServoRestyleDamage::RECONSTRUCT_FLOW
            ]
        )) ||
        restyle_damage_reflow!(
            old,
            new,
            damage,
            [
                ServoRestyleDamage::REPAINT,
                ServoRestyleDamage::REPOSITION,
                ServoRestyleDamage::STORE_OVERFLOW,
                ServoRestyleDamage::BUBBLE_ISIZES,
                ServoRestyleDamage::REFLOW_OUT_OF_FLOW,
                ServoRestyleDamage::REFLOW
            ]
        ) ||
        restyle_damage_reflow_out_of_flow!(
            old,
            new,
            damage,
            [
                ServoRestyleDamage::REPAINT,
                ServoRestyleDamage::REPOSITION,
                ServoRestyleDamage::STORE_OVERFLOW,
                ServoRestyleDamage::REFLOW_OUT_OF_FLOW
            ]
        ) ||
        restyle_damage_repaint!(old, new, damage, [ServoRestyleDamage::REPAINT]);

    // Paint worklets may depend on custom properties,
    // so if they have changed we should repaint.
    if !old.custom_properties_equal(new) {
        damage.insert(ServoRestyleDamage::REPAINT);
    }

    // If the layer requirements of this flow have changed due to the value
    // of the transform, then reflow is required to rebuild the layers.
    if old.transform_requires_layer() != new.transform_requires_layer() {
        damage.insert(ServoRestyleDamage::rebuild_and_reflow());
    }

    damage
}
