/*
 * 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.server.page;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.webfirmframework.wffweb.InvalidTagException;
import com.webfirmframework.wffweb.tag.html.AbstractHtml;
import com.webfirmframework.wffweb.tag.html.TagNameConstants;
import com.webfirmframework.wffweb.tag.html.html5.attribute.global.DataWffId;
import com.webfirmframework.wffweb.tag.html.listener.InnerHtmlAddListener;
import com.webfirmframework.wffweb.util.data.NameValue;

class InnerHtmlAddListenerImpl implements InnerHtmlAddListener {

    private static final long serialVersionUID = 1L;

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

    private BrowserPage browserPage;

    private Object accessObject;

    private Map<String, AbstractHtml> tagByWffId;

    @SuppressWarnings("unused")
    private InnerHtmlAddListenerImpl() {
        throw new AssertionError();
    }

    InnerHtmlAddListenerImpl(final BrowserPage browserPage,
            final Object accessObject,
            final Map<String, AbstractHtml> tagByWffId) {
        this.browserPage = browserPage;
        this.accessObject = accessObject;
        this.tagByWffId = tagByWffId;

    }

    /**
     * adds to wffid map
     *
     * @param tag
     * @since 2.0.0
     * @author WFF
     */
    private void addInWffIdMap(final AbstractHtml tag) {

        final Deque<Set<AbstractHtml>> childrenStack = new ArrayDeque<>();
        // passed 2 instead of 1 because the load factor is 0.75f
        final Set<AbstractHtml> initialSet = new HashSet<>(2);
        initialSet.add(tag);
        childrenStack.push(initialSet);

        Set<AbstractHtml> children;
        while ((children = childrenStack.poll()) != null) {
            for (final AbstractHtml child : children) {

                final DataWffId wffIdAttr = child.getDataWffId();

                if (wffIdAttr != null) {
                    tagByWffId.put(wffIdAttr.getValue(), child);
                }

                final Set<AbstractHtml> subChildren = child
                        .getChildren(accessObject);
                if (subChildren != null && subChildren.size() > 0) {
                    childrenStack.push(subChildren);
                }

            }
        }

    }

    @Override
    public void innerHtmlsAdded(final AbstractHtml parentTag,
            final Event... events) {

        //@formatter:off
        // removed all children tags task format :-
        // { "name": task_byte, "values" : [ADDED_INNER_HTML_byte_from_Task_enum]}, { "name": parent_tag_name, "values" : [ data-wff-id ] }, { "name": html_string, "values" : [ 1_if_there_was_a_previous_parent ]}
        // { "name": 2, "values" : [[3]]}, { "name":"C55", "values" : ["div", "", 1]}
        //@formatter:on

        final Collection<NameValue> nameValues = new ArrayDeque<>(
                events.length + 2);

        final NameValue task = Task.ADDED_INNER_HTML.getTaskNameValue();
        nameValues.add(task);

        try {

            final DataWffId dataWffId = parentTag.getDataWffId();

            if (dataWffId == null) {

                LOGGER.severe("Could not find data-wff-id from owner tag");
                return;
            }

            final byte[][] tagNameAndWffId = DataWffIdUtil
                    .getTagNameAndWffId(parentTag);

            final byte[] parentTagName = tagNameAndWffId[0];

            final byte[] parentWffIdBytes = tagNameAndWffId[1];

            final NameValue parentTagNameValue = new NameValue(parentTagName,
                    new byte[][] { parentWffIdBytes });

            nameValues.add(parentTagNameValue);

            for (final Event event : events) {

                final AbstractHtml innerHtmlTag = event.getInnerHtmlTag();
                final AbstractHtml previousParentTag = event
                        .getPreviousParentTag();

                final NameValue nameValue = new NameValue();

                try {
                    nameValue.setName(
                            innerHtmlTag.toWffBMBytes(StandardCharsets.UTF_8));
                } catch (final InvalidTagException e) {
                    if (LOGGER.isLoggable(Level.WARNING)) {
                        LOGGER.log(Level.WARNING,
                                "Do not append/add an empty NoTag as child tag, eg: new NoTag(null, \"\").\n"
                                        .concat("To make a tag's children as empty then invoke removeAllChildren() method in it."),
                                e);
                    }
                    continue;
                }

                if (previousParentTag != null) {
                    nameValue.setValues(new byte[] { 1 });
                } else {
                    nameValue.setValues(new byte[0][0]);
                }

                addInWffIdMap(innerHtmlTag);

                nameValues.add(nameValue);

            }

            browserPage
                    .push(nameValues.toArray(new NameValue[nameValues.size()]));

            final String parentTagNameString = new String(parentTagName,
                    StandardCharsets.UTF_8);
            if (TagNameConstants.TEXTAREA.equals(parentTagNameString)) {
                //@formatter:off
                // removed all children tags task format :-
                // { "name": task_byte, "values" : [COPY_INNER_TEXT_TO_VALUE_byte_from_Task_enum]}, { "name": parent_tag_name, "values" : [ data-wff-id ] }
                //@formatter:on
                final NameValue copyInnerTexToValueTask = Task.COPY_INNER_TEXT_TO_VALUE
                        .getTaskNameValue();

                browserPage.push(copyInnerTexToValueTask, parentTagNameValue);
            }

        } catch (final UnsupportedEncodingException e) {
            if (LOGGER.isLoggable(Level.SEVERE)) {
                LOGGER.log(Level.SEVERE, e.getMessage(), e);
            }
        }

    }

    @Override
    public void innerHtmlAdded(final Event event) {
        innerHtmlsAdded(event.getParentTag(), event);
    }

}


