/*
 * Viewport.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/>.
 */

package org.fit.cssbox.layout;

import java.util.Vector;

import org.fit.cssbox.render.BoxRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;

import cz.vutbr.web.css.CSSFactory;

/**
 * The viewport is a special case of BlockElement that has several widths and heights:
 * 
 * <ul>
 * <li><strong>Viewport size</strong> - the width and height of the visible area used for
 * computing the sizes of the contained blocks.</li>
 * <li><strong>Canvas size</strong> - the width and height of the whole rendered page</li> 
 * 
 * @author radek
 */
public class Viewport extends BlockBox
{
    private static Logger log = LoggerFactory.getLogger(Viewport.class);
    
    /** Total canvas width */
	private float width;
	/** Total canvas height */
	private float height;
	/** Visible rectagle -- the position and size of the CSS viewport */
    private Rectangle visibleRect;
	
    protected BrowserConfig config;
	private BoxFactory factory;
	private BoxRenderer renderer;
	private Element root; //the DOM root
	private ElementBox rootBox; //the box that corresponds to the root node. It should be one of the child boxes.
    private boolean rootOverflowVisible = true; //has the root box originally had overflow:visible?
    private float maxx; //maximal X position of all the content
    private float maxy; //maximal Y position of all the content
    private boolean recomputeAbs; //indicates that the absolute positions need to be recomputed
    
    /**
     * Creates a new Viewport with the given initial size. The actual size may be increased during the layout. 
     *  
     * @param e The anonymous element representing the viewport.
     * @param ctx
     * @param factory The factory used for creating the child boxes.
     * @param root The root element of the rendered document.
     * @param width Preferred (minimal) width.
     * @param height Preferred (minimal) height.
     */
    public Viewport(Element e, VisualContext ctx, BoxFactory factory, Element root, float width, float height)
	{
		super(e, ctx);
		ctx.setViewport(this);
		this.factory = factory;
		this.root = root;
		style = CSSFactory.createNodeData(); //Viewport starts with an empty style
        nested = new Vector<Box>();
        startChild = 0;
        endChild = 0;
		this.width = width;
		this.height = height;
        isblock = true;
        contblock = true;
        root = null;
        visibleRect = new Rectangle(0, 0, width, height);
	}
    
    /**
     * Obtains the position of the visible part (CSS viewport) in the canvas.
     * @return the visible rectangle
     */
    public Rectangle getVisibleRect()
    {
        return visibleRect;
    }

    /**
     * Sets the position of the visible part (CSS viewport) in the canvas.
     * @param visibleRect the visible rectangle to be set
     */
    public void setVisibleRect(Rectangle visibleRect)
    {
        this.visibleRect = visibleRect;
        this.content = visibleRect.getSize();
    }

    /**
     * Obtains the width of the whole canvas that represents the whole rendered page.
     * @return The canvas size.
     */
    public float getCanvasWidth()
    {
        return width;
    }
    
    /**
     * Obtains the height of the whole canvas that represents the whole rendered page.
     * @return The canvas size.
     */
    public float getCanvasHeight()
    {
        return height;
    }
    
    /**
     * Obtains the current browser configuration.
     * @return current configuration.
     */
    public BrowserConfig getConfig()
    {
        return config;
    }

    /**
     * Sets the browser configuration used for rendering.
     * @param config the new configuration.
     */
    public void setConfig(BrowserConfig config)
    {
        this.config = config;
        overflowX = overflowY = config.getClipViewport() ? OVERFLOW_HIDDEN : OVERFLOW_VISIBLE;
    }
    
    @Override
    public void initSubtree()
    {
        super.initSubtree();
    }
    
    @Override
    public String toString()
    {
        return "Viewport " + width + "x" + height + 
                "[visible " + visibleRect.x + "," +visibleRect.y + "," +
                visibleRect.width + "," + visibleRect.height + "]"; 
    }
    
    public BoxFactory getFactory()
    {
        return factory;
    }

    public float getMinimalWidthLimit()
    {
    	return width;
    }
    
    /**
     * Obtains the DOM root element.
     * @return The root element of the document.
     */
    public Element getRootElement()
    {
        return root;
    }

    /**
     * Obtains the child box the corresponds to the DOM root element.
     * @return the corresponding element box or <code>null</code> if the viewport is empty.
     */
    public ElementBox getRootBox()
    {
        return rootBox;
    }
    
    public ElementBox getElementBoxByName(String name, boolean case_sensitive)
    {
        return recursiveFindElementBoxByName(this, name, case_sensitive);
    }
    
	@Override
    public void addSubBox(Box box)
    {
        super.addSubBox(box);
        if (box instanceof ElementBox && ((ElementBox) box).getElement() == root)
        {
            if (rootBox != null)
                log.debug("Viewport warning: another root box '" + box + "' in addition to previous '" + rootBox + "'");
            box.makeRoot();
            rootBox = (ElementBox) box;
        }
    }

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

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

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

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

