/*
 * {{{ header & license
 * Copyright (c) 2004, 2005 Joshua Marinacci, Torbj�rn Gannholm
 *
 * This program 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 2.1
 * of the License, or (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 * }}}
 */
package org.xhtmlrenderer.layout;

import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.xhtmlrenderer.context.ContentFunctionFactory;
import org.xhtmlrenderer.context.StyleReference;
import org.xhtmlrenderer.css.constants.CSSName;
import org.xhtmlrenderer.css.constants.IdentValue;
import org.xhtmlrenderer.css.parser.CounterData;
import org.xhtmlrenderer.css.style.CalculatedStyle;
import org.xhtmlrenderer.css.style.CssContext;
import org.xhtmlrenderer.css.value.FontSpecification;
import org.xhtmlrenderer.extend.FontContext;
import org.xhtmlrenderer.extend.NamespaceHandler;
import org.xhtmlrenderer.extend.ReplacedElementFactory;
import org.xhtmlrenderer.extend.TextRenderer;
import org.xhtmlrenderer.extend.UserAgentCallback;
import org.xhtmlrenderer.render.Box;
import org.xhtmlrenderer.render.FSFont;
import org.xhtmlrenderer.render.FSFontMetrics;
import org.xhtmlrenderer.render.MarkerData;
import org.xhtmlrenderer.render.PageBox;
import org.xhtmlrenderer.swing.RootPanel;

/**
 * This class tracks state which changes over the course of a layout run.
 * Generally speaking, if possible, state information should be stored in the box
 * tree and not here.  It also provides pass-though calls to many methods in
 * {@link SharedContext}.
 */
public class LayoutContext implements CssContext {
    private SharedContext _sharedContext;

    private Layer _rootLayer;

    private StyleTracker _firstLines;
    private StyleTracker _firstLetters;
    private MarkerData _currentMarkerData;

    private LinkedList _bfcs;
    private LinkedList _layers;

    private FontContext _fontContext;

    private ContentFunctionFactory _contentFunctionFactory = new ContentFunctionFactory();

    private int _extraSpaceTop;
    private int _extraSpaceBottom;
    
    private Map _counterContextMap = new HashMap();
    
    private String _pendingPageName;
    private String _pageName;
    
    private int _noPageBreak = 0;
    
    private Layer _rootDocumentLayer;
    private PageBox _page;
    
    private boolean _mayCheckKeepTogether = true;
    
    private BreakAtLineContext _breakAtLineContext;
    
    public TextRenderer getTextRenderer() {
        return _sharedContext.getTextRenderer();
    }

    public StyleReference getCss() {
        return _sharedContext.getCss();
    }

    public RootPanel getCanvas() {
        return _sharedContext.getCanvas();
    }

    public Rectangle getFixedRectangle() {
        return _sharedContext.getFixedRectangle();
    }

    public NamespaceHandler getNamespaceHandler() {
        return _sharedContext.getNamespaceHandler();
    }

    //the stuff that needs to have a separate instance for each run.
    LayoutContext(SharedContext sharedContext) {
        _sharedContext = sharedContext;
        _bfcs = new LinkedList();
        _layers = new LinkedList();

        _firstLines = new StyleTracker();
        _firstLetters = new StyleTracker();
    }

    public void reInit(boolean keepLayers) {
        _firstLines = new StyleTracker();
        _firstLetters = new StyleTracker();
        _currentMarkerData = null;

        _bfcs = new LinkedList();
        
        if (! keepLayers) {
            _rootLayer = null;
            _layers = new LinkedList();
        }

        _extraSpaceTop = 0;
        _extraSpaceBottom = 0;
    }

    public LayoutState captureLayoutState() {
        LayoutState result = new LayoutState();

        result.setFirstLines(_firstLines);
        result.setFirstLetters(_firstLetters);
        result.setCurrentMarkerData(_currentMarkerData);

        result.setBFCs(_bfcs);
        
        if (isPrint()) {
            result.setPageName(getPageName());
            result.setExtraSpaceBottom(getExtraSpaceBottom());
            result.setExtraSpaceTop(getExtraSpaceTop());
            result.setNoPageBreak(getNoPageBreak());
        }

        return result;
    }

    public void restoreLayoutState(LayoutState layoutState) {
        _firstLines = layoutState.getFirstLines();
        _firstLetters = layoutState.getFirstLetters();

        _currentMarkerData = layoutState.getCurrentMarkerData();

        _bfcs = layoutState.getBFCs();
        
        if (isPrint()) {
            setPageName(layoutState.getPageName());
            setExtraSpaceBottom(layoutState.getExtraSpaceBottom());
            setExtraSpaceTop(layoutState.getExtraSpaceTop());
            setNoPageBreak(layoutState.getNoPageBreak());
        }
    }

