/* 
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.  
 */
package org.apache.wiki.rss;

import org.apache.log4j.Logger;
import org.apache.wiki.WikiContext;
import org.apache.wiki.WikiEngine;
import org.apache.wiki.WikiPage;
import org.apache.wiki.WikiProvider;
import org.apache.wiki.WikiSession;
import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
import org.apache.wiki.api.exceptions.ProviderException;
import org.apache.wiki.attachment.Attachment;
import org.apache.wiki.auth.permissions.PagePermission;
import org.apache.wiki.pages.PageTimeComparator;
import org.apache.wiki.util.TextUtil;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;

/**
 *  The master class for generating different kinds of Feeds (including RSS1.0, 2.0 and Atom).
 *  <p>
 *  This class can produce quite a few different styles of feeds.  The following modes are
 *  available:
 *  
 *  <ul>
 *  <li><b>wiki</b> - All the changes to the given page are enumerated and announced as diffs.</li>
 *  <li><b>full</b> - Each page is only considered once.  This produces a very RecentChanges-style feed,
 *                   where each page is only listed once, even if it has changed multiple times.</li>
 *  <li><b>blog</b> - Each page change is assumed to be a blog entry, so no diffs are produced, but
 *                    the page content is always completely in the entry in rendered HTML.</li>
 *
 *  @since  1.7.5.
 */
// FIXME: Limit diff and page content size.
// FIXME3.0: This class would need a bit of refactoring.  Method names, e.g. are confusing.
public class RSSGenerator {

    static Logger              log = Logger.getLogger( RSSGenerator.class );
    private WikiEngine         m_engine;

    private String             m_channelDescription = "";
    private String             m_channelLanguage    = "en-us";
    private boolean            m_enabled = true;

    /** Parameter value to represent RSS 1.0 feeds.  Value is <tt>{@value}</tt>. */
    public static final String RSS10 = "rss10";

    /** Parameter value to represent RSS 2.0 feeds.  Value is <tt>{@value}</tt>. */
    public static final String RSS20 = "rss20";
    
    /** Parameter value to represent Atom feeds.  Value is <tt>{@value}</tt>.  */
    public static final String ATOM  = "atom";

    /** Parameter value to represent a 'blog' style feed. Value is <tt>{@value}</tt>. */
    public static final String MODE_BLOG = "blog";
    
    /** Parameter value to represent a 'wiki' style feed. Value is <tt>{@value}</tt>. */
    public static final String MODE_WIKI = "wiki";

    /** Parameter value to represent a 'full' style feed. Value is <tt>{@value}</tt>. */
    public static final String MODE_FULL = "full";

    /**
     *  Defines the property name for the RSS channel description.  Default value for the
     *  channel description is an empty string.
     *  @since 1.7.6.
     */
    public static final String PROP_CHANNEL_DESCRIPTION = "jspwiki.rss.channelDescription";

    /**
     *  Defines the property name for the RSS channel language.  Default value for the language is "en-us".
     *  @since 1.7.6.
     */
    public static final String PROP_CHANNEL_LANGUAGE    = "jspwiki.rss.channelLanguage";

    /** Defines the property name for the RSS channel title.  Value is <tt>{@value}</tt>. */
    public static final String PROP_CHANNEL_TITLE       = "jspwiki.rss.channelTitle";

    /**
     *  Defines the property name for the RSS generator main switch.
     *  @since 1.7.6.
     */
    public static final String PROP_GENERATE_RSS        = "jspwiki.rss.generate";

    /**
     *  Defines the property name for the RSS file that the wiki should generate.
     *  @since 1.7.6.
     */
    public static final String PROP_RSSFILE             = "jspwiki.rss.fileName";

    /**
     *  Defines the property name for the RSS generation interval in seconds.
     *  @since 1.7.6.
     */
    public static final String PROP_INTERVAL            = "jspwiki.rss.interval";

    /** Defines the property name for the RSS author.  Value is <tt>{@value}</tt>. */
    public static final String PROP_RSS_AUTHOR          = "jspwiki.rss.author";

    /** Defines the property name for the RSS author email.  Value is <tt>{@value}</tt>. */
    public static final String PROP_RSS_AUTHOREMAIL     = "jspwiki.rss.author.email";

    private static final int MAX_CHARACTERS             = Integer.MAX_VALUE-1;