    @Override
    public boolean visibleInClip(Box box)
    {
        if (config.getClipViewport())
            return super.visibleInClip(box);
        else
        {
            //not clipping - everything that is in positive coordinates is visible in viewport
            Rectangle bb;
            if (box instanceof ElementBox)
                bb = ((ElementBox) box).getAbsoluteBorderBounds();
            else
                bb = box.getAbsoluteBounds();
            return (bb.x + bb.width > 0) && (bb.y + bb.height > 0);
        }
    }
    
    @Override
    protected boolean separatedFromTop(ElementBox box)
    {
        return true;
    }

    @Override
    protected boolean separatedFromBottom(ElementBox box)
    {
        return true;
    }

	@Override
	public void setSize(float width, float height)
    {
        this.width = width;
        this.height = height;
        content = new Dimension(width, height);
        bounds = new Rectangle(0, 0, totalWidth(), totalHeight());
    }
    
	@Override
	public Viewport getViewport()
	{
		return this;
	}

    @Override
    protected void loadPosition()
    {
        position = BlockBox.POS_ABSOLUTE;
        topset = true;
        leftset = true;
        bottomset = false;
        rightset = false;
        coords = new LengthSet(0, 0, 0, 0);
    }
    
	@Override
	protected void loadSizes(boolean update)
	{
		if (!update)
		{
			margin = new LengthSet();
			emargin = new LengthSet();
			declMargin = new LengthSet();
			border = new LengthSet();
			padding = new LengthSet();
			content = new Dimension(width, height);
			min_size = new Dimension(-1, -1);
			max_size = new Dimension(-1, -1);
			loadPosition();
		}
		bounds = new Rectangle(0, 0, totalWidth(), totalHeight());
	}

	@Override
    public void setContentWidth(float width)
    {
	    //do not descrease the viewport width under the initial value
        if (width > this.width)
            super.setContentWidth(width);
    }

    @Override
    public void setContentHeight(float height)
    {
        //do not descrease the viewport height under the initial value
        if (height > this.height)
            super.setContentHeight(height);
    }

    /**
     * Computes the offset of the background image positions given by the difference between the background
     * source element position and the viewport position.
     * @return the background offset
     */
    public Dimension getBackgroundOffset()
    {
        if (getRootBox() != null)
        {
            final Rectangle rbg = getRootBox().getAbsoluteBorderBounds();
            return new Dimension(rbg.x, rbg.y);
        }
        else
            return new Dimension();
    }

    /**
	 * Calculates the absolute positions and updates the viewport size
	 * in order to enclose all the boxes.
	 * @param min the minimal viewport dimensions
	 */
	public void updateBounds(Dimension min)
	{
		//first round - compute the viewport size
		maxx = min.width;
		maxy = min.height;
        absolutePositionsChildren();

		//update the size
		if (width < maxx) width = maxx;
		if (height < maxy) height = maxy;
		loadSizes();
	}

    @Override
    public boolean doLayout(float availw, boolean force, boolean linestart)
    {
        //remove previously splitted children from possible previous layout
        clearSplitted();

        //viewport has a siplified width computation algorithm
        float min = getMinimalContentWidth();
        float pref = Math.max(min, width);
        setContentWidth(pref);
        updateChildSizes();
        
        //the width should be fixed from this point
        widthComputed = true;
        
        /* Always try to use the full width. If the box is not in flow, its width
         * is updated after the layout */
        setAvailableWidth(totalWidth());
        
        if (!contblock)  //block elements containing inline elements only
            layoutInline();
        else //block elements containing block elements
            layoutBlocks();
        
        //allways fits as well possible
        return true;
    }
	
    @Override
    public boolean formsStackingContext()
    {
        return true;
    }
    
	@Override
    public void absolutePositions()
    {
	    if (parent == null) //viewport may be the root
	        absbounds = new Rectangle(bounds);
	    else //or a nested viewport
	        absbounds = new Rectangle(parent.getAbsoluteContentBounds());
	    
	    //clear this context if it exists (remove old children)
	    if (scontext != null)
            scontext.clear();
	    
	    absolutePositionsChildren();
    }
	
	/**
	 * Computes the absolute positions of the child boxes.
	 */
	protected void absolutePositionsChildren()
	{
        //first round: position most boxes
        recomputeAbs = false;
        for (int i = 0; i < getSubBoxNumber(); i++)
            getSubBox(i).absolutePositions();
        if (recomputeAbs)
        {
            //second round: some reference boxes used, recompute once again
            if (scontext != null) //clear the stacking context if it exists -- the child contexts will register again
                scontext.clear();
            for (int i = 0; i < getSubBoxNumber(); i++)
                getSubBox(i).absolutePositions();
            recomputeAbs = false;
        }
	}
	
    /**
     * Sets the current renderer and draws the whole subtree using the given renderer.
     * @param renderer The renderer to be used for drawing.
     */
    public void draw(BoxRenderer renderer)
    {
        this.renderer = renderer;
        drawStackingContext(false);
    }
	
    /**
     * Obtains the current renderer used for painting the boxes.
     * @return current renderer.
     */
    public BoxRenderer getRenderer()
    {
        return renderer;
    }
    