    public LayoutState copyStateForRelayout() {
        LayoutState result = new LayoutState();

        result.setFirstLetters(_firstLetters.copyOf());
        result.setFirstLines(_firstLines.copyOf());
        result.setCurrentMarkerData(_currentMarkerData);
        
        if (isPrint()) {
            result.setPageName(getPageName());
        }

        return result;
    }

    public void restoreStateForRelayout(LayoutState layoutState) {
        _firstLines = layoutState.getFirstLines();
        _firstLetters = layoutState.getFirstLetters();

        _currentMarkerData = layoutState.getCurrentMarkerData();
        
        if (isPrint()) {
            setPageName(layoutState.getPageName());
        }
    }

    public BlockFormattingContext getBlockFormattingContext() {
        return (BlockFormattingContext) _bfcs.getLast();
    }

    public void pushBFC(BlockFormattingContext bfc) {
        _bfcs.add(bfc);
    }

    public void popBFC() {
        _bfcs.removeLast();
    }

    public void pushLayer(Box master) {
        Layer layer = null;

        if (_rootLayer == null) {
            layer = new Layer(master);
            _rootLayer = layer;
        } else {
            Layer parent = getLayer();

            layer = new Layer(parent, master);

            parent.addChild(layer);
        }

        pushLayer(layer);
    }

    public void pushLayer(Layer layer) {
        _layers.add(layer);
    }

    public void popLayer() {
        Layer layer = getLayer();

        layer.finish(this);

        _layers.removeLast();
    }

    public Layer getLayer() {
        return (Layer) _layers.getLast();
    }

    public Layer getRootLayer() {
        return _rootLayer;
    }

    public void translate(int x, int y) {
        getBlockFormattingContext().translate(x, y);
    }

    /* code to keep track of all of the id'd boxes */
    public void addBoxId(String id, Box box) {
        _sharedContext.addBoxId(id, box);
    }

    public void removeBoxId(String id) {
        _sharedContext.removeBoxId(id);
    }

    public boolean isInteractive() {
        return _sharedContext.isInteractive();
    }

    public float getMmPerDot() {
        return _sharedContext.getMmPerPx();
    }

    public int getDotsPerPixel() {
        return _sharedContext.getDotsPerPixel();
    }

    public float getFontSize2D(FontSpecification font) {
        return _sharedContext.getFont(font).getSize2D();
    }

    public float getXHeight(FontSpecification parentFont) {
        return _sharedContext.getXHeight(getFontContext(), parentFont);
    }

    public FSFont getFont(FontSpecification font) {
        return _sharedContext.getFont(font);
    }

    public UserAgentCallback getUac() {
        return _sharedContext.getUac();
    }

    public boolean isPrint() {
        return _sharedContext.isPrint();
    }

    public StyleTracker getFirstLinesTracker() {
        return _firstLines;
    }

    public StyleTracker getFirstLettersTracker() {
        return _firstLetters;
    }

    public MarkerData getCurrentMarkerData() {
        return _currentMarkerData;
    }

    public void setCurrentMarkerData(MarkerData currentMarkerData) {
        _currentMarkerData = currentMarkerData;
    }

    public ReplacedElementFactory getReplacedElementFactory() {
        return _sharedContext.getReplacedElementFactory();
    }

    public FontContext getFontContext() {
        return _fontContext;
    }

    public void setFontContext(FontContext fontContext) {
        _fontContext = fontContext;
    }

    public ContentFunctionFactory getContentFunctionFactory() {
        return _contentFunctionFactory;
    }

    public SharedContext getSharedContext() {
        return _sharedContext;
    }

    public int getExtraSpaceBottom() {
        return _extraSpaceBottom;
    }

    public void setExtraSpaceBottom(int extraSpaceBottom) {
        _extraSpaceBottom = extraSpaceBottom;
    }

    public int getExtraSpaceTop() {
        return _extraSpaceTop;
    }

    public void setExtraSpaceTop(int extraSpaceTop) {
        _extraSpaceTop = extraSpaceTop;
    }

    public void resolveCounters(CalculatedStyle style) {
        //new context for child elements
        CounterContext cc = new CounterContext(style);
        _counterContextMap.put(style, cc);
    }

    public CounterContext getCounterContext(CalculatedStyle style) {
        return (CounterContext) _counterContextMap.get(style);
    }

    public FSFontMetrics getFSFontMetrics(FSFont font) {
        return getTextRenderer().getFSFontMetrics(getFontContext(), font, "");
    }

    public class CounterContext {
        private Map _counters = new HashMap();
        /**
         * This is different because it needs to work even when the counter- properties cascade
         * and it should also logically be redefined on each level (think list-items within list-items)
         */
        private CounterContext _parent;