    /**
     *  Initialize the RSS generator for a given WikiEngine.
     *
     *  @param engine The WikiEngine.
     *  @param properties The properties.
     *  @throws NoRequiredPropertyException If something is missing from the given property set.
     */
    public RSSGenerator( WikiEngine engine, Properties properties ) {
        m_engine = engine;
        m_channelDescription = properties.getProperty( PROP_CHANNEL_DESCRIPTION, m_channelDescription );
        m_channelLanguage    = properties.getProperty( PROP_CHANNEL_LANGUAGE, m_channelLanguage );
    }

    /**
     *  Does the required formatting and entity replacement for XML.
     *  
     *  @param s String to format.
     *  @return A formatted string.
     */
    // FIXME: Replicates Feed.format().
    public static String format( String s )
    {
        s = TextUtil.replaceString( s, "&", "&amp;" );
        s = TextUtil.replaceString( s, "<", "&lt;" );
        s = TextUtil.replaceString( s, "]]>", "]]&gt;" );

        return s.trim();
    }

    private String getAuthor( WikiPage page )
    {
        String author = page.getAuthor();

        if( author == null ) author = "An unknown author";

        return author;
    }

    private String getAttachmentDescription( Attachment att )
    {
        String author = getAuthor(att);
        StringBuilder sb = new StringBuilder();

        if( att.getVersion() != 1 )
        {
            sb.append(author+" uploaded a new version of this attachment on "+att.getLastModified() );
        }
        else
        {
            sb.append(author+" created this attachment on "+att.getLastModified() );
        }

        sb.append("<br /><hr /><br />");
        sb.append( "Parent page: <a href=\""+
                   m_engine.getURL( WikiContext.VIEW, att.getParentName(), null, true ) +
                   "\">"+att.getParentName()+"</a><br />" );
        sb.append( "Info page: <a href=\""+
                   m_engine.getURL( WikiContext.INFO, att.getName(), null, true ) +
                   "\">"+att.getName()+"</a>" );

        return sb.toString();
    }

    private String getPageDescription( WikiPage page )
    {
    	StringBuilder buf = new StringBuilder();
        String author = getAuthor(page);

        WikiContext ctx = new WikiContext( m_engine, page );
        if( page.getVersion() > 1 )
        {
            String diff = m_engine.getDiff( ctx,
                                            page.getVersion()-1, // FIXME: Will fail when non-contiguous versions
                                            page.getVersion() );

            buf.append(author+" changed this page on "+page.getLastModified()+":<br /><hr /><br />" );
            buf.append(diff);
        }
        else
        {
            buf.append(author+" created this page on "+page.getLastModified()+":<br /><hr /><br />" );
            buf.append(m_engine.getHTML( page.getName() ));
        }

        return buf.toString();
    }

    private String getEntryDescription( WikiPage page )
    {
        String res;

        if( page instanceof Attachment )
        {
            res = getAttachmentDescription( (Attachment)page );
        }
        else
        {
            res = getPageDescription( page );
        }

        return res;
    }

    // FIXME: This should probably return something more intelligent
    private String getEntryTitle( WikiPage page )
    {
        return page.getName()+", version "+page.getVersion();
    }

    /**
     *  Generates the RSS resource.  You probably want to output this
     *  result into a file or something, or serve as output from a servlet.
     *  
     *  @return A RSS 1.0 feed in the "full" mode.
     */
    public String generate()
    {
        WikiContext context = new WikiContext( m_engine,new WikiPage( m_engine, "__DUMMY" ) );
        context.setRequestContext( WikiContext.RSS );
        Feed feed = new RSS10Feed( context );

        String result = generateFullWikiRSS( context, feed );

        result = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + result;

        return result;
    }

    /**
     * Returns the content type of this RSS feed.
     *  @since 2.3.15
     * @param mode the RSS mode: {@link #RSS10}, {@link #RSS20} or {@link #ATOM}.
     * @return the content type
     */
    public static String getContentType( String mode )
    {
        if( mode.equals( RSS10 )||mode.equals(RSS20) )
        {
            return "application/rss+xml";
        }
        else if( mode.equals(ATOM) )
        {
            return "application/atom+xml";
        }

        return "application/octet-stream"; // Unknown type
    }