/*
 * 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.Objects;
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;

/**
 *

 * The border-right shorthand property sets all the right border properties in one declaration.
 *
 * The properties that can be set, are (in order): border-right-width, border-right-style, and border-right-color.
 *
 * If one of the values above are missing, e.g. border-right:solid #ff0000, the default value for the missing property will be inserted, if any.
 * Default value:  medium none color
 * Inherited:      no
 * Animatable:     yes
 * Version:        CSS1
 * JavaScript syntax:      object.style.borderRight="3px dashed blue"
 *

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

    private static final long serialVersionUID = 1_0_0L;

    public static final Logger LOGGER = Logger
            .getLogger(BorderRight.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 BorderRightWidth borderRightWidth;

    private BorderRightStyle borderRightStyle;

    private BorderRightColor borderRightColor;

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

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

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

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

    /*
     * (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 BorderRight 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(BorderRightColor.INITIAL)) {
                throw new InvalidValueException(
                        "The string 'initial' makes the given cssValue invalid, please remove 'initial' from it and try.");
            }
            if (trimmedCssValue.contains(BorderRightColor.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 (borderRightWidth != null) {
                    borderRightWidth.setAlreadyInUse(false);
                    borderRightWidth = null;
                }

                borderRightStyle = null;

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

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

        BorderRightWidth borderRightWidth = null;
        BorderRightStyle borderRightStyle = null;
        BorderRightColor borderRightColor = null;

        for (final String eachPart : cssValueParts) {
            if (borderRightWidth == null
                    && BorderRightWidth.isValid(eachPart)) {
                if (this.borderRightWidth == null) {
                    borderRightWidth = new BorderRightWidth(eachPart);
                    borderRightWidth.setStateChangeInformer(this);
                    borderRightWidth.setAlreadyInUse(true);
                } else {
                    this.borderRightWidth.setCssValue(eachPart);
                    borderRightWidth = this.borderRightWidth;
                }
            } else if (borderRightStyle == null
                    && BorderRightStyle.isValid(eachPart)) {
                borderRightStyle = BorderRightStyle.getThis(eachPart);
            } else if (borderRightColor == null
                    && BorderRightColor.isValid(eachPart)) {
                if (this.borderRightColor == null) {
                    borderRightColor = new BorderRightColor(eachPart);
                    borderRightColor.setStateChangeInformer(this);
                    borderRightColor.setAlreadyInUse(true);
                } else {
                    this.borderRightColor.setCssValue(eachPart);
                    borderRightColor = this.borderRightColor;
                }
            }
        }

        final StringBuilder cssValueBuilder = new StringBuilder();
        boolean invalid = true;
        if (borderRightWidth != null) {
            cssValueBuilder.append(borderRightWidth.getCssValue()).append(' ');
            invalid = false;
        } else if (this.borderRightWidth != null) {
            this.borderRightWidth.setAlreadyInUse(false);
        }
        if (borderRightStyle != null) {
            cssValueBuilder.append(borderRightStyle.getCssValue()).append(' ');
            invalid = false;
        }
        if (borderRightColor != null) {
            cssValueBuilder.append(borderRightColor.getCssValue()).append(' ');
            invalid = false;
        } else if (this.borderRightColor != null) {
            this.borderRightColor.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.borderRightWidth = borderRightWidth;
        this.borderRightStyle = borderRightStyle;
        this.borderRightColor = borderRightColor;

        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);

        BorderRightWidth borderRightWidth = null;
        BorderRightStyle borderRightStyle = null;
        BorderRightColor borderRightColor = null;

        for (final String eachPart : cssValueParts) {

            boolean invalid = true;
            if (borderRightWidth == null
                    && BorderRightWidth.isValid(eachPart)) {
                borderRightWidth = new BorderRightWidth(eachPart);
                invalid = false;
            } else if (borderRightStyle == null
                    && BorderRightStyle.isValid(eachPart)) {
                borderRightStyle = BorderRightStyle.getThis(eachPart);
                invalid = false;
            } else if (borderRightColor == null
                    && BorderRightColor.isValid(eachPart)) {
                borderRightColor = new BorderRightColor(eachPart);
                invalid = false;
            }
            if (invalid) {
                return false;
            }
        }

        return borderRightWidth != null || borderRightStyle != null
                || borderRightColor != 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 BorderRightColor getBorderRightColor() {
        return borderRightColor;
    }

    public BorderRightStyle getBorderRightStyle() {
        return borderRightStyle;
    }

    public BorderRightWidth getBorderRightWidth() {
        return borderRightWidth;
    }

    public BorderRight setBorderRightWidth(
            final BorderRightWidth borderRightWidth) {

        final StringBuilder cssValueBuilder = new StringBuilder();

        if (borderRightWidth != null) {

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

            cssValueBuilder.append(borderRightWidth.getCssValue()).append(' ');
        }

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

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

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

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

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

        this.borderRightWidth = borderRightWidth;

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

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

    public BorderRight setBorderRightStyle(
            final BorderRightStyle borderRightStyle) {

        final StringBuilder cssValueBuilder = new StringBuilder();

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

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

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

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

        this.borderRightStyle = borderRightStyle;

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

    public BorderRight setBorderRightColor(
            final BorderRightColor borderRightColor) {

        final StringBuilder cssValueBuilder = new StringBuilder();

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

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

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

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

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

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

        this.borderRightColor = borderRightColor;

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

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

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

            final BorderRightColor borderRightColor = (BorderRightColor) stateChangedObject;

            final String cssValue = borderRightColor.getCssValue();

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

            setBorderRightColor(borderRightColor);
        }
    }

    /**
     *
     * @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;

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

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

/**
 *

 * For absolutely positioned elements, the left property sets the left edge of an element to a unit above/below the left edge of its containing element.
 *
 * For relatively positioned elements, the left property sets the left edge of an element to a unit above/below its normal position.
 *
 * Note: If "position:static", the left property has no effect.
 * Default value:  auto
 * Inherited:      no
 * Animatable:     yes
 * Version:        CSS2
 * JavaScript syntax:      object.style.left="100px"
 *

 *
 * @author WFF
 * @since 1.0.0
 */
public class Left extends AbstractCssProperty<Left> {

    private static final long serialVersionUID = 1_0_0L;

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

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

    private String cssValue;
    private Float value;
    private CssLengthUnit cssLengthUnit;

    /**
     * The {@code auto} will be set as the value
     */
    public Left() {
        setCssValue(AUTO);
    }

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

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

    /**
     * @param percent
     *                    the percentage value to set. The cssLengthUnit will
     *                    automatically set to %.
     * @since 1.0.0
     * @author WFF
     */
    public Left(final float percent) {
        cssLengthUnit = CssLengthUnit.PER;
        value = percent;
        cssValue = String.valueOf(percent) + cssLengthUnit;
    }

    /**
     * @param value
     * @param cssLengthUnit
     */
    public Left(final float value, final CssLengthUnit cssLengthUnit) {
        this.value = value;
        this.cssLengthUnit = cssLengthUnit;
        cssValue = String.valueOf(value) + cssLengthUnit;
    }

    /**
     * @param value
     * @param cssLengthUnit
     * @return the current object
     * @since 1.0.0
     * @author WFF
     */
    public Left setValue(final float value, final CssLengthUnit cssLengthUnit) {
        this.value = value;
        this.cssLengthUnit = cssLengthUnit;
        cssValue = String.valueOf(value) + cssLengthUnit;
        if (getStateChangeInformer() != null) {
            getStateChangeInformer().stateChanged(this);
        }
        return this;
    }

    /**
     * @param percent
     *                    the percent to set
     * @since 1.0.0
     * @author WFF
     */
    public void setPercent(final float percent) {
        value = percent;
        cssLengthUnit = CssLengthUnit.PER;
        cssValue = String.valueOf(percent) + cssLengthUnit;
        if (getStateChangeInformer() != null) {
            getStateChangeInformer().stateChanged(this);
        }
    }

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

    /*
     * (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();
    }

    /**
     * gets the width in float value. {@code Left#getUnit()} should be used to
     * get the cssLengthUnit for this value.
     *
     * @return the value in float or null if the cssValue is
     *         initial or inherit.
     * @since 1.0.0
     * @author WFF
     */
    public Float getValue() {
        return value;
    }

    /**
     * @return the cssLengthUnit {@code PX}/{@code PER}, or {@code null} if the
     *         value is any inbuilt value like {@code inherit}.
     * @since 1.0.0
     * @author WFF
     */
    public CssLengthUnit getUnit() {
        return cssLengthUnit;
    }

    /**
     * @param cssValue
     *                     the value should be in the format of
     *                     55px or 95%. {@code null}
     *                     is considered as an invalid value and it will throw
     *                     {@code NullValueException}.
     * @since 1.0.0
     * @author WFF
     */
    @Override
    public Left setCssValue(final String cssValue) {
        final String previousCssValue = this.cssValue;
        try {
            if (cssValue == null) {
                throw new NullValueException(
                        "null is an invalid value. The value format should be as for example 75px or 85%. Or, initial/inherit.");
            } else {
                final String trimmedCssValue = TagStringUtil
                        .toLowerCase(StringUtil.strip(cssValue));
                boolean invalidValue = true;
                for (final CssLengthUnit cssLengthUnit : CssLengthUnit
                        .values()) {
                    final String unit = cssLengthUnit.getUnit();
                    if (trimmedCssValue.endsWith(unit)) {
                        final String valueOnly = trimmedCssValue
                                .replaceFirst(unit, "");
                        try {
                            value = Float.parseFloat(valueOnly);
                            // it could be -ve when it comes as sub
                            // if (value < 0) {
                            // throw new InvalidValueException(
                            // "left cannot be a negative value");
                            // }
                        } catch (final NumberFormatException e) {
                            break;
                        }
                        this.cssLengthUnit = cssLengthUnit;
                        this.cssValue = cssValue;
                        invalidValue = false;
                        break;
                    }
                }
                if (PREDEFINED_CONSTANTS.contains(trimmedCssValue)) {
                    this.cssValue = trimmedCssValue;
                    cssLengthUnit = null;
                    value = null;
                    invalidValue = false;
                }
                if (invalidValue) {
                    throw new InvalidValueException(cssValue
                            + " is an invalid value. The value format should be as for example 75px, 85%, initial, inherit etc..");
                }
            }
            if (getStateChangeInformer() != null) {
                getStateChangeInformer().stateChanged(this);
            }
        } catch (final NumberFormatException e) {
            this.cssValue = previousCssValue;
            throw new InvalidValueException(cssValue
                    + " is an invalid value. The value format should be as for example 75px or 85%. Or, initial/inherit.",
                    e);
        } catch (final InvalidValueException e) {
            this.cssValue = previousCssValue;
            throw e;
        }
        return this;

    }

