/*
 * Copyright 2014-2019 Web Firm Framework
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * @author WFF
 */
package com.webfirmframework.wffweb.css;

import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.webfirmframework.wffweb.InvalidValueException;
import com.webfirmframework.wffweb.NullValueException;
import com.webfirmframework.wffweb.css.core.AbstractCssProperty;
import com.webfirmframework.wffweb.css.core.CssProperty;
import com.webfirmframework.wffweb.informer.StateChangeInformer;
import com.webfirmframework.wffweb.util.CssValueUtil;
import com.webfirmframework.wffweb.util.StringBuilderUtil;
import com.webfirmframework.wffweb.util.StringUtil;
import com.webfirmframework.wffweb.util.TagStringUtil;

/**
 *

 *
 * outline: outline-color outline-style outline-width|initial|inherit;
 *
 * An outline is a line that is drawn around elements (outside the borders) to make the element "stand out".
 *
 * The outline shorthand property sets all the outline properties in one declaration.
 *
 * The properties that can be set, are (in order): outline-color, outline-style, outline-width.
 *
 * If one of the values above are missing, e.g. "outline:solid #ff0000;", the default value for the missing property will be inserted, if any.
 *
 * Note: The outline is not a part of the element's dimensions, therefore the element's width and height properties do not contain the width of the outline.
 * Default value:  invert none medium
 * Inherited:      no
 * Animatable:     yes
 * Version:        CSS2
 * JavaScript syntax:      object.style.outline="#0000FF dotted 5px"
 *

 *
 * @author WFF
 * @since 1.0.0
 */
