public abstract class BackgroundImage
{
    private static Logger log = LoggerFactory.getLogger(BackgroundImage.class);

    private CSSProperty.BackgroundPosition position;
    private CSSProperty.BackgroundRepeat repeat;
    private CSSProperty.BackgroundAttachment attachment;
    private CSSProperty.BackgroundOrigin origin;
    private CSSProperty.BackgroundSize size;
    private TermList positionValues;
    private TermList sizeValues;

    private ElementBox owner;
    
    //the coordinates of the image within the element
    private float imgx;
    private float imgy;
    private float imgw;
    private float imgh;
    private boolean repeatx;
    private boolean repeaty;
    
    
    /**
     * Ceates a new background image with the specified CSS property values.
     * @param owner
     * @param url
     * @param position
     * @param positionValues
     * @param repeat
     * @param attachment
     * @param origin
     * @param size
     * @param sizeValues
     */
    public BackgroundImage(ElementBox owner, URL url, BackgroundPosition position, TermList positionValues, 
                            BackgroundRepeat repeat, BackgroundAttachment attachment, BackgroundOrigin origin,
                            BackgroundSize size, TermList sizeValues)
    {
        setOwner(owner);
        this.position = position;
        this.positionValues = positionValues;
        this.size = size;
        this.sizeValues = sizeValues;
        this.repeat = repeat;
        this.attachment = attachment;
        this.origin = origin;
        repeatx = (repeat == BackgroundRepeat.REPEAT || repeat == BackgroundRepeat.REPEAT_X);
        repeaty = (repeat == BackgroundRepeat.REPEAT || repeat == BackgroundRepeat.REPEAT_Y);
    }

    /**
     * Obtains the element box the background belongs to.
     * @return
     */
    public ElementBox getOwner()
    {
        return owner;
    }
    
    public void setOwner(ElementBox owner)
    {
        this.owner = owner;
    }

    /**
     * Obtains the used value of the CSS <code>background-position</code> property
     * for this image.
     * @return
     */
    public CSSProperty.BackgroundPosition getPosition()
    {
        return position;
    }

    /**
     * Obtains the used value of the CSS <code>background-repeat</code> property.
     * for this image.
     * @return
     */
    public CSSProperty.BackgroundRepeat getRepeat()
    {
        return repeat;
    }

    /**
     * Obtains the used value of the CSS <code>background-attachment</code> property.
     * for this image.
     * @return
     */
    public CSSProperty.BackgroundAttachment getAttachment()
    {
        return attachment;
    }
    
    /**
     * Obtains the used value of the CSS <code>background-origin</code> property.
     * for this image.
     * @return
     */
    public CSSProperty.BackgroundOrigin getOrigin()
    {
        return origin;
    }

    /**
     * Obtains the used value of the CSS <code>background-size</code> property.
     * for this image.
     * @return
     */
    public CSSProperty.BackgroundSize getSize()
    {
        return size;
    }
    
    /**
     * Computes the coordinates of the image within the owner element border box.
     * @return the coordinates within the owning element border box
     */
    public Rectangle getComputedPosition()
    {
        computeCoordinates(getOwner());
        return new Rectangle(imgx, imgy, imgw, imgh);
    }

    /**
     * Computes the coordinates of the image within the specified element border box.
     * @return the coordinates within the owning element border box
     */
    public Rectangle getComputedPosition(ElementBox contextBox)
    {
        computeCoordinates(contextBox);
        return new Rectangle(imgx, imgy, imgw, imgh);
    }

    /**
     * Obtains the used value of the repeat-x CSS property.
     * @return <code>true</code> if the image is repeated in the X direction
     */
    public boolean isRepeatX()
    {
        return repeatx;
    }

    /**
     * Obtains the used value of the repeat-y CSS property.
     * @return <code>true</code> if the image is repeated in the Y direction
     */
    public boolean isRepeatY()
    {
        return repeaty;
    }

    //===========================================================================
    
    /**
     * Obtains the intrinsic width of the image.
     * @return the intrinsic width
     */
    public abstract float getIntrinsicWidth();

    /**
     * Checks whether the image has an intrinsic width.
     * @return {@code true} when the image has an intrinsic width
     */
    public abstract boolean hasIntrinsicWidth();
    
    /**
     * Obtains the intrinsic height of the image.
     * @return the intrinsic height
     */
    public abstract float getIntrinsicHeight();

    /**
     * Checks whether the image has an intrinsic height.
     * @return {@code true} when the image has an intrinsic height
     */
    public abstract boolean hasIntrinsicHeight();
    
    /**
     * Obtains the intrinsic aspect ratio of the image (width / height).
     * @return the intrinsic aspect ratio
     */
    public abstract float getIntrinsicRatio();
    
    /**
     * Checks whether the image has an intrinsic ratio.
     * @return {@code true} when the image has an intrinsic ratio
     */
    public abstract boolean hasIntrinsicRatio();
    
    //===========================================================================
    
    protected float getAutoWidth(Rectangle bounds)
    {
        return hasIntrinsicWidth() ? getIntrinsicWidth() : bounds.width;
    }
    
    protected float getAutoHeight(Rectangle bounds)
    {
        return hasIntrinsicHeight() ? getIntrinsicHeight() : bounds.height;
    }
    
    //===========================================================================
    
    /**
     * Gets the computed X coordinate of the image within the owner element border box.
     * @return the x coordinate
     */
    public float getImgX()
    {
        return imgx;
    }

    /**
     * Gets the computed Y coordinate of the image within the owner element border box.
     * @return the y coordinate
     */
    public float getImgY()
    {
        return imgy;
    }
    
    /**
     * Gets the computed image width.
     * @return the image width
     */
    public float getImgWidth()
    {
        return imgw;
    }

    /**
     * Gets the computed image height.
     * @return the image height
     */
    public float getImgHeight()
    {
        return imgh;
    }

    /**
     * Computes the image coordinates within the border box of the element according to the origin.
     */
    protected void computeCoordinates(ElementBox contextBox)
    {
        switch (getOrigin())
        {
            case BORDER_BOX:
                computeCoordinates(contextBox.getAbsoluteBorderBounds(), contextBox);
                break;
            case CONTENT_BOX:
                computeCoordinates(contextBox.getAbsoluteContentBounds(), contextBox);
                imgx += contextBox.getBorder().left + contextBox.getPadding().left; //recompute to border box
                imgy += contextBox.getBorder().top + contextBox.getPadding().top;
                break;
            default: //PADDING_BOX
                computeCoordinates(contextBox.getAbsolutePaddingBounds(), contextBox);
                imgx += contextBox.getBorder().left; //recompute to border box
                imgy += contextBox.getBorder().top;
                break;
        }
    }
    
    protected void computeCoordinates(Rectangle bounds, ElementBox contextBox)
    {
        CSSDecoder dec = new CSSDecoder(contextBox.getVisualContext());
        computeSize(bounds, dec);
        
        //X position
        if (position == BackgroundPosition.LEFT)
            imgx = 0;
        else if (position == BackgroundPosition.RIGHT)
            imgx = bounds.width - imgw;
        else if (position == BackgroundPosition.CENTER)
            imgx = (bounds.width - imgw) / 2;
        else if (position == BackgroundPosition.list_values)
        {
            imgx = dec.getLength((TermLengthOrPercent) positionValues.get(0), false, 0, 0, bounds.width - imgw);
        }
        else
            imgx = 0;
        
        //Y position
        if (position == BackgroundPosition.TOP)
            imgy = 0;
        else if (position == BackgroundPosition.BOTTOM)
            imgy = bounds.height - imgh;
        else if (position == BackgroundPosition.CENTER)
            imgy = (bounds.height - imgh) / 2;
        else if (position == BackgroundPosition.list_values)
        {
            int i = positionValues.size() > 1 ? 1 : 0;
            imgy = dec.getLength((TermLengthOrPercent) positionValues.get(i), false, 0, 0, bounds.height - imgh);
        }
        else
            imgy = 0;
    }

