/*
 * Copyright (C) 2004 TiongHiang Lee
 *
 * This library 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 library 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 library; if not,  write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Email: thlee@onemindsoft.org
 */

package org.onemind.jxp;

import java.io.*;
import java.util.HashMap;
import java.util.Iterator;
import org.onemind.commons.java.datastructure.MruMap;
import org.onemind.jxp.parser.*;
/**
 * A caching page source will cache the page source
 * @author TiongHiang Lee (thlee@onemindsoft.org)
 * 
 */
public abstract class CachingPageSource extends JxpPageSource
{

    /** the cache * */
    private MruMap _pageCache;

    /** flag to turn caching on/off * */
    private boolean _caching = true;

    /** whether invalidate the cache on parse error * */
    private boolean _invalidateCacheOnParseError = false;

    /** the page static variables **/
    private HashMap _pageStaticVariables = new HashMap();

    /** the encoding **/
    private String _encoding;

    /**
     * Constructor
     * (default use 200 cache limit, unlimit timeout)
     */
    public CachingPageSource()
    {
        this(200);
    }

    /**
     * Constructor
     * @param cacheSize The default cache size
     */
    public CachingPageSource(int cacheSize)
    {
        this(cacheSize, 0);
    }
    
    /**
     * Constructor
     * @param cacheSize The default cache size
     * @param timeout time to expire inactive cache
     */
    public CachingPageSource(int cacheSize, int timeout)
    {
        _pageCache = new MruMap(cacheSize, timeout);
    }


    /**
     * {@inheritDoc}
     */
    public final JxpPage getJxpPage(String id) throws JxpPageNotFoundException
    {
        JxpPage page = null;
        if (_caching)
        {
            page = (JxpPage) _pageCache.get(id);
            if (page != null)
            {
                return page;
            }
        }
        if (hasStream(id))
        {
            page = new CachedJxpPage(this, id, getEncoding());
        } else
        {
            throw new JxpPageNotFoundException("Page " + id + " not found");
        }
        // TODO: synchronize by id so that no two same JxpPage will be loaded        
        if (_caching && page != null)
        {
            _pageCache.put(id, page);
        }
        return page;
    }

    /**
     * Invalidate the page cache
     * @param page the page
     */
    protected final void invalidatePageCache(JxpPage page)
    {
        _pageCache.put(page.getName(), null);
    }

    /**
     * Return whether this is doing caching
     * @return true if caching
     */
    public final boolean isCaching()
    {
        return _caching;
    }

    /**
     * Set caching on/off
     * @param b true if caching on
     */
    public final void setCaching(boolean b)
    {
        _caching = b;
    }

    /**
     * {@inheritDoc}
     */
    public final AstJxpDocument getJxpDocument(JxpPage page) throws JxpPageParseException
    {
        CachedJxpPage cachedPage = (CachedJxpPage) page;
        boolean expired = false;
        if (cachedPage.getDocument() == null || (expired = isExpired(cachedPage)))
        {
            if (expired)
            {
                cachedPage.setParseException(null);
                cachedPage.setDocument(null);
                purgeStaticVariables(cachedPage);
            } else
            {
                if (cachedPage.hasParseError())
                {
                    throw cachedPage.getParseException();
                }
            }
            //need to parse it
            synchronized (cachedPage) //synchronize
            {
                if (cachedPage.getDocument() == null) // no one has parse it since the before synchronize
                { //check one more time to make sure no previous locker has done it
                    if (cachedPage.hasParseError())
                    { //still have problem
                        throw cachedPage.getParseException();
                    } //otherwise reparse
                    try
                    {
                        try
                        {
                            InputStream in = loadStream(cachedPage);
                            JxpParser parser = (getEncoding() == null) ? new JxpParser(in) : new JxpParser(new InputStreamReader(
                                    in, getEncoding()));
                            AstJxpDocument doc = parser.JxpDocument();
                            cachedPage.setDocument(doc);
                            return doc;
                        } catch (ParseException e)
                        {
                            String message = "Problem parsing page " + page.getName() + ": " + e.getMessage();
                            throw new JxpPageParseException(message, e);
                        } catch (IOException e)
                        {
                            throw new JxpPageParseException("Problem parsing page " + page.getName() + ": " + e.getMessage(), e);
                        }
                    } catch (JxpPageParseException e)
                    {
                        cachedPage.setParseException(e);
                        //TODO: determine if this is needed
                        if (_invalidateCacheOnParseError)
                        {
                            invalidatePageCache(page);
                        }
                        //rethrow
                        throw e;
                    }
                }
            }
        }
        return cachedPage.getDocument();
    }