	/**
	 * Updates the maximal viewport size according to the element bounds
	 */
	public void updateBoundsFor(Rectangle bounds)
	{
		float x = bounds.x + bounds.width - 1;
		if (maxx < x) maxx = x;
		float y = bounds.y + bounds.height - 1;
		if (maxy < y) maxy = y;
	}
	
	/**
	 * Indicates that the absolute positions need to be recomputed onec again. This happens when
	 * some absolutely positioned box has a 'static' position depending on some in-flow box
	 * and the position of the in-flow box changed.
	 */
	public void requireRecomputePositions()
	{
	    recomputeAbs = true;
	}
	
	/**
	 * Uses the given block as a clipping block instead of the default Viewport.
	 * @param block the new clipping block
	 */
	public void clipByBlock(BlockBox block)
	{
	    recursivelySetClipBlock(this, block);
	}
	
	private void recursivelySetClipBlock(Box root, BlockBox clip)
	{
	    if (root == this || root.getClipBlock() == this)
	    {
	        root.setClipBlock(clip);
	        if (root instanceof ElementBox)
	        {
	            ElementBox eb = (ElementBox) root;
	            for (int i = eb.getStartChild(); i < eb.getEndChild(); i++)
	                recursivelySetClipBlock(eb.getSubBox(i), clip);
	        }
	    }
	}
	
	/**
	 * Tries to propagate the overflow value to viewport from the element based on
	 * https://drafts.csswg.org/css-overflow-3/#overflow-propagation
	 * If the conditions are met, the overflow values are propagated. Othervise, no action is taken.
	 * 
	 * @param block the box to be used as a possible source of the overflow values
	 * @return {@code true} when the propagation is finished (no more boxes may be used as the source), {@code false} when other boxes should be tried.
	 */
	public boolean checkPropagateOverflow(BlockBox block)
	{
	    if (rootBox == null && block.getElement() == root) //this block will become a new root block; use it
	    {
	        rootOverflowVisible = (block.getOverflowX() == BlockBox.OVERFLOW_VISIBLE);
	        takeBoxOverflow(block);
	        return !rootOverflowVisible || !config.getUseHTML(); //in HTML mode, the "body" element may be used as well thus we should continue when root element is not visible
	    }
	    else if ("body".equals(block.getElement().getTagName()))
	    {
	        if (config.getUseHTML())
	            takeBoxOverflow(block);
	        return true; //that should be all, we are finished
	    }
	    else
	        return false; //this one could not be used, go on
	}
	
	private void takeBoxOverflow(BlockBox srcBlock)
	{
        //use the propagated value for the viewport
        overflowX = srcBlock.overflowX;
        overflowY = srcBlock.overflowY;
        //use VISIBLE for the original block
        srcBlock.overflowX = BlockBox.OVERFLOW_VISIBLE;
        srcBlock.overflowY = BlockBox.OVERFLOW_VISIBLE;
        //apply CSSBox-specific viewport clipping config
        if (config.getClipViewport())
        {
            overflowX = BlockBox.OVERFLOW_HIDDEN;
            overflowY = BlockBox.OVERFLOW_HIDDEN;
        }
        else
        {
            //CSSBox treats the overflow:visible as scrollable for now, i.e. scroll and auto values should be converted to visible
            if (overflowX != BlockBox.OVERFLOW_HIDDEN)
                overflowX = BlockBox.OVERFLOW_VISIBLE;
            if (overflowY != BlockBox.OVERFLOW_HIDDEN)
                overflowY = BlockBox.OVERFLOW_VISIBLE;
        }
	}
	
    //===================================================================================
	
    @Override
    public float getContentX()
    {
        return visibleRect.x;
    }

    @Override
    public float getContentY()
    {
        return visibleRect.y;
    }

    @Override
    public float getContentWidth()
    {
        return visibleRect.width;
    }

    @Override
    public float getContentHeight()
    {
        return visibleRect.height;
    }

	@Override
    public Rectangle getAbsoluteBorderBounds()
    {
        return new Rectangle(visibleRect);
    }
	
    @Override
    public Rectangle getClippedBounds()
    {
        if (config.getClipViewport())
            return getAbsoluteBounds();
        else
            return new Rectangle(0, 0, width, height);
    }

    @Override
    public Rectangle getClippedContentBounds()
    {
        if (config.getClipViewport())
            return getAbsoluteBounds();
        else
            return new Rectangle(0, 0, width, height);
    }
	
    //===================================================================================
    
    private ElementBox recursiveFindElementBoxByName(ElementBox ebox, String name, boolean case_sensitive)
    {
        boolean eq;
        if (case_sensitive)
            eq = ebox.getElement().getTagName().equals(name);
        else
            eq = ebox.getElement().getTagName().equalsIgnoreCase(name);
        
        if (eq)
            return ebox;
        else
        {
            ElementBox ret = null;
            for (int i = 0; i < ebox.getSubBoxNumber() && ret == null; i++)
            {
                Box child = ebox.getSubBox(i);
                if (child instanceof ElementBox)
                    ret = recursiveFindElementBoxByName((ElementBox) child, name, case_sensitive);
            }
            return ret;
        }
    }

}