    /**
     * 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);
    }

    /**
     * sets as {@code auto}
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsAuto() {
        setCssValue(AUTO);
    }

    /**
     * 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));

        if (StringUtil.containsSpace(trimmedCssValue)) {
            return false;
        }

        for (final CssLengthUnit cssLengthUnit : CssLengthUnit.values()) {
            final String unit = cssLengthUnit.getUnit();
            if (trimmedCssValue.endsWith(unit)) {
                final String valueOnly = trimmedCssValue.replaceFirst(unit, "");
                try {
                    // it could be negative
                    Float.parseFloat(valueOnly);
                } catch (final NumberFormatException e) {
                    break;
                }
                return true;
            }
        }

        return PREDEFINED_CONSTANTS.contains(trimmedCssValue);
    }
}


/*
 * 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.attribute.core;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.WeakHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;

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.listener.PushQueue;
import com.webfirmframework.wffweb.tag.html.model.AbstractHtml5SharedObject;
import com.webfirmframework.wffweb.util.StringBuilderUtil;
import com.webfirmframework.wffweb.util.WffBinaryMessageUtil;

public abstract class AbstractAttribute extends AbstractTagBase {

    private static final long serialVersionUID = 1_1_1L;

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

    private static final Security ACCESS_OBJECT;

    private String attributeName;

    private volatile String attributeValue;

    private volatile Map<String, String> attributeValueMap;

    private volatile Set<String> attributeValueSet;

    private transient final Set<AbstractHtml> ownerTags;

    private final StringBuilder tagBuilder;

    // private AttributeValueChangeListener valueChangeListener;

    private volatile Set<AttributeValueChangeListener> valueChangeListeners;

    private transient Charset charset = Charset.defaultCharset();

    private final boolean nullableAttrValueMapValue;

    // for security purpose, the class name should not be modified
    private static final class Security implements Serializable {

        private static final long serialVersionUID = 1L;

        private Security() {
        }
    }

    static {
        ACCESS_OBJECT = new Security();
    }

    {
        tagBuilder = new StringBuilder();
        // almost all attributes may have only one owner tag
        // in a practical scenario so passed 2 here because the load factor is
        // 0.75f
        ownerTags = Collections
                .newSetFromMap(new WeakHashMap<AbstractHtml, Boolean>(2));

        setRebuild(true);
    }

    public AbstractAttribute() {
        nullableAttrValueMapValue = false;
    }

    /**
     * this constructor is only for the attribute whose value is an attribute
     * value map, eg: style
     *
     * @param nullableAttrValueMapValue
     *                                      only
     */
    protected AbstractAttribute(final boolean nullableAttrValueMapValue) {
        this.nullableAttrValueMapValue = nullableAttrValueMapValue;
    }

    /**
     * @return {@code String} equalent to the html string of the tag including
     *         the child tags.
     * @since 1.0.0
     * @author WFF
     */
    protected String getPrintStructure() {
        String printStructure = null;
        if (isRebuild() || isModified()) {
            printStructure = getPrintStructure(isRebuild());
            setRebuild(false);
        } else {
            printStructure = tagBuilder.toString();
        }
        return printStructure;
    }

    protected String getPrintStructure(final boolean rebuild) {
        final String attributeValue = this.attributeValue;
        String result = "";
        if (rebuild || isRebuild() || isModified()) {
            beforePrintStructure();

            if (tagBuilder.length() > 0) {
                tagBuilder.delete(0, tagBuilder.length());
            }
            // tagBuildzer.append(' ');
            tagBuilder.append(attributeName);
            if (attributeValue != null) {
                tagBuilder.append(new char[] { '=', '"' })
                        .append(attributeValue);
                result = StringBuilderUtil.getTrimmedString(tagBuilder) + '"';
                tagBuilder.append('"');
            } else if (attributeValueMap != null
                    && attributeValueMap.size() > 0) {
                tagBuilder.append(new char[] { '=', '"' });
                final Set<Entry<String, String>> entrySet = getAttributeValueMap()
                        .entrySet();
                for (final Entry<String, String> entry : entrySet) {
                    tagBuilder.append(entry.getKey()).append(':')
                            .append(entry.getValue()).append(';');
                }

                result = StringBuilderUtil.getTrimmedString(tagBuilder) + '"';
                tagBuilder.append('"');
            } else if (attributeValueSet != null
                    && attributeValueSet.size() > 0) {
                tagBuilder.append(new char[] { '=', '"' });
                for (final String each : getAttributeValueSet()) {
                    tagBuilder.append(each).append(' ');
                }
                result = StringBuilderUtil.getTrimmedString(tagBuilder) + '"';
                tagBuilder.append('"');
            } else {
                result = tagBuilder.toString();
            }
            /*
             * int lastIndex = tagBuilder.length() - 1;
             *
             * should be analyzed for optimization (as the deleteCharAt method
             * might compromise performance).
             *
             * if (Character.isWhitespace(tagBuilder.charAt(lastIndex))) {
             * tagBuilder.deleteCharAt(lastIndex); }
             */
            // result = StringBuilderUtil.getTrimmedString(tagBuilder) + "\"";
            // tagBuilder.append("\"");
            setRebuild(false);
            tagBuilder.trimToSize();
        }

        return result;
    }

    /**
     * gets the attribute name and value in the format of name=value.

     * Eg: style=color:green;background:blue

     * This reduces 2 bytes taken for ".
     *
     * @return the attribute name and value in the format of name=value. Eg:
     *         style=color:green;background:blue
     * @since 2.0.0
     * @author WFF
     */
    protected String getWffPrintStructure() {
        final String attributeValue = this.attributeValue;
        String result = "";
        beforeWffPrintStructure();
        final StringBuilder attrBuilder = new StringBuilder();

        attrBuilder.append(attributeName);
        if (attributeValue != null) {
            attrBuilder.append(new char[] { '=' }).append(attributeValue);
            result = StringBuilderUtil.getTrimmedString(attrBuilder);
        } else if (attributeValueMap != null && attributeValueMap.size() > 0) {
            attrBuilder.append(new char[] { '=' });
            final Set<Entry<String, String>> entrySet = getAttributeValueMap()
                    .entrySet();
            for (final Entry<String, String> entry : entrySet) {
                attrBuilder.append(entry.getKey()).append(':')
                        .append(entry.getValue()).append(';');
            }

            result = StringBuilderUtil.getTrimmedString(attrBuilder);
        } else if (attributeValueSet != null && attributeValueSet.size() > 0) {
            attrBuilder.append(new char[] { '=' });
            for (final String each : getAttributeValueSet()) {
                attrBuilder.append(each).append(' ');
            }
            result = StringBuilderUtil.getTrimmedString(attrBuilder);
        } else {
            result = attrBuilder.toString();
        }

        return result;
    }

    /**
     * gets the attribute name and value in the format of name=value.

     * Eg: style=color:green;background:blue

     * This reduces 2 bytes taken for ".
     *
     * @return the attribute name and value in the format of name=value. Eg:
     *         style=color:green;background:blue;
     * @since 2.0.0
     * @author WFF
     */
    public String toWffString() {
        return getWffPrintStructure();
    }

    protected void beforeWffPrintStructure() {
        // NOT override and use if required
    }