    /**
     *  Generates a feed based on a context and list of changes.
     * @param wikiContext The WikiContext
     * @param changed A list of Entry objects
     * @param mode The mode (wiki/blog)
     * @param type The type (RSS10, RSS20, ATOM).  Default is RSS 1.0
     * @return Fully formed XML.
     *
     * @throws ProviderException If the underlying provider failed.
     * @throws IllegalArgumentException If an illegal mode is given.
     */
    public String generateFeed( WikiContext wikiContext, List< WikiPage > changed, String mode, String type )
        throws ProviderException, IllegalArgumentException
    {
        Feed feed = null;
        String res = null;

        if( ATOM.equals(type) )
        {
            feed = new AtomFeed( wikiContext );
        }
        else if( RSS20.equals( type ) )
        {
            feed = new RSS20Feed( wikiContext );
        }
        else
        {
            feed = new RSS10Feed( wikiContext );
        }

        feed.setMode( mode );

        if( MODE_BLOG.equals( mode ) )
        {
            res = generateBlogRSS( wikiContext, changed, feed );
        }
        else if( MODE_FULL.equals(mode) )
        {
            res = generateFullWikiRSS( wikiContext, feed );
        }
        else if( MODE_WIKI.equals(mode) )
        {
            res = generateWikiPageRSS( wikiContext, changed, feed );
        }
        else
        {
            throw new IllegalArgumentException( "Invalid value for feed mode: "+mode );
        }

        return res;
    }

    /**
     * Returns <code>true</code> if RSS generation is enabled.
     * @return whether RSS generation is currently enabled
     */
    public boolean isEnabled()
    {
        return m_enabled;
    }

    /**
     * Turns RSS generation on or off. This setting is used to set
     * the "enabled" flag only for use by callers, and does not
     * actually affect whether the {@link #generate()} or
     * {@link #generateFeed(WikiContext, List, String, String)}
     * methods output anything.
     * @param enabled whether RSS generation is considered enabled.
     */
    public synchronized void setEnabled( boolean enabled )
    {
        m_enabled = enabled;
    }

    /**
     *  Generates an RSS feed for the entire wiki.  Each item should be an instance of the RSSItem class.
     *  
     *  @param wikiContext A WikiContext
     *  @param feed A Feed to generate the feed to.
     *  @return feed.getString().
     */
    protected String generateFullWikiRSS( WikiContext wikiContext, Feed feed )
    {
        feed.setChannelTitle( m_engine.getApplicationName() );
        feed.setFeedURL( m_engine.getBaseURL() );
        feed.setChannelLanguage( m_channelLanguage );
        feed.setChannelDescription( m_channelDescription );

        Set< WikiPage > changed = m_engine.getRecentChanges();

        WikiSession session = WikiSession.guestSession( m_engine );
        int items = 0;
        for( Iterator< WikiPage > i = changed.iterator(); i.hasNext() && items < 15; items++ )
        {
            WikiPage page = i.next();

            //
            //  Check if the anonymous user has view access to this page.
            //

            if( !m_engine.getAuthorizationManager().checkPermission(session, new PagePermission(page,PagePermission.VIEW_ACTION) ) )
            {
                // No permission, skip to the next one.
                continue;
            }

            Entry e = new Entry();

            e.setPage( page );

            String url;

            if( page instanceof Attachment )
            {
                url = m_engine.getURL( WikiContext.ATTACH,
                                       page.getName(),
                                       null,
                                       true );
            }
            else
            {
                url = m_engine.getURL( WikiContext.VIEW,
                                       page.getName(),
                                       null,
                                       true );
            }

            e.setURL( url );
            e.setTitle( page.getName() );
            e.setContent( getEntryDescription(page) );
            e.setAuthor( getAuthor(page) );

            feed.addEntry( e );
        }

        return feed.getString();
    }