    protected void computeSize(Rectangle bounds, CSSDecoder dec)
    {
        if (size == BackgroundSize.COVER)
        {
            if (hasIntrinsicRatio())
            {
                final float ir = getIntrinsicRatio();
                float w1 = bounds.width;
                float h1 = w1 / ir;
                float h2 = bounds.height;
                float w2 = h2 * ir;
                if (h1 - bounds.height > w2 - bounds.width)
                {
                    imgw = w1; imgh = h1;
                }
                else
                {
                    imgw = w2; imgh = h2;
                }
            }
            else
            {
                imgw = bounds.width;
                imgh = bounds.height;
            }
        }
        else if (size == BackgroundSize.CONTAIN)
        {
            if (hasIntrinsicRatio())
            {
                final float ir = getIntrinsicRatio();
                float w1 = bounds.width;
                float h1 = w1 / ir;
                float h2 = bounds.height;
                float w2 = h2 * ir;
                if (h1 - bounds.height < w2 - bounds.width)
                {
                    imgw = w1; imgh = h1;
                }
                else
                {
                    imgw = w2; imgh = h2;
                }
            }
            else
            {
                imgw = bounds.width;
                imgh = bounds.height;
            }
        }
        else if (size == BackgroundSize.list_values)
        {
            if (sizeValues == null) //no values provided: auto,auto is the default
            {
                imgw = getAutoWidth(bounds);
                imgh = getAutoHeight(bounds);
            }
            else if (sizeValues.size() == 2) //two values should be provided by jStyleParser
            {
                Term<?> w = sizeValues.get(0);
                Term<?> h = sizeValues.get(1);
                if (w instanceof TermLengthOrPercent && h instanceof TermLengthOrPercent)
                {
                    imgw = dec.getLength((TermLengthOrPercent) w, false, 0, 0, bounds.width);                    
                    imgh = dec.getLength((TermLengthOrPercent) h, false, 0, 0, bounds.height);                    
                }
                else if (w instanceof TermLengthOrPercent)
                {
                    imgw = dec.getLength((TermLengthOrPercent) w, false, 0, 0, bounds.width);
                    if (hasIntrinsicRatio())
                        imgh = Math.round(imgw / getIntrinsicRatio());
                    else
                        imgh = getAutoHeight(bounds);
                }
                else if (h instanceof TermLengthOrPercent)
                {
                    imgh = dec.getLength((TermLengthOrPercent) h, false, 0, 0, bounds.height);
                    if (hasIntrinsicRatio())
                        imgw = Math.round(imgh * getIntrinsicRatio());
                    else
                        imgw = getAutoWidth(bounds);
                }
                else
                {
                    imgw = getAutoWidth(bounds);
                    imgh = getAutoHeight(bounds);
                }
            }
            else //this should not happen
            {
                log.error("Invalid number BackgroundSize values: {}", sizeValues);
                imgw = getAutoWidth(bounds);
                imgh = getAutoHeight(bounds);
            }
        }
        
    }
    
} public class CSSDecoder 
{
    protected static final Logger log = LoggerFactory.getLogger(CSSDecoder.class);

    private VisualContext context;
    
    /**
     * Creates a new VisualDecoder in the specified context. The context is
     * used for the unit conversion (e.g. the <code>em</code> units etc.)
     * @param c the visual contect to be assigned  
     */
    public CSSDecoder(VisualContext c)
    {
        context = c;
    }
    
    /**
     * Returns the visual context of this decoder.
     * @return the assigned visual context
     */
    public VisualContext getContext()
    {
        return context;
    }

    /**
     * Changes the visual context assigned to this decoder.
     * @param c The new visual context to be assigned
     */
    public void setContext(VisualContext c)
    {
        context = c;
    }
    
    /** 
     * Returns the length in pixels from a CSS definition. <code>null</code> values
     * of the lengths are interpreted as zero.
     * @param value The length or percentage value to be converted
     * @param auto True, if the property is set to <code>auto</code>
     * @param defval The length value to be used when the first one is null
     * @param autoval The value to be used when "auto" is specified
     * @param whole the length to be returned as 100% (in case of percentage values)
     */
    public float getLength(TermLengthOrPercent value, boolean auto, TermLengthOrPercent defval, TermLengthOrPercent autoval, float whole)
    {
        TermLengthOrPercent val = value;
        if (value == null) val = defval;
        if (auto) val = autoval;
        if (val != null)
            return context.pxLength(val, whole);
        else
        	return 0;
    }
    
    /** 
     * Returns the length in pixels from a CSS definition
     * @param value The length or percentage value to be converted
     * @param auto True, if the property is set to <code>auto</code>
     * @param defval The length value to be used when the first one is null
     * @param autoval The value to be used when "auto" is specified
     * @param whole the length to be returned as 100% (in case of percentage values)
     */
    public float getLength(TermLengthOrPercent value, boolean auto, float defval, float autoval, float whole)
    {
        if (auto)
            return autoval;
        else if (value == null)
            return defval;
        else
            return context.pxLength(value, whole);
    }
    
    /**
     * Converts an angle from a CSS angle to 'rad'.
     * @param spec the CSS angle specification
     * @return the corresponding angle in radians
     */
    public double getAngle(TermAngle angle)
    {
        return context.radAngle(angle);
    }
    
    /**
     * Computes the width and height of a replaced object based on the following properties:
     * <ul>
     * <li>Intrinsic width and height</li>
     * <li>The <code>width</code> and <code>height</code> attributes</li>
     * <li>Effective style</li>
     * </ul>
     * @param obj The replaced content object
     * @param box The element box whose size should be computed
     * @return A rectangle with the width and height set accordingly
     */
    public static Rectangle computeReplacedObjectSize(ReplacedContent obj, ElementBox box)
    {
        float boxw; //resulting size
        float boxh;
        
        float intw; //intrinsic sizes
        float inth;
        float intr;
        if (obj != null)
        {
            intw = obj.getIntrinsicWidth();
            inth = obj.getIntrinsicHeight();
            if (intw == 0 || inth == 0)
            {
                log.warn("Obtained a zero intrinsic width or height for " + obj.toString());
                intw = inth = 1; //a fallback for avoiding zeros in ratios
            }
            intr = (float) intw / inth;
            boxw = intw;
            boxh = inth;
        }
        else
        {
            boxw = intw = 20; //some reasonable default values
            boxh = inth = 20;
            intr = 1.0f;
        }
        
        Rectangle cb = box.getContainingBlock();
        
        //decode the width/height attributes
        Element el = box.getElement();
        float atrw = -1;
        float atrh = -1;
        try {
            if (!HTMLNorm.getAttribute(el, "width").equals(""))
            {
                atrw = HTMLNorm.computeAttributeLength(HTMLNorm.getAttribute(el, "width"), cb.width);
            }
        } catch (NumberFormatException e) {
            log.info("Invalid width value: " + HTMLNorm.getAttribute(el, "width"));
        }
        try {
            if (!HTMLNorm.getAttribute(el, "height").equals(""))
            {
                //Total heights used for percentages -- find the nearest containing block with a fixed height
                //This is a quirks mode behavior (percentages in attributes should not be used in standards mode).
                Box cbox = box.getContainingBlockBox();
                while (cbox != null && !cbox.hasFixedHeight() && !(cbox instanceof Viewport))
                    cbox = cbox.getContainingBlockBox();
                atrh = HTMLNorm.computeAttributeLength(HTMLNorm.getAttribute(el, "height"), cbox.getContentHeight());
            }
        } catch (NumberFormatException e) {
            log.info("Invalid height value: " + HTMLNorm.getAttribute(el, "width"));
        }

        //use standard CSS2.1 computation of the containing block height
        final float twidth = cb.width;
        final float theight = cb.height;
        //compute dimensions from styles (styles should override the attributes)
        CSSDecoder dec = new CSSDecoder(box.getVisualContext());
        CSSProperty.Width width = box.getStyle().getProperty("width");
        TermLengthOrPercent vWidth = null;
        if (width == CSSProperty.Width.AUTO)
            width = null; //auto and null are equal for width
        if (width != null)
            vWidth = box.getLengthValue("width");
        
        CSSProperty.Height height = box.getStyle().getProperty("height");
        TermLengthOrPercent vHeight = null;
        if (height == CSSProperty.Height.AUTO)
            height = null; //auto and null are equal for height
        if (height != null)
        {
            vHeight = box.getLengthValue("height");
            if (vHeight.isPercentage() && !box.getContainingBlockBox().hasFixedHeight())
                height = null; //the percentage heights are treated as 'auto' when the containing block height is not set
        }
        
        if (width == null && height == null && atrw == -1 && atrh == -1)
        {
            final float[] result = applyCombinedLimits(boxw, boxh, box, dec, twidth, theight);
            boxw = result[0];
            boxh = result[1];
        }
        else if (width == null && atrw == -1 && (height != null || atrh != -1))
        {
            //compute boxh, boxw is intrinsic
            boxh = dec.getLength(vHeight, false, atrh, 0, theight);
            boxh = applyHeightLimits(boxh, box, dec, theight);
            //boxw intrinsic value
            boxw = intr * boxh;
            boxw = applyWidthLimits(boxw, box, dec, twidth);
        }
        else if ((width != null || atrw != -1) && height == null && atrh == -1)
        {
            //compute boxw, boxh is intrinsic
            boxw = dec.getLength(vWidth, false, atrw, 0, twidth);
            boxw = applyWidthLimits(boxw, box, dec, twidth);
            //boxh intrinsic value
            boxh = boxw / intr;
            boxh = applyHeightLimits(boxh, box, dec, theight);
        }
        else
        {
            boxw = dec.getLength(vWidth, false, atrw, 0, twidth);
            boxw = applyWidthLimits(boxw, box, dec, twidth);
            boxh = dec.getLength(vHeight, false, atrh, 0, theight);
            boxh = applyHeightLimits(boxh, box, dec, theight);
        }
        
        return new Rectangle(boxw, boxh);
    }

    public static float applyWidthLimits(float width, ElementBox box, CSSDecoder dec, float twidth)
    {
        float ret = width;
        if (twidth < 0) twidth = 0; //if the containing block's width is negative, the used value is zero.
        CSSProperty.MaxWidth max = box.getStyle().getProperty("max-width");
        if (max != null && max != CSSProperty.MaxWidth.NONE)
        {
            final float maxval = dec.getLength(box.getLengthValue("max-width"), false, -1, -1, twidth);
            if (ret > maxval)
                ret = maxval;
        }
        CSSProperty.MinWidth min = box.getStyle().getProperty("min-width");
        if (min != null)
        {
            final float minval = dec.getLength(box.getLengthValue("min-width"), false, -1, -1, twidth);
            if (ret < minval)
                ret = minval;
        }
        return ret;
    }
    
    public static float applyHeightLimits(float height, ElementBox box, CSSDecoder dec, float theight)
    {
        float ret = height;
        ElementBox cbox = box.getContainingBlockBox();
        CSSProperty.MaxHeight max = box.getStyle().getProperty("max-height");
        if (max != null && max != CSSProperty.MaxHeight.NONE)
        {
            final TermLengthOrPercent val = box.getLengthValue("max-height");
            if (val != null && !(val.isPercentage() && !cbox.hasFixedHeight()))
            {
                final float maxval = dec.getLength(val, false, -1, -1, theight);
                if (ret > maxval)
                    ret = maxval;
            }
        }
        CSSProperty.MinHeight min = box.getStyle().getProperty("min-height");
        if (min != null)
        {
            final TermLengthOrPercent val = box.getLengthValue("min-height");
            if (val != null && !(val.isPercentage() && !cbox.hasFixedHeight()))
            {
                final float minval = dec.getLength(box.getLengthValue("min-height"), false, -1, -1, theight);
                if (ret < minval)
                    ret = minval;
            }
        }
        return ret;
    }
    
    public static float[] applyCombinedLimits(float w, float h, ElementBox box, CSSDecoder dec, float twidth, float theight)
    {
        ElementBox cbox = box.getContainingBlockBox();
        //decode min/max values from the style
        final CSSProperty.MinWidth pminw = box.getStyle().getProperty("min-width");
        final CSSProperty.MaxWidth pmaxw = box.getStyle().getProperty("max-width");
        final CSSProperty.MinHeight pminh = box.getStyle().getProperty("min-height");
        final CSSProperty.MaxHeight pmaxh = box.getStyle().getProperty("max-height");
        
        final float minw, maxw, minh, maxh;
        if (pminw != null)
            minw = dec.getLength(box.getLengthValue("min-width"), false, 0, 0, twidth);
        else
            minw = 0;
        
        if (pmaxw != null && pmaxw != CSSProperty.MaxWidth.NONE)
            maxw = Math.max(minw, dec.getLength(box.getLengthValue("max-width"), false, 0, 0, twidth)); //maxw >= minw musth hold
        else
            maxw = Integer.MAX_VALUE;
        
        if (pminh != null)
        {
            TermLengthOrPercent val = box.getLengthValue("min-height");
            if (val != null && !(val.isPercentage() && !cbox.hasFixedHeight()))
                minh = dec.getLength(val, false, 0, 0, theight);
            else
                minh = 0;
        }
        else
            minh = 0;
        
        if (pmaxh != null && pmaxh != CSSProperty.MaxHeight.NONE)
        {
            TermLengthOrPercent val = box.getLengthValue("max-height");
            if (val != null && !(val.isPercentage() && !cbox.hasFixedHeight()))
                maxh = Math.max(minh, dec.getLength(val, false, 0, 0, theight));
            else
                maxh = Integer.MAX_VALUE;
        }
        else
            maxh = Integer.MAX_VALUE;
        
        //apply the CSS2 constraints from the table
        //http://www.w3.org/TR/CSS21/visudet.html#min-max-widths
        final float hwr = (float) h / w;
        final float whr = (float) w / h;
        final float retw, reth;
        if (w > maxw && h < minh) 
        { 
            retw = maxw;
            reth = minh; 
        }
        else if (w < minw && h > maxh)
        { 
            retw = minw;
            reth = maxh; 
        }
        else if (w < minw && h < minh)
        {
            if (minw / w > minh / h)
            {
                retw = minw;
                reth = Math.min(maxh, minw * hwr);
            }
            else
            {
                retw = Math.min(maxw, minh * whr);
                reth = minh;
            }
        }
        else if (w > maxw && h > maxh)
        {
            if (maxw / w > maxh / h)
            {
                retw = Math.max(minw, maxh * whr);
                reth = maxh;
            }
            else
            {
                retw = maxw;
                reth = Math.max(minh, maxw * hwr);
            }
        }
        else if (h < minh)
        {
            retw = Math.min(minh * whr, maxw);
            reth = minh;
        }
        else if (h > maxh)
        {
            retw = Math.max(maxh * whr, minw);
            reth = maxh;
        }
        else if (w < minw)
        {
            retw = minw;
            reth = Math.min(minw * hwr, maxh);
        }
        else if (w > maxw)
        {
            retw = maxw;
            reth = Math.max(maxw * hwr, minh);
        }
        else
        {
            retw = w;
            reth = h;
        }
        
        return new float[]{retw, reth};
    }
    
} abstract public class ElementBox extends Box
{
    private static Logger log = LoggerFactory.getLogger(ElementBox.class);
    
    public static final CSSProperty.Display DISPLAY_ANY = null;
    public static final CSSProperty.Display DISPLAY_NONE = CSSProperty.Display.NONE;
    public static final CSSProperty.Display DISPLAY_INLINE = CSSProperty.Display.INLINE;
    public static final CSSProperty.Display DISPLAY_BLOCK = CSSProperty.Display.BLOCK;
    public static final CSSProperty.Display DISPLAY_LIST_ITEM = CSSProperty.Display.LIST_ITEM;
    public static final CSSProperty.Display DISPLAY_RUN_IN = CSSProperty.Display.RUN_IN;
    public static final CSSProperty.Display DISPLAY_INLINE_BLOCK = CSSProperty.Display.INLINE_BLOCK;
    public static final CSSProperty.Display DISPLAY_TABLE = CSSProperty.Display.TABLE;
    public static final CSSProperty.Display DISPLAY_INLINE_TABLE = CSSProperty.Display.INLINE_TABLE;
    public static final CSSProperty.Display DISPLAY_TABLE_ROW_GROUP = CSSProperty.Display.TABLE_ROW_GROUP;
    public static final CSSProperty.Display DISPLAY_TABLE_HEADER_GROUP = CSSProperty.Display.TABLE_HEADER_GROUP;
    public static final CSSProperty.Display DISPLAY_TABLE_FOOTER_GROUP = CSSProperty.Display.TABLE_FOOTER_GROUP;
    public static final CSSProperty.Display DISPLAY_TABLE_ROW = CSSProperty.Display.TABLE_ROW;
    public static final CSSProperty.Display DISPLAY_TABLE_COLUMN_GROUP = CSSProperty.Display.TABLE_COLUMN_GROUP;
    public static final CSSProperty.Display DISPLAY_TABLE_COLUMN = CSSProperty.Display.TABLE_COLUMN;
    public static final CSSProperty.Display DISPLAY_TABLE_CELL = CSSProperty.Display.TABLE_CELL;
    public static final CSSProperty.Display DISPLAY_TABLE_CAPTION = CSSProperty.Display.TABLE_CAPTION;
    
    public static final CSSProperty.Transform TRANSFORM_NONE = CSSProperty.Transform.NONE;
    
    public static final CSSProperty.Position POS_STATIC = CSSProperty.Position.STATIC;
    public static final CSSProperty.Position POS_RELATIVE = CSSProperty.Position.RELATIVE;
    public static final CSSProperty.Position POS_ABSOLUTE = CSSProperty.Position.ABSOLUTE;
    public static final CSSProperty.Position POS_FIXED = CSSProperty.Position.FIXED;
    
    public static final CSSProperty.WhiteSpace WHITESPACE_NORMAL = CSSProperty.WhiteSpace.NORMAL;
    public static final CSSProperty.WhiteSpace WHITESPACE_PRE = CSSProperty.WhiteSpace.PRE;
    public static final CSSProperty.WhiteSpace WHITESPACE_NOWRAP = CSSProperty.WhiteSpace.NOWRAP;
    public static final CSSProperty.WhiteSpace WHITESPACE_PRE_WRAP = CSSProperty.WhiteSpace.PRE_WRAP;
    public static final CSSProperty.WhiteSpace WHITESPACE_PRE_LINE = CSSProperty.WhiteSpace.PRE_LINE;
    
    /** Default line height if nothing or 'normal' is specified */
    private static final float DEFAULT_LINE_HEIGHT = 1.12f;
    
    /** Assigned element */
    protected Element el;

    /** First DOM child node index covered by this box (inclusive) */
    protected int firstDOMChild;
    
    /** First DOM child node index covered by this box (exclusive) */
    protected int lastDOMChild;
    
    /** Pre-created box to be added to this box before the DOM nodes are processed. Used during the box tree creation only. */
    protected Box preadd;
    
    /** Other boxes to be added to the tree after this one. Used during the box tree creation only. */
    protected Vector<Box> postadd;
    
    /** Current DOM child during the tree creation */
    protected BoxTreeCreationStatus curstat;
    
    /** Previous copy of the same box if the box has been split */
    protected ElementBox previousTwin;
    
    /** Next copy of the same box if the box has been split */
    protected ElementBox nextTwin;
    
    /** The style of the node (for element nodes only) with no pseudo classes */
    protected NodeData style;
    
    /** Efficient styles for the pseudo classes */
    protected Map<Selector.PseudoClassType, NodeData> pseudoStyle;
    
    /** Set to true when the element box contains only text boxes */
    protected boolean textonly;
    
    /** The map of related pseudo-elements (if any) */
    protected Map<Selector.PseudoElementType, ElementBox> pseudoElements;
    
    //============================== Computed style ======================
    
    /** The display property value */
    protected CSSProperty.Display display;
    
    /** Tansform property value (output transformations) */
    protected CSSProperty.Transform transform;
    
    /** Position property */
    protected CSSProperty.Position position;
    
    /** Position coordinates */
    protected LengthSet coords;
    
    /** The top position coordinate is set explicitely */
    protected boolean topset;
    
    /** The top position coordinate is set explicitely */
    protected boolean leftset;
    
    /** The top position coordinate is set explicitely */
    protected boolean bottomset;
    
    /** The top position coordinate is set explicitely */
    protected boolean rightset;
    
    /** The white-space property value */
    protected CSSProperty.WhiteSpace whitespace;
    
    /** Margin widths */
    protected LengthSet margin;
    
    /** Effective top and bottom margins (after collapsing with the contained boxes) */
    protected LengthSet emargin;
    
    /** Padding widths */
    protected LengthSet border;
    
    /** Border widths */
    protected LengthSet padding;
    
    /** Content sizes */
    protected Dimension content;
    
    /** Minimal absolute bounds. */
    protected Rectangle minAbsBounds;
    
    /** the computed value of line-height */
    protected float lineHeight;
    
    /** the z-index flag: true when z-index is different from <code>auto</code> */
    protected boolean zset;
    
    /** the z-index value when set */
    protected int zIndex;
    
    //============================== Child boxes ======================
    
    /** First valid child */
    protected int startChild;
    
    /** Last valid child (excl) */
    protected int endChild;

    /** A list of nested boxes (possibly empty). The box can contain either 
     * only block boxes or only inline boxes. The inline boxes can only
     * contain inline boxes */
    protected Vector<Box> nested;
    
    /** Corresponding stacking context if this box creates one. */
    protected StackingContext scontext;
    
    //=======================================================================
    
    /**
     * Creates a new element box from a DOM element
     * @param n the DOM element
     * @param g current graphics context
     * @param ctx current visual context
     */
    public ElementBox(Element n, VisualContext ctx)
    {
        super(n, ctx);
        minAbsBounds = null;
        style = null;
        pseudoStyle = new HashMap<>();
        if (n != null)
        {
	        el = n;
	        firstDOMChild = 0;
	        lastDOMChild = n.getChildNodes().getLength();
	        previousTwin = null;
	        nextTwin = null;
	        
	        nested = new Vector<Box>();
	        pseudoElements = new HashMap<>();
	        startChild = 0;
	        endChild = 0;
	        isblock = false;
	        textonly = true;
        }
        transform = TRANSFORM_NONE;
        position = POS_STATIC;
        topset = false;
        leftset = false;
        bottomset = false;
        rightset = false;
    }
    
    /**
     * Copy the values from another element box.
     * @param src source element box
     */ 
    public void copyValues(ElementBox src)
    {
        super.copyValues(src);
        nested.addAll(src.nested);
        textonly = src.textonly;
        pseudoElements = new HashMap<>(src.pseudoElements);
        style = src.style; 
        pseudoStyle = new HashMap<>(src.pseudoStyle);
        startChild = src.startChild;
        endChild = src.endChild;
        isblock = src.isblock;
        style = src.style;
        display = src.display;
        lineHeight = src.lineHeight;
        whitespace = src.whitespace;
        transform = src.transform;
        position = src.position;
        topset = src.topset;
        leftset = src.leftset;
        bottomset = src.bottomset;
        rightset = src.rightset;
        
        if (src.coords != null)
            coords = new LengthSet(src.coords);
        
        if (src.margin != null)
            margin = new LengthSet(src.margin);
        if (src.emargin != null)
            emargin = new LengthSet(src.emargin);
        if (src.border != null)
            border = new LengthSet(src.border);
        if (src.padding != null)
            padding = new LengthSet(src.padding);
        if (src.content != null)
            content = new Dimension(src.content);
    }
    
    /** Create a new box from the same DOM node in the same context */
    abstract public ElementBox copyBox();
    
    @Override
    public void initSubtree()
    {
        initBox();
        loadSizes();
        
        for (int i = 0; i < getSubBoxNumber(); i++)
            getSubBox(i).initSubtree();
        
        computeEfficientMargins();
    }
    
    //=======================================================================
    
    /**
     * @return the corresponding DOM element
     */
    public Element getElement()
    {
        return el;
    }
    
    /**
     * Indicates whether the backgound and borders should be rendered for this element.
     * This is true for normal elements but it is false e.g. for table row and body
     * elements
     * @return {@code true} when the background should be rendered
     */
    public boolean rendersBackground()
    {
        return true;
    }
    
    /**
     * Assign a new style to this box
     * @param s the new style declaration
     */
    public void setStyle(NodeData s)
    {
    	style = s;
    	loadBasicStyle();
    }
    
    /**
     * Returns the style of the DOM node that forms this box.
     * @return the style declaration
     */
    public NodeData getStyle()
    {
        return style;
    }
    
    /**
     * Obtains the string representation of the current style of the box in the CSS syntax.
     * @return The style string representation.
     */
    public String getStyleString()
    {
        if (style != null)
            return style.toString();
        else
            return "";
    }
    
    /**
     * Obtains the string value of the position: property
     * @return The string like "static", "absolute", "relative" or "fixed"
     */
    public String getPositionString()
    {
        return position.toString();
    }
    
    /**
     * Reads the string value of the specified property of the element style.
     * @param property the property name
     * @return the property value
     */
    public String getStylePropertyValue(String property)
    {
        Object t = style.getValue(Term.class, property);
        if (t == null)
            return "";
        else
            return t.toString();
    }
    
    /**
     * Reads a CSS length value of a style property of the box.
     * @param name property name
     * @return the length value
     */
    public TermLengthOrPercent getLengthValue(String name)
    {
        if (style != null)
            return style.getValue(TermLengthOrPercent.class, name);
        else
            return null;
    }
    
    /**
     * Reads the value of a border width specified by a CSS property.
     * @param dec a CSS decoder used for converting the values
     * @param property the property name, e.g. "border-top-width"
     * @return the border width in pixels
     */
    public float getBorderWidth(CSSDecoder dec, String property)
    {
        if (style != null)
        {
            CSSProperty.BorderWidth prop = style.getProperty(property);
            if (prop == CSSProperty.BorderWidth.length)
            {
                TermLengthOrPercent w = style.getValue(TermLengthOrPercent.class, property);
                if (w == null)
                    return CSSUnits.MEDIUM_BORDER;
                else
                {
                    float pxw = dec.getContext().pxLength(w);
                    if (pxw > 0 && pxw < 1)
                        return 1.0f; //thinner borders should be drawn with 1px line
                    else
                        return pxw;
                }
            }
            else
                return CSSUnits.convertBorderWidth(prop);
        }
        else
            return 0;
    }
    
    /**
     * Returns the value of the display property
     * @return One of the ElementBox.DISPLAY_XXX constants
     */
    public CSSProperty.Display getDisplay()
    {
    	return display;
    }
    
    public String getDisplayString()
    {
        if (display != null)
            return display.toString();
        else
            return "";
    }
    
    /**
     * Returns the value of the white-space property
     * @return one of the ElementBox.WHITESPACE_XXX constants
     */
    public CSSProperty.WhiteSpace getWhiteSpace()
    {
        return whitespace;
    }
    
    /**
     * Checks whether the whitespaces should be collapsed within in the element according to its style.
     * @return <code>true</code> if the whitespace sequences should be collapsed.
     */
    @Override
    public boolean collapsesSpaces()
    {
        return (whitespace != WHITESPACE_PRE && whitespace != WHITESPACE_PRE_WRAP);
    }
    
    /**
     * Checks whether this element should preserve the line breaks according to its style.
     * @return <code>true</code> when the line breaks should be preserved
     */
    @Override
    public boolean preservesLineBreaks()
    {
        return (whitespace != WHITESPACE_NORMAL && whitespace != WHITESPACE_NOWRAP);
    }
    
    /**
     * Checks whether this box allows line wrapping on whitespaces according to the whit-space setting.
     * @return <code>true</code> when line wrapping is allowed
     */
    @Override
    public boolean allowsWrapping()
    {
        return (whitespace == ElementBox.WHITESPACE_NORMAL
                || whitespace == ElementBox.WHITESPACE_PRE_WRAP
                || whitespace== ElementBox.WHITESPACE_PRE_LINE);
    }
    
    /**
     * Obtains the efficient background color to be used for drawing the background. This is a convenience
     * method for obtaining the color only. Other background properties such as images should
     * be obtained using {@link BackgroundDecoder}.
     * @return the background color or null when transparent
     */
    public Color getBgcolor()
    {
        CSSProperty.BackgroundColor bg = style.getProperty("background-color");
        if (bg == CSSProperty.BackgroundColor.color)
        {
            TermColor bgc = style.getSpecifiedValue(TermColor.class, "background-color");
            if (bgc.isTransparent())
                return null;
            else
                return bgc.getValue();
        }
        else
            return null;
    }
    
    /**
     * @return the number of subboxes in this box
     */
    public int getSubBoxNumber()
    {
        return nested.size();
    }

    /**
     * @return list of all the subboxes 
     */
    public List<Box> getSubBoxList()
    {
        return nested;
    }
    
    /**
     * @param index the sub box index in the range from 0 to n-1 
     * @return the appropriate sub box
     */
    public Box getSubBox(int index)
    {
        return nested.elementAt(index);
    }
    
    /**
     * Adds a new sub box to the end of the sub box list.
     * @param box the new sub box to add
     */
    public void addSubBox(Box box)
    {
        //find the last non-empty inline box to match white spaces
        Box last = null;
        int i = nested.size() - 1;
        while (i >= 0)
        {
            Box cand = nested.get(i);
            if (cand instanceof Inline && !((Inline) cand).collapsedCompletely())
            {
                last = cand;
                break;
            }
            i--;
        }
        //add the subbox
        box.setParent(this);
        nested.add(box);
        endChild++;
        if (isDisplayed() && !box.isEmpty())
            isempty = false;
        if (!(box instanceof TextBox))
            textonly = false;
        //collapse initiall white spaces when necessary
        if (last != null && last.collapsesSpaces() && last.endsWithWhitespace())
            box.setIgnoreInitialWhitespace(true);
    }
    
    /**
     * Removes a sub box from the subbox list
     * @param box the new sub box to add
     */
    public void removeSubBox(Box box)
    {
        if (nested.remove(box))
            endChild--;
    }
    
    /**
     * Removes all sub boxes from the subbox list
     */
    public void removeAllSubBoxes()
    {
        nested.removeAllElements();
        endChild = 0;
    }
    
    /**
     * Inserts a new sub box before a specified sub box
     * @param where the box already existing in the list
     * @param what the new box to add
     */
    public void insertSubBoxBefore(Box where, Box what)
    {
        int pos = nested.indexOf(where);
        nested.insertElementAt(what, pos);
        endChild++;
    }

    /**
     * Inserts a new sub box after a specified sub box
     * @param where the box already existing in the list
     * @param what the new box to add
     */
    public void insertSubBoxAfter(Box where, Box what)
    {
        int pos = nested.indexOf(where);
        nested.insertElementAt(what, pos+1);
        endChild++;
    }

    /**
     * Inserts a new sub box at a specified index
     * @param index the index where the new box will be placed
     * @param what the new box to add
     */
    public void insertSubBox(int index, Box what)
    {
        nested.insertElementAt(what, index);
        endChild++;
    }
    
    /**
     * Obtains the stacking context if this box creates one.
     */
    public StackingContext getStackingContext()
    {
        if (scontext == null)
        {
            if (formsStackingContext())
                scontext = new StackingContext(this);
            else
                log.warn("getStackingContext: obtaining a stacking context from element that does not create one");
        }
        return scontext;
    }
    
    /**
     * Sets related pseudo-element boxes
     * @param pseudo the name of the pseudo-element
     * @param box the corresponding pseudo-element box
     */
    public void setPseudoElement(Selector.PseudoElementType pseudo, ElementBox box)
    {
        pseudoElements.put(pseudo, box);
    }
    
    /**
     * Gets the list of related pseudo-element boxes
     * @param pseudo the name of the pseudo-element
     * @return the related box
     */
    public ElementBox getPseudoElement(Selector.PseudoElementType pseudo)
    {
        return pseudoElements.get(pseudo);
    }
    
    /**
     * Checks whether the element box has a related pseudo-element box
     * @param pseudo the name of the pseudo-element
     * @return the related box
     */
    public boolean hasPseudoElement(Selector.PseudoElementType pseudo)
    {
        return pseudoElements.containsKey(pseudo);
    }
    
    /**
     * @return the width of the content without any margins and borders
     */
    public float getContentWidth()
    {
    	return content.width;
    }
    
    /**
     * @return the height of the content without any margins and borders
     */
    public float getContentHeight()
    {
    	return content.height;
    }
    
    /**
     * Obtains the computed value of the declared line height of the element.
     * @return the line height in pixels
     */
    public float getLineHeight()
    {
        return lineHeight;
    }

    /**
     * @return the margin sizes
     */
    public LengthSet getMargin()
    {
        return margin;
    }
    
    /**
     * @return the effective margin sizes (after collapsing)
     */
    public LengthSet getEMargin()
    {
        return emargin;
    }
    
    /**
     * Obtains the position coordinates (top, left, bottom, right properties)
     * @return the set of coordinates
     */
    public LengthSet getCoords()
    {
        return coords;
    }
    
    /**
     * @return the border sizes (0 when no border is displayed)
     */
    public LengthSet getBorder()
    {
        return border;
    }
    
    /**
     * @return the padding sizes
     */
    public LengthSet getPadding()
    {
        return padding;
    }
    
    /**
     * @return the content sizes
     */
    public Dimension getContent()
    {
        return content;
    }
    
    /**
     * Checks whether the z-index is non-auto for this box.
     * @return <code>true</code> when z-index has a value different form <code>auto</code>.
     */
    public boolean hasZIndex()
    {
        return zset;
    }
    
    /**
     * Obtains the declared z-index for this element.
     * @return the z-index value
     */
    public int getZIndex()
    {
        return zIndex;
    }
    
    /**
     * Check whether the element forms a new stacking context.
     * @return <code>true</code> when the element creates a new stacking context.
     */
    public boolean formsStackingContext()
    {
        return position != POS_STATIC || transform != TRANSFORM_NONE;
    }
    
    /**
     * @return the first child from the list that is considered for rendering
     */
    public int getStartChild()
    {
        return startChild;
    }
    
    /**
     * @param index the index of the first child from the list that is considered for rendering
     */
    public void setStartChild(int index)
    {
        startChild = index;
    }
    
    /**
     * @return the last child from the list that is considered for rendering (not included)
     */
    public int getEndChild()
    {
        return endChild;
    }
    
    /**
     * @param index the index of the last child from the list that is considered for rendering
     */
    public void setEndChild(int index)
    {
        endChild = index;
    }
    
    /**
     * Sets the parent of the valid children to this (used while splitting the boxes)
     */
    public void adoptChildren()
    {
        for (int i = startChild; i < endChild; i++)
            nested.elementAt(i).setParent(this);
    }
    
    //=======================================================================

    /**
     * Computes the distance of the content from the left edge of the whole box
     * (a convenience function for margin + border + padding).
     * @return the distance 
     */
    public float getContentOffsetX()
    {
        return margin.left + border.left + padding.left;
    }
    
    /**
     * Computes the distance of the content from the top edge of the whole box. 
     * (a convenience function for margin + border + padding).
     * @return the distance
     */
    public float getContentOffsetY()
    {
        return emargin.top + border.top + padding.top;
    }
    
    public float getContentX()
    {
        return bounds.x + emargin.left + border.left + padding.left;
    }
    
    public float getContentY()
    {
        return bounds.y + emargin.top + border.top + padding.top;
    }
    
    public float getAbsoluteContentX()
    {
        return absbounds.x + emargin.left + border.left + padding.left;
    }
    
    public float getAbsoluteContentY()
    {
        return absbounds.y + emargin.top + border.top + padding.top;
    }
    
    public float totalWidth()
    {
        return emargin.left + border.left + padding.left + content.width +
            padding.right + border.right + emargin.right;
    }
    
    //totalHeight() differs for inline and block boxes
    
    public float getAvailableContentWidth()
    {
        return availwidth - emargin.left - border.left - padding.left 
                  - padding.right - border.right - emargin.right;
    }
    
    @Override
    public Rectangle getMinimalAbsoluteBounds()
    {
    	if (minAbsBounds == null)
    		minAbsBounds = computeMinimalAbsoluteBounds();
    	return minAbsBounds;
    }
    
    private Rectangle computeMinimalAbsoluteBounds()
    {
    	float rx1 = 0, ry1 = 0, rx2 = 0, ry2 = 0;
    	boolean valid = false;
    	for (int i = startChild; i < endChild; i++)
		{
			Box sub = getSubBox(i);
			Rectangle sb = sub.getMinimalAbsoluteBounds();
			if (sub.isDisplayed() && sub.isVisible() && sb.width > 0 && sb.height > 0)
			{
				if (sb.x < rx1 || !valid) rx1 = sb.x;
				if (sb.y < ry1 || !valid) ry1 = sb.y;
				if (sb.x + sb.width > rx2 || !valid) rx2 = sb.x + sb.width;
				if (sb.y + sb.height > ry2 || !valid) ry2 = sb.y + sb.height;
				valid = true;
			}
		}
    	return new Rectangle(rx1, ry1, rx2 - rx1, ry2 - ry1);
    }
    
    @Override
    public boolean affectsDisplay()
    {
        boolean ret = !isEmpty();
        //non-zero top or bottom border
        if (border.top > 0 || border.bottom > 0)
            ret = true;
        //the same with padding
        if (padding.top > 0 || padding.bottom > 0)
            ret = true;
        
        return ret;
    }
    
    @Override
    public boolean isVisible()
    {
        return visible && clipblock.isVisible() && clipblock.visibleInClip(this);
    }
    
    /**
     * Checks whether a box is visible in the content area of this box when this is a clipping block.
     * @param box The box to test.
     * @return {@code true} when at least a part of the given box is inside of our clipping area.
     */
    public boolean visibleInClip(Box box)
    {
        if (box instanceof ElementBox)
        {
            //the margin is never visible - use the border bounds instead of the full bounds
            return getClippedContentBounds().intersects(((ElementBox) box).getAbsoluteBorderBounds());
        }
        else
        {
            return getClippedContentBounds().intersects(box.getAbsoluteBounds());
        }
    }
    
    /**
     * Returns the bounds of the padding edge (the content and padding)
     * @return a Rectangle representing the absolute padding bounds
     */
    public Rectangle getPaddingBounds()
    {
        return new Rectangle(bounds.x + emargin.left + border.left,
                             bounds.y + emargin.top + border.top,
                             content.width + padding.left + padding.right,
                             content.height + padding.top + padding.bottom);
    }

    /**
     * Returns the absolute bounds of the padding edge (the content and padding)
     * @return a Rectangle representing the absolute padding bounds
     */
    public Rectangle getAbsolutePaddingBounds()
    {
        return new Rectangle(absbounds.x + emargin.left + border.left,
                             absbounds.y + emargin.top + border.top,
                             content.width + padding.left + padding.right,
                             content.height + padding.top + padding.bottom);
    }

    /**
     * Returns the bounds of the border edge (the content, padding and border)
     * @return a Rectangle representing the absolute border bounds
     */
    public Rectangle getAbsoluteBorderBounds()
    {
        return new Rectangle(absbounds.x + emargin.left,
                             absbounds.y + emargin.top,
                             content.width + padding.left + padding.right + border.left + border.right,
                             content.height + padding.top + padding.bottom + border.top + border.bottom);
    }
    
    /**
     * Computes the absolute clipping rectangle coordinates if this box is used as a clipping block.
     * @return the clipping rectangle coordinates
     */
    public Rectangle getClippedContentBounds()
    {
        if (clipblock == null)
            return getAbsolutePaddingBounds();
        else
            return clipblock.getClippedContentBounds().intersection(getAbsolutePaddingBounds());
    }
    
    @Override
    public String getText()
    {
        String ret = "";
        for (int i = startChild; i < endChild; i++)
        	ret += getSubBox(i).getText();
        return ret;
    }
    
    @Override
    public boolean isWhitespace()
    {
        for (int i = startChild; i < endChild; i++)
            if (!getSubBox(i).isWhitespace())
                return false;
        return true;
    }
        
    /**
     * Checks if the element contains only text boxes (no nested elements)
     * @return <code>true</code> when only text boxes are contained in this element
     */
    public boolean containsTextOnly()
    {
        for (Box child : nested)
            if (!(child instanceof TextBox))
                return false;
        return true;
    }
    
    /**
     * Checks if the element contains the mix of text boxes and elements
     * @return <code>true</code> when both text boxes and elements are directly contained in this element
     */
    public boolean containsMixedContent()
    {
        boolean ctext = false;
        boolean celem = false;
        for (Box child : nested)
        {
            if (child instanceof TextBox)
                ctext = true;
            else
                celem = true;
            
            if (ctext && celem)
                return true;
        }
        return false;
    }
    
    //=======================================================================
    
    /**
     * Draws the subtree as if this was a stacking context root.
     * @param include include new stacking contexts to this context (currently unused)
     */
    public void drawStackingContext(boolean include)
    {
        if (isDisplayed() && isDeclaredVisible())
        {
            Integer[] clevels = formsStackingContext() ? getStackingContext().getZIndices() : new Integer[0]; 
            
            //1.the background and borders of the element forming the stacking context.
            if (this.formsStackingContext() && this.rendersBackground())
                getViewport().getRenderer().renderElementBackground(this);
            
            getViewport().getRenderer().startElementContents(this);
            
            if (this.isReplaced() && this.formsStackingContext())
                getViewport().getRenderer().renderReplacedContent((ReplacedBox) this);
            
            //2.the child stacking contexts with negative stack levels (most negative first).
            int zi = 0;
            while (zi < clevels.length && clevels[zi] < 0)
            {
                drawChildContexts(clevels[zi]);
                zi++;
            }
            //3.the in-flow, non-inline-level, non-positioned descendants.
            drawChildren(DrawStage.DRAW_NONINLINE);
            //4.the non-positioned floats. 
            drawChildren(DrawStage.DRAW_FLOAT);
            //5.the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks. 
            drawChildren(DrawStage.DRAW_INLINE);
            //6.the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
            if (zi < clevels.length && clevels[zi] == 0)
            {
                drawChildContexts(0);
                zi++;
            }
            //7.the child stacking contexts with positive stack levels (least positive first).
            while (zi < clevels.length)
            {
                drawChildContexts(clevels[zi]);
                zi++;
            }
            
            getViewport().getRenderer().finishElementContents(this);
            
        }
    }
    
    /**
     * Draws all the sub-boxes in the given stage.
     * @param turn The current drawing stage.
     */
    protected void drawChildren(DrawStage turn)
    {
        for (int i = startChild; i < endChild; i++)
        {
            Box subbox = getSubBox(i);
            subbox.draw(turn);
        }
    }
    
    /**
     * Draws child stacking contexts of the given z-index.
     * @param zindex The z-index of the contexts to draw. Only the child contexts with the given z-index will be drawn.
     */
    protected void drawChildContexts(int zindex)
    {
        Vector<ElementBox> list = getStackingContext().getElementsForZIndex(zindex);
        if (list != null)
        {
            for (ElementBox elem : list)
            {
                elem.drawStackingContext(!elem.hasZIndex());
            }
        }
    }
    
    //=======================================================================
    
    /**
     * @return <code> true when this box may contain block boxes (it may be a containing-block)
     */
    abstract public boolean mayContainBlocks();
    
    /**
     * Load the box sizes from the CSS properties.
     */
    abstract protected void loadSizes();
    
    /**
     * Update the box sizes according to the new parent size. Should be
     * called when the parent has been resized.
     */
    abstract public void updateSizes(); 
    
    /**
     * Re-calculates the sizes of all the child block boxes recursively.
     */
    public void updateChildSizes()
    {
        for (int i = 0; i < getSubBoxNumber(); i++)
        {
            Box child = getSubBox(i);
            if (child instanceof BlockBox)
            {
                //update the children only when some size has changed
                BlockBox block = (BlockBox) child;
                float oldw = block.getContentWidth();
                float oldh = block.getContentHeight();
                block.updateSizes();
                block.setSize(block.totalWidth(), block.totalHeight());
                if (block.getContentWidth() != oldw || block.getContentHeight() != oldh)
                {
                    block.updateChildSizes();
                }
            }
            else if (child instanceof ElementBox)
            {
                //always update the non-block elements
                ElementBox eb = (ElementBox) child;
                eb.updateSizes();
                eb.setSize(eb.totalWidth(), eb.totalHeight());
                eb.updateChildSizes();
            }
        }
    }
    
    /**
     * Computes the number of expansion points (generally spaces between words) that can be used
     * for extending the box width
     * @param start the first child to be processed
     * @param end the last child (the first one not to be processed)
     * @param atLineStart is the start box at the start of a line?
     * @param atLineEnd is the last box at the end of a line line?
     * @return the number of expansion points
     */
    protected int countInlineExpansionPoints(int start, int end, boolean atLineStart, boolean atLineEnd)
    {
        int cnt = 0;
        for (int i = start; i < end; i++)
        {
            final Box subbox = getSubBox(i);
            if (subbox instanceof Inline)
                cnt += ((Inline) subbox).getWidthExpansionPoints(atLineStart && (i == start), atLineEnd && (i == end - 1));
        }
        return cnt;
    }
    
    /**
     * Extends the total width of inline childs boxes by the given offset considering the 'expandability' of every child.
     * @param ofs the additional width in pixels (positive or negative)
     * @param start the first child to be processed
     * @param end the last child (the first one not to be processed)
     * @param atLineStart is the start box at the start of a line?
     * @param atLineEnd is the last box at the end of a line line?
     */
    protected void extendInlineChildWidths(float dif, int start, int end, boolean atLineStart, boolean atLineEnd)
    {
        //compute total width of child boxes
        final int total = countInlineExpansionPoints(start, end, atLineStart, atLineEnd);
        if (total > 0)
        {
            //distribute the offset among the children
            float ofsx = 0;
            float remain = dif;
            for (int i = start; i < end; i++)
            {
                final Box subbox = getSubBox(i);
                if (subbox instanceof Inline)
                {
                    final boolean lstart = atLineStart && (i == start);
                    final boolean lend = atLineEnd && (i == end - 1);
                    final int childexp = ((Inline) subbox).getWidthExpansionPoints(lstart, lend);
                    float toadd = dif * childexp / total;
                    if (toadd > remain)
                        toadd = remain;
                    subbox.moveRight(ofsx);
                    ((Inline) subbox).extendWidth(toadd, lstart, lend);
                    ofsx += toadd;
                    remain -= toadd;
                }
            }
        }
    }
    
    /**
     * Computes efficient top and bottom margins for collapsing.
     */
    abstract public void computeEfficientMargins();
    
    /**
     * Checks if the element's own top and bottom margins are adjoining 
     * according to the CSS specifiaction.
     * @return <code>true</code> if the margins are adjoining
     */
    abstract public boolean marginsAdjoin();
    
    /**
     * @return <code>true</code> if the box has a visible border around
     */
    protected boolean borderVisible(String dir)
    {
        CSSProperty.BorderStyle st = style.getProperty("border-"+dir+"-style");
        return (st != null && st != CSSProperty.BorderStyle.NONE  && st != CSSProperty.BorderStyle.HIDDEN); 
    }
    
    /**
     * Loads the border sizes from the style.
     * 
     * @param dec CSS decoder used for decoding the style
     * @param contw containing block width for decoding percentages
     */
    protected void loadBorders(CSSDecoder dec, float contw)
    {
        border = new LengthSet();
        if (borderVisible("top"))
            border.top = getBorderWidth(dec, "border-top-width");
        else
            border.top = 0;
        if (borderVisible("right"))
            border.right = getBorderWidth(dec, "border-right-width");
        else
            border.right = 0;
        if (borderVisible("bottom"))
            border.bottom = getBorderWidth(dec, "border-bottom-width");
        else
            border.bottom = 0;
        if (borderVisible("left"))
            border.left = getBorderWidth(dec, "border-left-width");
        else
            border.left = 0;
    }
    
    /**
     * Load the basic style from the CSS properties. This includes the display
     * properties, floating, positioning, color and font properties.
     */
    protected void loadBasicStyle()
    {
        // update the visual context based on the current style
        ctx.update(style);
        
        // decode additional style properties
        display = style.getProperty("display");
        if (display == null) display = CSSProperty.Display.INLINE;
        
        CSSProperty.Float floating = style.getProperty("float");
        if (floating == null) floating = BlockBox.FLOAT_NONE;
        
        position = style.getProperty("position");
        if (position == null) position = BlockBox.POS_STATIC;
        
        //apply combination rules
        //http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
        if (display == ElementBox.DISPLAY_NONE)
        {
            position = BlockBox.POS_STATIC;
            floating = BlockBox.FLOAT_NONE;
        }
        else if (position == BlockBox.POS_ABSOLUTE || position == BlockBox.POS_FIXED)
        {
            floating = BlockBox.FLOAT_NONE;
        }
        //compute the display computed value
        if (floating != BlockBox.FLOAT_NONE || position == BlockBox.POS_ABSOLUTE || position == BlockBox.POS_FIXED || isRootElement())
        {
            if (display == DISPLAY_INLINE_TABLE)
                display = DISPLAY_TABLE;
            else if (display == DISPLAY_INLINE ||
                     display == DISPLAY_RUN_IN ||
                     display == DISPLAY_TABLE_ROW_GROUP ||
                     display == DISPLAY_TABLE_COLUMN ||
                     display == DISPLAY_TABLE_COLUMN_GROUP ||
                     display == DISPLAY_TABLE_HEADER_GROUP ||
                     display == DISPLAY_TABLE_FOOTER_GROUP ||
                     display == DISPLAY_TABLE_ROW ||
                     display == DISPLAY_TABLE_CELL ||
                     display == DISPLAY_TABLE_CAPTION ||
                     display == DISPLAY_INLINE_BLOCK)
                display = DISPLAY_BLOCK;
        }

        isblock = (display == DISPLAY_BLOCK);
        displayed = (display != DISPLAY_NONE && display != DISPLAY_TABLE_COLUMN);
        visible = (style.getProperty("visibility") != CSSProperty.Visibility.HIDDEN); 
        
        //line height
        CSSProperty.LineHeight lh = style.getProperty("line-height");
        if (lh == null || lh == CSSProperty.LineHeight.NORMAL)
            lineHeight = DEFAULT_LINE_HEIGHT * ctx.getFontHeight();
        else if (lh == CSSProperty.LineHeight.length)
        {
            TermLength len = style.getValue(TermLength.class, "line-height");
            lineHeight = ctx.pxLength(len);
        }
        else if (lh == CSSProperty.LineHeight.percentage)
        {
            TermPercent len = style.getValue(TermPercent.class, "line-height");
            lineHeight = ctx.pxLength(len, ctx.getFontHeight()); 
        }
        else //must be INTEGER or NUMBER
        {
            Term<?> len = style.getValue("line-height", true);
            float r;
            if (len instanceof TermInteger)
                r = ((TermInteger) len).getValue();
            else
                r = ((TermNumber) len).getValue();
            lineHeight = r * ctx.getFontHeight();
        }

        //whitespace
        whitespace = style.getProperty("white-space");
        if (whitespace == null) whitespace = WHITESPACE_NORMAL;
        
        //z-index
        CSSProperty.ZIndex z = style.getProperty("z-index");
        if (z != null && z != ZIndex.AUTO)
        {
            zset = true;
            Term<?> zterm = style.getValue("z-index", true);
            if (zterm instanceof TermInteger)
                zIndex = ((TermInteger) zterm).getValue().intValue();
            else
                zset = false;
        }
        else
            zset = false;
        
        //transformations -- applied on block-level or atomic inline-level elements only
        if (isBlock() || isReplaced())
            transform = style.getProperty("transform");
        if (transform == null) transform = CSSProperty.Transform.NONE;
    }
    
    /**
     * Loads the top, left, bottom and right coordinates from the style
     */
    protected void loadPosition()
    {
        CSSDecoder dec = new CSSDecoder(ctx);

        float contw = getContainingBlock().width;
        float conth = getContainingBlock().height;
        
        CSSProperty.Top ptop = style.getProperty("top");
        CSSProperty.Right pright = style.getProperty("right");
        CSSProperty.Bottom pbottom = style.getProperty("bottom");
        CSSProperty.Left pleft = style.getProperty("left");

        topset = !(ptop == null || ptop == CSSProperty.Top.AUTO);
        rightset = !(pright == null || pright == CSSProperty.Right.AUTO);
        bottomset = !(pbottom == null || pbottom == CSSProperty.Bottom.AUTO);
        leftset = !(pleft == null || pleft == CSSProperty.Left.AUTO);
        
        coords = new LengthSet();
        if (topset)
            coords.top = dec.getLength(getLengthValue("top"), (ptop == CSSProperty.Top.AUTO), 0, 0, conth);
        if (rightset)
            coords.right = dec.getLength(getLengthValue("right"), (pright == CSSProperty.Right.AUTO), 0, 0, contw);
        if (bottomset)
            coords.bottom = dec.getLength(getLengthValue("bottom"), (pbottom == CSSProperty.Bottom.AUTO), 0, 0, conth);
        if (leftset)
            coords.left = dec.getLength(getLengthValue("left"), (pleft == CSSProperty.Left.AUTO), 0, 0, contw);
    }
    
    /**
     * Updates the stacking parent values and registers the z-index for this parent.
     */
    @Override
    protected void updateStackingContexts()
    {
        super.updateStackingContexts();
        if (stackingParent != null)
        {
            if (formsStackingContext()) //all the positioned boxes are considered as separate stacking contexts
            {
                stackingParent.getStackingContext().registerChildContext(this);
                if (scontext != null) //clear this context if it exists (remove old children)
                    scontext.clear();
            }
        }
    }
    
} public class Rectangle
{
    public float x;
    public float y;
    public float width;
    public float height;
    
    public Rectangle(float x, float y, float width, float height)
    {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    public Rectangle(float width, float height)
    {
        this.x = 0;
        this.y = 0;
        this.width = width;
        this.height = height;
    }

    public Rectangle()
    {
        this.x = 0;
        this.y = 0;
        this.width = 0;
        this.height = 0;
    }
    
    public Rectangle(Rectangle src)
    {
        this.x = src.x;
        this.y = src.y;
        this.width = src.width;
        this.height = src.height;
    }
    
    public Rectangle(Dimension src)
    {
        this.x = 0;
        this.y = 0;
        this.width = src.width;
        this.height = src.height;
    }
    
    public float getX()
    {
        return x;
    }

    public void setX(float x)
    {
        this.x = x;
    }

    public float getY()
    {
        return y;
    }

    public void setY(float y)
    {
        this.y = y;
    }

    public float getWidth()
    {
        return width;
    }

    public void setWidth(float width)
    {
        this.width = width;
    }

    public float getHeight()
    {
        return height;
    }

    public void setHeight(float height)
    {
        this.height = height;
    }
    
    public void setLocation(float x, float y)
    {
        this.x = x;
        this.y = y;
    }
    
    public void setSize(float width, float height)
    {
        this.width = width;
        this.height = height;
    }
    
    public Dimension getSize()
    {
        return new Dimension(width, height);
    }
    
    public boolean contains(float x, float y)
    {
        if (width <= 0 || height <= 0)
            return false;
        return (x >= this.x && x < this.x + this.width &&
                y >= this.y && y < this.y + this.height);
    }
    
    public boolean intersects(Rectangle r)
    {
        float tw = this.width;
        float th = this.height;
        float rw = r.width;
        float rh = r.height;
        if (rw <= 0 || rh <= 0 || tw <= 0 || th <= 0) {
            return false;
        }
        float tx = this.x;
        float ty = this.y;
        float rx = r.x;
        float ry = r.y;
        rw += rx;
        rh += ry;
        tw += tx;
        th += ty;
        //      overflow || intersect
        return ((rw < rx || rw > tx) &&
                (rh < ry || rh > ty) &&
                (tw < tx || tw > rx) &&
                (th < ty || th > ry));
    }
    
    public Rectangle intersection(Rectangle r) 
    {
        float tx1 = this.x;
        float ty1 = this.y;
        float rx1 = r.x;
        float ry1 = r.y;
        float tx2 = tx1; tx2 += this.width;
        float ty2 = ty1; ty2 += this.height;
        float rx2 = rx1; rx2 += r.width;
        float ry2 = ry1; ry2 += r.height;
        if (tx1 < rx1) tx1 = rx1;
        if (ty1 < ry1) ty1 = ry1;
        if (tx2 > rx2) tx2 = rx2;
        if (ty2 > ry2) ty2 = ry2;
        tx2 -= tx1;
        ty2 -= ty1;
        return new Rectangle(tx1, ty1, tx2, ty2);
    }

    @Override
    public String toString()
    {
        return "Rectangle[x=" + x + ", y=" + y + ", width=" + width
                + ", height=" + height + "]";
    }
} public abstract class VisualContext 
{
    protected static final Logger log = LoggerFactory.getLogger(VisualContext.class);

    private VisualContext parent;
    private VisualContext rootContext; //the visual context of the root element
    private BrowserConfig config; //used engine configuration
    private FontTable fontTable; //a table of CSS-defined fonts
    private Viewport viewport; //the viewport used for obtaining the vw sizes
    private float fontSize; //font size in pt
    private CSSProperty.FontWeight fontWeight;
    private CSSProperty.FontStyle fontStyle;
    private CSSProperty.FontVariant fontVariant;
    private List<CSSProperty.TextDecoration> textDecoration;
    private float letterSpacing; //additional letter spacing in pt
    private float rem; // 1rem length in points 
    
    public Color color; //current text color

    private PxEvaluator pxEval; //expression evaluator for obtaining pixel values of expressions
    private PtEvaluator ptEval; //expression evaluator for obtaining points values of expressions
    private DegEvaluator degEval; //expression evaluator for obtaining degree values of expressions
    private RadEvaluator radEval; //expression evaluator for obtaining radian values of expressions

    
    public VisualContext(VisualContext parent, BrowserConfig config, FontTable fontTable)
    {
        this.parent = parent;
        this.config = config;
        this.fontTable = fontTable;
        rootContext = (parent == null) ? this : parent.rootContext;
        fontSize = CSSUnits.medium_font;
        fontWeight = CSSProperty.FontWeight.NORMAL;
        fontStyle = CSSProperty.FontStyle.NORMAL;
        fontVariant = CSSProperty.FontVariant.NORMAL;
        rem = fontSize;
        textDecoration = new ArrayList<CSSProperty.TextDecoration>(2); //it is not very probable to have more than two decorations
        letterSpacing = 0.0f;
        color = new Color(0, 0, 0);
    }
    
    public void copyVisualContext(VisualContext src)
    {
        viewport = src.viewport;
        rootContext = src.rootContext;
        rem = src.rem;
        fontSize = src.fontSize;
        fontWeight = src.fontWeight;
        fontStyle = src.fontStyle;
        fontVariant = src.fontVariant;
        textDecoration = new ArrayList<CSSProperty.TextDecoration>(src.textDecoration);
        letterSpacing = src.letterSpacing;
        color = src.color;
    }
   
    abstract public VisualContext create();
    
    //=========================================================================
    
    public VisualContext getParentContext()
    {
        return parent;
    }
    
    public void setParentContext(VisualContext parent)
    {
        this.parent = parent;
    }
    
    public BrowserConfig getConfig()
    {
        return config;
    }

    public FontTable getFontTable()
    {
        return fontTable;
    }

    public Viewport getViewport()
    {
        return viewport;
    }

    public void setViewport(Viewport viewport)
    {
        this.viewport = viewport;
    }

    public boolean isRootContext()
    {
        return (this == rootContext);
    }

    public void makeRootContext()
    {
        if (this.rootContext != null)
            this.rootContext.rootContext = this; //the old root now points to us
        this.rootContext = this; //we also point to us
    }

    /**
     * Gets the information about the current font.
     * @return current font information
     */
    abstract public FontInfo getFontInfo();
    
    /**
     * Obtains the specified font size in pt.
     * @return the font size in pt
     */
    public float getFontSize()
    {
        return fontSize;
    }
    
    /**
     * The font variant used for the box.
     * @return <code>normal</code> or <code>small-caps</code>
     */
    public String getFontVariant()
    {
        return fontVariant.toString();
    }
    
    /**
     * Returns the text decoration used for the box.
     * @return <code>none</code> or a string of space separated keywords <code>underline</code>, <code>overline</code>, <code>line-through</code> or <code>blink</code>
     */
    public String getTextDecorationString()
    {
        if (textDecoration.isEmpty())
            return "none";
        else
        {
            StringBuilder ret = new StringBuilder();
            for (CSSProperty.TextDecoration dec : textDecoration)
            {
                if (ret.length() > 0)
                    ret.append(' ');
                ret.append(dec.toString());
            }
            return ret.toString();
        }
    }
    
    /**
     * Returns the text decoration used for the box.
     * @return a list of TextDecoration values
     */
    public List<CSSProperty.TextDecoration> getTextDecoration()
    {
        return textDecoration;
    }
    
    /**
     * Returns the letter spacing used for the box.
     * @return letter spacing
     */
    public float getLetterSpacing()
    {
        return letterSpacing;
    }

    /**
     * The text color used for the box.
     * @return color specification
     */
    public Color getColor()
    {
        return color;
    }
    
    /**
     * Gets the em value for the current context. It is always equal to FontSize.
     * @return the em value of the context
     */
    public float getEm()
    {
        return fontSize;
    }

    /**
     * Gets the 1rem length in points
     * @return the rem size in points
     */
    public float getRem()
    {
        return rem;
    }

    /**
     * Gets the 1ex length in points
     * @return the ex size in points
     */
    abstract public float getEx();

    /**
     * Gets the 1ch length in points
     * @return the ch size in points
     */
    abstract public float getCh();
    
    protected void setRem(float rem)
    {
        this.rem = rem;
    }

    /**
     * Gets the current font family.
     * @return The font family name
     */
    abstract public String getFontFamily();
    
    /**
     * Computes the pixel width of the given string in the visual context.
     * @param text The text sting
     * @return the resulting widtg in pixels
     */
    abstract public float stringWidth(String text);
    
    //=========================================================================
    
    /** 
     * Updates the context according to the given element style. The properties that are not defined 
     * in the style are left unchanged.
     * @param style the style data 
     */
    public void update(NodeData style)
    {
        // font style and weight
        CSSProperty.FontWeight weight = style.getProperty("font-weight");
        if (weight != null) fontWeight = weight;
        CSSProperty.FontStyle fstyle =  style.getProperty("font-style");
        if (fstyle != null) fontStyle = fstyle;
        
        // font family
        String family = null;
        CSSProperty.FontFamily ff = style.getProperty("font-family");
        if (ff == null)
        {
            family = getFontFamily(); //use current
        }
        else if (ff == CSSProperty.FontFamily.list_values)
        {
            TermList fmlspec = style.getValue(TermList.class, "font-family");
            if (fmlspec == null)
                family = getFontFamily();
            else
                family = findFontName(fmlspec, fontWeight, fontStyle);
        }
        else
        {
            family = findLogicalFont(ff, fontWeight, fontStyle);
        }
        if (family == null) {
            //no alternative was usable try 'Serif' logical font
            family = findLogicalFont(CSSProperty.FontFamily.SERIF, fontWeight, fontStyle);
            //even Serif is not available - try the last fallback
            if (family == null)
            {
                family = getFallbackFont();
                log.warn("Couldn't find any usable font for {}, using {} as the last option", ff, family);
            }
        }
        
        // font size in pt
        float size;
        final float psize = (parent == null) ? CSSUnits.medium_font : parent.getEm();
        final CSSProperty.FontSize fsize = style.getProperty("font-size");
        if (fsize == null) //no specification - use the parent size
            size = psize;
        else if (fsize == CSSProperty.FontSize.length || fsize == CSSProperty.FontSize.percentage)
        {
            TermLengthOrPercent lenspec = style.getValue(TermLengthOrPercent.class, "font-size");
            if (lenspec != null)
            {
                if (parent != null)
                    size = parent.ptLength(lenspec, psize);
                else
                    size = rootContext.ptLength(lenspec, psize);
            }
            else
                size = psize;
        }
        else // size keywords
            size = CSSUnits.convertFontSize(psize, fsize);
        fontSize = size;
        rem = rootContext.getEm();
        
        // font variant
        CSSProperty.FontVariant variant = style.getProperty("font-variant");
        if (variant != null) fontVariant = variant;

        // text decoration
        CSSProperty.TextDecoration decor = style.getProperty("text-decoration");
        textDecoration.clear();
        if (decor != null)
        {
            if (decor == CSSProperty.TextDecoration.list_values)
            {
                TermList list = style.getValue(TermList.class, "text-decoration");
                for (Term<?> t : list)
                {
                    if (t.getValue() instanceof CSSProperty.TextDecoration)
                        textDecoration.add((CSSProperty.TextDecoration) t.getValue());
                }
            }
            else if (decor != CSSProperty.TextDecoration.NONE)
                textDecoration.add(decor);
        }
        
        // letter spacing
        CSSProperty.LetterSpacing spacing = style.getProperty("letter-spacing");
        if (spacing != null)
        {
            if (spacing == CSSProperty.LetterSpacing.NORMAL)
                letterSpacing = 0.0f;
            else
            {
                TermLength lenspec = style.getValue(TermLength.class, "letter-spacing");
                if (lenspec != null)
                    letterSpacing = ptLength(lenspec);
            }
        }
        
        // color
        TermColor clr = style.getSpecifiedValue(TermColor.class, "color");
        if (clr != null) color = clr.getValue();
        
        // update the font settings
        setCurrentFont(family, size, fontWeight, fontStyle, letterSpacing);
    }
    
    //-----------------------------------------------------------------------
    
    /**
     * Sets current font according to its given parametres.
     *  
     * @param family font family
     * @param size font size in pt
     * @param weight font weight
     * @param style font style
     * @param spacing letter spacing in pt
     */
    abstract public void setCurrentFont(String family, float size, CSSProperty.FontWeight weight, CSSProperty.FontStyle style, float spacing);
    
    /**
     * Computes current text line height.
     * @return the height of the normal text line in pixels
     */
    abstract public float getFontHeight();
    
    /**
     * Obtains the maximal distance from the line top to the baseline
     * for the current font.
     * @return the baseline <em>y</em> offset.
     */
    abstract public float getBaselineOffset();
    
    /** 
     * Converts a length from a CSS length or percentage to 'pt'. The current font size is used for em units.
     * @param spec the CSS length specification
     * @param whole the value that corresponds to 100%. It is used only when spec is a percentage.
     * @return the length in 'pt' 
     */
    public float ptLength(TermLengthOrPercent spec, float whole)
    {
        float nval = spec.getValue();
        if (spec.isPercentage())
        {
            return (whole * nval) / 100;
        }
        else if (spec instanceof TermCalc)
        {
            final CalcArgs args = ((TermCalc) spec).getArgs();
            return args.evaluate(getPtEval().setWhole(whole));
        }
        else
        {
            final TermLength.Unit unit = spec.getUnit();
            if (unit == null)
                return 0;
            switch (unit)
            {
                case pt:
                    return nval;
                case in:
                    return nval * 72;
                case cm:
                    return (nval * 72) / 2.54f;
                case mm:
                    return (nval * 72) / 25.4f;
                case q:
                    return (nval * 72) / (2.54f * 40.0f);
                case pc:
                    return nval * 12;
                case px:
                    return CSSUnits.points(nval);
                case em:
                    return getFontSize() * nval;
                case rem:
                    return getRem() * nval;
                case ex:
                    return getEx() * nval;
                case ch:
                    return getCh() * nval;
                case vw:
                    return CSSUnits.points(viewport.getVisibleRect().getWidth()) * nval / 100.0f;
                case vh:
                    return CSSUnits.points(viewport.getVisibleRect().getWidth()) * nval / 100.0f;
                case vmin:
                    return CSSUnits.points(Math.min(viewport.getVisibleRect().getWidth(), viewport.getVisibleRect().getHeight())) * nval / 100.0f;
                case vmax:
                    return CSSUnits.points(Math.max(viewport.getVisibleRect().getWidth(), viewport.getVisibleRect().getHeight())) * nval / 100.0f;
                default:
                    return 0;
            }
        }
    }

    /** 
     * Converts a length from a CSS length to 'pt'. Percentages are always evaluated to 0.
     * @param spec the CSS length specification
     * @return font size in 'pt' 
     */
    public float ptLength(TermLengthOrPercent spec)
    {
        return ptLength(spec, 0);
    }
    
    /** 
     * Converts a length from a CSS length or percentage to 'px'. The current font size is used for em units.
     * @param spec the CSS length specification
     * @param whole the value that corresponds to 100%. It is used only when spec is a percentage.
     * @return the length in 'px' 
     */
    public float pxLength(TermLengthOrPercent spec, float whole)
    {
        float nval = spec.getValue();
        if (spec.isPercentage())
        {
            return (whole * nval) / 100.0f;
        }
        else if (spec instanceof TermCalc)
        {
            final CalcArgs args = ((TermCalc) spec).getArgs();
            return args.evaluate(getPxEval().setWhole(whole));
        }
        else
        {
            final TermLength.Unit unit = spec.getUnit();
            if (unit == null)
                return 0;
            switch (unit)
            {
                case pt:
                    return nval * CSSUnits.dpi / 72.0f;
                case in:
                    return nval * CSSUnits.dpi;
                case cm:
                    return (nval * CSSUnits.dpi) / 2.54f;
                case mm:
                    return (nval * CSSUnits.dpi) / 25.4f;
                case q:
                    return (nval * CSSUnits.dpi) / (2.54f * 40.0f);
                case pc:
                    return (nval * 12 * CSSUnits.dpi) / 72;
                case px:
                    return nval;
                case em:
                    return CSSUnits.pixels(getFontSize() * nval); //font size is in pt
                case rem:
                    return CSSUnits.pixels(getRem() * nval);
                case ex:
                    return CSSUnits.pixels(getEx() * nval);
                case ch:
                    return CSSUnits.pixels(getCh() * nval);
                case vw:
                    return viewport.getVisibleRect().getWidth() * nval / 100.0f;
                case vh:
                    return viewport.getVisibleRect().getHeight() * nval / 100.0f;
                case vmin:
                    return Math.min(viewport.getVisibleRect().getWidth(), viewport.getVisibleRect().getHeight()) * nval / 100.0f;
                case vmax:
                    return Math.max(viewport.getVisibleRect().getWidth(), viewport.getVisibleRect().getHeight()) * nval / 100.0f;
                default:
                    return 0;
            }
        }
    }
    
    /** 
     * Converts a length from a CSS length to 'px'. Percentages are always evaluated to 0.
     * @param spec the CSS length specification
     * @return font size in 'px' 
     */
    public float pxLength(TermLengthOrPercent spec)
    {
        return pxLength(spec, 0);
    }
    
    /**
     * Converts an angle from a CSS angle to 'deg'.
     * @param spec the CSS angle specification
     * @return the corresponding angle in radians
     */
    public float degAngle(TermAngle spec)
    {
        float nval = spec.getValue();
        final TermLength.Unit unit = spec.getUnit();
        if (spec instanceof TermCalcAngleImpl)
        {
            final CalcArgs args = ((TermCalc) spec).getArgs();
            return args.evaluate(getDegEval());
        }
        else
        {
            if (unit == null)
                return 0;
            switch (unit)
            {
                case deg:
                    return nval; 
                case grad:
                    return (nval * 200.0f) / (float) Math.PI;
                case rad:
                    return (nval * 180.0f) / (float) Math.PI;
                case turn:
                    return nval * 360.0f;
                default:
                    return 0;
            }
        }
    }
    
    /**
     * Converts an angle from a CSS angle to 'rad'.
     * @param spec the CSS angle specification
     * @return the corresponding angle in radians
     */
    public float radAngle(TermAngle spec)
    {
        float nval = spec.getValue();
        final TermLength.Unit unit = spec.getUnit();
        if (spec instanceof TermCalcAngleImpl)
        {
            final CalcArgs args = ((TermCalc) spec).getArgs();
            return args.evaluate(getRadEval());
        }
        else
        {
            if (unit == null)
                return 0;
            switch (unit)
            {
                case deg:
                    return (nval * (float) Math.PI) / 180.0f; 
                case grad:
                    return (nval * (float) Math.PI) / 200.0f;
                case rad:
                    return nval;
                case turn:
                    return nval * 2.0f * (float) Math.PI;
                default:
                    return 0.0f;
            }
        }
    }
    
    //===================================================================================
    
    /** 
     * Scans a list of font definitions and chooses the first one that is available
     * @param list of terms obtained from the font-family property
     * @return a font name string according to java.awt.Font
     */
    protected String findFontName(TermList list, CSSProperty.FontWeight weight, CSSProperty.FontStyle style)
    {
        String ret = null;
        for (Term<?> term : list)
        {
            Object value = term.getValue();
            if (value instanceof CSSProperty.FontFamily) //logical font
            {
                ret = findLogicalFont((CSSProperty.FontFamily) value, weight, style);
            }
            else //physical font
            {
                ret = lookupFont(value.toString(), weight, style);
            }
            if (ret != null)
                break;
        }
        return ret;
    }

    /**
     * Tries to find a physical font name for a given logical font
     * @param ff the logical font family according to CSS specification
     * @param weight the font weight to match
     * @param style the font style to match
     * @return physical font family name or {@code null} when nothing has been found
     */
    protected String findLogicalFont(CSSProperty.FontFamily ff, CSSProperty.FontWeight weight, CSSProperty.FontStyle style)
    {
        final List<String> flist = getConfig().getLogicalFont(ff.toString());
        for (String cand : flist)
        {
            String found = lookupFont(cand, weight, style);
            if (found != null)
                return found;
        }
        return null;
    }
    
    /**
     * Checks if the font family is available either among the CSS defined fonts or the system fonts.
     * If found, registers a system font with the given name. 
     * @param family Required font family
     * @param weight Required font weight
     * @param style Required font style
     * @return The corresponding system font name or {@code null} when no candidates have been found. 
     */
    protected String lookupFont(String family, CSSProperty.FontWeight weight, CSSProperty.FontStyle style)
    {
        //try to look in the style font table
        String nameFound = null;
        FontSpec spec = new FontSpec(family, weight, style);
        boolean isItalic = (style == FontStyle.ITALIC || style == FontStyle.OBLIQUE);
        boolean isBold = FontSpec.representsBold(weight);
        List<RuleFontFace.Source> srcs = findMatchingFontSources(spec);
        if (srcs != null)
        {
            for (RuleFontFace.Source src : srcs)
            {
                if (src instanceof RuleFontFace.SourceLocal)
                {
                    String name = fontAvailable(((RuleFontFace.SourceLocal) src).getName(), isItalic, isBold);
                    if (name != null)
                    {
                        nameFound = name;
                        break;
                    }
                }
                else if (src instanceof RuleFontFace.SourceURL && getViewport().getConfig().isLoadFonts())
                {
                    try
                    {
                        final TermURI urlstring = ((RuleFontFace.SourceURL) src).getURI();
                        final String format = ((RuleFontFace.SourceURL) src).getFormat();
                        nameFound = registerExternalFont(urlstring, format);
                    } catch (MalformedURLException e) {
                        log.error("Couldn't load font with URI {} ({})", ((RuleFontFace.SourceURL) src).getURI(), e.getMessage());
                    } catch (IOException e) {
                        log.error("Couldn't load font with URI {} ({})", ((RuleFontFace.SourceURL) src).getURI(), e.getMessage());
                    }
                }
            }
        }
        //if nothing found, try the system font names
        if (nameFound == null)
        {
            nameFound = fontAvailable(family, isItalic, isBold);
        }
        //create the font when found
        return nameFound;
    }

    /**
     * Finds the CSS font definitions matching the font specification.
     * @param spec The font specification to match
     * @return a list of available font sources or {@code null} when the font tables are not available
     */
    protected List<RuleFontFace.Source> findMatchingFontSources(FontSpec spec)
    {
        if (getFontTable() != null)
            return getFontTable().findBestMatch(spec);
        else
            return null;
    }
    
    /** 
     * Find the font family available with the given family name, bold and italic properties,
     * or return {@code null} if the font is not available
     * @return The exact name of the font family or {@code null} if the font is not available
     */
    protected abstract String fontAvailable(String family, boolean isBold, boolean isItalic);

    /**
     * Gets the name of any usable font in the system. This is used as the last fallback
     * if all other alternatives have failed.
     * @return Any usable physical font name
     */
    protected abstract String getFallbackFont();
    
    /**
     * Decodes and registers a new font based on its URL.
     * @param urlstring the external font URL
     * @param format the font format according to CSS spec (e.g. 'truetype')
     * @return The registed font name or {@code null} when failed
     * @throws MalformedURLException
     * @throws IOException
     */
    protected abstract String registerExternalFont(TermURI urlstring, String format)
            throws MalformedURLException, IOException;
    
    //============================================================================================================================
    
    private PxEvaluator getPxEval()
    {
        if (pxEval == null)
            pxEval = new PxEvaluator(this);
        return pxEval;
    }
    
    private PtEvaluator getPtEval()
    {
        if (ptEval == null)
            ptEval = new PtEvaluator(this);
        return ptEval;
    }
    
    private DegEvaluator getDegEval()
    {
        if (degEval == null)
            degEval = new DegEvaluator(this);
        return degEval;
    }
    
    private RadEvaluator getRadEval()
    {
        if (radEval == null)
            radEval = new RadEvaluator(this);
        return radEval;
    }
    
    //============================================================================================================================
    
    /**
     * A base of all the evaluators that use the VisualContext for evaluating the calc() expressions.
     *
     * @author burgetr
     */
    private abstract class UnitEvaluator extends CalcArgs.FloatEvaluator
    {
        protected VisualContext ctx;
        protected float whole; //whole size used for percentages
        
        public UnitEvaluator(VisualContext ctx)
        {
            this.ctx = ctx;
        }

        public UnitEvaluator setWhole(float whole)
        {
            this.whole = whole;
            return this;
        }
    }
    
    private class PxEvaluator extends UnitEvaluator
    {
        public PxEvaluator(VisualContext ctx)
        {
            super(ctx);
        }

        @Override
        public float resolveValue(TermFloatValue val)
        {
            if (val instanceof TermLengthOrPercent)
                return ctx.pxLength((TermLengthOrPercent) val, whole);
            else
                return 0.0f; //this should not happen
        }
    }
    
    private class PtEvaluator extends UnitEvaluator
    {
        public PtEvaluator(VisualContext ctx)
        {
            super(ctx);
        }

        @Override
        public float resolveValue(TermFloatValue val)
        {
            if (val instanceof TermLengthOrPercent)
                return ctx.ptLength((TermLengthOrPercent) val, whole);
            else
                return 0.0f; //this should not happen
        }
    }
    
    private class RadEvaluator extends UnitEvaluator
    {
        public RadEvaluator(VisualContext ctx)
        {
            super(ctx);
        }

        @Override
        public float resolveValue(TermFloatValue val)
        {
            if (val instanceof TermLengthOrPercent)
                return ctx.radAngle((TermAngle) val);
            else
                return 0.0f; //this should not happen
        }
    }
    
    private class DegEvaluator extends UnitEvaluator
    {
        public DegEvaluator(VisualContext ctx)
        {
            super(ctx);
        }

        @Override
        public float resolveValue(TermFloatValue val)
        {
            if (val instanceof TermLengthOrPercent)
                return ctx.degAngle((TermAngle) val);
            else
                return 0.0f; //this should not happen
        }
    }

    //===================================================================================

    /**
     * Gets the image loader that can be used for obtaining ContentImage instances from URLs.
     * @return the image loader
     */
    public abstract ImageLoader getImageLoader();
    
} public class BackgroundImageGradient extends BackgroundImage
{
    private Gradient gradient;
    

    public BackgroundImageGradient(ElementBox owner, BackgroundPosition position, TermList positionValues,
            BackgroundRepeat repeat, BackgroundAttachment attachment, BackgroundOrigin origin, BackgroundSize size,
            TermList sizeValues)
    {
        super(owner, null, position, positionValues, repeat, attachment, origin, size, sizeValues);
    }
    
    public Gradient getGradient()
    {
        return gradient;
    }

    public void setGradient(Gradient gradient)
    {
        this.gradient = gradient;
    }

    @Override
    public float getIntrinsicWidth()
    {
        return 0;
    }

    @Override
    public float getIntrinsicHeight()
    {
        return 0;
    }

    @Override
    public float getIntrinsicRatio()
    {
        return 1;
    }

    @Override
    public boolean hasIntrinsicWidth()
    {
        return false;
    }

    @Override
    public boolean hasIntrinsicHeight()
    {
        return false;
    }

    @Override
    public boolean hasIntrinsicRatio()
    {
        return false;
    }

} public class BackgroundImageImage extends BackgroundImage
{
    /** Default image width used when there is no image data available */
    public static final int DEFAULT_IMAGE_WIDTH = 20;
    /** Default image height used when there is no image data available */
    public static final int DEFAULT_IMAGE_HEIGHT = 20;
    
    private URL url; //image url
    private ContentImage image; //contained image
    

    public BackgroundImageImage(ElementBox owner, URL url, BackgroundPosition position, TermList positionValues,
            BackgroundRepeat repeat, BackgroundAttachment attachment, BackgroundOrigin origin,
            BackgroundSize size, TermList sizeValues)
    {
        super(owner, url, position, positionValues, repeat, attachment, origin, size, sizeValues);
        this.url = url;
    }
    
    /**
     * Obtains the source URL of the image.
     * @return the source URL
     */
    public URL getUrl()
    {
        return url;
    }

    /**
     * Obtains the actual image used for creating this background image.
     * @return an image representation
     */
    public ContentImage getImage()
    {
        return image;
    }

    public void setImage(ContentImage image)
    {
        this.image = image;
    }

    public float getIntrinsicWidth()
    {
        if (getImage() != null)
            return getImage().getWidth();
        else
            return DEFAULT_IMAGE_WIDTH;
    }

    public float getIntrinsicHeight()
    {
        if (getImage() != null)
            return getImage().getHeight();
        else
            return DEFAULT_IMAGE_HEIGHT;
    }

    public float getIntrinsicRatio()
    {
        return getIntrinsicWidth() / getIntrinsicHeight();
    }

    @Override
    public boolean hasIntrinsicWidth()
    {
        return true;
    }

    @Override
    public boolean hasIntrinsicHeight()
    {
        return true;
    }

    @Override
    public boolean hasIntrinsicRatio()
    {
        return true;
    }

} public abstract class Gradient
{
    private boolean repeating;
    private List<GradientStop> stops;
    private float maxPercentage;
    

    public Gradient()
    {
        repeating = false;
        stops = new ArrayList<>();
        maxPercentage = 0f;
    }
    
    public boolean isRepeating()
    {
        return repeating;
    }

    public void setRepeating(boolean repeating)
    {
        this.repeating = repeating;
    }

    /**
     * Obtains the total gradient length (100%) for computing the stop percentages.
     * @return the gradient length in pixels
     */
    public abstract float getLength();
    
    public List<GradientStop> getStops()
    {
        return stops;
    }
    
    public void addStop(GradientStop stop)
    {
        stops.add(stop);
    }

    /**
     * Returns the absolute length if it is specified for the last step.
     * @return The length converted to pixels or {@code null} if the stop list is empty or there
     * is no absolute length specified for the last stop.
     */
    public Float getLastLengthPx()
    {
        if (!stops.isEmpty())
            return stops.get(stops.size() - 1).getPxLength();
        else
            return null;
    }
    
    /**
     * Computes missing percentages for stops.
     */
    public void recomputeStops()
    {
        recomputePercentages();
        if (!stops.isEmpty())
        {
            //the first one is 0% if missing
            GradientStop first = stops.get(0);
            if (first.getPercentage() == null)
                first.setPercentage(0.0f);
            //the last missing stop is 100%
            GradientStop last = stops.get(stops.size() - 1);
            if (last.getPercentage() == null)
                last.setPercentage(100.0f);
            //remaining stops are half between
            int f = -1; //first unknown percentage position
            for (int i = 1; i < stops.size(); i++)
            {
                GradientStop stop = stops.get(i);
                if (stop.getPercentage() == null)
                {
                    if (f == -1)
                    {
                        f = i;
                    }
                }
                else
                {
                    if (f != -1)
                    {
                        float fPerc = stops.get(f - 1).getPercentage();
                        float lPerc = stop.getPercentage();
                        float frac = (lPerc - fPerc) / (i - f + 1);
                        for (int j = f; j < i; j++)
                        {
                            stops.get(j).setPercentage(fPerc + frac * (j - f + 1));
                        }
                        f = -1;
                    }
                }
            }
        }
    }
    
    private void recomputePercentages()
    {
        maxPercentage = 0f;
        final float whole = (isRepeating() && getLastLengthPx() != null) ? getLastLengthPx() : getLength();
        //final float whole = getLength();
        for (GradientStop stop : stops)
        {
            //recompute the percentages for the absolute stops
            if (stop.getPxLength() != null)
            {
                stop.setPercentage((stop.getPxLength() / whole) * 100.0f);
            }
            //ensure that the percentages are increasing
            if (stop.getPercentage() != null)
            {
                if (stop.getPercentage() < maxPercentage)
                    stop.setPercentage(maxPercentage);
                else
                    maxPercentage = stop.getPercentage();
            }
        }
    }
    
} public class GradientStop
{
    private Color color;
    private Float percentage;
    private Float pxLength;
    
    /**
     * Creates a new gradient stop.
     * 
     * @param color stop color (must be always specified)
     * @param percentage the percentage value (if specified)
     * @param pxLength the absolute length value in pixels (if specified)
     */
    public GradientStop(Color color, Float percentage, Float pxLength)
    {
        this.color = color;
        this.percentage = percentage;
        this.pxLength = pxLength;
    }

    public Color getColor()
    {
        return color;
    }

    public Float getPercentage()
    {
        return percentage;
    }

    public Float getPxLength()
    {
        return pxLength;
    }

    public void setColor(Color color)
    {
        this.color = color;
    }

    public void setPercentage(Float percentage)
    {
        this.percentage = percentage;
    }

    public void setPxLength(Float pxLength)
    {
        this.pxLength = pxLength;
    }

}
 public class LinearGradient extends Gradient
{
    //gradient area width and height
    private float width;
    private float height;
    
    //gradient starting and ending points in percentages of the width/height
    private float x1;
    private float y1;
    private float x2;
    private float y2;

    public LinearGradient()
    {
        super();
    }
    
    @Override
    public float getLength()
    {
        final double dx = Math.abs(x2 - x1);
        final double dy = Math.abs(y2 - y1);
        return (float) Math.hypot(dx, dy);
    }
    
    public float getWidth()
    {
        return width;
    }

    public float getHeight()
    {
        return height;
    }

    /**
     * Gets the start point X coordinate.
     * @return
     */
    public float getX1()
    {
        return x1;
    }

    /**
     * Gets the start point Y coordinate.
     * @return
     */
    public float getY1()
    {
        return y1;
    }

    /**
     * Gets the end point X coordinate.
     * @return
     */
    public float getX2()
    {
        return x2;
    }

    /**
     * Gets the end point Y coordinate.
     * @return
     */
    public float getY2()
    {
        return y2;
    }

    /**
     * Gets the end point X coordinate after limiting the length of the gradient by the
     * distance of the last stop (when specified). For non-repeating gradients or
     * when the last stop distance is not explicitily specified, X2 is returned.
     * @return
     */
    public float getEfficientX2()
    {
        if (isRepeating() && getLastLengthPx() != null)
            return ((getX2() - getX1()) * getLastLengthPx() / getLength()) + getX1();
        else
            return getX2();
    }
    
    /**
     * Gets the end point X coordinate after limiting the length of the gradient by the
     * distance of the last stop (when specified). For non-repeating gradients or
     * when the last stop distance is not explicitily specified, Y2 is returned.
     * @return
     */
    public float getEfficientY2()
    {
        if (isRepeating() && getLastLengthPx() != null)
            return ((getY2() - getY1()) * getLastLengthPx() / getLength()) + getY1();
        else
            return getY2();
    }
    
    /**
     * Sets the gradient angle and computes the coordinates.
     * 
     * @param deg the gradient angle
     * @param w containing element width
     * @param h containing element height
     */
    public void setAngleDeg(double deg, float w, float h)
    {
        this.width = w;
        this.height = h;
        
        final double procDeg = (deg % 360 + 360) % 360;
        final double normDeg = 90 - procDeg;

        x1 = x2 = y1 = y2 = 0;

        final float sx = w / 2;
        final float sy = h / 2;

        // calculating coordinates of corners of the element
        final float ax = 0;
        final float ay = 0;

        final float bx = w;
        final float by = 0;

        final float cx = w;
        final float cy = h;

        final float dx = 0;
        final float dy = h;

        if (procDeg == 0)
        {
            x1 = w / 2;
            y1 = h;
            x2 = w / 2;
            y2 = 0;
        }
        else if (procDeg == 90)
        {
            x1 = 0;
            y1 = h / 2;
            x2 = w;
            y2 = h / 2;
        }
        else if (procDeg == 180)
        {
            x1 = w / 2;
            y1 = 0;
            x2 = w / 2;
            y2 = h;
        }
        else if (procDeg == 270)
        {
            x1 = w;
            y1 = h / 2;
            x2 = 0;
            y2 = h / 2;
        }
        else
        {
            final double tan = Math.tan((normDeg / 180) * Math.PI);

            //compute the direction of the gradient axis - y=k*x+q
            double k = -tan;
            double q = sy - k * sx;

            //direction of the perpendiculars - y=kn*x+q1 and y=kn*x+q2 
            double kn = 1 / tan;
            double q1, q2;
            if (procDeg > 0 && procDeg <= 90)
            {
                q1 = dy - kn * dx;
                q2 = by - kn * bx;
            }
            else if (procDeg > 90 && procDeg < 180)
            {
                q1 = ay - kn * ax;
                q2 = cy - kn * cx;
            }
            else if (procDeg > 180 && procDeg < 270)
            {
                q1 = by - kn * bx;
                q2 = dy - kn * dx;
            }
            else //if (procDeg > 270 && procDeg < 360)
            {
                q1 = cy - kn * cx;
                q2 = ay - kn * ax;
            }
            x2 = (float) ((q2 - q) / (k - kn));
            y2 = (float) (k * x2 + q);
            x1 = (float) ((q1 - q) / (k - kn));
            y1 = (float) (k * x1 + q);
        }
    }
    
}
 public class RadialGradient extends Gradient
{
    // an elipse/circle switch
    private boolean circle;

    // center point and radiuses
    private float cx;
    private float cy;
    private float rx;
    private float ry;

    /** Position and size of the gradient in the background area */
    private Rectangle bgRect;

    /** Total gradient area width and height */
    private float gradWidth;
    private float gradHeight;

    /** Gradient size keywords */
    public enum GradientSize
    {
        CLOSEST_CORNER, CLOSEST_SIDE, FARTHEST_CORNER, FARTHEST_SIDE
    }

    /**
     * Creates a radial gradient with the given size.
     *
     * @param bgRect
     */
    public RadialGradient(Rectangle bgRect)
    {
        super();
        circle = true;
        this.bgRect = bgRect;
    }

    /**
     * Checks whether this is a circle gradient.
     * @return {@code true} for circle, {@code false} for ellipse.
     */
    public boolean isCircle()
    {
        return circle;
    }

    public float getCx()
    {
        return cx;
    }

    public float getCy()
    {
        return cy;
    }

    public float getRx()
    {
        return rx;
    }

    public float getRy()
    {
        return ry;
    }

    /**
     * Gets the X radius after limiting the length of the gradient by the
     * distance of the last stop (when specified). For non-repeating gradients or
     * when the last stop distance is not explicitily specified, Rx is returned.
     * @return
     */
    public float getEfficientRx()
    {
        if (isRepeating() && getLastLengthPx() != null)
            return getRx()  * getLastLengthPx() / getLength();
        else
            return getRx();
    }
    
    /**
     * Gets the X radius after limiting the length of the gradient by the
     * distance of the last stop (when specified). For non-repeating gradients or
     * when the last stop distance is not explicitily specified, Rx is returned.
     * @return
     */
    public float getEfficientRy()
    {
        if (isRepeating() && getLastLengthPx() != null)
            return getRy()  * getLastLengthPx() / getLength();
        else
            return getRy();
    }
    
    @Override
    public float getLength()
    {
        return Math.max(rx, ry);
    }
    
    public Rectangle getBgRect()
    {
        return bgRect;
    }

    public float getGradWidth()
    {
        return gradWidth;
    }

    public float getGradHeight()
    {
        return gradHeight;
    }

    //==============================================================================
    
    /**
     * Creates an ellipse gradient from the radius and center point in absolute values.
     *
     * @param rx x radius
     * @param ry y radius
     * @param sx center point x
     * @param sy center point y
     */
    public void setEllipse(float rx, float ry, float sx, float sy)
    {
        circle = false;
        this.cx = sx;
        this.cy = sy;
        this.rx = rx;
        this.ry = ry;

        // compute the width and height of the gradient area
        if (ry > rx)
        {
            gradHeight = bgRect.width * ry / rx;
            gradWidth = bgRect.width;
        }
        else
        {
            gradWidth = bgRect.height * rx / ry;
            gradHeight = bgRect.height;
        }
    }

    /**
     * Creates an ellipse gradient from a size keyword and center point in absolute values.
     *
     * @param size
     * @param sx center point x
     * @param sy center point y
     */
    public void setEllipse(GradientSize size, float sx, float sy)
    {
        float r[];
        switch (size)
        {
            case CLOSEST_CORNER:
                r = computeAxesFromCorner(sx, sy, getIndexOfMinCornerDistance(sx, sy));
                break;
            case FARTHEST_CORNER:
                r = computeAxesFromCorner(sx, sy, getIndexOfMaxCornerDistance(sx, sy));
                break;
            case CLOSEST_SIDE:
                r = new float[2];
                r[0] = closestSideDistanceX(sx);
                r[1] = closestSideDistanceY(sy);
                break;
            case FARTHEST_SIDE:
                r = new float[2];
                r[0] = farthestSideDistanceX(sx);
                r[1] = farthestSideDistanceY(sy);
                break;
            default:
                r = new float[2];
                break;
        }
        setEllipse(r[0], r[1], sx, sy);
    }

    /**
     * Creates a circle gradient from the radius and center point in absolute values.
     *
     * @param rxy the radius
     * @param sx center point x
     * @param sy center point y
     */
    public void setCircle(float rxy, float sx, float sy)
    {
        circle = true;

        cx = sx;
        cy = sy;
        rx = ry = rxy;
    }

    /**
     * Creates a circle gradient from the size keyword and center point in absolute values.
     *
     * @param rxy the radius
     * @param sx center point x
     * @param sy center point y
     */
    public void setCircleDataRadLengths(GradientSize rl, float sx, float sy)
    {
        float rxy = 0;
        List<Float> l;
        switch (rl)
        {
            case CLOSEST_CORNER:
                l = getAllCornersDistance(sx, sy);
                rxy = Collections.min(l);
                break;
            case CLOSEST_SIDE:
                l = getAllSidesDistance(sx, sy);
                rxy = Collections.min(l);
                break;
            case FARTHEST_CORNER:
                l = getAllCornersDistance(sx, sy);
                rxy = Collections.max(l);
                break;
            case FARTHEST_SIDE:
                l = getAllSidesDistance(sx, sy);
                rxy = Collections.max(l);
                break;
        }
        setCircle(rxy, sx, sy);
    }

    //==============================================================================
    
    private float closestSideDistanceX(float sx)
    {
        return Math.min(coordinateDistance(sx, 0), coordinateDistance(sx, bgRect.width));
    }
    
    private float farthestSideDistanceX(float sx)
    {
        return Math.max(coordinateDistance(sx, 0), coordinateDistance(sx, bgRect.width));
    }
    
    private float closestSideDistanceY(float sy)
    {
        return Math.min(coordinateDistance(sy, 0), coordinateDistance(sy, bgRect.height));
    }
    
    private float farthestSideDistanceY(float sy)
    {
        return Math.max(coordinateDistance(sy, 0), coordinateDistance(sy, bgRect.height));
    }
    
    private int getIndexOfMinCornerDistance(float sx, float sy)
    {
        List<Float> l = getAllCornersDistance(sx, sy);
        return l.indexOf(Collections.min(l));
    }

    private int getIndexOfMaxCornerDistance(float sx, float sy)
    {
        List<Float> l = getAllCornersDistance(sx, sy);
        return l.indexOf(Collections.max(l));
    }

    private List<Float> getAllCornersDistance(float centerX, float centerY)
    {
        List<Float> l = new ArrayList<Float>(4);
        l.add(pointsDistance(centerX, centerY, 0, 0)); // A
        l.add(pointsDistance(centerX, centerY, 0, bgRect.height)); // D
        l.add(pointsDistance(centerX, centerY, bgRect.width, 0)); // B
        l.add(pointsDistance(centerX, centerY, bgRect.width, bgRect.height)); // C
        return l;
    }

    private List<Float> getAllSidesDistance(float centerX, float centerY)
    {
        List<Float> l = new ArrayList<Float>(4);
        l.add(coordinateDistance(centerX, 0));
        l.add(coordinateDistance(centerX, bgRect.width));
        l.add(coordinateDistance(centerY, 0));
        l.add(coordinateDistance(centerY, bgRect.height));
        return l;
    }

    /**
     * Computes the rx and ry radiuses from the center and selected corner index so that the
     * ellipse end shape exactly meets the given cornet.
     * @param sx ellipse center x coordinate
     * @param sy ellipse center y coordinate
     * @param i the corner index, see {@link #getCornerX(int)}
     * @return
     */
    private float[] computeAxesFromCorner(float sx, float sy, int i)
    {
        float[] r = new float[2];
        
        final float xx = getCornerX(i);
        final float yy = getCornerY(i);
        final float dx = coordinateDistance(sx, xx);
        final float dy = coordinateDistance(sy, yy);
        if (dy == 0)
        {
            r[0] = r[1] = 0;
        }
        else
        {
            float ratio = dx / dy;
            float ry2 = (xx - sx) * (xx - sx) / (ratio * ratio) + (yy - sy) * (yy - sy);
            r[1] = (float) Math.sqrt(ry2);
            r[0] = ratio * r[1];
        }
        return r;
    }
    
    /**
     * Gets the X coordinate of a corner.
     * @param pointIndex 0 for top left, 1 for bottom left, 2 for top right, 3 for bottom right
     * @return the given point X coordinate.
     */
    private float getCornerX(int pointIndex)
    {
        switch (pointIndex)
        {
            case 0:
            case 1:
                return 0;
            case 2:
            case 3:
                return bgRect.width;
            default:
                return 0;
        }
    }
    
    /**
     * Gets the Y coordinate of a corner.
     * @param pointIndex 0 for top left, 1 for bottom left, 2 for top right, 3 for bottom right
     * @return the given point Y coordinate.
     */
    private float getCornerY(int pointIndex)
    {
        switch (pointIndex)
        {
            case 0:
            case 2:
                return 0;
            case 1:
            case 3:
                return bgRect.height;
            default:
                return 0;
        }
    }
    
    private float pointsDistance(float x1, float y1, float x2, float y2)
    {
        final float dx = Math.abs(x1 - x2);
        final float dy = Math.abs(y1 - y2);
        return (float) Math.sqrt(dx * dx + dy * dy);
    }

    private float coordinateDistance(float x1, float x2)
    {
        return Math.abs(x1 - x2);
    }

    //==============================================================================
    
    public static GradientSize decodeSizeIdent(TermIdent ident)
    {
        if (ident != null)
        {
            switch (ident.getValue())
            {
                case "closest-side":
                    return GradientSize.CLOSEST_SIDE;
                case "closest-corner":
                    return GradientSize.CLOSEST_CORNER;
                case "farthest-side":
                    return GradientSize.FARTHEST_SIDE;
                case "farthest-corner":
                    return GradientSize.FARTHEST_CORNER;
                default:
                    return null;
            }
        }
        else
            return null;
    }
    
}

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.fit.cssbox.layout.BackgroundImage;
import org.fit.cssbox.layout.CSSDecoder;
import org.fit.cssbox.layout.ElementBox;
import org.fit.cssbox.layout.Rectangle;
import org.fit.cssbox.layout.VisualContext;
import org.fit.cssbox.render.BackgroundImageGradient;
import org.fit.cssbox.render.BackgroundImageImage;
import org.fit.cssbox.render.Gradient;
import org.fit.cssbox.render.GradientStop;
import org.fit.cssbox.render.LinearGradient;
import org.fit.cssbox.render.RadialGradient;
import org.fit.net.DataURLHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cz.vutbr.web.css.CSSProperty;
import cz.vutbr.web.css.NodeData;
import cz.vutbr.web.css.TermColor;
import cz.vutbr.web.css.TermFunction;
import cz.vutbr.web.css.TermLengthOrPercent;
import cz.vutbr.web.css.TermList;
import cz.vutbr.web.css.TermURI;
import cz.vutbr.web.css.CSSProperty.BackgroundAttachment;
import cz.vutbr.web.css.CSSProperty.BackgroundOrigin;
import cz.vutbr.web.css.CSSProperty.BackgroundRepeat;
import cz.vutbr.web.css.CSSProperty.BackgroundSize;
import cz.vutbr.web.csskit.Color;
/*
 * BackgroundDecoder.java
 * Copyright (c) 2005-2020 Radek Burget
 *
 * CSSBox is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *  
 * CSSBox is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *  
 * You should have received a copy of the GNU Lesser General Public License
 * along with CSSBox. If not, see <http://www.gnu.org/licenses/>.
 *
 * Created on 11. 4. 2020, 12:20:05 by burgetr
 */
package org.fit.cssbox.css;




/**
 * A decoder of background color and image declarations for a given element box. It loads the
 * element style and provides the color and a representation of the images used in its
 * background. 
 * 
 * @author burgetr
 */
public class BackgroundDecoder
{
    private static Logger log = LoggerFactory.getLogger(BackgroundDecoder.class);
    
    /** The owning element box */
    private ElementBox owner;
    private VisualContext ctx;
    private NodeData style;
    
    /** Background color or null when transparent */
    private Color bgcolor;
    
    /** Background images or null when there are no background images */
    private List<BackgroundImage> bgimages;
    
    
    /**
     * Creates a background decoder for a given element box.
     * 
     * @param owner The element box that owns the background.
     */
    public BackgroundDecoder(ElementBox owner)
    {
        this.owner = owner;
        ctx = owner.getVisualContext();
        style = owner.getStyle();
        loadBackground();
    }

    /**
     * Returns the element box that owns the background.
     * @return the owner element box
     */
    public ElementBox getOwner()
    {
        return owner;
    }

    /**
     * Obtains the efficient background color to be used for drawing the background.
     * @return the background color or null when transparent
     */
    public Color getBgcolor()
    {
        return bgcolor;
    }

    /**
     * Obtains the list of background images of the element.
     * @return a list of the background images
     */
    public List<BackgroundImage> getBackgroundImages()
    {
        return bgimages;
    }

    /**
     * Checks whether there is no background defined for the owner element.
     * @return {@code true} when the background is empty.
     */
    public boolean isBackgroundEmpty()
    {
        return (bgcolor == null) && (bgimages == null);
    }
    
    /**
     * Loads the background information from the style
     */
    protected void loadBackground()
    {
        if (style != null)
        {
            CSSProperty.BackgroundColor bg = style.getProperty("background-color");
            if (bg == CSSProperty.BackgroundColor.color)
            {
                TermColor bgc = style.getSpecifiedValue(TermColor.class, "background-color");
                if (bgc.isTransparent())
                    bgcolor = null;
                else
                    bgcolor = bgc.getValue();
            }
            else
                bgcolor = null;
    
            bgimages = loadBackgroundImages();
        }
    }
    
    /**
     * Loads background images from the owner element style. Considers their positions, repetition, etc.
     * @return a list of images
     */
    protected List<BackgroundImage> loadBackgroundImages()
    {
        final int count = style.getListSize("background-image", true);
        if (count > 0)
        {
            List<BackgroundImage> bgimages = new ArrayList<BackgroundImage>(count);
            for (int i = 0; i < count; i++)
            {
                final BackgroundImage bgimg = loadBackgroundImage(style, i);
                if (bgimg != null)
                    bgimages.add(bgimg);
            }
            return bgimages.isEmpty() ? null : bgimages; //return null when there are no images
        }
        else
            return null;
    }
    
    /**
     * Creates background a image structure based on a style definition.
     * @param style the style definition
     * @param index index of the image to take (there may be multiple images defined, 0 for the first image defined)
     * @return The corresponding BackgroundImage object.
     */
    protected BackgroundImage loadBackgroundImage(NodeData style, int index)
    {
        CSSProperty.BackgroundImage image = style.getProperty("background-image", index);
        if (image == CSSProperty.BackgroundImage.uri || image == CSSProperty.BackgroundImage.gradient)
        {
            try {
                BackgroundImage ret = null;
                //position
                CSSProperty.BackgroundPosition position = style.getProperty("background-position", index);
                TermList positionValues = style.getValue(TermList.class, "background-position", index);
                //repeat
                CSSProperty.BackgroundRepeat repeat = style.getProperty("background-repeat", index);
                if (repeat == null) repeat = BackgroundRepeat.REPEAT;
                //origin
                CSSProperty.BackgroundOrigin origin = style.getProperty("background-origin", index);
                if (origin == null) origin = BackgroundOrigin.PADDING_BOX;
                //attachment
                CSSProperty.BackgroundAttachment attachment = style.getProperty("background-attachment", index);
                if (attachment == null) attachment = BackgroundAttachment.SCROLL;
                //size
                CSSProperty.BackgroundSize size = style.getProperty("background-size", index);
                TermList sizeValues = null;
                if (size == null) size = BackgroundSize.list_values;
                else if (size == BackgroundSize.list_values)
                    sizeValues = style.getValue(TermList.class, "background-size", index);
                //image
                if (image == CSSProperty.BackgroundImage.uri)
                {
                    TermURI urlstring = style.getValue(TermURI.class, "background-image", index);
                    URL url = DataURLHandler.createURL(urlstring.getBase(), urlstring.getValue());
                    BackgroundImageImage bgimg =
                            new BackgroundImageImage(owner, url, position, positionValues, repeat, 
                                    attachment, origin, size, sizeValues);
                    if (ctx.getConfig().getLoadBackgroundImages())
                        bgimg.setImage(ctx.getImageLoader().loadImage(url));
                    ret = bgimg;
                }
                else if (image == CSSProperty.BackgroundImage.gradient)
                {
                    BackgroundImageGradient bgimg =
                            new BackgroundImageGradient(owner, position, positionValues, repeat, 
                                    attachment, origin, size, sizeValues);
                    Rectangle bgsize = bgimg.getComputedPosition();
                    
                    TermFunction.Gradient values = style.getValue(TermFunction.Gradient.class, "background-image", index);
                    if (values instanceof TermFunction.LinearGradient)