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

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

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

/**
 *

 *
 * border-width: medium|thin|thick|length|initial|inherit;
 *
 * The border-width property sets the width of an element's four borders. This property can have from one to four values.
 *
 * Examples:
 *
 *     border-width:thin medium thick 10px;
 *         top border is thin
 *         right border is medium
 *         bottom border is thick
 *         left border is 10px
 *
 *     border-width:thin medium thick;
 *         top border is thin
 *         right and left borders are medium
 *         bottom border is thick
 *
 *     border-width:thin medium;
 *         top and bottom borders are thin
 *         right and left borders are medium
 *
 *     border-width:thin;
 *         all four borders are thin
 *
 * Note: Always declare the border-style property before the border-width property. An element must have borders before you can set the width.
 *

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

    private static final long serialVersionUID = 1_0_0L;

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

    public static final String MEDIUM = "medium";
    public static final String THIN = "thin";
    public static final String THICK = "thick";
    public static final String INITIAL = "initial";
    public static final String INHERIT = "inherit";

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

    private String cssValue;

    private BorderTopWidth borderTopWidth;
    private BorderRightWidth borderRightWidth;
    private BorderBottomWidth borderBottomWidth;
    private BorderLeftWidth borderLeftWidth;

    /**
     * The {@code medium} will be set as the value
     */
    public BorderWidth() {
        setCssValue(MEDIUM);
    }

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

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

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

    /**
     * @param value
     * @param cssLengthUnit
     */
    public BorderWidth(final float value, final CssLengthUnit cssLengthUnit) {
        setCssValue(String.valueOf(value) + cssLengthUnit);
    }

    /**
     * @param value
     * @param cssLengthUnit
     * @return the current object
     * @since 1.0.0
     * @author WFF
     */
    public BorderWidth setValue(final float value,
            final CssLengthUnit cssLengthUnit) {
        setCssValue(String.valueOf(value) + cssLengthUnit);
        return this;
    }

    /**
     * @param percent
     *                    the percent to set
     * @since 1.0.0
     * @author WFF
     */
    public void setPercent(final float percent) {
        setCssValue(String.valueOf(percent) + CssLengthUnit.PER);
    }

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

    /*
     * (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
     *                     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 BorderWidth setCssValue(final String cssValue) {
        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/medium/thin/thick.");
        } else if (StringUtil.isBlank(cssValue)) {
            throw new InvalidValueException(
                    "blank string is an invalid value. The value format should be as for example 75px or 85%. Or, initial/inherit/medium/thin/thick.");
        } else {
            final String trimmedCssValue = StringUtil.strip(cssValue);

            if (PREDEFINED_CONSTANTS.contains(trimmedCssValue)) {
                this.cssValue = trimmedCssValue;
                if (borderTopWidth != null) {
                    borderTopWidth.setAlreadyInUse(false);
                    borderTopWidth = null;
                }
                if (borderRightWidth != null) {
                    borderRightWidth.setAlreadyInUse(false);
                    borderRightWidth = null;
                }
                if (borderBottomWidth != null) {
                    borderBottomWidth.setAlreadyInUse(false);
                    borderBottomWidth = null;
                }
                if (borderLeftWidth != null) {
                    borderLeftWidth.setAlreadyInUse(false);
                    borderLeftWidth = null;
                }
                if (getStateChangeInformer() != null) {
                    getStateChangeInformer().stateChanged(this);
                }

                return this;
            }

            final String borderWidthString = StringUtil
                    .convertToSingleSpace(trimmedCssValue);

            final String[] extractedWidths = StringUtil
                    .splitBySpace(borderWidthString);

            if (extractedWidths.length == 1) {
                if (borderTopWidth == null) {
                    borderTopWidth = new BorderTopWidth(extractedWidths[0]);
                    borderTopWidth.setStateChangeInformer(this);
                    borderTopWidth.setAlreadyInUse(true);
                } else {
                    borderTopWidth.setCssValue(extractedWidths[0]);
                }
                if (borderRightWidth == null) {
                    borderRightWidth = new BorderRightWidth(extractedWidths[0]);
                    borderRightWidth.setStateChangeInformer(this);
                    borderRightWidth.setAlreadyInUse(true);
                } else {
                    borderRightWidth.setCssValue(extractedWidths[0]);
                }
                if (borderBottomWidth == null) {
                    borderBottomWidth = new BorderBottomWidth(
                            extractedWidths[0]);
                    borderBottomWidth.setStateChangeInformer(this);
                    borderBottomWidth.setAlreadyInUse(true);
                } else {
                    borderBottomWidth.setCssValue(extractedWidths[0]);
                }
                if (borderLeftWidth == null) {
                    borderLeftWidth = new BorderLeftWidth(extractedWidths[0]);
                    borderLeftWidth.setStateChangeInformer(this);
                    borderLeftWidth.setAlreadyInUse(true);
                } else {
                    borderLeftWidth.setCssValue(extractedWidths[0]);
                }
            } else if (extractedWidths.length == 2) {
                if (borderTopWidth == null) {
                    borderTopWidth = new BorderTopWidth(extractedWidths[0]);
                    borderTopWidth.setStateChangeInformer(this);
                    borderTopWidth.setAlreadyInUse(true);
                } else {
                    borderTopWidth.setCssValue(extractedWidths[0]);
                }
                if (borderBottomWidth == null) {
                    borderBottomWidth = new BorderBottomWidth(
                            extractedWidths[0]);
                    borderBottomWidth.setStateChangeInformer(this);
                    borderBottomWidth.setAlreadyInUse(true);
                } else {
                    borderBottomWidth.setCssValue(extractedWidths[0]);
                }
                if (borderRightWidth == null) {
                    borderRightWidth = new BorderRightWidth(extractedWidths[1]);
                    borderRightWidth.setStateChangeInformer(this);
                    borderRightWidth.setAlreadyInUse(true);
                } else {
                    borderRightWidth.setCssValue(extractedWidths[1]);
                }
                if (borderLeftWidth == null) {
                    borderLeftWidth = new BorderLeftWidth(extractedWidths[1]);
                    borderLeftWidth.setStateChangeInformer(this);
                    borderLeftWidth.setAlreadyInUse(true);
                } else {
                    borderLeftWidth.setCssValue(extractedWidths[1]);
                }
            } else if (extractedWidths.length == 3) {
                if (borderTopWidth == null) {
                    borderTopWidth = new BorderTopWidth(extractedWidths[0]);
                    borderTopWidth.setStateChangeInformer(this);
                    borderTopWidth.setAlreadyInUse(true);
                } else {
                    borderTopWidth.setCssValue(extractedWidths[0]);
                }
                if (borderRightWidth == null) {
                    borderRightWidth = new BorderRightWidth(extractedWidths[1]);
                    borderRightWidth.setStateChangeInformer(this);
                    borderRightWidth.setAlreadyInUse(true);
                } else {
                    borderRightWidth.setCssValue(extractedWidths[1]);
                }
                if (borderLeftWidth == null) {
                    borderLeftWidth = new BorderLeftWidth(extractedWidths[1]);
                    borderLeftWidth.setStateChangeInformer(this);
                    borderLeftWidth.setAlreadyInUse(true);
                } else {
                    borderLeftWidth.setCssValue(extractedWidths[1]);
                }
                if (borderBottomWidth == null) {
                    borderBottomWidth = new BorderBottomWidth(
                            extractedWidths[2]);
                    borderBottomWidth.setStateChangeInformer(this);
                    borderBottomWidth.setAlreadyInUse(true);
                } else {
                    borderBottomWidth.setCssValue(extractedWidths[2]);
                }
            } else if (extractedWidths.length == 4) {
                if (borderTopWidth == null) {
                    borderTopWidth = new BorderTopWidth(extractedWidths[0]);
                    borderTopWidth.setStateChangeInformer(this);
                    borderTopWidth.setAlreadyInUse(true);
                } else {
                    borderTopWidth.setCssValue(extractedWidths[0]);
                }
                if (borderRightWidth == null) {
                    borderRightWidth = new BorderRightWidth(extractedWidths[1]);
                    borderRightWidth.setStateChangeInformer(this);
                    borderRightWidth.setAlreadyInUse(true);
                } else {
                    borderRightWidth.setCssValue(extractedWidths[1]);
                }
                if (borderBottomWidth == null) {
                    borderBottomWidth = new BorderBottomWidth(
                            extractedWidths[2]);
                    borderBottomWidth.setStateChangeInformer(this);
                    borderBottomWidth.setAlreadyInUse(true);
                } else {
                    borderBottomWidth.setCssValue(extractedWidths[2]);
                }
                if (borderLeftWidth == null) {
                    borderLeftWidth = new BorderLeftWidth(extractedWidths[3]);
                    borderLeftWidth.setStateChangeInformer(this);
                    borderLeftWidth.setAlreadyInUse(true);
                } else {
                    borderLeftWidth.setCssValue(extractedWidths[3]);
                }
            } else {
                throw new InvalidValueException(
                        "the given cssValue is invalid");
            }

            this.cssValue = borderWidthString;
            if (getStateChangeInformer() != null) {
                getStateChangeInformer().stateChanged(this);
            }
        }
        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 medium}.
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsMedium() {
        setCssValue(MEDIUM);
    }

    /**
     * sets as {@code thin}.
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsThin() {
        setCssValue(THIN);
    }

    /**
     * sets as {@code thick}.
     *
     * @since 1.0.0
     * @author WFF
     */
    public void setAsThick() {
        setCssValue(THICK);
    }

    /**
     * sets the top, right, bottom and left width in {@code BorderWidth}. If the
     * given argument is already used by another object, then the
     * existing/cloned object will be used. And throws
     * {@code NullValueException} if any of the given argument is null.
     *
     * @param borderTopWidth
     * @param borderRightWidth
     * @param borderBottomWidth
     * @param borderLeftWidth
     * @author WFF
     * @since 1.0.0
     */
    public void setBorderWidth(final BorderTopWidth borderTopWidth,
            final BorderRightWidth borderRightWidth,
            final BorderBottomWidth borderBottomWidth,
            final BorderLeftWidth borderLeftWidth) {

        if (borderTopWidth != null && borderRightWidth != null
                && borderBottomWidth != null && borderLeftWidth != null) {

            if (BorderTopWidth.INITIAL.equals(borderTopWidth.getCssValue())
                    || BorderTopWidth.INHERIT
                            .equals(borderTopWidth.getCssValue())
                    || BorderRightWidth.INITIAL
                            .equals(borderRightWidth.getCssValue())
                    || BorderRightWidth.INHERIT
                            .equals(borderRightWidth.getCssValue())
                    || BorderBottomWidth.INITIAL
                            .equals(borderBottomWidth.getCssValue())
                    || BorderBottomWidth.INHERIT
                            .equals(borderBottomWidth.getCssValue())
                    || BorderLeftWidth.INITIAL
                            .equals(borderLeftWidth.getCssValue())
                    || BorderLeftWidth.INHERIT
                            .equals(borderLeftWidth.getCssValue())) {
                throw new InvalidValueException(
                        "Any or all of the given arguments have initial/inherit constant value as its cssValue");
            }

            try {
                final BorderTopWidth borderTopWidthTemp;
                final BorderRightWidth borderRightWidthTemp;
                final BorderBottomWidth borderBottomWidthTemp;
                final BorderLeftWidth borderLeftWidthTemp;

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

                if (borderTopWidth.isAlreadyInUse()
                        && this.borderTopWidth != borderTopWidth) {
                    if (this.borderTopWidth != null) {
                        borderTopWidthTemp = this.borderTopWidth
                                .setCssValue(borderTopWidth.getCssValue());
                        if (LOGGER.isLoggable(Level.WARNING)) {
                            LOGGER.warning(
                                    "the given borderTopWidth is already used by another object so the existing object is used");
                        }
                    } else {
                        borderTopWidthTemp = borderTopWidth.clone();
                        if (LOGGER.isLoggable(Level.WARNING)) {
                            LOGGER.warning(
                                    "the given borderTopWidth is already used by another object so its clone is assigned");
                        }
                    }
                } else {
                    borderTopWidthTemp = borderTopWidth;
                }

                if (borderRightWidth.isAlreadyInUse()
                        && this.borderRightWidth != borderRightWidth) {
                    if (this.borderRightWidth != null) {
                        borderRightWidthTemp = this.borderRightWidth
                                .setCssValue(borderTopWidth.getCssValue());
                        if (LOGGER.isLoggable(Level.WARNING)) {
                            LOGGER.warning(
                                    "the given borderRightWidth is already used by another object so the existing object is used");
                        }
                    } else {
                        borderRightWidthTemp = borderRightWidth.clone();
                        if (LOGGER.isLoggable(Level.WARNING)) {
                            LOGGER.warning(
                                    "the given borderRightWidth is already used by another object so its clone is assigned");
                        }
                    }

                } else {
                    borderRightWidthTemp = borderRightWidth;
                }

                if (borderBottomWidth.isAlreadyInUse()
                        && this.borderBottomWidth != borderBottomWidth) {
                    if (this.borderBottomWidth != null) {
                        borderBottomWidthTemp = this.borderBottomWidth
                                .setCssValue(borderTopWidth.getCssValue());
                        if (LOGGER.isLoggable(Level.WARNING)) {
                            LOGGER.warning(
                                    "the given borderBottomWidth is already used by another object so the existing object is used");
                        }
                    } else {
                        borderBottomWidthTemp = borderBottomWidth.clone();
                        if (LOGGER.isLoggable(Level.WARNING)) {
                            LOGGER.warning(
                                    "the given borderBottomWidth is already used by another object so its clone is assigned");
                        }
                    }

                } else {
                    borderBottomWidthTemp = borderBottomWidth;
                }

                if (borderLeftWidth.isAlreadyInUse()
                        && this.borderLeftWidth != borderLeftWidth) {
                    if (this.borderLeftWidth != null) {
                        borderLeftWidthTemp = this.borderLeftWidth
                                .setCssValue(borderTopWidth.getCssValue());
                        if (LOGGER.isLoggable(Level.WARNING)) {
                            LOGGER.warning(
                                    "the given borderLeftWidth is already used by another object so the existing object is used");
                        }
                    } else {
                        borderLeftWidthTemp = borderLeftWidth.clone();
                        if (LOGGER.isLoggable(Level.WARNING)) {
                            LOGGER.warning(
                                    "the given borderLeftWidth is already used by another object so its clone is assigned");
                        }
                    }

                } else {
                    borderLeftWidthTemp = borderLeftWidth;
                }

                borderTopWidthTemp.setAlreadyInUse(true);
                borderTopWidthTemp.setStateChangeInformer(this);

                borderRightWidthTemp.setAlreadyInUse(true);
                borderRightWidthTemp.setStateChangeInformer(this);

                borderBottomWidthTemp.setAlreadyInUse(true);
                borderBottomWidthTemp.setStateChangeInformer(this);

                borderLeftWidthTemp.setAlreadyInUse(true);
                borderLeftWidthTemp.setStateChangeInformer(this);

                assignProducedCssValue(borderTopWidth, borderRightWidth,
                        borderBottomWidth, borderLeftWidth);

                this.borderTopWidth = borderTopWidthTemp;
                this.borderRightWidth = borderRightWidthTemp;
                this.borderBottomWidth = borderBottomWidthTemp;
                this.borderLeftWidth = borderLeftWidthTemp;

            } catch (final CloneNotSupportedException e) {
                throw new InvalidValueException(e);
            } catch (final Exception e) {
                throw new InvalidValueException(e);
            }

        } else {
            throw new NullValueException("cannot accept null arguments");
        }

    }

    /**
     * @return the borderTopWidth
     * @author WFF
     * @since 1.0.0
     */
    public BorderTopWidth getBorderTopWidth() {
        return borderTopWidth;
    }

    /**
     * @return the borderRightWidth
     * @author WFF
     * @since 1.0.0
     */
    public BorderRightWidth getBorderRightWidth() {
        return borderRightWidth;
    }

    /**
     * @return the borderBottomWidth
     * @author WFF
     * @since 1.0.0
     */
    public BorderBottomWidth getBorderBottomWidth() {
        return borderBottomWidth;
    }

    /**
     * @return the borderLeftWidth
     * @author WFF
     * @since 1.0.0
     */
    public BorderLeftWidth getBorderLeftWidth() {
        return borderLeftWidth;
    }

    /**
     *
     * @author WFF
     * @since 1.0.0
     */
    private void assignProducedCssValue(final BorderTopWidth borderTopWidth,
            final BorderRightWidth borderRightWidth,
            final BorderBottomWidth borderBottomWidth,
            final BorderLeftWidth borderLeftWidth) {

        final String borderTopWidthCssValue = borderTopWidth.getCssValue();
        final String borderRightWidthCssValue = borderRightWidth.getCssValue();
        final String borderBottomWidthCssValue = borderBottomWidth
                .getCssValue();
        final String borderLeftWidthCssValue = borderLeftWidth.getCssValue();

        if (borderTopWidthCssValue.equals(borderRightWidthCssValue)
                && borderRightWidthCssValue.equals(borderBottomWidthCssValue)
                && borderBottomWidthCssValue.equals(borderLeftWidthCssValue)) {

            cssValue = borderTopWidthCssValue;

            final StateChangeInformer<CssProperty> stateChangeInformer = getStateChangeInformer();
            if (stateChangeInformer != null) {
                stateChangeInformer.stateChanged(this);
            }
        } else if (borderTopWidthCssValue.equals(borderBottomWidthCssValue)
                && borderRightWidthCssValue.equals(borderLeftWidthCssValue)) {

            final StringBuilder cssValueBuilder = new StringBuilder(
                    borderTopWidthCssValue);
            cssValueBuilder.append(' ').append(borderRightWidthCssValue);

            cssValue = cssValueBuilder.toString();

            final StateChangeInformer<CssProperty> stateChangeInformer = getStateChangeInformer();
            if (stateChangeInformer != null) {
                stateChangeInformer.stateChanged(this);
            }

        } else if (borderRightWidthCssValue.equals(borderLeftWidthCssValue)) {
            cssValue = new StringBuilder(borderTopWidthCssValue).append(' ')
                    .append(borderRightWidthCssValue).append(' ')
                    .append(borderBottomWidthCssValue).toString();

            final StateChangeInformer<CssProperty> stateChangeInformer = getStateChangeInformer();
            if (stateChangeInformer != null) {
                stateChangeInformer.stateChanged(this);
            }
        } else {

            cssValue = new StringBuilder(borderTopWidthCssValue).append(' ')
                    .append(borderRightWidthCssValue).append(' ')
                    .append(borderBottomWidthCssValue).append(' ')
                    .append(borderLeftWidthCssValue).toString();

            final StateChangeInformer<CssProperty> stateChangeInformer = getStateChangeInformer();
            if (stateChangeInformer != null) {
                stateChangeInformer.stateChanged(this);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * com.webfirmframework.wffweb.informer.StateChangeInformer#stateChanged(
     * java.lang .Object)
     */
    @Override
    public void stateChanged(final CssProperty stateChangedObject) {

        if (stateChangedObject instanceof BorderTopWidth) {
            final BorderTopWidth borderTopWidth = (BorderTopWidth) stateChangedObject;
            if (BorderTopWidth.INITIAL.equals(borderTopWidth.getCssValue())
                    || BorderTopWidth.INHERIT
                            .equals(borderTopWidth.getCssValue())) {
                throw new InvalidValueException(
                        "borderTopWidth cannot have initial/inherit as its cssValue");
            }
        } else if (stateChangedObject instanceof BorderRightWidth) {
            final BorderRightWidth borderRightWidth = (BorderRightWidth) stateChangedObject;
            if (BorderRightWidth.INITIAL.equals(borderRightWidth.getCssValue())
                    || BorderRightWidth.INHERIT
                            .equals(borderRightWidth.getCssValue())) {
                throw new InvalidValueException(
                        "borderRightWidth cannot have initial/inherit as its cssValue");
            }
        } else if (stateChangedObject instanceof BorderBottomWidth) {
            final BorderBottomWidth borderBottomWidth = (BorderBottomWidth) stateChangedObject;
            if (BorderBottomWidth.INITIAL
                    .equals(borderBottomWidth.getCssValue())
                    || BorderBottomWidth.INHERIT
                            .equals(borderBottomWidth.getCssValue())) {
                throw new InvalidValueException(
                        "borderBottomWidth cannot have initial/inherit as its cssValue");
            }
        } else if (stateChangedObject instanceof BorderLeftWidth) {
            final BorderLeftWidth borderLeftWidth = (BorderLeftWidth) stateChangedObject;
            if (BorderLeftWidth.INITIAL.equals(borderLeftWidth.getCssValue())
                    || BorderLeftWidth.INHERIT
                            .equals(borderLeftWidth.getCssValue())) {
                throw new InvalidValueException(
                        "borderLeftWidth cannot have initial/inherit as its cssValue");
            }
        }

        assignProducedCssValue(borderTopWidth, borderRightWidth,
                borderBottomWidth, borderLeftWidth);

    }

    /**
     * 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 = StringUtil.strip(cssValue).toLowerCase();
        final String[] cssValueParts = StringUtil.splitBySpace(trimmedCssValue);
        if (cssValueParts.length > 4) {
            return false;
        }
        for (final String cssValuePart : cssValueParts) {
            boolean invalidValue = true;
            for (final CssLengthUnit cssLengthUnit : CssLengthUnit.values()) {
                final String unit = cssLengthUnit.getUnit();
                if (cssValuePart.endsWith(unit)) {
                    final String valueOnly = cssValuePart.replaceFirst(unit,
                            "");
                    try {
                        Float.parseFloat(valueOnly);
                    } catch (final NumberFormatException e) {
                        break;
                    }
                    invalidValue = false;
                    break;
                }
            }
            if (PREDEFINED_CONSTANTS.contains(cssValuePart)) {
                invalidValue = false;
            }
            if (invalidValue) {
                return false;
            }
        }
        return true;
    }

    /**
     * @return true if its cssValue is any of the values
     *          initial or inherit.
     * @author WFF
     * @since 1.0.0
     */
    public boolean hasPredefinedConstantValue() {
        return PREDEFINED_CONSTANTS.contains(cssValue);
    }

}


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

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import com.webfirmframework.wffweb.clone.CloneUtil;
import com.webfirmframework.wffweb.css.core.CssProperty;

/**
 *
 * @author WFF
 * @since 1.0.0
 */
public abstract class AbstractCssFileBlock implements CssFileBlock {

    private static final long serialVersionUID = 1_0_0L;

    private final Set<CssProperty> cssProperties;

    private final Map<String, CssProperty> cssPropertiesAsMap;

    private final Set<CssFile> cssFiles;

    private String selectors;

    private volatile boolean modified;

    private volatile boolean loadedOnce;

    private boolean excludeCssBlock;

    @SuppressWarnings("unused")
    private AbstractCssFileBlock() {
    }

    public AbstractCssFileBlock(final String selectors) {
        this.selectors = selectors;
    }

    {
        cssFiles = new LinkedHashSet<>();

        cssPropertiesAsMap = new LinkedHashMap<>();

        cssProperties = new LinkedHashSet<CssProperty>() {

            private static final long serialVersionUID = 1_0_0L;

            private StringBuilder toStringBuilder;

            {
                toStringBuilder = new StringBuilder();
            }

            @Override
            public boolean add(final CssProperty cssProperty) {
                final boolean added = super.add(cssProperty);
                if (added) {
                    setModified(added);
                    cssPropertiesAsMap.put(cssProperty.getCssName(),
                            cssProperty);
                }
                return added;
            }

            @Override
            public boolean addAll(
                    final Collection<? extends CssProperty> cssProperties) {
                final boolean addedAll = super.addAll(cssProperties);
                if (addedAll) {
                    setModified(addedAll);
                    for (final CssProperty cssProperty : cssProperties) {
                        cssPropertiesAsMap.put(cssProperty.getCssName(),
                                cssProperty);
                    }
                }
                return addedAll;
            }

            @Override
            public boolean remove(final Object o) {
                final boolean removed = super.remove(o);
                if (removed) {
                    setModified(removed);
                    if (o instanceof CssProperty) {
                        cssPropertiesAsMap
                                .remove(((CssProperty) o).getCssName());
                    }
                }
                return removed;
            }

            @Override
            public boolean removeAll(final Collection<?> c) {
                final boolean removedAll = super.removeAll(c);
                if (removedAll) {
                    setModified(removedAll);
                    for (final Object object : c) {
                        if (object instanceof CssProperty) {
                            cssPropertiesAsMap.remove(
                                    ((CssProperty) object).getCssName());
                        }
                    }
                }
                return removedAll;
            }

            @Override
            public void clear() {
                setModified(true);
                super.clear();
                cssPropertiesAsMap.clear();
            }

            @Override
            public String toString() {
                if (modified) {
                    synchronized (toStringBuilder) {
                        if (modified) {
                            toStringBuilder.delete(0, toStringBuilder.length());
                            for (final CssProperty cssProperty : this) {
                                toStringBuilder.append(cssProperty.getCssName())
                                        .append(':')
                                        .append(cssProperty.getCssValue())
                                        .append(';');
                            }
                            setModified(false);
                        }
                    }
                }
                return toStringBuilder.toString();
            }
        };

    }

    protected abstract void load(Set<CssProperty> cssProperties);

    void addCssFile(final CssFile cssFile) {
        cssFiles.add(cssFile);
    }

    void removeCssFile(final CssFile cssFile) {
        cssFiles.remove(cssFile);
    }

    @Override
    public String toString() {
        return super.toString();
    }

    /**
     * @return
     * @since 1.0.0
     * @author WFF
     */
    public String toCssString() {
        if (!loadedOnce) {
            synchronized (cssProperties) {
                if (!loadedOnce) {
                    cssProperties.clear();
                    load(cssProperties);
                    loadedOnce = true;
                    setModified(true);
                }
            }
        }
        return selectors + "{" + cssProperties.toString() + "}";
    }

    /**
     * @param rebuild
     * @return the css string.
     * @since 1.0.0
     * @author WFF
     */
    public String toCssString(final boolean rebuild) {
        if (rebuild || !loadedOnce) {
            synchronized (cssProperties) {
                if (rebuild || !loadedOnce) {
                    cssProperties.clear();
                    load(cssProperties);
                    loadedOnce = true;
                    setModified(true);
                }
            }
        }
        return selectors + "{" + cssProperties.toString() + "}";
    }

    @Override
    public AbstractCssFileBlock clone() throws CloneNotSupportedException {
        return CloneUtil.deepClone(this);
    }

    /**
     * @return the cssProperties
     * @since 1.0.0
     * @author WFF
     */
    public Set<CssProperty> getCssProperties() {
        if (!loadedOnce) {
            synchronized (cssProperties) {
                if (!loadedOnce) {
                    cssProperties.clear();
                    load(cssProperties);
                    loadedOnce = true;
                    setModified(true);
                }
            }
        }
        return cssProperties;
    }

    /**
     * @param modified
     *                     the modified to set
     * @since 1.0.0
     * @author WFF
     */
    private void setModified(final boolean modified) {
        if (modified) {
            for (final CssFile cssFile : cssFiles) {
                cssFile.setModified(true);
            }
        }
        this.modified = modified;
    }

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

    /**
     * rebuild true to rebuild, the load method will be invoked again.
     *
     * @return the cssProperties as map with key as the cssName and value as
     *         {@code CssProperty}.
     * @since 1.0.0
     * @author WFF
     */
    Map<String, CssProperty> getCssPropertiesAsMap(final boolean rebuild) {
        if (rebuild || !loadedOnce) {
            synchronized (cssProperties) {
                if (rebuild || !loadedOnce) {
                    cssProperties.clear();
                    load(cssProperties);
                    loadedOnce = true;
                    setModified(true);
                }
            }
        }
        return cssPropertiesAsMap;
    }

    /**
     * @return the excludeCssBlock true if the css block has been excluded, i.e.
     *         it will not be contained in the generated css.
     */
    public boolean isExcludeCssBlock() {
        return excludeCssBlock;
    }

    /**
     * @param excludeCssBlock
     *                            the excludeCssBlock to set. If it is set to
     *                            true, then this css block will not be
     *                            contained in the generated css.
     */
    protected void setExcludeCssBlock(final boolean excludeCssBlock) {
        this.excludeCssBlock = excludeCssBlock;
    }
}


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

import java.util.logging.Logger;

import com.webfirmframework.wffweb.tag.html.attribute.core.AbstractAttribute;
import com.webfirmframework.wffweb.util.StringUtil;

/**
 *
 * All event attributes will be extend by this class. It contains some common
 * features which all event attribute classes want.
 *
 * @since 2.0.0
 * @author WFF
 *
 */
public abstract class AbstractEventAttribute extends AbstractAttribute
        implements EventAttribute {

    private static final long serialVersionUID = 1_0_0L;

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

    private ServerAsyncMethod serverAsyncMethod;

    private String jsFilterFunctionBody;

    private String jsPreFunctionBody;

    private String jsPostFunctionBody;

    private volatile Object serverSideData;

    // short name for wffServerMethods is wffSM

    // for better readability it's not capitalized
    // short name for invokeAsyncWithPreFilterFun is iawpff
    private static final String invokeAsyncWithPreFilterFun = "wffSM.iawpff";

    // for better readability it's not capitalized
    // short name for invokeAsyncWithPreFun is iawpf
    private static final String invokeAsyncWithPreFun = "wffSM.iawpf";

    // for better readability it's not capitalized
    // short name for invokeAsyncWithFilterFun is iawff
    private static final String invokeAsyncWithFilterFun = "wffSM.iawff";

    // for better readability it's not capitalized
    // short name for invokeAsync is ia
    private static final String invokeAsync = "wffSM.ia";

    {
        init();
    }

    protected AbstractEventAttribute() {
    }

    /**
     *
     * @param value
     *                  the value for the attribute
     * @since 2.0.0
     * @author WFF
     */
    protected AbstractEventAttribute(final String attributeName,
            final String value) {
        super.setAttributeName(attributeName);
        super.setAttributeValue(value);
    }

    /**
     * @param attributeName
     * @param jsPreFunctionBody
     *                                 the body part javascript function
     *                                 (without function declaration). It must
     *                                 return true/false. This function will
     *                                 invoke at client side before
     *                                 {@code serverAsyncMethod}. If the
     *                                 jsPrefunction returns true then only
     *                                 {@code serverAsyncMethod} method will
     *                                 invoke (if it is implemented). It has
     *                                 implicit objects like {@code event} and
     *                                 {@code source} which gives the reference
     *                                 of the current tag.

     *                                 Eg:-
     *
     *

     *            if (source.type == 'button') {
     *               return true;
     *            }
     *            return false;
     *

     *
     * @param serverAsyncMethod
     *                                 This method will invoke at server side
     *                                 with an argument {@code wffBMObject}. The
     *                                 {@code wffBMObject} is the
     *                                 representational javascript object
     *                                 returned by {@code jsFilterFunctionBody}.
     *
     * @param jsFilterFunctionBody
     *                                 The body part of a javascript function
     *                                 (without function declaration). It can
     *                                 return a javascript object so that it
     *                                 will be available at server side in
     *                                 {@code serverAsyncMethod} as
     *                                 {@code wffBMObject} parameter. There are
     *                                 implicit objects {@code event} and
     *                                 {@code source} in the scope.

     *                                 Eg:-
     *
     *

     *            var bName = source.name;
     *            return {buttonName: bName, author:'wff', dateOfYear: 2014};
     *

     *
     * @param jsPostFunctionBody
     *                                 The body part of a javascript function
     *                                 (without function declaration). The
     *                                 {@code wffBMObject} returned by
     *                                 {@code serverAsyncMethod} will be
     *                                 available as an implicit object
     *                                 {@code jsObject} in the scope. There are
     *                                 common implicit objects {@code event} and
     *                                 {@code source} in the scope.
     * @since 2.0.0
     * @author WFF
     */
    protected AbstractEventAttribute(final String attributeName,
            final String jsPreFunctionBody,
            final ServerAsyncMethod serverAsyncMethod,
            final String jsFilterFunctionBody,
            final String jsPostFunctionBody) {

        setAttributeName(attributeName);
        setServerAsyncMethod(jsPreFunctionBody, serverAsyncMethod,
                jsFilterFunctionBody, jsPostFunctionBody);
    }

    /**
     * @param attributeName
     * @param jsPreFunctionBody
     *                                 the body part javascript function
     *                                 (without function declaration). It must
     *                                 return true/false. This function will
     *                                 invoke at client side before
     *                                 {@code serverAsyncMethod}. If the
     *                                 jsPrefunction returns true then only
     *                                 {@code serverAsyncMethod} method will
     *                                 invoke (if it is implemented). It has
     *                                 implicit objects like {@code event} and
     *                                 {@code source} which gives the reference
     *                                 of the current tag.

     *                                 Eg:-
     *
     *

     *            if (source.type == 'button') {
     *               return true;
     *            }
     *            return false;
     *

     *
     * @param serverAsyncMethod
     *                                 This method will invoke at server side
     *                                 with an argument {@code wffBMObject}. The
     *                                 {@code wffBMObject} is the
     *                                 representational javascript object
     *                                 returned by {@code jsFilterFunctionBody}.
     *
     * @param jsFilterFunctionBody
     *                                 The body part of a javascript function
     *                                 (without function declaration). It can
     *                                 return a javascript object so that it
     *                                 will be available at server side in
     *                                 {@code serverAsyncMethod} as
     *                                 {@code wffBMObject} parameter. There are
     *                                 implicit objects {@code event} and
     *                                 {@code source} in the scope.

     *                                 Eg:-
     *
     *

     *            var bName = source.name;
     *            return {buttonName: bName, author:'wff', dateOfYear: 2014};
     *

     *
     * @param jsPostFunctionBody
     *                                 The body part of a javascript function
     *                                 (without function declaration). The
     *                                 {@code wffBMObject} returned by
     *                                 {@code serverAsyncMethod} will be
     *                                 available as an implicit object
     *                                 {@code jsObject} in the scope. There are
     *                                 common implicit objects {@code event} and
     *                                 {@code source} in the scope.
     * @param serverSideData
     *                                 this data will be available in the Event
     *                                 object of ServerAsyncMethod.asyncMethod
     *                                 method.
     * @since 3.0.2
     * @author WFF
     */
    protected AbstractEventAttribute(final String attributeName,
            final String jsPreFunctionBody,
            final ServerAsyncMethod serverAsyncMethod,
            final String jsFilterFunctionBody, final String jsPostFunctionBody,
            final Object serverSideData) {
        setAttributeName(attributeName);
        setServerAsyncMethod(jsPreFunctionBody, serverAsyncMethod,
                jsFilterFunctionBody, jsPostFunctionBody, serverSideData);
    }

    /**
     * invokes only once per object
     *
     * @author WFF
     * @since 2.0.0
     */
    protected void init() {
        // to override and use this method
    }

    /**
     * sets the value for this attribute
     *
     * @param value
     *                  the value for the attribute.
     * @since 2.0.0
     * @author WFF
     */
    public void setValue(final String value) {
        super.setAttributeValue(value);
    }

    /**
     * gets the value of this attribute
     *
     * @return the value of the attribute
     * @since 2.0.0
     * @author WFF
     */
    public String getValue() {
        return super.getAttributeValue();
    }

    /**
     * @param serverAsyncMethod
     *                              the {@code ServerAsyncMethod} object to set.
     * @author WFF
     */
    public void setServerAsyncMethod(
            final ServerAsyncMethod serverAsyncMethod) {
        if (serverAsyncMethod != null) {

            setServerAsyncMethod(jsPreFunctionBody, serverAsyncMethod,
                    jsFilterFunctionBody, jsPostFunctionBody);
        }
    }

    private static String getPreparedJsFunctionBody(
            final String jsfunctionBody) {

        final String functionBody = StringUtil.strip(jsfunctionBody);
        final StringBuilder builder = new StringBuilder(26);

        builder.append("function(event, source){").append(functionBody);
        if (functionBody.charAt(functionBody.length() - 1) != ';') {
            builder.append(';');
        }

        return builder.append('}').toString();
    }

    /**
     * @param jsPreFunctionBody
     *                                 the body part javascript function
     *                                 (without function declaration). It must
     *                                 return true/false. This function will
     *                                 invoke at client side before
     *                                 {@code serverAsyncMethod}. If the
     *                                 jsPrefunction returns true then only
     *                                 {@code serverAsyncMethod} method will
     *                                 invoke (if it is implemented). It has
     *                                 implicit objects like {@code event} and
     *                                 {@code source} which gives the reference
     *                                 of the current tag.

     *                                 Eg:-
     *
     *

     *            if (source.type == 'button') {
     *               return true;
     *            }
     *            return false;
     *

     *
     * @param serverAsyncMethod
     *                                 This method will invoke at server side
     *                                 with an argument {@code wffBMObject}. The
     *                                 {@code wffBMObject} is the
     *                                 representational javascript object
     *                                 returned by {@code jsFilterFunctionBody}.
     *
     * @param jsFilterFunctionBody
     *                                 The body part of a javascript function
     *                                 (without function declaration). It can
     *                                 return a javascript object so that it
     *                                 will be available at server side in
     *                                 {@code serverAsyncMethod} as
     *                                 {@code wffBMObject} parameter. There are
     *                                 implicit objects {@code event} and
     *                                 {@code source} in the scope.

     *                                 Eg:-
     *
     *

     *            var bName = source.name;
     *            return {buttonName: bName, author:'wff', dateOfYear: 2014};
     *

     *
     * @param jsPostFunctionBody
     *                                 The body part of a javascript function
     *                                 (without function declaration). The
     *                                 {@code wffBMObject} returned by
     *                                 {@code serverAsyncMethod} will be
     *                                 available as an implicit object
     *                                 {@code jsObject} in the scope. There are
     *                                 common implicit objects {@code event} and
     *                                 {@code source} in the scope.
     * @since 2.0.0
     * @author WFF
     */
    public void setServerAsyncMethod(final String jsPreFunctionBody,
            final ServerAsyncMethod serverAsyncMethod,
            final String jsFilterFunctionBody,
            final String jsPostFunctionBody) {

        setServerAsyncMethod(jsPreFunctionBody, serverAsyncMethod,
                jsFilterFunctionBody, jsPostFunctionBody, serverSideData);
    }

    /**
     * @param jsPreFunctionBody
     *                                 the body part javascript function
     *                                 (without function declaration). It must
     *                                 return true/false. This function will
     *                                 invoke at client side before
     *                                 {@code serverAsyncMethod}. If the
     *                                 jsPrefunction returns true then only
     *                                 {@code serverAsyncMethod} method will
     *                                 invoke (if it is implemented). It has
     *                                 implicit objects like {@code event} and
     *                                 {@code source} which gives the reference
     *                                 of the current tag.

     *                                 Eg:-
     *
     *

     *            if (source.type == 'button') {
     *               return true;
     *            }
     *            return false;
     *

     *
     * @param serverAsyncMethod
     *                                 This method will invoke at server side
     *                                 with an argument {@code wffBMObject}. The
     *                                 {@code wffBMObject} is the
     *                                 representational javascript object
     *                                 returned by {@code jsFilterFunctionBody}.
     *
     * @param jsFilterFunctionBody
     *                                 The body part of a javascript function
     *                                 (without function declaration). It can
     *                                 return a javascript object so that it
     *                                 will be available at server side in
     *                                 {@code serverAsyncMethod} as
     *                                 {@code wffBMObject} parameter. There are
     *                                 implicit objects {@code event} and
     *                                 {@code source} in the scope.

     *                                 Eg:-
     *
     *

     *            var bName = source.name;
     *            return {buttonName: bName, author:'wff', dateOfYear: 2014};
     *

     *
     * @param jsPostFunctionBody
     *                                 The body part of a javascript function
     *                                 (without function declaration). The
     *                                 {@code wffBMObject} returned by
     *                                 {@code serverAsyncMethod} will be
     *                                 available as an implicit object
     *                                 {@code jsObject} in the scope. There are
     *                                 common implicit objects {@code event} and
     *                                 {@code source} in the scope.
     * @param serverSideData
     *                                 this data will be available in the Event
     *                                 object of ServerAsyncMethod.asyncMethod
     *                                 method.
     * @since 3.0.2
     * @author WFF
     */
    public void setServerAsyncMethod(final String jsPreFunctionBody,
            final ServerAsyncMethod serverAsyncMethod,
            final String jsFilterFunctionBody, final String jsPostFunctionBody,
            final Object serverSideData) {

        this.serverSideData = serverSideData;

        if (serverAsyncMethod != null) {

            if (jsPreFunctionBody != null && jsPostFunctionBody != null
                    && jsFilterFunctionBody != null) {

                super.setAttributeValue(
                        new StringBuilder(invokeAsyncWithPreFilterFun)
                                .append("(event, this,'")
                                .append(getAttributeName()).append("',")
                                .append(getPreparedJsFunctionBody(
                                        jsPreFunctionBody))
                                .append(',')
                                .append(getPreparedJsFunctionBody(
                                        jsFilterFunctionBody))
                                .append(')').toString());

            } else if (jsPreFunctionBody != null
                    && jsPostFunctionBody != null) {

                super.setAttributeValue(new StringBuilder(invokeAsyncWithPreFun)
                        .append("(event, this,'").append(getAttributeName())
                        .append("',")
                        .append(getPreparedJsFunctionBody(jsPreFunctionBody))
                        .append(')').toString());

            } else if (jsPreFunctionBody != null
                    && jsFilterFunctionBody != null) {

                super.setAttributeValue(
                        new StringBuilder(invokeAsyncWithPreFilterFun)
                                .append("(event, this,'")
                                .append(getAttributeName()).append("',")
                                .append(getPreparedJsFunctionBody(
                                        jsPreFunctionBody))
                                .append(',')
                                .append(getPreparedJsFunctionBody(
                                        jsFilterFunctionBody))
                                .append(')').toString());

            } else if (jsPostFunctionBody != null
                    && jsFilterFunctionBody != null) {

                super.setAttributeValue(
                        new StringBuilder(invokeAsyncWithFilterFun)
                                .append("(event, this,'")
                                .append(getAttributeName()).append("',")
                                .append(getPreparedJsFunctionBody(
                                        jsFilterFunctionBody))
                                .append(')').toString());

            } else if (jsPreFunctionBody != null) {

                super.setAttributeValue(new StringBuilder(invokeAsyncWithPreFun)
                        .append("(event, this,'").append(getAttributeName())
                        .append("',")
                        .append(getPreparedJsFunctionBody(jsPreFunctionBody))
                        .append(')').toString());

            } else if (jsFilterFunctionBody != null) {

                super.setAttributeValue(
                        new StringBuilder(invokeAsyncWithFilterFun)
                                .append("(event, this,'")
                                .append(getAttributeName()).append("',")
                                .append(getPreparedJsFunctionBody(
                                        jsFilterFunctionBody))
                                .append(')').toString());
            } else if (jsPostFunctionBody != null) {
                super.setAttributeValue(new StringBuilder(invokeAsync)
                        .append("(event, this,'").append(getAttributeName())
                        .append("')").toString());
            } else {

                super.setAttributeValue(new StringBuilder(invokeAsync)
                        .append("(event, this,'").append(getAttributeName())
                        .append("')").toString());
            }

            this.jsPreFunctionBody = jsPreFunctionBody;
            this.jsFilterFunctionBody = jsFilterFunctionBody;
            this.jsPostFunctionBody = jsPostFunctionBody;
            this.serverAsyncMethod = serverAsyncMethod;
        } else {
            LOGGER.warning(
                    "serverAsyncMethod is null so jsPreFunctionBody, jsFilterFunctionBody and jsPostFunctionBody are not also set.They are valid only if serverAsyncMethod is NOT null.");
        }
    }

    @Override
    public ServerAsyncMethod getServerAsyncMethod() {
        return serverAsyncMethod;
    }

    @Override
    public String getJsPostFunctionBody() {
        return jsPostFunctionBody;
    }

    /**
     * @return
     * @since 2.1.9
     * @author WFF
     */
    public String getJsFilterFunctionBody() {
        return jsFilterFunctionBody;
    }

    /**
     * @return
     * @since 2.1.9
     * @author WFF
     */
    public String getJsPreFunctionBody() {
        return jsPreFunctionBody;
    }

    /**
     * Sets the post function body JavaScript.
     *
     * @param jsPostFunctionBody
     *                               the post function body JavaScript.
     * @author WFF
     */
    public void setJsPostFunctionBody(final String jsPostFunctionBody) {
        setServerAsyncMethod(jsPreFunctionBody, serverAsyncMethod,
                jsFilterFunctionBody, jsPostFunctionBody);
    }

    /**
     * Sets the pre function body JavaScript.
     *
     * @param jsPreFunctionBody
     * @since 2.1.9
     * @author WFF
     */
    public void setJsPreFunctionBody(final String jsPreFunctionBody) {
        setServerAsyncMethod(jsPreFunctionBody, serverAsyncMethod,
                jsFilterFunctionBody, jsPostFunctionBody);
    }

    /**
     * Sets the filter function body JavaScript.
     *
     * @param jsFilterFunctionBody
     * @since 2.1.9
     * @author WFF
     */
    public void setJsFilterFunctionBody(final String jsFilterFunctionBody) {
        setServerAsyncMethod(jsPreFunctionBody, serverAsyncMethod,
                jsFilterFunctionBody, jsPostFunctionBody);
    }

    /**
     * @return the serverSideData
     * @since 3.0.2
     */
    @Override
    public Object getServerSideData() {
        return serverSideData;
    }

    /**
     * @param serverSideData
     *                           the serverSideData to set
     * @since 3.0.2
     */
    public void setServerSideData(final Object serverSideData) {
        this.serverSideData = serverSideData;
    }
}


/*
 * 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-bottom shorthand property sets all the bottom border properties in one declaration.
 *
 * The properties that can be set, are (in order): border-bottom-width, border-bottom-style, and border-bottom-color.
 *
 * If one of the values above are missing, e.g. border-bottom: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.borderBottom="3px dashed blue"
 *

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

    private static final long serialVersionUID = 1_0_0L;

    public static final Logger LOGGER = Logger
            .getLogger(BorderBottom.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 BorderBottomWidth borderBottomWidth;

    private BorderBottomStyle borderBottomStyle;

    private BorderBottomColor borderBottomColor;

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

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

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

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

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

                borderBottomStyle = null;

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

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

        BorderBottomWidth borderBottomWidth = null;
        BorderBottomStyle borderBottomStyle = null;
        BorderBottomColor borderBottomColor = null;

        for (final String eachPart : cssValueParts) {
            if (borderBottomWidth == null
                    && BorderBottomWidth.isValid(eachPart)) {
                if (this.borderBottomWidth == null) {
                    borderBottomWidth = new BorderBottomWidth(eachPart);
                    borderBottomWidth.setStateChangeInformer(this);
                    borderBottomWidth.setAlreadyInUse(true);
                } else {
                    this.borderBottomWidth.setCssValue(eachPart);
                    borderBottomWidth = this.borderBottomWidth;
                }
            } else if (borderBottomStyle == null
                    && BorderBottomStyle.isValid(eachPart)) {
                borderBottomStyle = BorderBottomStyle.getThis(eachPart);
            } else if (borderBottomColor == null
                    && BorderBottomColor.isValid(eachPart)) {
                if (this.borderBottomColor == null) {
                    borderBottomColor = new BorderBottomColor(eachPart);
                    borderBottomColor.setStateChangeInformer(this);
                    borderBottomColor.setAlreadyInUse(true);
                } else {
                    this.borderBottomColor.setCssValue(eachPart);
                    borderBottomColor = this.borderBottomColor;
                }
            }
        }

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

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

        BorderBottomWidth borderBottomWidth = null;
        BorderBottomStyle borderBottomStyle = null;
        BorderBottomColor borderBottomColor = null;

        for (final String eachPart : cssValueParts) {

            boolean invalid = true;
            if (borderBottomWidth == null
                    && BorderBottomWidth.isValid(eachPart)) {
                borderBottomWidth = new BorderBottomWidth(eachPart);
                invalid = false;
            } else if (borderBottomStyle == null
                    && BorderBottomStyle.isValid(eachPart)) {
                borderBottomStyle = BorderBottomStyle.getThis(eachPart);
                invalid = false;
            } else if (borderBottomColor == null
                    && BorderBottomColor.isValid(eachPart)) {
                borderBottomColor = new BorderBottomColor(eachPart);
                invalid = false;
            }
            if (invalid) {
                return false;
            }
        }

        return borderBottomWidth != null || borderBottomStyle != null
                || borderBottomColor != 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 BorderBottomColor getBorderBottomColor() {
        return borderBottomColor;
    }

    public BorderBottomStyle getBorderBottomStyle() {
        return borderBottomStyle;
    }

    public BorderBottomWidth getBorderBottomWidth() {
        return borderBottomWidth;
    }

    public BorderBottom setBorderBottomWidth(
            final BorderBottomWidth borderBottomWidth) {

        final StringBuilder cssValueBuilder = new StringBuilder();

        if (borderBottomWidth != null) {

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

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

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

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

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

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

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

        this.borderBottomWidth = borderBottomWidth;

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

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

    public BorderBottom setBorderBottomStyle(
            final BorderBottomStyle borderBottomStyle) {

        final StringBuilder cssValueBuilder = new StringBuilder();

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

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

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

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

        this.borderBottomStyle = borderBottomStyle;

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

    public BorderBottom setBorderBottomColor(
            final BorderBottomColor borderBottomColor) {

        final StringBuilder cssValueBuilder = new StringBuilder();

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

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

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

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

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

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

        this.borderBottomColor = borderBottomColor;

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

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

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

            final BorderBottomColor borderBottomColor = (BorderBottomColor) stateChangedObject;

            final String cssValue = borderBottomColor.getCssValue();

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

            setBorderBottomColor(borderBottomColor);
        }
    }

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

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 *
 * @author WFF
 * @since 1.0.0
 */
public final class CharsetUtil {

    private static final Map<Integer, char[]> CHACHED_UPPER_CASE_CHARSETS = new ConcurrentHashMap<>();

    private static final Map<Integer, char[]> CHACHED_LOWER_CASE_CHARSETS = new ConcurrentHashMap<>();

    private CharsetUtil() {
        throw new AssertionError();
    }

    /**
     * gets the char array of upper case chars from 0 to the given {@code upto}
     * value.
     *
     * @param upto
     * @return
     * @since 1.0.0
     * @author WFF
     */
    public static char[] getUpperCaseCharset(final int upto) {
        final int uptoPlusOne = upto + 1;
        final char[] characters = CHACHED_UPPER_CASE_CHARSETS.get(uptoPlusOne);
        if (characters != null) {
            return characters;
        }
        final char[] charset = new char[uptoPlusOne];
        char index = 0;
        while (index < uptoPlusOne) {
            charset[index] = Character.toUpperCase(index);
            index++;
        }
        final char[] previous = CHACHED_UPPER_CASE_CHARSETS
                .putIfAbsent(uptoPlusOne, charset);
        if (previous != null) {
            return previous;
        }
        return charset;
    }

    /**
     * gets the char array of lower case chars from 0 to the given {@code upto}
     * value.
     *
     * @param upto
     * @return
     * @since 1.0.0
     * @author WFF
     */
    public static char[] getLowerCaseCharset(final int upto) {
        final int uptoPlusOne = upto + 1;
        final char[] characters = CHACHED_LOWER_CASE_CHARSETS.get(uptoPlusOne);
        if (characters != null) {
            return characters;
        }
        final char[] charset = new char[uptoPlusOne];
        char index = 0;
        while (index < uptoPlusOne) {
            charset[index] = Character.toLowerCase(index);
            index++;
        }
        final char[] previous = CHACHED_LOWER_CASE_CHARSETS
                .putIfAbsent(uptoPlusOne, charset);
        if (previous != null) {
            return previous;
        }
        return charset;
    }

}


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

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

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

/**
 * list-style: list-style-type list-style-position
 * list-style-image|initial|inherit;
 *
 *
 * Formal syntax: {@code <'list-style-type'> || <'list-style-position'> ||
 * <'list-style-image'>}
 *
 * @author WFF
 * @since 1.0.0
 */
public class ListStyle extends AbstractCssProperty<ListStyle>
        implements StateChangeInformer<CssProperty> {

    private static final long serialVersionUID = 1_0_0L;

    public static final Logger LOGGER = Logger
            .getLogger(ListStyle.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 ListStyleType listStyleType;

    private ListStylePosition listStylePosition;

    private ListStyleImage listStyleImage;

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

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

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

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

    /*
     * (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
     *                     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 ListStyle setCssValue(final String cssValue) {
        // NB: cssValue can contain upper case so it should not be converted to
        // lower case.
        final String trimmedCssValue;
        if (cssValue == null) {
            throw new NullValueException(
                    "null is an invalid value. The value format should be as for example cjk-ideographic inside url(\"Sqpurple.gif\"). Or, initial/inherit.");
        } else if ((trimmedCssValue = StringUtil.strip(cssValue)).isEmpty()) {
            throw new NullValueException(cssValue
                    + " is an invalid value. The value format should be as for example cjk-ideographic inside url(\"Sqpurple.gif\"). Or, initial/inherit.");
        }

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

        if (cssValueParts.length > 1) {
            if (trimmedCssValue.contains(ListStyleImage.INITIAL)) {
                throw new InvalidValueException(
                        "The string 'initial' makes the given cssValue invalid, please remove 'initial' from it and try.");
            }
            if (trimmedCssValue.contains(ListStyleImage.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)) {
                listStyleType = null;
                listStylePosition = null;
                if (listStyleImage != null) {
                    listStyleImage.setAlreadyInUse(false);
                    listStyleImage = null;
                }

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

        ListStyleType listStyleType = null;
        ListStylePosition listStylePosition = null;
        ListStyleImage listStyleImage = null;

        for (final String eachPart : cssValueParts) {
            if (listStyleType == null && ListStyleType.isValid(eachPart)) {
                listStyleType = ListStyleType.getThis(eachPart);
            } else if (listStylePosition == null
                    && ListStylePosition.isValid(eachPart)) {
                listStylePosition = ListStylePosition.getThis(eachPart);
            } else if (listStyleImage == null
                    && ListStyleImage.isValid(eachPart)) {
                if (this.listStyleImage == null) {
                    listStyleImage = new ListStyleImage(eachPart);
                    listStyleImage.setStateChangeInformer(this);
                    listStyleImage.setAlreadyInUse(true);
                } else {
                    this.listStyleImage.setCssValue(eachPart);
                    listStyleImage = this.listStyleImage;
                }
            } else {
                throw new InvalidValueException(cssValue
                        + " is an invalid value. The value format should be as for example cjk-ideographic inside url(\"Sqpurple.gif\"). Or, initial/inherit.");
            }
        }

        final StringBuilder cssValueBuilder = new StringBuilder();
        boolean invalid = true;
        if (listStyleType != null) {
            cssValueBuilder.append(listStyleType.getCssValue()).append(' ');
            invalid = false;
        }
        if (listStylePosition != null) {
            cssValueBuilder.append(listStylePosition.getCssValue()).append(' ');
            invalid = false;
        }
        if (listStyleImage != null) {
            cssValueBuilder.append(listStyleImage.getCssValue()).append(' ');
            invalid = false;
        }
        if (invalid) {
            throw new InvalidValueException(cssValue
                    + " is an invalid value. The value format should be as for example cjk-ideographic inside url(\"Sqpurple.gif\"). Or, initial/inherit.");
        }
        this.cssValue = StringBuilderUtil.getTrimmedString(cssValueBuilder);
        this.listStyleType = listStyleType;
        this.listStylePosition = listStylePosition;
        this.listStyleImage = listStyleImage;

        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 String[] cssValueParts = StringUtil.splitBySpace(cssValue);

        ListStyleType listStyleType = null;
        ListStylePosition listStylePosition = null;
        ListStyleImage listStyleImage = null;

        for (final String eachPart : cssValueParts) {
            boolean invalid = true;
            if (listStyleType == null && ListStyleType.isValid(eachPart)) {
                listStyleType = ListStyleType.getThis(eachPart);
                invalid = false;
            } else if (listStylePosition == null
                    && ListStylePosition.isValid(eachPart)) {
                listStylePosition = ListStylePosition.getThis(eachPart);
                invalid = false;
            } else if (listStyleImage == null
                    && ListStyleImage.isValid(eachPart)) {
                listStyleImage = new ListStyleImage(eachPart);
                invalid = false;
            }
            if (invalid) {
                return false;
            }
        }

        return listStyleType != null || listStylePosition != null
                || listStyleImage != 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 ListStyleImage getListStyleImage() {
        return listStyleImage;
    }

    public ListStylePosition getListStylePosition() {
        return listStylePosition;
    }

    public ListStyleType getListStyleType() {
        return listStyleType;
    }

    public ListStyle setListStyleType(final ListStyleType listStyleType) {

        final StringBuilder cssValueBuilder = new StringBuilder();

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

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

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

        this.listStyleType = listStyleType;

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

    public ListStyle setListStylePosition(
            final ListStylePosition listStylePosition) {

        final StringBuilder cssValueBuilder = new StringBuilder();

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

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

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

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

        this.listStylePosition = listStylePosition;

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

    public ListStyle setListStyleImage(final ListStyleImage listStyleImage) {

        final StringBuilder cssValueBuilder = new StringBuilder();

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

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

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

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

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

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

        this.listStyleImage = listStyleImage;

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

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

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

            final ListStyleImage listStyleImage = (ListStyleImage) stateChangedObject;

            final String cssValue = listStyleImage.getCssValue();

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

            setListStyleImage(listStyleImage);
        }
    }

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