    /**
     *  Create RSS/Atom as if this page was a wikipage (in contrast to Blog mode).
     *
     * @param wikiContext The WikiContext
     * @param changed A List of changed WikiPages.
     * @param feed A Feed object to fill.
     * @return the RSS representation of the wiki context
     */
    protected String generateWikiPageRSS( WikiContext wikiContext, List< WikiPage > changed, Feed feed )
    {
        feed.setChannelTitle( m_engine.getApplicationName()+": "+wikiContext.getPage().getName() );
        feed.setFeedURL( wikiContext.getViewURL( wikiContext.getPage().getName() ) );
        String language = m_engine.getVariable( wikiContext, PROP_CHANNEL_LANGUAGE );

        if( language != null )
            feed.setChannelLanguage( language );
        else
            feed.setChannelLanguage( m_channelLanguage );

        String channelDescription = m_engine.getVariable( wikiContext, PROP_CHANNEL_DESCRIPTION );

        if( channelDescription != null )
        {
            feed.setChannelDescription( channelDescription );
        }

        Collections.sort( changed, new PageTimeComparator() );

        int items = 0;
        for( Iterator< WikiPage > i = changed.iterator(); i.hasNext() && items < 15; items++ )
        {
            WikiPage page = i.next();

            Entry e = new Entry();

            e.setPage( page );

            String url;

            if( page instanceof Attachment )
            {
                url = m_engine.getURL( WikiContext.ATTACH,
                                       page.getName(),
                                       "version="+page.getVersion(),
                                       true );
            }
            else
            {
                url = m_engine.getURL( WikiContext.VIEW,
                                       page.getName(),
                                       "version="+page.getVersion(),
                                       true );
            }

            // Unfortunately, this is needed because the code will again go through
            // replacement conversion

            url = TextUtil.replaceString( url, "&amp;", "&" );

            e.setURL( url );
            e.setTitle( getEntryTitle(page) );
            e.setContent( getEntryDescription(page) );
            e.setAuthor( getAuthor(page) );

            feed.addEntry( e );
        }

        return feed.getString();
    }


    /**
     *  Creates RSS from modifications as if this page was a blog (using the WeblogPlugin).
     *
     *  @param wikiContext The WikiContext, as usual.
     *  @param changed A list of the changed pages.
     *  @param feed A valid Feed object.  The feed will be used to create the RSS/Atom, depending
     *              on which kind of an object you want to put in it.
     *  @return A String of valid RSS or Atom.
     *  @throws ProviderException If reading of pages was not possible.
     */
    protected String generateBlogRSS( WikiContext wikiContext, List< WikiPage > changed, Feed feed )
        throws ProviderException
    {
        if( log.isDebugEnabled() ) log.debug("Generating RSS for blog, size="+changed.size());

        String ctitle = m_engine.getVariable( wikiContext, PROP_CHANNEL_TITLE );

        if( ctitle != null )
            feed.setChannelTitle( ctitle );
        else
            feed.setChannelTitle( m_engine.getApplicationName()+":"+wikiContext.getPage().getName() );

        feed.setFeedURL( wikiContext.getViewURL( wikiContext.getPage().getName() ) );

        String language = m_engine.getVariable( wikiContext, PROP_CHANNEL_LANGUAGE );

        if( language != null )
            feed.setChannelLanguage( language );
        else
            feed.setChannelLanguage( m_channelLanguage );

        String channelDescription = m_engine.getVariable( wikiContext, PROP_CHANNEL_DESCRIPTION );

        if( channelDescription != null )
        {
            feed.setChannelDescription( channelDescription );
        }

        Collections.sort( changed, new PageTimeComparator() );

        int items = 0;
        for( Iterator< WikiPage > i = changed.iterator(); i.hasNext() && items < 15; items++ )
        {
            WikiPage page = i.next();

            Entry e = new Entry();

            e.setPage( page );

            String url;

            if( page instanceof Attachment )
            {
                url = m_engine.getURL( WikiContext.ATTACH,
                                       page.getName(),
                                       null,
                                       true );
            }
            else
            {
                url = m_engine.getURL( WikiContext.VIEW,
                                       page.getName(),
                                       null,
                                       true );
            }

            e.setURL( url );

            //
            //  Title
            //

            String pageText = m_engine.getPureText(page.getName(), WikiProvider.LATEST_VERSION );

            String title = "";
            int firstLine = pageText.indexOf('\n');

            if( firstLine > 0 )
            {
                title = pageText.substring( 0, firstLine ).trim();
            }

            if( title.length() == 0 ) title = page.getName();

            // Remove wiki formatting
            while( title.startsWith("!") ) title = title.substring(1);

            e.setTitle( title );

            //
            //  Description
            //

            if( firstLine > 0 )
            {
                int maxlen = pageText.length();
                if( maxlen > MAX_CHARACTERS ) maxlen = MAX_CHARACTERS;

                if( maxlen > 0 )
                {
                    pageText = m_engine.textToHTML( wikiContext,
                                                    pageText.substring( firstLine+1,
                                                                        maxlen ).trim() );

                    if( maxlen == MAX_CHARACTERS ) pageText += "...";

                    e.setContent( pageText );
                }
                else
                {
                    e.setContent( title );
                }
            }
            else
            {
                e.setContent( title );
            }

            e.setAuthor( getAuthor(page) );

            feed.addEntry( e );
        }

        return feed.getString();
    }

}