    /**
     * Whether a page is expired
     * @param page the page
     * @return true if expired
     */
    protected abstract boolean isExpired(CachedJxpPage page);

    /**
     * Whether there's input stream from given page name
     * @param pageName the page name
     * @return true if has input stream
     */
    protected abstract boolean hasStream(String pageName);

    /**
     * Load the input stream for the page
     * @param pageName the page
     * @return the input stream
     */
    protected abstract InputStream loadStream(CachedJxpPage page) throws IOException;

    /**
     * Whehter the page identified by id is cached
     * @param id the id
     * @return true if cached
     */
    public final boolean isJxpPageCached(String id)
    {
        return _pageCache.containsKey(id);
    }

    /**
     * Declare a page static variable
     * @param page the page
     * @param name the name
     * @param value the value
     */
    /* package */final Object declarePageStaticVariable(JxpPage page, String name, Object value)
    {
        String fullName = page.getName() + "." + name;
        if (_pageStaticVariables.containsKey(fullName))
        {
            throw new IllegalArgumentException("Static variable " + name + " has been declared");
        } else
        {
            return _pageStaticVariables.put(fullName, value);
        }
    }

    /**
     * Return whether there's a variable declared
     * @param page the page
     * @param name the name
     * @return true if declared
     */
    /* package */final boolean hasPageStaticVariable(JxpPage page, String name)
    {
        String fullName = page.getName() + "." + name;
        return _pageStaticVariables.containsKey(fullName);
    }

    /**
     * Get the page static variable
     * @param page the page
     * @param name the nama
     * @return the value
     */
    /* package */final Object getPageStaticVariable(JxpPage page, String name)
    {
        String fullName = page.getName() + "." + name;
        return _pageStaticVariables.get(fullName);
    }

    /**
     * {@inheritDoc}
     */
    public StringBuffer getErrorSource(JxpPage page, int line, int col) throws IOException
    {
        StringBuffer sb = new StringBuffer("Error at page ");
        sb.append(page.getName());
        sb.append(" at line ");
        sb.append(line);
        sb.append(", column ");
        sb.append(col);
        return sb;
    }

    /**
     * @param page
     * @param name
     * @param value
     * @return
     */
    /* package */final Object assignPageStaticVariable(CachedJxpPage page, String name, Object value)
    {
        String fullName = page.getName() + "." + name;
        if (!_pageStaticVariables.containsKey(fullName))
        {
            throw new IllegalArgumentException("Static variable " + name + " has not been declared");
        } else
        {
            return _pageStaticVariables.put(fullName, value);
        }
    }

    /**
     * Purge the static variables for a page
     * @param page the page
     */
    protected final void purgeStaticVariables(CachedJxpPage page)
    {
        Iterator it = _pageStaticVariables.keySet().iterator();
        String prefix = page.getName() + ".";
        while (it.hasNext())
        {
            String key = (String) it.next();
            if (key.startsWith(prefix))
            {
                _pageStaticVariables.remove(key);
            }
        }
    }

    /**
     * Set the encoding
     * @param encoding the encoding
     */
    public final void setEncoding(String encoding)
    {
        _encoding = encoding;
    }

    /**
     * Return the encoding
     * @return the encoding
     */
    public final String getEncoding()
    {
        return _encoding;
    }

    /** 
     * {@inheritDoc}
     */
    public final boolean hasJxpPage(String id)
    {
        return ((_caching && _pageCache.containsKey(id)) || hasStream(id));        
    }
}