public class Outline extends AbstractCssProperty<Outline>
        implements StateChangeInformer<CssProperty> {

    private static final long serialVersionUID = 1_0_0L;

    public static final Logger LOGGER = Logger
            .getLogger(Outline.class.getName());

    public static final String INITIAL = "initial";
    public static final String INHERIT = "inherit";

    private static final List<String> PREDEFINED_CONSTANTS = Arrays
            .asList(INITIAL, INHERIT);

    private String cssValue;

    private OutlineColor outlineColor;

    private OutlineStyle outlineStyle;

    private OutlineWidth outlineWidth;

    /**
     * The value medium none black will be assigned as the
     * cssValue.
     *
     * @author WFF
     * @since 1.0.0
     */
    public Outline() {
        setCssValue("medium none black");
    }

    /**
     * @param cssValue
     *                     the css value to set.
     */
    public Outline(final String cssValue) {
        setCssValue(cssValue);
    }

    /**
     * @param outline
     *                    the {@code Outline} object from which the cssValue to
     *                    set.And, {@code null} will throw
     *                    {@code NullValueException}
     */
    public Outline(final Outline outline) {
        if (outline == null) {
            throw new NullValueException("outline can not be null");
        }
        setCssValue(outline.getCssValue());
    }

    /*
     * (non-Javadoc)
     *
     * @see com.webfirmframework.wffweb.css.CssProperty#getCssName()
     *
     * @since 1.0.0
     *
     * @author WFF
     */
    @Override
    public String getCssName() {
        return CssNameConstants.OUTLINE;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.webfirmframework.wffweb.css.CssProperty#getCssValue()
     *
     * @since 1.0.0
     *
     * @author WFF
     */
    @Override
    public String getCssValue() {
        return cssValue;
    }

    @Override
    public String toString() {
        return getCssName() + ": " + getCssValue();
    }

    /**
     * @param cssValue
     *                     the value should be in the format of
     *                     medium none color, initial
     *                     or inherit. {@code null} is considered
     *                     as an invalid value and it will throw
     *                     {@code NullValueException}.
     * @since 1.0.0
     * @author WFF
     */
    @Override
    public Outline setCssValue(final String cssValue) {
        final String trimmedCssValue;
        if (cssValue == null) {
            throw new NullValueException(
                    "null is an invalid value. The value format should be as for example medium none color Or initial/inherit.");
        } else if ((trimmedCssValue = TagStringUtil
                .toLowerCase(StringUtil.strip(cssValue))).isEmpty()) {
            throw new NullValueException(cssValue
                    + " is an invalid value. The value format should be as for example medium none color Or initial/inherit.");
        }

        final List<String> cssValueParts = CssValueUtil.split(trimmedCssValue);

        if (cssValueParts.size() > 1) {
            if (trimmedCssValue.contains(OutlineColor.INITIAL)) {
                throw new InvalidValueException(
                        "The string 'initial' makes the given cssValue invalid, please remove 'initial' from it and try.");
            }
            if (trimmedCssValue.contains(OutlineColor.INHERIT)) {
                throw new InvalidValueException(
                        "The string 'inherit' makes the given cssValue invalid, please remove 'inherit' from it and try.");
            }
        } else {
            if (PREDEFINED_CONSTANTS.contains(trimmedCssValue)) {
                if (outlineColor != null) {
                    outlineColor.setAlreadyInUse(false);
                    outlineColor = null;
                }

                outlineStyle = null;

                if (outlineWidth != null) {
                    outlineWidth.setAlreadyInUse(false);
                    outlineWidth = null;
                }

                this.cssValue = trimmedCssValue;
                if (getStateChangeInformer() != null) {
                    getStateChangeInformer().stateChanged(this);
                }
                return this;
            }
        }

        OutlineColor outlineColor = null;
        OutlineStyle outlineStyle = null;
        OutlineWidth outlineWidth = null;

        for (final String eachPart : cssValueParts) {
            if (outlineColor == null && OutlineColor.isValid(eachPart)) {
                if (this.outlineColor == null) {
                    outlineColor = new OutlineColor(eachPart);
                    outlineColor.setStateChangeInformer(this);
                    outlineColor.setAlreadyInUse(true);
                } else {
                    this.outlineColor.setCssValue(eachPart);
                    outlineColor = this.outlineColor;
                }
            } else if (outlineStyle == null && OutlineStyle.isValid(eachPart)) {
                outlineStyle = OutlineStyle.getThis(eachPart);
            } else if (outlineWidth == null && OutlineWidth.isValid(eachPart)) {
                if (this.outlineWidth == null) {
                    outlineWidth = new OutlineWidth(eachPart);
                    outlineWidth.setStateChangeInformer(this);
                    outlineWidth.setAlreadyInUse(true);
                } else {
                    this.outlineWidth.setCssValue(eachPart);
                    outlineWidth = this.outlineWidth;
                }
            }
        }

        final StringBuilder cssValueBuilder = new StringBuilder();
        boolean invalid = true;

        if (outlineColor != null) {
            cssValueBuilder.append(outlineColor.getCssValue()).append(' ');
            invalid = false;
        } else if (this.outlineColor != null) {
            this.outlineColor.setAlreadyInUse(false);
        }

        if (outlineStyle != null) {
            cssValueBuilder.append(outlineStyle.getCssValue()).append(' ');
            invalid = false;
        }

        if (outlineWidth != null) {
            cssValueBuilder.append(outlineWidth.getCssValue()).append(' ');
            invalid = false;
        } else if (this.outlineWidth != null) {
            this.outlineWidth.setAlreadyInUse(false);
        }

        if (invalid) {
            throw new InvalidValueException(cssValue
                    + " is an invalid value. The value format should be as for example medium none color Or initial/inherit.");
        }
        this.cssValue = StringBuilderUtil.getTrimmedString(cssValueBuilder);
        this.outlineColor = outlineColor;
        this.outlineWidth = outlineWidth;
        this.outlineStyle = outlineStyle;

        if (getStateChangeInformer() != null) {
            getStateChangeInformer().stateChanged(this);
        }

        return this;
    }

    /**
     * checks the given css value is valid for this class. It does't do a strict
     * validation.
     *
     * @param cssValue
     * @return
     * @since 1.0.0
     * @author WFF
     */
    public static boolean isValid(final String cssValue) {
        // TODO modify to make a strict validation
        if (cssValue == null || StringUtil.isBlank(cssValue)) {
            return false;
        }

        final List<String> cssValueParts = CssValueUtil.split(cssValue);

        OutlineColor outlineColor = null;
        OutlineStyle outlineStyle = null;
        OutlineWidth outlineWidth = null;

        for (final String eachPart : cssValueParts) {

            boolean invalid = true;
            if (outlineColor == null && OutlineColor.isValid(eachPart)) {
                outlineColor = new OutlineColor(eachPart);
                invalid = false;
            } else if (outlineStyle == null && OutlineStyle.isValid(eachPart)) {
                outlineStyle = OutlineStyle.getThis(eachPart);
                invalid = false;
            } else if (outlineWidth == null && OutlineWidth.isValid(eachPart)) {
                outlineWidth = new OutlineWidth(eachPart);
                invalid = false;
            }
            if (invalid) {
                return false;
            }
        }

        return outlineWidth != null || outlineStyle != null
                || outlineColor != null;
    }

    /**
     * sets as initial
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsInitial() {
        setCssValue(INITIAL);
    }

    /**
     * sets as inherit
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsInherit() {
        setCssValue(INHERIT);
    }

    public OutlineColor getOutlineColor() {
        return outlineColor;
    }

    public OutlineStyle getOutlineStyle() {
        return outlineStyle;
    }

    public OutlineWidth getOutlineWidth() {
        return outlineWidth;
    }

    public Outline setOutlineWidth(final OutlineWidth outlineWidth) {

        final StringBuilder cssValueBuilder = new StringBuilder();

        if (outlineColor != null) {
            cssValueBuilder.append(outlineColor.getCssValue()).append(' ');
        }

        if (outlineStyle != null) {
            cssValueBuilder.append(outlineStyle.getCssValue()).append(' ');
        }

        if (outlineWidth != null) {

            final String outlineWidthCssValue = outlineWidth.getCssValue();
            if (OutlineWidth.INITIAL.equals(outlineWidthCssValue)
                    || OutlineWidth.INHERIT.equals(outlineWidthCssValue)) {
                throw new InvalidValueException(
                        "outlineWidth cannot have initial/inherit as its cssValue");
            }

            cssValueBuilder.append(outlineWidth.getCssValue());
        }

        final String trimmedCssValue = StringBuilderUtil
                .getTrimmedString(cssValueBuilder).toString();
        cssValue = trimmedCssValue.isEmpty() ? INHERIT : trimmedCssValue;

        if (outlineWidth != null && outlineWidth.isAlreadyInUse()
                && this.outlineWidth != outlineWidth) {
            if (LOGGER.isLoggable(Level.WARNING)) {
                LOGGER.warning(
                        "the given outlineWidth is already used by another object so a new object or the previous object (if it exists) of OutlineWidth will be used");
            }
            return setCssValue(cssValue);
        }

        if (this.outlineWidth != null) {
            this.outlineWidth.setAlreadyInUse(false);
        }

        this.outlineWidth = outlineWidth;

        if (this.outlineWidth != null) {
            this.outlineWidth.setStateChangeInformer(this);
            this.outlineWidth.setAlreadyInUse(true);
        }

        if (getStateChangeInformer() != null) {
            getStateChangeInformer().stateChanged(this);
        }
        return this;
    }

    public Outline setOutlineStyle(final OutlineStyle outlineStyle) {

        final StringBuilder cssValueBuilder = new StringBuilder();

        if (outlineColor != null) {
            cssValueBuilder.append(outlineColor.getCssValue()).append(' ');
        }

        if (outlineStyle != null) {
            cssValueBuilder.append(outlineStyle.getCssValue()).append(' ');
        }

        if (outlineWidth != null) {
            cssValueBuilder.append(outlineWidth.getCssValue());
        }

        final String trimmedCssValue = StringBuilderUtil
                .getTrimmedString(cssValueBuilder).toString();
        cssValue = trimmedCssValue.isEmpty() ? INHERIT : trimmedCssValue;

        this.outlineStyle = outlineStyle;

        if (getStateChangeInformer() != null) {
            getStateChangeInformer().stateChanged(this);
        }
        return this;
    }

    public Outline setOutlineColor(final OutlineColor outlineColor) {

        final StringBuilder cssValueBuilder = new StringBuilder();

        if (outlineColor != null) {
            final String outlineColorCssValue = outlineColor.getCssValue();
            if (OutlineColor.INITIAL.equals(outlineColorCssValue)
                    || OutlineColor.INHERIT.equals(outlineColorCssValue)) {
                throw new InvalidValueException(
                        "outlineColor cannot have initial/inherit as its cssValue");
            }
            cssValueBuilder.append(outlineColorCssValue).append(' ');
        }

        if (outlineStyle != null) {
            cssValueBuilder.append(outlineStyle.getCssValue()).append(' ');
        }
        if (outlineWidth != null) {
            cssValueBuilder.append(outlineWidth.getCssValue());
        }

        final String trimmedCssValue = StringBuilderUtil
                .getTrimmedString(cssValueBuilder).toString();
        cssValue = trimmedCssValue.isEmpty() ? INHERIT : trimmedCssValue;

        if (outlineColor != null && outlineColor.isAlreadyInUse()
                && this.outlineColor != outlineColor) {
            if (LOGGER.isLoggable(Level.WARNING)) {
                LOGGER.warning(
                        "the given outlineColor is already used by another object so a new object or the previous object (if it exists) of OutlineColor will be used");
            }
            return setCssValue(cssValue);
        }

        if (this.outlineColor != null) {
            this.outlineColor.setAlreadyInUse(false);
        }

        this.outlineColor = outlineColor;

        if (this.outlineColor != null) {
            this.outlineColor.setStateChangeInformer(this);
            this.outlineColor.setAlreadyInUse(true);
        }

        if (getStateChangeInformer() != null) {
            getStateChangeInformer().stateChanged(this);
        }
        return this;
    }

    @Override
    public void stateChanged(final CssProperty stateChangedObject) {
        if (stateChangedObject instanceof OutlineColor) {

            final OutlineColor outlineColor = (OutlineColor) stateChangedObject;

            final String cssValue = outlineColor.getCssValue();

            if (OutlineColor.INITIAL.equals(cssValue)
                    || OutlineColor.INHERIT.equals(cssValue)) {
                throw new InvalidValueException(
                        "initial/inherit cannot be set as outlineColor cssValue");
            }

            setOutlineColor(outlineColor);
        }
    }

    /**
     *
     * @author WFF
     * @since 1.0.0
     */
    protected void addPredefinedConstant(final String constant) {
        PREDEFINED_CONSTANTS.add(constant);
    }

}


/*
 * Copyright 2014-2019 Web Firm Framework
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * @author WFF
 */
package com.webfirmframework.wffweb.css.css3;

import java.util.Arrays;
import java.util.List;

import com.webfirmframework.wffweb.InvalidValueException;
import com.webfirmframework.wffweb.NullValueException;
import com.webfirmframework.wffweb.css.CssNameConstants;
import com.webfirmframework.wffweb.css.core.AbstractCssProperty;
import com.webfirmframework.wffweb.util.StringUtil;
import com.webfirmframework.wffweb.util.TagStringUtil;

/**
 * border-image-repeat: stretch|repeat|round|initial|inherit;
 *
 *

 * The border-image-repeat property specifies whether the border image should be repeated, rounded or stretched.
 *
 * Tip: Also look at the border-image property (a shorthand property for setting all the border-image-* properties).
 * Default value:  stretch
 * Inherited:      no
 * Animatable:     no
 * Version:        CSS3
 * JavaScript syntax:      object.style.borderImageRepeat="round"
 * JavaScript syntax:      object.style.borderImageRepeat="round stretch"
 *
 *

 *
 * @author WFF
 *
 */
public class BorderImageRepeat extends AbstractCssProperty<BorderImageRepeat> {

    private static final long serialVersionUID = 1_0_0L;

    public static final String INITIAL = "initial";
    public static final String INHERIT = "inherit";
    public static final String STRETCH = "stretch";
    public static final String REPEAT = "repeat";
    public static final String ROUND = "round";

    private static final List<String> PREDEFINED_CONSTANTS = Arrays
            .asList(INITIAL, INHERIT, STRETCH, REPEAT, ROUND);

    private String cssValue;

    private String vertical;
    private String horizontal;

    /**
     * The {@code stretch} will be set as the value
     */
    public BorderImageRepeat() {
        setCssValue(STRETCH);
    }

    /**
     * @param cssValue
     *                     the css value to set.
     */
    public BorderImageRepeat(final String cssValue) {
        setCssValue(cssValue);
    }

    /**
     * @param borderImageRepeat
     *                              the {@code BorderImageRepeat} object from
     *                              which the cssValue to set.And, {@code null}
     *                              will throw {@code NullValueException}
     */
    public BorderImageRepeat(final BorderImageRepeat borderImageRepeat) {
        if (borderImageRepeat == null) {
            throw new NullValueException("borderImageRepeat can not be null");
        }
        setCssValue(borderImageRepeat.getCssValue());
    }

    /*
     * (non-Javadoc)
     *
     * @see com.webfirmframework.wffweb.css.CssProperty#getCssName()
     *
     * @since 1.0.0
     *
     * @author WFF
     */
    @Override
    public String getCssName() {
        return CssNameConstants.BORDER_IMAGE_REPEAT;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.webfirmframework.wffweb.css.CssProperty#getCssValue()
     *
     * @since 1.0.0
     *
     * @author WFF
     */
    @Override
    public String getCssValue() {
        return cssValue;
    }

    @Override
    public String toString() {
        return getCssName() + ": " + getCssValue();
    }

    /**
     * @param cssValue
     *                     the value should be in the format of
     *                     round stretch or round.
     *                     {@code null} is considered as an invalid value and it
     *                     will throw {@code NullValueException}.
     * @since 1.0.0
     * @author WFF
     */
    @Override
    public BorderImageRepeat setCssValue(final String cssValue) {
        final String previousCssValue = this.cssValue;
        final String previousVertical = vertical;
        final String previousHorizontal = horizontal;
        try {
            if (cssValue == null) {
                throw new NullValueException(
                        "null is an invalid value. The value format should be as for example 'round stretch'/'round'. Or, initial/inherit.");
            } else {

                final String trimmedCssValue = StringUtil.convertToSingleSpace(
                        TagStringUtil.toLowerCase(StringUtil.strip(cssValue)));

                final String[] verticalHorzontal = StringUtil
                        .splitBySpace(trimmedCssValue);

                if (verticalHorzontal.length == 1) {
                    if (trimmedCssValue.equalsIgnoreCase(INITIAL)
                            || trimmedCssValue.equalsIgnoreCase(INHERIT)) {
                        this.cssValue = trimmedCssValue;
                        vertical = null;
                        horizontal = null;
                    } else if (PREDEFINED_CONSTANTS
                            .contains(verticalHorzontal[0])) {
                        vertical = horizontal = verticalHorzontal[0];
                        this.cssValue = trimmedCssValue;
                    } else {
                        throw new InvalidValueException("the given cssValue '"
                                + cssValue + "' is invalid");
                    }
                } else if (verticalHorzontal.length == 2) {

                    if (PREDEFINED_CONSTANTS.contains(verticalHorzontal[0])
                            && PREDEFINED_CONSTANTS
                                    .contains(verticalHorzontal[1])) {
                        horizontal = verticalHorzontal[0];
                        vertical = verticalHorzontal[1];
                        this.cssValue = trimmedCssValue;
                    } else {
                        throw new InvalidValueException("the given cssValue '"
                                + cssValue
                                + "' is invalid. The value format should be as for example 'round stretch'/'round'. Or, initial/inherit.");
                    }
                } else {
                    throw new InvalidValueException(
                            "The given cssValue should not contain more that 2 length values. The value format should be as for example 'round stretch'/'round'. Or, initial/inherit.");
                }

                if (getStateChangeInformer() != null) {
                    getStateChangeInformer().stateChanged(this);
                }
            }
        } catch (final InvalidValueException e) {
            this.cssValue = previousCssValue;
            vertical = previousVertical;
            horizontal = previousHorizontal;
            throw e;
        }
        return this;

    }

    /**
     * @param vertical
     *                     the vertical to set
     * @author WFF
     * @since 1.0.0
     */
    public void setVertical(final String vertical) {
        if (vertical == null) {
            throw new NullValueException("The vertical value cannot be null.");
        }
        final String inputVertical = TagStringUtil
                .toLowerCase(StringUtil.strip(vertical));
        if (!INITIAL.equals(vertical) && !INHERIT.equals(inputVertical)
                && PREDEFINED_CONSTANTS.contains(inputVertical)) {
            this.vertical = inputVertical;
            if (horizontal != null) {
                cssValue = horizontal.concat(" ").concat(inputVertical);
            } else {
                cssValue = inputVertical;
            }
        } else {
            throw new InvalidValueException(
                    "The given vertical value '" + vertical + "' is invalid.");
        }
    }

    /**
     * @param horizontal
     *                       the horizontal to set
     * @author WFF
     * @since 1.0.0
     */
    public void setHorizontal(final String horizontal) {
        if (horizontal == null) {
            throw new NullValueException("The vertical value cannot be null.");
        }
        final String inputHorizontal = TagStringUtil
                .toLowerCase(StringUtil.strip(horizontal));
        if (!INITIAL.equals(vertical) && !INHERIT.equals(inputHorizontal)
                && PREDEFINED_CONSTANTS.contains(inputHorizontal)) {
            this.horizontal = inputHorizontal;
            if (vertical != null) {
                cssValue = inputHorizontal.concat(" ").concat(vertical);
            } else {
                cssValue = inputHorizontal;
            }
        } else {
            throw new InvalidValueException(
                    "The given vertical value '" + vertical + "' is invalid.");
        }
    }

    /**
     * @return the vertical
     * @author WFF
     * @since 1.0.0
     */
    public String getVertical() {
        return vertical;
    }

    /**
     * @return the horizontal
     * @author WFF
     * @since 1.0.0
     */
    public String getHorizontal() {
        return horizontal;
    }

    /**
     * sets as {@code initial}
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsInitial() {
        setCssValue(INITIAL);
    }

    /**
     * sets as {@code inherit}
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsInherit() {
        setCssValue(INHERIT);
    }

    /**
     * validates if the given cssValue is valid for this class.
     *
     * @param cssValue
     *                     the value to check.
     * @return true if valid and false if invalid.
     * @author WFF
     * @since 1.0.0
     */
    public static boolean isValid(final String cssValue) {
        final String trimmedCssValue = TagStringUtil
                .toLowerCase(StringUtil.strip(cssValue));

        final String[] cssValueParts = StringUtil.splitBySpace(trimmedCssValue);
        if (cssValueParts.length > 2) {
            return false;
        }
        if (cssValueParts.length == 1) {
            return PREDEFINED_CONSTANTS.contains(trimmedCssValue);
        }

        boolean valid = false;
        for (final String each : cssValueParts) {

            if (!PREDEFINED_CONSTANTS.contains(each)) {
                return false;
            }
            if ((INITIAL.equals(each) || INHERIT.equals(each))
                    && cssValueParts.length == 2) {
                return false;

            }
            valid = true;
        }
        return valid;
    }
}


/*
 * Copyright 2014-2019 Web Firm Framework
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.webfirmframework.wffweb.tag.html;

import java.util.ArrayDeque;
import java.util.Collection;
import java.util.HashSet;
import java.util.concurrent.locks.Lock;
import java.util.stream.Stream;
import java.util.stream.Stream.Builder;

import com.webfirmframework.wffweb.wffbm.data.WffBMData;

/**
 * @author WFF
 * @since 2.1.8
 */
public abstract class AbstractHtmlRepository {

    /**
     * @param fromTags
     * @return the list of read lock
     * @since 3.0.1
     */
    protected static Collection<Lock> getReadLocks(
            final AbstractHtml... fromTags) {
        // passed 2 instead of 1 because the load factor is 0.75f
        final Collection<Lock> locks = fromTags.length > 1
                ? new HashSet<>(fromTags.length)
                : new ArrayDeque<>(2);
        for (final AbstractHtml tag : fromTags) {
            final Lock readLock = tag.getReadLock();
            if (readLock != null) {
                locks.add(readLock);
            }
        }
        return locks;
    }

    /**
     * @param parallel
     *                     true to use parallel stream.
     * @param parents
     *                     the tags from which the nested children to be taken.
     * @return stream of all nested children including the given parent.
     * @since 3.0.0
     * @author WFF
     */
    protected static Stream<AbstractHtml> getAllNestedChildrenIncludingParent(
            final boolean parallel, final AbstractHtml... parents) {

        final Builder<AbstractHtml> builder = Stream.builder();

        AbstractHtml.loopThroughAllNestedChildren(child -> {

            builder.add(child);

            return true;
        }, true, parents);

        // this way makes StackOverflowException
        // return AbstractHtml.getAllNestedChildrenIncludingParent(parent);

        // if (parallel) {
        // return AbstractHtml.getAllNestedChildrenIncludingParent(parent)
        // .parallel();
        // }

        if (parallel) {
            return builder.build().parallel();
        }
        return builder.build();
    }

    /**
     * Loops through all nested children tags (excluding the given tag) of the
     * given tag. The looping is in a random order to gain maximum performance
     * and minimum memory footprint.
     *
     * @param nestedChild
     *                           the object of NestedChild from which the
     *                           eachChild(AbstractHtml) to be invoked.
     * @param includeParents
     *                           true to include the given parent tags in the
     *                           loop
     * @param parents
     *                           the tags from which to loop through.
     *
     * @since 2.1.8
     * @author WFF
     */
    protected static void loopThroughAllNestedChildren(
            final NestedChild nestedChild, final boolean includeParents,
            final AbstractHtml... parents) {
        AbstractHtml.loopThroughAllNestedChildren(nestedChild, includeParents,
                parents);
    }

    /**
     * @param abstractHtml
     * @param key
     * @param wffBMData
     * @return
     * @since 2.1.8
     * @author WFF
     */
    protected static WffBMData addWffData(final AbstractHtml abstractHtml,
            final String key, final WffBMData wffBMData) {
        return abstractHtml.addWffData(key, wffBMData);
    }

    /**
     * @param abstractHtml
     * @param key
     * @return
     * @since 2.1.8
     * @author WFF
     */
    protected static WffBMData removeWffData(final AbstractHtml abstractHtml,
            final String key) {
        return abstractHtml.removeWffData(key);
    }

    /**
     * @param abstractHtml
     * @param key
     * @return
     * @since 3.0.1
     * @author WFF
     */
    protected static WffBMData getWffData(final AbstractHtml abstractHtml,
            final String key) {
        return abstractHtml.getWffData(key);
    }

}


/*
 * Copyright 2014-2019 Web Firm Framework
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * @author WFF
 */
package com.webfirmframework.wffweb.css;

import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.webfirmframework.wffweb.InvalidValueException;
import com.webfirmframework.wffweb.NullValueException;
import com.webfirmframework.wffweb.css.core.AbstractCssProperty;
import com.webfirmframework.wffweb.css.core.CssProperty;
import com.webfirmframework.wffweb.informer.StateChangeInformer;
import com.webfirmframework.wffweb.util.StringBuilderUtil;
import com.webfirmframework.wffweb.util.StringUtil;

/**
 *

 * font: font-style font-variant font-weight font-size/line-height font-family|caption|icon|menu|message-box|small-caption|status-bar|initial|inherit;
 *
 * The font shorthand property sets all the font properties in one declaration.
 *
 * The properties that can be set, are (in order): "font-style font-variant font-weight font-size/line-height font-family"
 *
 * The font-size and font-family values are required. If one of the other values are missing, the default values will be inserted, if any.
 *
 * Note: The line-height property sets the space between lines.
 * Default value:  The default value of all the font properties
 * Inherited:      yes
 * Animatable:     yes, see individual properties. Read about animatable
 * Version:        CSS1
 * JavaScript syntax:      object.style.font="italic small-caps bold 12px/1.4 arial,sans-serif"
 *

 *
 * @author WFF
 * @since 1.0.0
 */
public class Font extends AbstractCssProperty
        implements StateChangeInformer<CssProperty> {

    private static final long serialVersionUID = 1_0_0L;

    public static final Logger LOGGER = Logger.getLogger(Font.class.getName());

    public static final String INITIAL = "initial";
    public static final String INHERIT = "inherit";
    public static final String CAPTION = "caption";
    public static final String ICON = "icon";
    public static final String MENU = "menu";
    public static final String MESSAGE_BOX = "message-box";
    public static final String SMALL_CAPTION = "small-caption";
    public static final String STATUS_BAR = "status-bar";

    private static final List<String> PREDEFINED_CONSTANTS = Arrays.asList(
            INITIAL, INHERIT, CAPTION, ICON, MENU, MESSAGE_BOX, SMALL_CAPTION,
            STATUS_BAR);

    private String cssValue;

    private FontStyle fontStyle;

    private FontVariant fontVariant;

    private FontWeight fontWeight;

    private FontSize fontSize;

    private LineHeight lineHeight;

    private FontFamily fontFamily;

    /**
     * The value initial will be assigned as the cssValue.
     *
     * @author WFF
     * @since 1.0.0
     */
    public Font() {
        setCssValue("initial");
    }

    /**
     * @param cssValue
     *                     the css value to set.
     */
    public Font(final String cssValue) {
        setCssValue(cssValue);
    }

    /**
     * @param font
     *                 the {@code Font} object from which the cssValue to
     *                 set.And, {@code null} will throw
     *                 {@code NullValueException}
     */
    public Font(final Font font) {
        if (font == null) {
            throw new NullValueException("font can not be null");
        }
        setCssValue(font.getCssValue());
    }

    /*
     * (non-Javadoc)
     *
     * @see com.webfirmframework.wffweb.css.CssProperty#getCssName()
     *
     * @since 1.0.0
     *
     * @author WFF
     */
    @Override
    public String getCssName() {
        return CssNameConstants.FONT;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.webfirmframework.wffweb.css.CssProperty#getCssValue()
     *
     * @since 1.0.0
     *
     * @author WFF
     */
    @Override
    public String getCssValue() {
        return cssValue;
    }

    @Override
    public String toString() {
        return getCssName() + ": " + getCssValue();
    }

    /**
     * @param cssValue
     *                     the value should be in the format of
     *                     italic small-caps bold 12px arial,sans-serif
     *                     or initial/inherit. {@code null} is
     *                     considered as an invalid value and it will throw
     *                     {@code NullValueException}.
     * @since 1.0.0
     * @author WFF
     */
    @Override
    public Font setCssValue(final String cssValue) {
        // NB: cssValue can contain upper case so it should not be converted to
        // lower case.
        final String trimmedCssValue;
        if (cssValue == null) {
            throw new NullValueException(
                    "null is an invalid value. The value format should be as for example italic small-caps bold 12px arial,sans-serif Or initial/inherit.");
        } else if ((trimmedCssValue = StringUtil.strip(cssValue)).isEmpty()) {
            throw new NullValueException(cssValue
                    + " is an invalid value. The value format should be as for example italic small-caps bold 12px arial,sans-serif Or initial/inherit.");
        }

        final String[] cssValueParts = getExtractedSubCssValues(
                trimmedCssValue);

        // @formatter:off
        /*
        if (cssValueParts.length > 1) {
            may not be fine, if the predefined constant comes as a substring of font-family name in future.
             for (String constant : PREDEFINED_CONSTANTS) {
                if (trimmedCssValue.contains(constant)) {
                    throw new InvalidValueException(
                            "The string '"
                                    + constant
                                    + "' makes the given cssValue invalid, please remove '"
                                    + constant + "' from it and try.");
                }
            }

        } else
        */
        // @formatter:on

        if (cssValueParts.length == 1
                && PREDEFINED_CONSTANTS.contains(trimmedCssValue)) {

            fontStyle = null;
            fontVariant = null;
            fontWeight = null;
            if (fontSize != null) {
                fontSize.setAlreadyInUse(false);
                fontSize = null;
            }
            if (lineHeight != null) {
                lineHeight.setAlreadyInUse(false);
                lineHeight = null;
            }
            if (fontFamily != null) {
                fontFamily.setAlreadyInUse(false);
                fontFamily = null;
            }

            this.cssValue = trimmedCssValue;
            if (getStateChangeInformer() != null) {
                getStateChangeInformer().stateChanged(this);
            }
            return this;
        }

        FontStyle fontStyle = null;
        FontVariant fontVariant = null;
        FontWeight fontWeight = null;
        FontSize fontSize = null;
        LineHeight lineHeight = null;
        FontFamily fontFamily = null;

        for (final String eachPart : cssValueParts) {
            if (fontStyle == null && FontStyle.isValid(eachPart)) {
                fontStyle = FontStyle.getThis(eachPart);
            } else if (fontVariant == null && FontVariant.isValid(eachPart)) {
                fontVariant = FontVariant.getThis(eachPart);
            } else if (fontWeight == null && FontWeight.isValid(eachPart)) {
                fontWeight = FontWeight.getThis(eachPart);
            } else if (fontSize == null && FontSize.isValid(eachPart)) {
                if (this.fontSize == null) {
                    fontSize = new FontSize(eachPart);
                    fontSize.setStateChangeInformer(this);
                    fontSize.setAlreadyInUse(true);
                } else {
                    this.fontSize.setCssValue(eachPart);
                    fontSize = this.fontSize;
                }
            } else if (lineHeight == null && LineHeight.isValid(eachPart)) {
                if (this.lineHeight == null) {
                    lineHeight = new LineHeight(eachPart);
                    lineHeight.setStateChangeInformer(this);
                    lineHeight.setAlreadyInUse(true);
                } else {
                    this.lineHeight.setCssValue(eachPart);
                    lineHeight = this.lineHeight;
                }
            } else if (fontFamily == null
                    && (!FontFamily.isValidateFontFamilyNameGlobally()
                            || FontFamily.isValid(eachPart))) {
                if (this.fontFamily == null) {
                    fontFamily = new FontFamily(eachPart);
                    fontFamily.setStateChangeInformer(this);
                    fontFamily.setAlreadyInUse(true);
                } else {
                    this.fontFamily.setCssValue(eachPart);
                    fontFamily = this.fontFamily;
                }
            } else {
                throw new InvalidValueException(cssValue
                        + " is an invalid value. The value format should be as for example italic small-caps bold 12px arial,sans-serif Or initial/inherit.");
            }
        }

        final StringBuilder cssValueBuilder = new StringBuilder();
        boolean invalid = true;
        if (fontStyle != null) {
            cssValueBuilder.append(fontStyle.getCssValue()).append(' ');
            invalid = false;
        }

        if (fontVariant != null) {
            cssValueBuilder.append(fontVariant.getCssValue()).append(' ');
            invalid = false;
        }

        if (fontWeight != null) {
            cssValueBuilder.append(fontWeight.getCssValue()).append(' ');
            invalid = false;
        }

        if (fontSize != null) {
            cssValueBuilder.append(fontSize.getCssValue());
            if (lineHeight == null) {
                cssValueBuilder.append(' ');
            }
            invalid = false;
        }

        if (lineHeight != null) {
            if (fontSize != null) {
                cssValueBuilder.append('/');
            }
            cssValueBuilder.append(lineHeight.getCssValue()).append(' ');
            invalid = false;
        }

        if (fontFamily != null) {
            cssValueBuilder.append(fontFamily.getCssValue()).append(' ');
            invalid = false;
        }
        if (invalid) {
            throw new InvalidValueException(cssValue
                    + " is an invalid value. The value format should be as for example italic small-caps bold 12px arial,sans-serif Or initial/inherit.");
        }
        this.cssValue = StringBuilderUtil.getTrimmedString(cssValueBuilder);
        this.fontStyle = fontStyle;
        this.fontVariant = fontVariant;
        this.fontWeight = fontWeight;
        this.fontSize = fontSize;
        this.lineHeight = lineHeight;
        this.fontFamily = fontFamily;

        if (getStateChangeInformer() != null) {
            getStateChangeInformer().stateChanged(this);
        }

        return this;
    }

    /**
     * @param cssValue
     * @return the sub properties
     * @author WFF
     * @since 1.0.0
     */
    protected static String[] getExtractedSubCssValues(final String cssValue) {
        final String convertedToSingleSpace = StringUtil
                .convertToSingleSpace(cssValue);
        final String[] subCssValues = convertedToSingleSpace.replace(", ", ",")
                .split("[ /]");
        return subCssValues;
    }

    /**
     * checks the given css value is valid for this class. It does't do a strict
     * validation.
     *
     * @param cssValue
     * @return
     * @since 1.0.0
     * @author WFF
     */
    public static boolean isValid(final String cssValue) {
        // TODO verify this method. the fontFamily cannot be validated so this
        // method may be invalid. remove after verified

        if (cssValue == null || StringUtil.isBlank(cssValue)) {
            return false;
        }

        final String[] cssValueParts = getExtractedSubCssValues(cssValue);

        FontStyle fontStyle = null;
        FontVariant fontVariant = null;
        FontWeight fontWeight = null;
        FontSize fontSize = null;
        LineHeight lineHeight = null;
        FontFamily fontFamily = null;

        for (final String eachPart : cssValueParts) {
            boolean invalid = true;
            if (fontStyle == null && FontStyle.isValid(eachPart)) {
                fontStyle = FontStyle.getThis(eachPart);
                invalid = false;
            } else if (fontVariant == null && FontVariant.isValid(eachPart)) {
                fontVariant = FontVariant.getThis(eachPart);
                invalid = false;
            } else if (fontWeight == null && FontWeight.isValid(eachPart)) {
                fontWeight = FontWeight.getThis(eachPart);
                invalid = false;
            } else if (fontSize == null && FontSize.isValid(eachPart)) {
                fontSize = new FontSize(eachPart);
                invalid = false;
            } else if (lineHeight == null && LineHeight.isValid(eachPart)) {
                lineHeight = new LineHeight(eachPart);
                invalid = false;
            } else if (fontFamily == null
                    && (!FontFamily.isValidateFontFamilyNameGlobally()
                            || FontFamily.isValid(eachPart))) {
                fontFamily = new FontFamily(eachPart);
                invalid = false;
            }
            if (invalid) {
                return false;
            }
        }

        return fontStyle != null || fontVariant != null || fontWeight != null
                || fontSize != null || lineHeight != null || fontFamily != null;
    }

    /**
     * sets as initial
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsInitial() {
        setCssValue(INITIAL);
    }

    /**
     * sets as inherit
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsInherit() {
        setCssValue(INHERIT);
    }

    /**
     * sets as caption
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsCaption() {
        setCssValue(CAPTION);
    }

    /**
     * sets as icon
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsIcon() {
        setCssValue(ICON);
    }

    /**
     * sets as menu
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsMenu() {
        setCssValue(MENU);
    }

    /**
     * sets as message-box
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsMessageBox() {
        setCssValue(MESSAGE_BOX);
    }

    /**
     * sets as small-caption
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsSmallCaption() {
        setCssValue(SMALL_CAPTION);
    }

    /**
     * sets as status-bar
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsStatusBar() {
        setCssValue(STATUS_BAR);
    }

    public FontSize getFontSize() {
        return fontSize;
    }

    /**
     * @return the lineHeight
     * @author WFF
     * @since 1.0.0
     */
    public LineHeight getLineHeight() {
        return lineHeight;
    }

    /**
     * @return the fontFamily
     * @author WFF
     * @since 1.0.0
     */
    public FontFamily getFontFamily() {
        return fontFamily;
    }

    public FontVariant getFontVariant() {
        return fontVariant;
    }

    /**
     * @return the fontWeight
     * @author WFF
     * @since 1.0.0
     */
    public FontWeight getFontWeight() {
        return fontWeight;
    }

    public FontStyle getFontStyle() {
        return fontStyle;
    }

    public Font setFontStyle(final FontStyle fontStyle) {

        final StringBuilder cssValueBuilder = new StringBuilder();

        if (fontStyle != null) {
            cssValueBuilder.append(fontStyle.getCssValue()).append(' ');
        }
        if (fontVariant != null) {
            cssValueBuilder.append(fontVariant.getCssValue()).append(' ');
        }

        if (fontWeight != null) {
            cssValueBuilder.append(fontWeight.getCssValue()).append(' ');
        }

        if (fontSize != null) {
            cssValueBuilder.append(fontSize.getCssValue());
            if (lineHeight == null) {
                cssValueBuilder.append(' ');
            }
        }

        if (lineHeight != null) {
            if (fontSize != null) {
                cssValueBuilder.append('/');
            }
            cssValueBuilder.append(lineHeight.getCssValue()).append(' ');
        }

        if (fontFamily != null) {
            cssValueBuilder.append(fontFamily.getCssValue()).append(' ');
        }

        final String trimmedCssValue = StringBuilderUtil
                .getTrimmedString(cssValueBuilder).toString();
        cssValue = trimmedCssValue.isEmpty() ? INHERIT : trimmedCssValue;

        this.fontStyle = fontStyle;

        if (getStateChangeInformer() != null) {
            getStateChangeInformer().stateChanged(this);
        }
        return this;
    }

    public Font setFontVariant(final FontVariant fontVariant) {

        final StringBuilder cssValueBuilder = new StringBuilder();

        if (fontStyle != null) {
            cssValueBuilder.append(fontStyle.getCssValue()).append(' ');
        }

        if (fontVariant != null) {
            cssValueBuilder.append(fontVariant.getCssValue()).append(' ');
        }

        if (fontWeight != null) {
            cssValueBuilder.append(fontWeight.getCssValue()).append(' ');
        }

        if (fontSize != null) {
            cssValueBuilder.append(fontSize.getCssValue());
            if (lineHeight == null) {
                cssValueBuilder.append(' ');
            }
        }

        if (lineHeight != null) {
            if (fontSize != null) {
                cssValueBuilder.append('/');
            }
            cssValueBuilder.append(lineHeight.getCssValue()).append(' ');
        }

        if (fontFamily != null) {
            cssValueBuilder.append(fontFamily.getCssValue()).append(' ');
        }

        final String trimmedCssValue = StringBuilderUtil
                .getTrimmedString(cssValueBuilder).toString();
        cssValue = trimmedCssValue.isEmpty() ? INHERIT : trimmedCssValue;

        this.fontVariant = fontVariant;

        if (getStateChangeInformer() != null) {
            getStateChangeInformer().stateChanged(this);
        }
        return this;
    }

    public Font setFontWeight(final FontWeight fontWeight) {

        final StringBuilder cssValueBuilder = new StringBuilder();

        if (fontStyle != null) {
            cssValueBuilder.append(fontStyle.getCssValue()).append(' ');
        }

        if (fontVariant != null) {
            cssValueBuilder.append(fontVariant.getCssValue()).append(' ');
        }

        if (fontWeight != null) {
            cssValueBuilder.append(fontWeight.getCssValue()).append(' ');
        }

        if (fontSize != null) {
            cssValueBuilder.append(fontSize.getCssValue());
            if (lineHeight == null) {
                cssValueBuilder.append(' ');
            }
        }

        if (lineHeight != null) {
            if (fontSize != null) {
                cssValueBuilder.append('/');
            }
            cssValueBuilder.append(lineHeight.getCssValue()).append(' ');
        }

        if (fontFamily != null) {
            cssValueBuilder.append(fontFamily.getCssValue()).append(' ');
        }

        final String trimmedCssValue = StringBuilderUtil
                .getTrimmedString(cssValueBuilder).toString();
        cssValue = trimmedCssValue.isEmpty() ? INHERIT : trimmedCssValue;

        this.fontWeight = fontWeight;

        if (getStateChangeInformer() != null) {
            getStateChangeInformer().stateChanged(this);
        }
        return this;
    }

    public Font setFontSize(final FontSize fontSize) {

        final StringBuilder cssValueBuilder = new StringBuilder();

        if (fontStyle != null) {
            cssValueBuilder.append(fontStyle.getCssValue()).append(' ');
        }

        if (fontVariant != null) {
            cssValueBuilder.append(fontVariant.getCssValue()).append(' ');
        }

        if (fontWeight != null) {
            cssValueBuilder.append(fontWeight.getCssValue()).append(' ');
        }

        if (fontSize != null) {
            final String fontSizeCssValue = fontSize.getCssValue();
            if (FontSize.INITIAL.equals(fontSizeCssValue)
                    || FontSize.INHERIT.equals(fontSizeCssValue)) {
                throw new InvalidValueException(
                        "fontSize cannot have initial/inherit as its cssValue");
            }
            cssValueBuilder.append(fontSizeCssValue);
            if (lineHeight == null) {
                cssValueBuilder.append(' ');
            }
        }

        if (lineHeight != null) {
            if (fontSize != null) {
                cssValueBuilder.append('/');
            }
            final String lineHeightCssValue = lineHeight.getCssValue();
            cssValueBuilder.append(lineHeightCssValue).append(' ');
        }

        if (fontFamily != null) {
            final String fontFamilyCssValue = fontFamily.getCssValue();
            cssValueBuilder.append(fontFamilyCssValue).append(' ');
        }

        final String trimmedCssValue = StringBuilderUtil
                .getTrimmedString(cssValueBuilder).toString();
        cssValue = trimmedCssValue.isEmpty() ? INHERIT : trimmedCssValue;

        if (fontSize != null && fontSize.isAlreadyInUse()
                && this.fontSize != fontSize) {
            if (LOGGER.isLoggable(Level.WARNING)) {
                LOGGER.warning(
                        "the given fontSize is already used by another object so a new object or the previous object (if it exists) of FontSize will be used");
            }
            return setCssValue(cssValue);
        }

        if (this.fontSize != null) {
            this.fontSize.setAlreadyInUse(false);
        }

        this.fontSize = fontSize;

        if (this.fontSize != null) {
            this.fontSize.setStateChangeInformer(this);
            this.fontSize.setAlreadyInUse(true);
        }

        if (getStateChangeInformer() != null) {
            getStateChangeInformer().stateChanged(this);
        }
        return this;
    }

    public Font setLineHeight(final LineHeight lineHeight) {

        final StringBuilder cssValueBuilder = new StringBuilder();

        if (fontStyle != null) {
            cssValueBuilder.append(fontStyle.getCssValue()).append(' ');
        }

        if (fontVariant != null) {
            cssValueBuilder.append(fontVariant.getCssValue()).append(' ');
        }

        if (fontWeight != null) {
            cssValueBuilder.append(fontWeight.getCssValue()).append(' ');
        }

        if (fontSize != null) {
            final String fontSizeCssValue = fontSize.getCssValue();
            cssValueBuilder.append(fontSizeCssValue);
            if (lineHeight == null) {
                cssValueBuilder.append(' ');
            }
        }

        if (lineHeight != null) {
            final String lineHeightCssValue = lineHeight.getCssValue();
            if (FontSize.INITIAL.equals(lineHeightCssValue)
                    || FontSize.INHERIT.equals(lineHeightCssValue)) {
                throw new InvalidValueException(
                        "fontSize cannot have initial/inherit as its cssValue");
            }
            if (fontSize != null) {
                cssValueBuilder.append('/');
            }
            cssValueBuilder.append(lineHeightCssValue).append(' ');
        }

        if (fontFamily != null) {
            final String fontFamilyCssValue = fontFamily.getCssValue();
            cssValueBuilder.append(fontFamilyCssValue).append(' ');
        }

        final String trimmedCssValue = StringBuilderUtil
                .getTrimmedString(cssValueBuilder).toString();
        cssValue = trimmedCssValue.isEmpty() ? INHERIT : trimmedCssValue;

        if (lineHeight != null && lineHeight.isAlreadyInUse()
                && this.lineHeight != lineHeight) {
            if (LOGGER.isLoggable(Level.WARNING)) {
                LOGGER.warning(
                        "the given lineHeight is already used by another object so a new object or the previous object (if it exists) of FontSize will be used");
            }
            return setCssValue(cssValue);
        }

        if (this.lineHeight != null) {
            this.lineHeight.setAlreadyInUse(false);
        }

        this.lineHeight = lineHeight;

        if (this.lineHeight != null) {
            this.lineHeight.setStateChangeInformer(this);
            this.lineHeight.setAlreadyInUse(true);
        }

        if (getStateChangeInformer() != null) {
            getStateChangeInformer().stateChanged(this);
        }
        return this;
    }

    public Font setFontFamily(final FontFamily fontFamily) {

        final StringBuilder cssValueBuilder = new StringBuilder();

        if (fontStyle != null) {
            cssValueBuilder.append(fontStyle.getCssValue()).append(' ');
        }

        if (fontVariant != null) {
            cssValueBuilder.append(fontVariant.getCssValue()).append(' ');
        }

        if (fontWeight != null) {
            cssValueBuilder.append(fontWeight.getCssValue()).append(' ');
        }

        if (fontSize != null) {
            final String fontSizeCssValue = fontSize.getCssValue();
            cssValueBuilder.append(fontSizeCssValue);
            if (lineHeight == null) {
                cssValueBuilder.append(' ');
            }
        }

        if (lineHeight != null) {
            final String lineHeightCssValue = lineHeight.getCssValue();
            if (fontSize != null) {
                cssValueBuilder.append('/');
            }
            cssValueBuilder.append(lineHeightCssValue).append(' ');
        }

        if (fontFamily != null) {
            final String fontFamilyCssValue = fontFamily.getCssValue();
            if (FontFamily.INITIAL.equals(fontFamilyCssValue)
                    || FontFamily.INHERIT.equals(fontFamilyCssValue)) {
                throw new InvalidValueException(
                        "fontFamily cannot have initial/inherit as its cssValue");
            }
            cssValueBuilder.append(fontFamilyCssValue).append(' ');
        }

        final String trimmedCssValue = StringBuilderUtil
                .getTrimmedString(cssValueBuilder).toString();
        cssValue = trimmedCssValue.isEmpty() ? INHERIT : trimmedCssValue;

        if (fontFamily != null && fontFamily.isAlreadyInUse()
                && this.fontFamily != fontFamily) {
            if (LOGGER.isLoggable(Level.WARNING)) {
                LOGGER.warning(
                        "the given fontFamily is already used by another object so a new object or the previous object (if it exists) of FontSize will be used");
            }
            return setCssValue(cssValue);
        }

        if (this.fontFamily != null) {
            this.fontFamily.setAlreadyInUse(false);
        }

        this.fontFamily = fontFamily;

        if (this.fontFamily != null) {
            this.fontFamily.setStateChangeInformer(this);
            this.fontFamily.setAlreadyInUse(true);
        }

        if (getStateChangeInformer() != null) {
            getStateChangeInformer().stateChanged(this);
        }
        return this;
    }

    @Override
    public void stateChanged(final CssProperty stateChangedObject) {
        if (stateChangedObject instanceof FontSize) {

            final FontSize fontSize = (FontSize) stateChangedObject;

            final String cssValue = fontSize.getCssValue();

            if (FontSize.INITIAL.equals(cssValue)
                    || FontSize.INHERIT.equals(cssValue)) {
                throw new InvalidValueException(
                        "initial/inherit cannot be set as fontSize cssValue");
            }

            setFontSize(fontSize);
        } else if (stateChangedObject instanceof LineHeight) {

            final LineHeight lineHeight = (LineHeight) stateChangedObject;

            final String cssValue = lineHeight.getCssValue();

            if (LineHeight.INITIAL.equals(cssValue)
                    || LineHeight.INHERIT.equals(cssValue)) {
                throw new InvalidValueException(
                        "initial/inherit cannot be set as lineHeight cssValue");
            }

            setLineHeight(lineHeight);
        } else if (stateChangedObject instanceof FontFamily) {

            final FontFamily fontFamily = (FontFamily) stateChangedObject;

            final String cssValue = fontFamily.getCssValue();

            if (FontFamily.INITIAL.equals(cssValue)
                    || FontFamily.INHERIT.equals(cssValue)) {
                throw new InvalidValueException(
                        "initial/inherit cannot be set as fontFamily cssValue");
            }

            setFontFamily(fontFamily);
        }
    }

    /**
     *
     * @author WFF
     * @since 1.0.0
     */
    protected void addPredefinedConstant(final String constant) {
        PREDEFINED_CONSTANTS.add(constant);
    }
}


/*
 * Copyright 2014-2019 Web Firm Framework
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * @author WFF
 */
package com.webfirmframework.wffweb.tag.html.model;

import java.io.Serializable;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import com.webfirmframework.wffweb.DataWffIdOutOfRangeError;
import com.webfirmframework.wffweb.WffSecurityException;
import com.webfirmframework.wffweb.security.object.SecurityClassConstants;
import com.webfirmframework.wffweb.tag.core.AbstractTagBase;
import com.webfirmframework.wffweb.tag.html.AbstractHtml;
import com.webfirmframework.wffweb.tag.html.attribute.listener.AttributeValueChangeListener;
import com.webfirmframework.wffweb.tag.html.html5.attribute.global.DataWffId;
import com.webfirmframework.wffweb.tag.html.listener.AttributeAddListener;
import com.webfirmframework.wffweb.tag.html.listener.AttributeRemoveListener;
import com.webfirmframework.wffweb.tag.html.listener.ChildTagAppendListener;
import com.webfirmframework.wffweb.tag.html.listener.ChildTagRemoveListener;
import com.webfirmframework.wffweb.tag.html.listener.InnerHtmlAddListener;
import com.webfirmframework.wffweb.tag.html.listener.InsertBeforeListener;
import com.webfirmframework.wffweb.tag.html.listener.PushQueue;
import com.webfirmframework.wffweb.tag.html.listener.WffBMDataDeleteListener;
import com.webfirmframework.wffweb.tag.html.listener.WffBMDataUpdateListener;

/**
 *
 * @author WFF
 * @since 1.0.0
 */
public class AbstractHtml5SharedObject implements Serializable {

    private static final long serialVersionUID = 1_0_1L;

    private boolean childModified;

    private transient volatile Set<AbstractTagBase> rebuiltTags;

    private ChildTagAppendListener childTagAppendListener;

    private ChildTagRemoveListener childTagRemoveListener;

    private AttributeAddListener attributeAddListener;

    private AttributeRemoveListener attributeRemoveListener;

    private InnerHtmlAddListener innerHtmlAddListener;

    /**
     * key : "S" + dataWffId and value : abstractHtml tag
     */
    private final Map<String, AbstractHtml> tagByWffId = new ConcurrentHashMap<>();

    private AttributeValueChangeListener valueChangeListener;

    private InsertBeforeListener insertBeforeListener;

    private WffBMDataDeleteListener wffBMDataDeleteListener;

    private WffBMDataUpdateListener wffBMDataUpdateListener;

    private PushQueue pushQueue;

    /**
     * no need to make it volatile
     */
    private final AtomicInteger dataWffId = new AtomicInteger(-1);

    private final AtomicBoolean dataWffIdSecondCycle = new AtomicBoolean(false);

    private final AbstractHtml rootTag;

    private final AtomicReference<Object> sharedData = new AtomicReference<>();

    /**
     * Java 8's {@code StampedLock} is not suitable here because it seems to be
     * thread independent. Eg:-
     *
     *

     * ReadWriteLock lock = new StampedLock().asReadWriteLock();
     * lock.writeLock().lock();
     * System.out.println("after writeLock");
     * lock.readLock().lock();
     * System.out.println("after readLock");
     *

     *
     * If the lock is a {@code StampedLock} then the code will block at
     * lock.readLock().lock(); even in the same thread. But
     * {@code ReentrantReadWriteLock} will not block in the same thread, it will
     * print both system print.
     *
     * @since 3.0.1
     */
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(
            true);

    public AbstractHtml5SharedObject(final AbstractHtml rootTag) {
        this.rootTag = rootTag;
    }

    /**
     * @return unique data-wff-id attribute
     * @since 2.0.0
     * @author WFF
     */
    public DataWffId getNewDataWffId(final Object accessObject) {

        if (accessObject == null || !((SecurityClassConstants.ABSTRACT_HTML
                .equals(accessObject.getClass().getName()))
                || (SecurityClassConstants.BROWSER_PAGE
                        .equals(accessObject.getClass().getName())))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }

        // needs further improvement for atomic operation

        while (tagByWffId.size() < Integer.MAX_VALUE) {

            final int incrementedDataWffId = dataWffId.incrementAndGet();

            if (incrementedDataWffId < 0 || dataWffIdSecondCycle.get()) {

                dataWffIdSecondCycle.compareAndSet(false, true);

                int newDataWffId = incrementedDataWffId < 0
                        ? ((incrementedDataWffId - Integer.MAX_VALUE) - 1)
                        : incrementedDataWffId;

                String id = "S" + (newDataWffId);

                while (tagByWffId.containsKey(id)) {
                    newDataWffId++;
                    if (newDataWffId < 0) {
                        newDataWffId = (newDataWffId - Integer.MAX_VALUE) - 1;
                    }
                    id = "S" + newDataWffId;
                }

                if (dataWffId.compareAndSet(incrementedDataWffId,
                        newDataWffId)) {
                    return new DataWffId("S" + newDataWffId);
                }
                continue;
            }

            return new DataWffId("S" + incrementedDataWffId);
        }
        throw new DataWffIdOutOfRangeError(
                "BrowserPage object has reached an impossible worst case! No enough DataWffId available to assign to a new tag.");

    }

    /**
     * @param accessObject
     * @return the ReadWriteLock lock
     * @since 3.0.1
     */
    public ReentrantReadWriteLock getLock(final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.ABSTRACT_HTML
                .equals(accessObject.getClass().getName())
                || SecurityClassConstants.ABSTRACT_ATTRIBUTE
                        .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        return lock;
    }

    public int getLastDataWffId(final Object accessObject) {

        if (accessObject == null || !((SecurityClassConstants.ABSTRACT_HTML
                .equals(accessObject.getClass().getName())))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        return dataWffId.get();
    }

    /**
     * @return the childModified true if any of the children has been modified
     * @since 1.0.0
     * @author WFF
     */
    public boolean isChildModified() {
        return childModified;
    }

    /**
     * set true if any of the children has been modified.

     * NB:- it's for internal use
     *
     * @param childModified
     *                          the childModified to set
     * @since 1.0.0
     * @author WFF
     */
    public void setChildModified(final boolean childModified) {
        this.childModified = childModified;
    }

    /**
     * gets the set containing the objects which are rebuilt after modified by
     * its {@code AbstractTagBase} method. NB:- only for internal use. currently
     * it's not used anywhere
     *
     * @return the rebuiltTags
     * @since 1.0.0
     * @author WFF
     * @deprecated only for internal use currently it's not used anywhere. Needs
     *             to remove this method later.
     */
    @Deprecated
    public Set<AbstractTagBase> getRebuiltTags(final Object accessObject) {

        // TODO remove this method later
        if (accessObject == null || !(SecurityClassConstants.ABSTRACT_HTML
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }

        if (rebuiltTags == null) {
            synchronized (this) {
                if (rebuiltTags == null) {
                    rebuiltTags = Collections.newSetFromMap(
                            new WeakHashMap<AbstractTagBase, Boolean>());
                }
            }
        }
        return rebuiltTags;
    }

    /**
     * NB:- This method is for only for internal use
     *
     * @return the childTagAppendListener
     */
    public ChildTagAppendListener getChildTagAppendListener(
            final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.ABSTRACT_HTML
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        return childTagAppendListener;
    }

    /**
     * NB:- This method is for only for internal use
     *
     * @param childTagAppendListener
     *                                   the childTagAppendListener to set
     */
    public void setChildTagAppendListener(
            final ChildTagAppendListener childTagAppendListener,
            final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.BROWSER_PAGE
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        this.childTagAppendListener = childTagAppendListener;
    }

    /**
     * NB:- This method is for only for internal use
     *
     * @return the childTagRemoveListener
     */
    public ChildTagRemoveListener getChildTagRemoveListener(
            final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.ABSTRACT_HTML
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        return childTagRemoveListener;
    }

    /**
     * NB:- This method is for only for internal use
     *
     * @param childTagRemoveListener
     *                                   the childTagRemoveListener to set
     */
    public void setChildTagRemoveListener(
            final ChildTagRemoveListener childTagRemoveListener,
            final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.BROWSER_PAGE
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        this.childTagRemoveListener = childTagRemoveListener;
    }

    /**
     * @return the attributeAddListener
     */
    public AttributeAddListener getAttributeAddListener(
            final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.ABSTRACT_HTML
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        return attributeAddListener;
    }

    /**
     * @param attributeAddListener
     *                                 the attributeAddListener to set
     */
    public void setAttributeAddListener(
            final AttributeAddListener attributeAddListener,
            final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.BROWSER_PAGE
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        this.attributeAddListener = attributeAddListener;
    }

    /**
     * @return the attributeRemoveListener
     */
    public AttributeRemoveListener getAttributeRemoveListener(
            final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.ABSTRACT_HTML
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        return attributeRemoveListener;
    }

    /**
     * @param attributeRemoveListener
     *                                    the attributeRemoveListener to set
     */
    public void setAttributeRemoveListener(
            final AttributeRemoveListener attributeRemoveListener,
            final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.BROWSER_PAGE
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        this.attributeRemoveListener = attributeRemoveListener;
    }

    /**
     * @return the innerHtmlAddListener
     */
    public InnerHtmlAddListener getInnerHtmlAddListener(
            final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.ABSTRACT_HTML
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        return innerHtmlAddListener;
    }

    /**
     * @param innerHtmlAddListener
     *                                 the innerHtmlAddListener to set
     */
    public void setInnerHtmlAddListener(
            final InnerHtmlAddListener innerHtmlAddListener,
            final Object accessObject) {

        if (accessObject == null || !(SecurityClassConstants.BROWSER_PAGE
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        this.innerHtmlAddListener = innerHtmlAddListener;
    }

    /**
     * @param accessObject
     * @return the map containing wffid and tag
     * @since 2.0.0
     * @author WFF
     */
    public Map<String, AbstractHtml> getTagByWffId(final Object accessObject) {
        if (accessObject == null || !((SecurityClassConstants.ABSTRACT_HTML
                .equals(accessObject.getClass().getName()))
                || (SecurityClassConstants.BROWSER_PAGE
                        .equals(accessObject.getClass().getName())))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        return tagByWffId;
    }

    /**
     * @param accessObject
     * @since 2.0.0
     * @author WFF
     * @return the map containing wffid and tag
     */
    public Map<String, AbstractHtml> initTagByWffId(final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.BROWSER_PAGE
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        return tagByWffId;
    }

    /**
     * NB:- This listener is used for internal purpose and should not be
     * consumed. Instead, use addValueChangeListener and getValueChangeListeners
     * methods.
     *
     * @param accessObject
     *                         access object of this method
     * @return the valueChangeListener
     */
    public AttributeValueChangeListener getValueChangeListener(
            final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.ABSTRACT_ATTRIBUTE
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. Instead, use addValueChangeListener and getValueChangeListeners methods.");
        }
        return valueChangeListener;
    }

    /**
     * NB:- This listener is used for internal purpose and should not be
     * consumed. Instead, use addValueChangeListener and getValueChangeListeners
     * methods.
     *
     * @param valueChangeListener
     *                                the valueChangeListener to set
     * @param accessObject
     *                                access object of this method
     */
    public void setValueChangeListener(
            final AttributeValueChangeListener valueChangeListener,
            final Object accessObject) {

        if (accessObject == null || !(SecurityClassConstants.BROWSER_PAGE
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. Instead, use addValueChangeListener and getValueChangeListeners methods.");
        }
        this.valueChangeListener = valueChangeListener;
    }

    /**
     * NB:- This method is for only for internal use
     *
     * @return the insertBeforeListener
     *
     * @since 2.1.1
     */
    public InsertBeforeListener getInsertBeforeListener(
            final Object accessObject) {

        if (accessObject == null || !(SecurityClassConstants.ABSTRACT_HTML
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }

        return insertBeforeListener;
    }

    /**
     * NB:- This method is for only for internal use
     *
     * @param insertBeforeListener
     *                                 the insertBeforeListener to set
     * @since 2.1.1
     */
    public void setInsertBeforeListener(
            final InsertBeforeListener insertBeforeListener,
            final Object accessObject) {

        if (accessObject == null || !(SecurityClassConstants.BROWSER_PAGE
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }

        this.insertBeforeListener = insertBeforeListener;
    }

    /**
     * NB:- This method is for only for internal use
     *
     * @param wffBMDataDeleteListener
     *                                    the wffDataDeleteListener to set
     * @since 2.1.8
     */
    public void setWffBMDataDeleteListener(
            final WffBMDataDeleteListener wffBMDataDeleteListener,
            final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.BROWSER_PAGE
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        this.wffBMDataDeleteListener = wffBMDataDeleteListener;
    }

    /**
     * NB:- This method is for only for internal use
     *
     * @param wffBMDataUpdateListener
     *                                    the wffDataUpdateListener to set
     * @since 2.1.8
     */
    public void setWffBMDataUpdateListener(
            final WffBMDataUpdateListener wffBMDataUpdateListener,
            final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.BROWSER_PAGE
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        this.wffBMDataUpdateListener = wffBMDataUpdateListener;
    }

    /**
     * NB:- This method is for only for internal use
     *
     * @return the insertBeforeListener
     *
     * @since 2.1.8
     */
    public WffBMDataDeleteListener getWffBMDataDeleteListener(
            final Object accessObject) {

        if (accessObject == null || !(SecurityClassConstants.ABSTRACT_JS_OBJECT
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }

        return wffBMDataDeleteListener;
    }

    /**
     * NB:- This method is for only for internal use
     *
     * @return the insertBeforeListener
     *
     * @since 2.1.8
     */
    public WffBMDataUpdateListener getWffBMDataUpdateListener(
            final Object accessObject) {

        if (accessObject == null || !(SecurityClassConstants.ABSTRACT_JS_OBJECT
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }

        return wffBMDataUpdateListener;
    }

    /**
     * @return the rootTag set through the constructor.
     * @since 2.1.11
     * @author WFF
     */
    public AbstractHtml getRootTag() {
        return rootTag;
    }

    /**
     * Gets the object which is accessible in all of this tag hierarchy.
     *
     * @return the sharedData object set by setSharedData method. This object is
     *         same across all of this tag hierarchy.
     * @since 2.1.11
     * @author WFF
     */
    public Object getSharedData() {
        return sharedData.get();
    }

    /**
     * Sets the object which will be accessible by getSharedData method in all
     * of this tag hierarchy. {@code setData} sets an object for the specific
     * tag but {@code setSharedData} sets an object for all of the tag
     * hierarchy.
     *
     * @param sharedData
     *                       the object to access through all of this tag
     *                       hierarchy.
     * @since 2.1.11
     * @author WFF
     */
    public void setSharedData(final Object sharedData) {
        this.sharedData.set(sharedData);
    }

    /**
     * @param sharedData
     * @return true if set or false if it already had a value
     * @since 3.0.1
     */
    public boolean setSharedDataIfNull(final Object sharedData) {
        return this.sharedData.compareAndSet(null, sharedData);
    }

    /**
     * NB:- This method is for only for internal use
     *
     * @param pushQueue
     *                      the BrowserPage push queue to set
     */
    public void setPushQueue(final PushQueue pushQueue,
            final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.BROWSER_PAGE
                .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        this.pushQueue = pushQueue;
    }

    /**
     * NB:- This method is for only for internal use
     *
     * @return the the push queue of BrowserPage
     */
    public PushQueue getPushQueue(final Object accessObject) {
        if (accessObject == null || !(SecurityClassConstants.ABSTRACT_HTML
                .equals(accessObject.getClass().getName())
                || SecurityClassConstants.ABSTRACT_ATTRIBUTE
                        .equals(accessObject.getClass().getName())
                || SecurityClassConstants.ABSTRACT_JS_OBJECT
                        .equals(accessObject.getClass().getName()))) {
            throw new WffSecurityException(
                    "Not allowed to consume this method. This method is for internal use.");
        }
        return pushQueue;
    }

}