        /**
         * A CounterContext should really be reflected in the element hierarchy, but CalculatedStyles
         * reflect the ancestor hierarchy just as well and also handles pseudo-elements seamlessly.
         *
         * @param style
         */
        CounterContext(CalculatedStyle style) {
            _parent = (LayoutContext.CounterContext) _counterContextMap.get(style.getParent());
            if (_parent == null) _parent = new CounterContext();//top-level context, above root element
            //first the explicitly named counters
            List resets = style.getCounterReset();
            if (resets != null) for (Iterator i = resets.iterator(); i.hasNext();) {
                CounterData cd = (CounterData) i.next();
                _parent.resetCounter(cd);
            }

            List increments = style.getCounterIncrement();
            if (increments != null) for (Iterator i = increments.iterator(); i.hasNext();) {
                CounterData cd = (CounterData) i.next();
                if (!_parent.incrementCounter(cd)) {
                    _parent.resetCounter(new CounterData(cd.getName(), 0));
                    _parent.incrementCounter(cd);
                }
            }

            //then the implicit list-item counter
            if (style.isIdent(CSSName.DISPLAY, IdentValue.LIST_ITEM)) {
                _parent.incrementListItemCounter(1);
            }
        }

        private CounterContext() {

        }

        /**
         * @param cd
         * @return true if a counter was found and incremented
         */
        private boolean incrementCounter(CounterData cd) {
            if ("list-item".equals(cd.getName())) {//reserved name for list-item counter in CSS3
                incrementListItemCounter(cd.getValue());
                return true;
            } else {
                Integer currentValue = (Integer) _counters.get(cd.getName());
                if (currentValue == null) {
                    if (_parent == null) return false;
                    return _parent.incrementCounter(cd);
                } else {
                    _counters.put(cd.getName(), new Integer(currentValue.intValue() + cd.getValue()));
                    return true;
                }
            }
        }

        private void incrementListItemCounter(int increment) {
            Integer currentValue = (Integer) _counters.get("list-item");
            if (currentValue == null) {
                currentValue = new Integer(0);
            }
            _counters.put("list-item", new Integer(currentValue.intValue() + increment));
        }

        private void resetCounter(CounterData cd) {
            _counters.put(cd.getName(), new Integer(cd.getValue()));
        }

        public int getCurrentCounterValue(String name) {
            //only the counters of the parent are in scope
            //_parent is never null for a publicly accessible CounterContext
            Integer value = _parent.getCounter(name);
            if (value == null) {
                _parent.resetCounter(new CounterData(name, 0));
                return 0;
            } else {
                return value.intValue();
            }
        }

        private Integer getCounter(String name) {
            Integer value = (Integer) _counters.get(name);
            if (value != null) return value;
            if (_parent == null) return null;
            return _parent.getCounter(name);
        }

        public List getCurrentCounterValues(String name) {
            //only the counters of the parent are in scope
            //_parent is never null for a publicly accessible CounterContext
            List values = new ArrayList();
            _parent.getCounterValues(name, values);
            if (values.size() == 0) {
                _parent.resetCounter(new CounterData(name, 0));
                values.add(new Integer(0));
            }
            return values;
        }

        private void getCounterValues(String name, List values) {
            if (_parent != null) _parent.getCounterValues(name, values);
            Integer value = (Integer) _counters.get(name);
            if (value != null) values.add(value);
        }
    }

    public String getPageName() {
        return _pageName;
    }

    public void setPageName(String currentPageName) {
        _pageName = currentPageName;
    }
    
    public int getNoPageBreak() {
        return _noPageBreak;
    }

    public void setNoPageBreak(int noPageBreak) {
        _noPageBreak = noPageBreak;
    }
    
    public boolean isPageBreaksAllowed() {
        return _noPageBreak == 0;
    }

    public String getPendingPageName() {
        return _pendingPageName;
    }

    public void setPendingPageName(String pendingPageName) {
        _pendingPageName = pendingPageName;
    }

    public Layer getRootDocumentLayer() {
        return _rootDocumentLayer;
    }

    public void setRootDocumentLayer(Layer rootDocumentLayer) {
        _rootDocumentLayer = rootDocumentLayer;
    }

    public PageBox getPage() {
        return _page;
    }

    public void setPage(PageBox page) {
        _page = page;
    }

    public boolean isMayCheckKeepTogether() {
        return _mayCheckKeepTogether;
    }

    public void setMayCheckKeepTogether(boolean mayKeepTogether) {
        _mayCheckKeepTogether = mayKeepTogether;
    }

    public BreakAtLineContext getBreakAtLineContext() {
        return _breakAtLineContext;
    }

    public void setBreakAtLineContext(BreakAtLineContext breakAtLineContext) {
        _breakAtLineContext = breakAtLineContext;
    }
}