    /**
     * gives compressed by index bytes for the attribute and value. The first
     * byte represents the attribute name index bytes length, the next bytes
     * represent the attribute name index bytes and the remaining bytes
     * represent attribute value without = and ".
     *
     * @param rebuild
     * @return the compressed by index bytes.
     * @throws IOException
     * @since 1.1.3
     * @author WFF
     */
    protected byte[] getBinaryStructureCompressedByIndex(final boolean rebuild)
            throws IOException {

        final String attributeValue = this.attributeValue;

        final Charset charset = this.charset;
        // TODO review code

        final ByteArrayOutputStream compressedByIndexBytes = new ByteArrayOutputStream();

        byte[] compressedBytes = new byte[0];

        // String result = "";
        if (rebuild || isRebuild() || isModified()) {

            beforePrintStructureCompressedByIndex();
            // tagBuildzer.append(' ');

            final int attributeNameIndex = AttributeRegistry.getAttributeNames()
                    .indexOf(attributeName);

            if (attributeNameIndex == -1) {

                compressedByIndexBytes.write(new byte[] { (byte) 0 });

                compressedByIndexBytes
                        .write(attributeName.concat("=").getBytes(charset));

                if (LOGGER.isLoggable(Level.WARNING)) {
                    LOGGER.warning(attributeName
                            + " is not indexed, please register it with AttributeRegistrar");
                }
            } else {

                final byte[] optimizedBytesFromInt = WffBinaryMessageUtil
                        .getOptimizedBytesFromInt(attributeNameIndex);
                compressedByIndexBytes.write(
                        new byte[] { (byte) optimizedBytesFromInt.length });
                compressedByIndexBytes.write(optimizedBytesFromInt);
            }

            if (attributeValue != null) {

                compressedByIndexBytes.write(attributeValue.getBytes(charset));
                compressedBytes = compressedByIndexBytes.toByteArray();
            } else if (attributeValueMap != null
                    && attributeValueMap.size() > 0) {
                final Set<Entry<String, String>> entrySet = getAttributeValueMap()
                        .entrySet();
                for (final Entry<String, String> entry : entrySet) {

                    compressedByIndexBytes
                            .write(entry.getKey().getBytes(charset));
                    compressedByIndexBytes.write(new byte[] { ':' });
                    compressedByIndexBytes
                            .write(entry.getValue().getBytes(charset));
                    compressedByIndexBytes.write(new byte[] { ';' });
                }
                compressedBytes = compressedByIndexBytes.toByteArray();
            } else if (attributeValueSet != null
                    && attributeValueSet.size() > 0) {
                for (final String each : getAttributeValueSet()) {

                    compressedByIndexBytes.write(each.getBytes(charset));
                    compressedByIndexBytes.write(new byte[] { ' ' });
                }
                compressedBytes = compressedByIndexBytes.toByteArray();
            } else {
                compressedBytes = compressedByIndexBytes.toByteArray();
            }

            setRebuild(false);
        }
        return compressedBytes;
    }

    /**
     * @return the attributeName set by
     *         {@code AbstractAttribute#setAttributeName(String)}
     * @since 1.0.0
     * @author WFF
     */
    public String getAttributeName() {
        return attributeName;
    }

    /**
     * Set attribute name, eg: width, height, name, type etc..
     *
     * @param attributeName
     *                          the attributeName to set
     * @since 1.0.0
     * @author WFF
     */
    protected void setAttributeName(final String attributeName) {
        this.attributeName = attributeName;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.webfirmframework.wffweb.tag.Base#toHtmlString()
     *
     * @since 1.0.0
     *
     * @author WFF
     */
    @Override
    public String toHtmlString() {
        return getPrintStructure(true);
    }

    public byte[] toCompressedBytesByIndex(final boolean rebuild)
            throws IOException {
        return getBinaryStructureCompressedByIndex(rebuild);
    }

    public byte[] toCompressedBytesByIndex(final boolean rebuild,
            final Charset charset) throws IOException {
        final Charset previousCharset = this.charset;
        try {
            this.charset = charset;
            return toCompressedBytesByIndex(rebuild);
        } finally {
            this.charset = previousCharset;
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see com.webfirmframework.wffweb.tag.core.TagBase#toHtmlString(java.nio.
     * charset.Charset)
     */
    @Override
    public String toHtmlString(final Charset charset) {
        final Charset previousCharset = this.charset;
        try {
            this.charset = charset;
            return toHtmlString();
        } finally {
            this.charset = previousCharset;
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see com.webfirmframework.wffweb.tag.core.TagBase#toHtmlString(java.lang.
     * String)
     */
    @Override
    public String toHtmlString(final String charset) {
        final Charset previousCharset = this.charset;
        try {
            this.charset = Charset.forName(charset);
            return toHtmlString();
        } finally {
            this.charset = previousCharset;
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see com.webfirmframework.wffweb.tag.Base#toHtmlString(boolean)
     *
     * @since 1.0.0
     *
     * @author WFF
     */
    @Override
    public String toHtmlString(final boolean rebuild) {
        return getPrintStructure(rebuild);
    }

    /*
     * (non-Javadoc)
     *
     * @see com.webfirmframework.wffweb.tag.core.TagBase#toHtmlString(boolean,
     * java.nio.charset.Charset)
     */
    @Override
    public String toHtmlString(final boolean rebuild, final Charset charset) {
        final Charset previousCharset = this.charset;
        try {
            this.charset = charset;
            return toHtmlString(rebuild);
        } finally {
            this.charset = previousCharset;
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see com.webfirmframework.wffweb.tag.core.TagBase#toHtmlString(boolean,
     * java.lang.String)
     */
    @Override
    public String toHtmlString(final boolean rebuild, final String charset) {
        final Charset previousCharset = this.charset;
        try {
            this.charset = Charset.forName(charset);
            return toHtmlString(rebuild);
        } finally {
            this.charset = previousCharset;
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return getPrintStructure();
    }

    /**
     * @return the attributeValueMap
     * @since 1.0.0
     * @author WFF
     */
    protected Map<String, String> getAttributeValueMap() {
        if (attributeValueMap == null) {
            synchronized (this) {
                if (attributeValueMap == null) {
                    setAttributeValueMap(new LinkedHashMap<String, String>());
                }
            }
        }
        return attributeValueMap;
    }

    /**
     * @param attributeValueMap
     *                              the attributeValueMap to set
     * @since 1.0.0
     * @author WFF
     */
    protected void setAttributeValueMap(
            final Map<String, String> attributeValueMap) {

        if (!Objects.equals(attributeValueMap, this.attributeValueMap)) {
            this.attributeValueMap = attributeValueMap;
            setModified(true);
        }

    }

    /**
     * adds the given key value.
     *
     * @param key
     * @param value
     * @return true if it is modified
     * @since 1.0.0
     * @author WFF
     */
    protected boolean addToAttributeValueMap(final String key,
            final String value) {

        final Collection<AbstractHtml5SharedObject> sharedObjects = getSharedObjects();
        boolean listenerInvoked = false;
        final Collection<WriteLock> writeLocks = lockAndGetWriteLocks();
        try {
            final Map<String, String> attributeValueMap = getAttributeValueMap();

            final String previousValue = attributeValueMap.put(key, value);

            if (!Objects.equals(previousValue, value)) {
                setModified(true);
                invokeValueChangeListeners(sharedObjects);
                listenerInvoked = true;
            }

        } finally {
            for (final Lock lock : writeLocks) {
                lock.unlock();
            }
        }

        pushQueues(sharedObjects, listenerInvoked);

        return listenerInvoked;
    }

    /**
     * this method should be called after changing of attribute value not before
     * changing value
     */
    private void invokeValueChangeListeners(
            final Collection<AbstractHtml5SharedObject> sharedObjects) {

        for (final AbstractHtml5SharedObject sharedObject : sharedObjects) {
            final AttributeValueChangeListener valueChangeListener = sharedObject
                    .getValueChangeListener(ACCESS_OBJECT);
            if (valueChangeListener != null) {

                // ownerTags should not be modified in the consuming
                // part, here
                // skipped it making unmodifiable to gain
                // performance
                final AttributeValueChangeListener.Event event = new AttributeValueChangeListener.Event(
                        this, ownerTags);
                valueChangeListener.valueChanged(event);
            }
        }

        if (valueChangeListeners != null) {
            for (final AttributeValueChangeListener listener : valueChangeListeners) {
                final AttributeValueChangeListener.Event event = new AttributeValueChangeListener.Event(
                        this, Collections.unmodifiableSet(ownerTags));
                listener.valueChanged(event);
            }
        }

    }

    private Collection<AbstractHtml5SharedObject> getSharedObjects() {

        // internally ownerTags.size() (WeakHashMap) contains synchronization so
        // better avoid calling it
        // normally there will be one sharedObject so the capacity may be
        // considered as 1
        final Collection<AbstractHtml5SharedObject> sharedObjects = new HashSet<>(
                1);

        for (final AbstractHtml ownerTag : ownerTags) {
            sharedObjects.add(ownerTag.getSharedObject());
        }
        return sharedObjects;
    }

    /**
     * adds all to the attribute value map.
     *
     * @param map
     * @since 1.0.0
     * @author WFF
     * @return true if it is modified
     */
    protected boolean addAllToAttributeValueMap(final Map<String, String> map) {

        if (map != null && map.size() > 0) {

            final Collection<AbstractHtml5SharedObject> sharedObjects = getSharedObjects();
            boolean listenerInvoked = false;

            final Collection<WriteLock> writeLocks = lockAndGetWriteLocks();
            try {

                getAttributeValueMap().putAll(map);

                setModified(true);

                invokeValueChangeListeners(sharedObjects);
                listenerInvoked = true;

            } finally {
                for (final Lock lock : writeLocks) {
                    lock.unlock();
                }
            }
            pushQueues(sharedObjects, listenerInvoked);
            return listenerInvoked;

        }
        return false;
    }

    /**
     * removes the key value for the input key.
     *
     * @param key
     * @since 1.0.0
     * @author WFF
     * @return true if the given key (as well as value contained corresponding
     *         to it) has been removed.
     */
    protected boolean removeFromAttributeValueMap(final String key) {
        return removeFromAttributeValueMapByKeys(key);
    }

    /**
     * removes the key value for the input key.
     *
     * @param keys
     * @since 3.0.1
     * @author WFF
     * @return true if any of the given keys (as well as value contained
     *         corresponding to it) has been removed.
     */
    protected boolean removeFromAttributeValueMapByKeys(final String... keys) {

        final Collection<AbstractHtml5SharedObject> sharedObjects = getSharedObjects();
        boolean listenerInvoked = false;
        final Collection<WriteLock> writeLocks = lockAndGetWriteLocks();
        boolean result = false;
        try {

            final Map<String, String> valueMap = getAttributeValueMap();

            if (nullableAttrValueMapValue) {
                for (final String key : keys) {
                    result = valueMap.containsKey(key);
                    if (result) {
                        break;
                    }
                }
            }

            for (final String key : keys) {
                final String previous = valueMap.remove(key);
                if (previous != null) {
                    result = true;
                }
            }

            if (result) {
                setModified(true);
                invokeValueChangeListeners(sharedObjects);
                listenerInvoked = true;
            }

        } finally {
            for (final Lock lock : writeLocks) {
                lock.unlock();
            }
        }
        pushQueues(sharedObjects, listenerInvoked);
        return result;
    }

    /**
     * pushes PushQueue from all shared object of parent tags.
     *
     * @param sharedObjects
     * @param listenerInvoked
     */
    private void pushQueues(
            final Collection<AbstractHtml5SharedObject> sharedObjects,
            final boolean listenerInvoked) {
        if (listenerInvoked) {
            for (final AbstractHtml5SharedObject sharedObject : sharedObjects) {
                final PushQueue pushQueue = sharedObject
                        .getPushQueue(ACCESS_OBJECT);
                if (pushQueue != null) {
                    pushQueue.push();
                }
            }
        }
    }

    /**
     * removes only if the key and value matches in the map for any particular
     * entry.
     *
     * @param key
     * @param value
     * @since 1.0.0
     * @author WFF
     * @return true if it is modified
     */
    protected boolean removeFromAttributeValueMap(final String key,
            final String value) {

        final Collection<AbstractHtml5SharedObject> sharedObjects = getSharedObjects();

        final Collection<WriteLock> writeLocks = lockAndGetWriteLocks();
        boolean listenerInvoked = false;
        try {

            final boolean removed = getAttributeValueMap().remove(key, value);

            if (removed) {
                setModified(true);
                invokeValueChangeListeners(sharedObjects);
                listenerInvoked = true;
            }

        } finally {
            for (final Lock lock : writeLocks) {
                lock.unlock();
            }
        }
        pushQueues(sharedObjects, listenerInvoked);
        return listenerInvoked;
    }

    protected void removeAllFromAttributeValueMap() {
        if (attributeValueMap != null && getAttributeValueMap().size() > 0) {
            final Collection<AbstractHtml5SharedObject> sharedObjects = getSharedObjects();
            boolean listenerInvoked = false;
            final Collection<WriteLock> writeLocks = lockAndGetWriteLocks();
            try {
                getAttributeValueMap().clear();
                setModified(true);

                invokeValueChangeListeners(sharedObjects);
                listenerInvoked = true;
            } finally {
                for (final Lock lock : writeLocks) {
                    lock.unlock();
                }
            }
            pushQueues(sharedObjects, listenerInvoked);
        }
    }

    /**
     * @return the attributeValue
     * @since 1.0.0
     * @author WFF
     */
    public String getAttributeValue() {
        // NB:- Do not call this method for internal operations
        // use attributeValue variable instead of it
        // because it may be overridden by tags, for eg: ClassAttribute
        return attributeValue;
    }

    /**
     * @param attributeValue
     *                           the attributeValue to set
     * @since 1.0.0
     * @author WFF
     */
    protected void setAttributeValue(final String attributeValue) {
        if (!Objects.equals(this.attributeValue, attributeValue)) {
            final Collection<AbstractHtml5SharedObject> sharedObjects = getSharedObjects();
            boolean listenerInvoked = false;
            final Collection<WriteLock> writeLocks = lockAndGetWriteLocks();

            try {

                // this.attributeValue = attributeValue must be
                // before invokeValueChangeListeners
                this.attributeValue = attributeValue;
                setModified(true);
                invokeValueChangeListeners(sharedObjects);
                listenerInvoked = true;
            } finally {
                for (final Lock lock : writeLocks) {
                    lock.unlock();
                }
            }

            pushQueues(sharedObjects, listenerInvoked);
        }
    }

    /**
     * @param updateClient
     *                           true to update client browser page if it is
     *                           available. The default value is true but it
     *                           will be ignored if there is no client browser
     *                           page.
     * @param attributeValue
     *                           the attributeValue to set
     * @since 2.1.15
     * @author WFF
     */
    protected void setAttributeValue(final boolean updateClient,
            final String attributeValue) {

        if (!Objects.equals(this.attributeValue, attributeValue)) {
            final Collection<AbstractHtml5SharedObject> sharedObjects = getSharedObjects();
            boolean listenerInvoked = false;
            final Collection<WriteLock> writeLocks = lockAndGetWriteLocks();

            try {

                // this.attributeValue = attributeValue must be
                // before invokeValueChangeListeners
                this.attributeValue = attributeValue;
                setModified(true);
                if (updateClient) {
                    invokeValueChangeListeners(sharedObjects);
                    listenerInvoked = true;
                }
            } finally {
                for (final Lock lock : writeLocks) {
                    lock.unlock();
                }
            }
            pushQueues(sharedObjects, listenerInvoked);
        }
    }

    /**
     *
     * @return one of the ownerTags
     * @since 1.0.0
     * @author WFF
     * @deprecated this method may be removed later as there could be multiple
     *             owner tags.
     */
    @Deprecated
    public AbstractHtml getOwnerTag() {
        if (ownerTags.iterator().hasNext()) {
            return ownerTags.iterator().next();
        }
        return null;
    }

    /**
     *
     * @return the tags which are consuming this attribute as an array. If there
     *         is no owner tag then it will return an empty array instead of
     *         null.
     * @since 2.0.0
     * @author WFF
     */
    public AbstractHtml[] getOwnerTags() {
        // returning the set is not good because
        // if the AbstractHtml needs to be
        // modified while iterating the set will cause
        // ConcurrentModificationException
        return ownerTags.toArray(new AbstractHtml[ownerTags.size()]);
    }

    /**
     * NB:- this method is used for internal purpose, so it should not be
     * consumed.
     *
     * @param ownerTag
     *                     the ownerTag to set
     * @since 1.0.0
     * @author WFF
     */
    public void setOwnerTag(final AbstractHtml ownerTag) {
        ownerTags.add(ownerTag);
    }

    /**
     * NB:- this method is used for internal purpose, so it should not be
     * consumed.
     *
     * @param ownerTag
     *                     the ownerTag to unset
     * @return true if the given ownerTag is an owner of the attribute.
     * @since 2.0.0
     * @author WFF
     */
    public boolean unsetOwnerTag(final AbstractHtml ownerTag) {
        return ownerTags.remove(ownerTag);
    }

    @Override
    public void setModified(final boolean modified) {
        super.setModified(modified);
        if (modified) {
            for (final AbstractHtml ownerTag : ownerTags) {
                ownerTag.setModified(modified);
                ownerTag.getSharedObject().setChildModified(modified);
            }
        }
    }

    /**
     * NB:- this is only for getting values. Use addToAttributeValueSet method
     * for adding
     *
     * @return the attributeValueSet
     * @since 1.0.0
     * @author WFF
     */
    protected Set<String> getAttributeValueSet() {
        if (attributeValueSet == null) {
            synchronized (this) {
                if (attributeValueSet == null) {
                    // because the load factor is 0.75f
                    attributeValueSet = new LinkedHashSet<>(2);
                }
            }
        }
        return attributeValueSet;
    }

    /**
     * @param attributeValueSet
     *                              the attributeValueSet to set
     * @since 1.0.0
     * @author WFF
     */
    protected void setAttributeValueSet(final Set<String> attributeValueSet) {
        if (!Objects.equals(this.attributeValueSet, attributeValueSet)) {
            this.attributeValueSet = attributeValueSet;
            setModified(true);
        }
    }

    /**
     * adds to the attribute value set.
     *
     * @param value
     * @since 1.0.0
     * @author WFF
     * @return
     */
    protected boolean addToAttributeValueSet(final String value) {
        final Collection<AbstractHtml5SharedObject> sharedObjects = getSharedObjects();
        boolean listenerInvoked = false;
        final Collection<WriteLock> writeLocks = lockAndGetWriteLocks();
        try {

            final boolean added = getAttributeValueSet().add(value);

            if (added) {
                setModified(true);

                invokeValueChangeListeners(sharedObjects);
                listenerInvoked = true;

            }

        } finally {
            for (final Lock lock : writeLocks) {
                lock.unlock();
            }
        }
        pushQueues(sharedObjects, listenerInvoked);
        return listenerInvoked;
    }

    /**
     * adds all to the attribute value set.
     *
     * @param values
     * @since 1.0.0
     * @author WFF
     */
    protected void addAllToAttributeValueSet(final Collection<String> values) {
        if (values != null) {
            final Collection<AbstractHtml5SharedObject> sharedObjects = getSharedObjects();
            boolean listenerInvoked = false;
            final Collection<WriteLock> writeLocks = lockAndGetWriteLocks();
            try {

                final boolean added = getAttributeValueSet().addAll(values);

                if (added) {
                    setModified(true);

                    invokeValueChangeListeners(sharedObjects);
                    listenerInvoked = true;
                }

            } finally {
                for (final Lock lock : writeLocks) {
                    lock.unlock();
                }
            }
            pushQueues(sharedObjects, listenerInvoked);
        }
    }

    /**
     * removes all and then adds all to the attribute value set.
     *
     * @param values
     * @since 3.0.1
     * @author WFF
     */
    protected void replaceAllInAttributeValueSet(
            final Collection<String> values) {
        replaceAllInAttributeValueSet(true, values);
    }

    /**
     * removes all and then adds all to the attribute value set.
     *
     * @param updateClient
     *                         true to update client browser page
     * @param values
     * @since 3.0.1
     * @author WFF
     */
    protected void replaceAllInAttributeValueSet(final boolean updateClient,
            final Collection<String> values) {
        if (values != null) {
            final Collection<AbstractHtml5SharedObject> sharedObjects = getSharedObjects();
            boolean listenerInvoked = false;
            final Collection<WriteLock> writeLocks = lockAndGetWriteLocks();
            try {

                final Set<String> attrValueSet = getAttributeValueSet();
                attrValueSet.clear();
                final boolean added = attrValueSet.addAll(values);

                if (added) {
                    setModified(true);

                    if (updateClient) {
                        invokeValueChangeListeners(sharedObjects);
                        listenerInvoked = true;
                    }
                }

            } finally {
                for (final Lock lock : writeLocks) {
                    lock.unlock();
                }
            }
            pushQueues(sharedObjects, listenerInvoked);
        }
    }

    /**
     * adds all to the attribute value set.
     *
     * @param updateClient
     *                         true to update client browser page if it is
     *                         available. The default value is true but it will
     *                         be ignored if there is no client browser page.
     * @param values
     * @since 2.1.15
     * @author WFF
     */
    protected void addAllToAttributeValueSet(final boolean updateClient,
            final Collection<String> values) {
        if (values != null) {
            final Collection<AbstractHtml5SharedObject> sharedObjects = getSharedObjects();
            boolean listenerInvoked = false;
            final Collection<WriteLock> writeLocks = lockAndGetWriteLocks();
            try {

                final boolean added = getAttributeValueSet().addAll(values);

                if (added) {
                    setModified(true);

                    if (updateClient) {
                        invokeValueChangeListeners(sharedObjects);
                        listenerInvoked = true;
                    }
                }
            } finally {
                for (final Lock lock : writeLocks) {
                    lock.unlock();
                }
            }
            pushQueues(sharedObjects, listenerInvoked);
        }
    }

    /**
     * removes the value from the the attribute set.
     *
     * @param value
     * @since 1.0.0
     * @author WFF
     */
    protected void removeFromAttributeValueSet(final String value) {
        final Collection<AbstractHtml5SharedObject> sharedObjects = getSharedObjects();
        boolean listenerInvoked = false;
        final Collection<WriteLock> writeLocks = lockAndGetWriteLocks();
        try {

            final boolean removed = getAttributeValueSet().remove(value);

            if (removed) {

                setModified(true);

                invokeValueChangeListeners(sharedObjects);
                listenerInvoked = true;
            }
        } finally {
            for (final Lock lock : writeLocks) {
                lock.unlock();
            }
        }
        pushQueues(sharedObjects, listenerInvoked);
    }

    /**
     * removes the value from the the attribute set.
     *
     * @param values
     * @since 1.0.0
     * @author WFF
     */
    protected void removeAllFromAttributeValueSet(
            final Collection<String> values) {
        final Collection<AbstractHtml5SharedObject> sharedObjects = getSharedObjects();
        boolean listenerInvoked = false;
        final Collection<WriteLock> writeLocks = lockAndGetWriteLocks();
        try {

            final boolean removedAll = getAttributeValueSet().removeAll(values);

            if (removedAll) {
                setModified(true);

                invokeValueChangeListeners(sharedObjects);
                listenerInvoked = true;
            }

        } finally {
            for (final Lock lock : writeLocks) {
                lock.unlock();
            }
        }
        pushQueues(sharedObjects, listenerInvoked);
    }

    /**
     * clears all values from the value set.
     *
     * @since 1.0.0
     * @author WFF
     */
    protected void removeAllFromAttributeValueSet() {

        final Collection<AbstractHtml5SharedObject> sharedObjects = getSharedObjects();
        boolean listenerInvoked = false;

        final Collection<WriteLock> writeLocks = lockAndGetWriteLocks();
        try {
            getAttributeValueSet().clear();
            setModified(true);

            invokeValueChangeListeners(sharedObjects);
            listenerInvoked = true;
        } finally {
            for (final Lock lock : writeLocks) {
                lock.unlock();
            }
        }
        pushQueues(sharedObjects, listenerInvoked);
    }

    /**
     * invokes just before {@code getPrintStructure(final boolean} method and
     * only if the getPrintStructure(final boolean} rebuilds the structure.
     *
     * @since 1.0.0
     * @author WFF
     */
    protected void beforePrintStructure() {
        // TODO override and use
    }

    /**
     * invokes just before
     * {@code getPrintStructureCompressedByIndex(final boolean} method and only
     * if the getPrintStructureCompressedByIndex(final boolean} rebuilds the
     * structure.
     *
     * @since 1.0.0
     * @author WFF
     */
    protected void beforePrintStructureCompressedByIndex() {
        // TODO override and use
    }

    /**
     * @return the charset
     */
    public Charset getCharset() {
        return charset;
    }

    /**
     * @param charset
     *                    the charset to set
     */
    public void setCharset(final Charset charset) {
        this.charset = charset;
    }

    /**
     * adds value change lister which will be invoked when the value changed
     *
     * @param valueChangeListener
     * @since 2.0.0
     * @author WFF
     */
    public void addValueChangeListener(
            final AttributeValueChangeListener valueChangeListener) {
        if (valueChangeListeners == null) {
            synchronized (this) {
                if (valueChangeListeners == null) {
                    valueChangeListeners = new LinkedHashSet<>();
                }
            }
        }

        valueChangeListeners.add(valueChangeListener);
    }

    /**
     * removes the corresponding value change listener
     *
     * @param valueChangeListener
     * @since 2.0.0
     * @author WFF
     */
    public void removeValueChangeListener(
            final AttributeValueChangeListener valueChangeListener) {
        if (valueChangeListeners != null) {
            valueChangeListeners.remove(valueChangeListener);
        }
    }

    /**
     * @return the set of value change listeners
     * @since 2.0.0
     * @author WFF
     */
    public Set<AttributeValueChangeListener> getValueChangeListeners() {
        return Collections.unmodifiableSet(valueChangeListeners);
    }

    /**
     * NB: this may not return the same locks as there could be the its
     * ownerTags change. Use it for only unlock.
     *
     * @return the set of write locks after locking
     */
    protected Collection<WriteLock> lockAndGetWriteLocks() {

        final Collection<WriteLock> writeLocks = new TreeSet<>((o1,
                o2) -> Integer.compare(o2.getHoldCount(), o1.getHoldCount()));

        for (final AbstractHtml ownerTag : ownerTags) {
            final WriteLock writeLock = ownerTag.getSharedObject()
                    .getLock(ACCESS_OBJECT).writeLock();
            if (writeLock != null) {
                try {
                    writeLocks.add(writeLock);
                } catch (final Exception e) {
                    e.printStackTrace();
                }
            }
        }
        // must be separately locked
        for (final WriteLock writeLock : writeLocks) {
            writeLock.lock();
        }

        return writeLocks;
    }

    /**
     * NB: this may not return the same locks as there could be the its
     * ownerTags change. Use it for only unlock.
     *
     * @return the set of read locks after locking
     */
    protected Collection<ReadLock> lockAndGetReadLocks() {

        // internally ownerTags.size() (WeakHashMap) contains synchronization
        // better avoid calling it
        // normally there will be one sharedObject so the capacity may be
        // considered as 2 because the load factor is 0.75f
        final Collection<ReadLock> readLocks = new HashSet<>(2);

        for (final AbstractHtml ownerTag : ownerTags) {
            final ReadLock readLock = ownerTag.getSharedObject()
                    .getLock(ACCESS_OBJECT).readLock();
            if (readLock != null) {
                readLocks.add(readLock);
            }
        }

        // must be separately locked
        for (final ReadLock readLock : readLocks) {
            try {
                readLock.lock();
                readLocks.add(readLock);
            } catch (final Exception e) {
                e.printStackTrace();
            }
        }

        return readLocks;
    }

    /**
     * NB: this may not return the same locks as there could be the its
     * ownerTags change. So call only once and reuse it for both lock and unlock
     * call.
     *
     * @return the set of write locks
     */
    protected Collection<WriteLock> getWriteLocks() {

        final Collection<WriteLock> writeLocks = new TreeSet<>((o1,
                o2) -> Integer.compare(o2.getHoldCount(), o1.getHoldCount()));

        for (final AbstractHtml ownerTag : ownerTags) {
            final WriteLock writeLock = ownerTag.getSharedObject()
                    .getLock(ACCESS_OBJECT).writeLock();
            if (writeLock != null) {
                try {
                    writeLocks.add(writeLock);
                } catch (final Exception e) {
                    e.printStackTrace();
                }
            }
        }

        return writeLocks;
    }

    /**
     * NB: this may not return the same locks as there could be the its
     * ownerTags change. So call only once and reuse it for both lock and unlock
     * call.
     *
     * @return the set of read locks
     */
    protected Collection<ReadLock> getReadLocks() {

        // internally ownerTags.size() (WeakHashMap) contains synchronization
        // better avoid calling it
        // normally there will be one sharedObject so the capacity may be
        // considered as 2 because the load factor is 0.75f
        final Collection<ReadLock> readLocks = new HashSet<>(2);

        for (final AbstractHtml ownerTag : ownerTags) {
            final ReadLock readLock = ownerTag.getSharedObject()
                    .getLock(ACCESS_OBJECT).readLock();
            if (readLock != null) {
                readLocks.add(readLock);
            }
        }

        return readLocks;
    }

}


/*
 * 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.wffbm.data;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

import com.webfirmframework.wffweb.InvalidValueException;
import com.webfirmframework.wffweb.WffRuntimeException;
import com.webfirmframework.wffweb.util.WffBinaryMessageUtil;
import com.webfirmframework.wffweb.util.data.NameValue;

/**
 * The java object representation for JavaScript object.

 * Sample code :-

 *
 *

 * WffBMObject bmObject = new WffBMObject();
 * bmObject.put("serverKey", BMValueType.STRING, "value from server");
 * bmObject.put("string", BMValueType.STRING, "sample string");
 * bmObject.put("nul", BMValueType.NULL, null);
 * bmObject.put("number", BMValueType.NUMBER, 555);
 * bmObject.put("undef", BMValueType.UNDEFINED, null);
 * bmObject.put("reg", BMValueType.REG_EXP, "[w]");
 * bmObject.put("bool", BMValueType.BOOLEAN, true);
 * bmObject.put("testFun", BMValueType.FUNCTION, "function(arg) {alert(arg);}");
 *

 *
 * The {@code WffBMObject} can also hold array and binary data (as byte array).
 * Check out {@code WffBMArray} and {@code WffBMByteArray} respectively.
 *
 * @author WFF
 * @see WffBMArray
 * @see WffBMByteArray
 *
 */
public class WffBMObject extends LinkedHashMap<String, ValueValueType>
        implements WffBMData {

    private static final long serialVersionUID = 1L;

    private boolean outer;

    private byte[] bMBytes;

    public WffBMObject() {
    }

    public WffBMObject(final boolean outer) {
        this.outer = outer;
    }

    public WffBMObject(final byte[] bMBytes) {
        try {
            initWffBMObject(bMBytes, true);
            this.bMBytes = bMBytes;
        } catch (final UnsupportedEncodingException e) {
            throw new WffRuntimeException("Could not create wff bm object", e);
        }
    }

    public WffBMObject(final byte[] bMBytes, final boolean outer) {
        try {
            initWffBMObject(bMBytes, outer);
            this.bMBytes = bMBytes;
        } catch (final UnsupportedEncodingException e) {
            throw new WffRuntimeException("Could not create wff bm object", e);
        }
    }

    @Override
    public ValueValueType put(final String key, final ValueValueType value) {
        // should be == here
        // as the cloned object returns true
        // with its equals method
        if (this == value.getValue()) {
            throw new InvalidValueException(
                    "The same instance cannot be passed as value in ValueValueType");
        }
        return super.put(key, value);
    }

    public void put(final String key, final BMValueType valueType,
            final Object value) {
        // should be == here
        // as the cloned object returns true
        // with its equals method
        if (this == value) {
            throw new InvalidValueException(
                    "The same instance cannot be passed as value");
        }
        super.put(key, new ValueValueType(key, valueType.getType(), value));
    }

    /**
     * @return
     * @throws UnsupportedEncodingException
     *                                          throwing this exception will be
     *                                          removed in future version
     *                                          because its internal
     *                                          implementation will never make
     *                                          this exception due to the code
     *                                          changes since 3.0.1.
     * @since 1.1.5
     * @author WFF
     */
    public byte[] build() throws UnsupportedEncodingException {
        return buildBytes(outer);
    }

    /**
     * NB: use buildBytes instead of this method.
     *
     * @param outer
     * @return the bytes representation of the object
     * @throws UnsupportedEncodingException
     *                                          throwing this exception will be
     *                                          removed in future version
     *                                          because its internal
     *                                          implementation will never make
     *                                          this exception due to the code
     *                                          changes since 3.0.1.
     * @since 1.1.5
     * @deprecated building bytes implementation doesn't throw
     *             UnsupportedEncodingException so deprecated this method. The
     *             same goal can be achieved using buildBytes method.
     */
    @Deprecated
    @Override
    public byte[] build(final boolean outer)
            throws UnsupportedEncodingException {
        return buildBytes(outer);
    }

    /**
     * @param outer
     * @return bytes for this WffBMObject
     *
     * @since 3.0.2
     * @author WFF
     */
    @Override
    public byte[] buildBytes(final boolean outer) {

        final Set<Entry<String, ValueValueType>> superEntrySet = super.entrySet();

        final int capacity = outer ? superEntrySet.size() + 1
                : superEntrySet.size();

        final Deque<NameValue> nameValues = new ArrayDeque<>(capacity);

        if (outer) {
            final NameValue typeNameValue = new NameValue();
            typeNameValue.setName(BMType.OBJECT.getType());
            nameValues.add(typeNameValue);
        }

        for (final Entry<String, ValueValueType> entry : superEntrySet) {
            final String key = entry.getKey();
            final ValueValueType valueValueType = entry.getValue();
            final byte valueType = valueValueType.getValueTypeByte();

            final NameValue nameValue = new NameValue();
            nameValue.setName(key.getBytes(StandardCharsets.UTF_8));

            nameValues.add(nameValue);

            if (valueType == BMValueType.STRING.getType()) {

                final String value = (String) valueValueType.getValue();

                nameValue.setValues(new byte[] { valueType },
                        value.getBytes(StandardCharsets.UTF_8));

            } else if (valueType == BMValueType.NUMBER.getType()) {

                final Number value = (Number) valueValueType.getValue();
                final byte[] valueBytes = WffBinaryMessageUtil
                        .getOptimizedBytesFromDouble(value.doubleValue());
                nameValue.setValues(new byte[] { valueType }, valueBytes);

            } else if (valueType == BMValueType.UNDEFINED.getType()) {

                final byte[] valueBytes = {};
                nameValue.setValues(new byte[] { valueType }, valueBytes);

            } else if (valueType == BMValueType.NULL.getType()) {
                final byte[] valueBytes = {};
                nameValue.setValues(new byte[] { valueType }, valueBytes);
            } else if (valueType == BMValueType.BOOLEAN.getType()) {

                final Boolean value = (Boolean) valueValueType.getValue();
                final byte[] valueBytes = {
                        (byte) (value.booleanValue() ? 1 : 0) };
                nameValue.setValues(new byte[] { valueType }, valueBytes);
            } else if (valueType == BMValueType.BM_OBJECT.getType()) {

                final WffBMObject value = (WffBMObject) valueValueType
                        .getValue();
                final byte[] valueBytes = value.buildBytes(false);
                nameValue.setValues(new byte[] { valueType }, valueBytes);

            } else if (valueType == BMValueType.BM_ARRAY.getType()) {

                final WffBMArray value = (WffBMArray) valueValueType.getValue();
                final byte[] valueBytes = value.buildBytes(false);
                nameValue.setValues(new byte[] { valueType }, valueBytes);

            } else if (valueType == BMValueType.BM_BYTE_ARRAY.getType()) {

                final WffBMByteArray value = (WffBMByteArray) valueValueType
                        .getValue();
                final byte[] valueBytes = value.build(false);
                nameValue.setValues(new byte[] { valueType }, valueBytes);

            } else if (valueType == BMValueType.REG_EXP.getType()) {
                final String value = (String) valueValueType.getValue();
                nameValue.setValues(new byte[] { valueType },
                        value.getBytes(StandardCharsets.UTF_8));
            } else if (valueType == BMValueType.FUNCTION.getType()) {

                final String value = (String) valueValueType.getValue();

                nameValue.setValues(new byte[] { valueType },
                        value.getBytes(StandardCharsets.UTF_8));

            } else if (valueType == BMValueType.INTERNAL_BYTE.getType()) {
                throw new WffRuntimeException(
                        "BMValueType.BYTE is only for internal use, use WffBMByteArray for row bytes.");
            }

        }

        return WffBinaryMessageUtil.VERSION_1
                .getWffBinaryMessageBytes(nameValues);
    }

    /**
     * @param bmObjectBytes
     * @param outer
     * @throws UnsupportedEncodingException
     *                                          throwing this exception will be
     *                                          removed in future version
     *                                          because its internal
     *                                          implementation will never make
     *                                          this exception due to the code
     *                                          changes since 3.0.1.
     */
    private void initWffBMObject(final byte[] bmObjectBytes,
            final boolean outer) throws UnsupportedEncodingException {

        final WffBMObject wffBMObject = this;

        final List<NameValue> bmObject = WffBinaryMessageUtil.VERSION_1
                .parse(bmObjectBytes);

        final Iterator<NameValue> iterator = bmObject.iterator();
        if (iterator.hasNext()) {

            if (outer) {
                final NameValue typeNameValue = iterator.next();
                if (typeNameValue.getName()[0] == BMType.OBJECT.getType()) {
                    wffBMObject.outer = true;
                } else {
                    throw new WffRuntimeException(
                            "Not a valid Wff BM Object bytes");
                }
            }

            while (iterator.hasNext()) {
                final NameValue nameValue = iterator.next();
                final String name = new String(nameValue.getName(),
                        StandardCharsets.UTF_8);
                final byte[][] values = nameValue.getValues();
                final byte valueType = values[0][0];
                final byte[] value = values[1];

                if (valueType == BMValueType.STRING.getType()) {
                    final ValueValueType valueValueType = new ValueValueType(
                            name, valueType,
                            new String(value, StandardCharsets.UTF_8));
                    wffBMObject.put(name, valueValueType);
                } else if (valueType == BMValueType.NUMBER.getType()) {

                    final double doubleValue = ByteBuffer.wrap(value)
                            .getDouble(0);

                    final ValueValueType valueValueType = new ValueValueType(
                            name, valueType, doubleValue);
                    wffBMObject.put(name, valueValueType);
                } else if (valueType == BMValueType.UNDEFINED.getType()) {
                    final ValueValueType valueValueType = new ValueValueType(
                            name, valueType, null);
                    wffBMObject.put(name, valueValueType);
                } else if (valueType == BMValueType.NULL.getType()) {
                    final ValueValueType valueValueType = new ValueValueType(
                            name, valueType, null);
                    wffBMObject.put(name, valueValueType);
                } else if (valueType == BMValueType.BOOLEAN.getType()) {
                    final ValueValueType valueValueType = new ValueValueType(
                            name, valueType, value[0] == 1);
                    wffBMObject.put(name, valueValueType);
                } else if (valueType == BMValueType.BM_OBJECT.getType()) {

                    final ValueValueType valueValueType = new ValueValueType(
                            name, valueType, new WffBMObject(value, false));
                    wffBMObject.put(name, valueValueType);

                } else if (valueType == BMValueType.BM_ARRAY.getType()) {
                    final ValueValueType valueValueType = new ValueValueType(
                            name, valueType, new WffBMArray(value, false));
                    wffBMObject.put(name, valueValueType);
                } else if (valueType == BMValueType.BM_BYTE_ARRAY.getType()) {
                    final WffBMByteArray byteArray = new WffBMByteArray(value,
                            false);
                    final ValueValueType valueValueType = new ValueValueType(
                            name, valueType, byteArray);
                    wffBMObject.put(name, valueValueType);
                } else if (valueType == BMValueType.REG_EXP.getType()) {
                    final ValueValueType valueValueType = new ValueValueType(
                            name, valueType,
                            new String(value, StandardCharsets.UTF_8));
                    wffBMObject.put(name, valueValueType);
                } else if (valueType == BMValueType.FUNCTION.getType()) {
                    final ValueValueType valueValueType = new ValueValueType(
                            name, valueType,
                            new String(value, StandardCharsets.UTF_8));
                    wffBMObject.put(name, valueValueType);
                } else if (valueType == BMValueType.INTERNAL_BYTE.getType()) {
                    throw new WffRuntimeException(
                            "BMValueType.BYTE is only for internal use, use WffBMByteArray for row bytes.");
                }

            }
        }

    }

    public byte[] getbMBytes() {
        return bMBytes;
    }

    public boolean isOuter() {
        return outer;
    }

    public void setOuter(final boolean outer) {
        this.outer = outer;
    }

    /**
     * @param key
     *                the key name
     * @since 2.0.0
     * @author WFF
     */
    public Object getValue(final String key) {
        final ValueValueType valueValueType = super.get(key);
        if (valueValueType == null) {
            return null;
        }

        return valueValueType.getValue();
    }

    /**
     *
     * @param key
     * @return the value type of this key
     * @since 2.0.0
     * @author WFF
     */
    public BMValueType getValueType(final String key) {
        final ValueValueType valueValueType = super.get(key);
        if (valueValueType == null) {
            return null;
        }
        return valueValueType.getValueType();
    }

    @Override
    public BMType getBMType() {
        return BMType.OBJECT;
    }

}