/*
    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.plugin;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.PatternCompiler;
import org.apache.oro.text.regex.PatternMatcher;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;
import org.apache.wiki.api.core.Context;
import org.apache.wiki.api.core.ContextEnum;
import org.apache.wiki.api.core.Engine;
import org.apache.wiki.api.core.Page;
import org.apache.wiki.api.exceptions.PluginException;
import org.apache.wiki.api.plugin.Plugin;
import org.apache.wiki.pages.PageManager;
import org.apache.wiki.references.ReferenceManager;
import org.apache.wiki.util.TextUtil;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;


/**
 *  Displays the pages referring to the current page.
 *
 *  <p>Parameters</p>
 *  <ul>
 *    <li><b>name</b> - Name of the root page. Default name of calling page
 *    <li><b>type</b> - local|externalattachment
 *    <li><b>depth</b> - How many levels of pages to be parsed.
 *    <li><b>include</b> - Include only these pages. (eg. include='UC.*|BP.*' )
 *    <li><b>exclude</b> - Exclude with this pattern. (eg. exclude='LeftMenu' )
 *    <li><b>format</b> -  full|compact, FULL now expands all levels correctly
 *  </ul>
 *
 */
public class ReferredPagesPlugin implements Plugin {

    private static final Logger LOG = LogManager.getLogger( ReferredPagesPlugin.class );
    private Engine m_engine;
    private int m_depth;
    private final HashSet< String > m_exists  = new HashSet<>();
    private final StringBuffer m_result  = new StringBuffer( 1024 );
    private final PatternMatcher m_matcher = new Perl5Matcher();
    private Pattern m_includePattern;
    private Pattern m_excludePattern;
    private int items;
    private boolean m_formatCompact = true;
    private boolean m_formatSort;

    /** The parameter name for the root page to start from.  Value is <tt>{@value}</tt>. */
    public static final String PARAM_ROOT = "page";

    /** The parameter name for the depth.  Value is <tt>{@value}</tt>. */
    public static final String PARAM_DEPTH = "depth";

    /** The parameter name for the type of the references.  Value is <tt>{@value}</tt>. */
    public static final String PARAM_TYPE = "type";

    /** The parameter name for the included pages.  Value is <tt>{@value}</tt>. */
    public static final String PARAM_INCLUDE = "include";

    /** The parameter name for the excluded pages.  Value is <tt>{@value}</tt>. */
    public static final String PARAM_EXCLUDE = "exclude";

    /** The parameter name for the format.  Value is <tt>{@value}</tt>. */
    public static final String PARAM_FORMAT = "format";

    /** Parameter name for setting the number of columns that will be displayed by the plugin.  Value is <tt>{@value}</tt>. Available since 2.11.0. */
    public static final String PARAM_COLUMNS = "columns";

    /** The minimum depth. Value is <tt>{@value}</tt>. */
    public static final int MIN_DEPTH = 1;

    /** The maximum depth. Value is <tt>{@value}</tt>. */
    public static final int MAX_DEPTH = 8;

    /**
     *  {@inheritDoc}
     */
    @Override
    public String execute( final Context context, final Map< String, String > params ) throws PluginException {
        m_engine = context.getEngine();
        final Page page = context.getPage();
        if( page == null ) {
            return "";
        }

        // parse parameters
        String rootname = params.get( PARAM_ROOT );
        if( rootname == null ) {
            rootname = page.getName() ;
        }

        String format = params.get( PARAM_FORMAT );
        if( format == null) {
            format = "";
        }
        if( format.contains( "full" ) ) {
            m_formatCompact = false ;
        }
        if( format.contains( "sort" ) ) {
            m_formatSort = true  ;
        }

        m_depth = TextUtil.parseIntParameter( params.get( PARAM_DEPTH ), MIN_DEPTH );
        if( m_depth > MAX_DEPTH ) {
            m_depth = MAX_DEPTH;
        }

        String includePattern = params.get(PARAM_INCLUDE);
        if( includePattern == null ) {
            includePattern = ".*";
        }

        String excludePattern = params.get(PARAM_EXCLUDE);
        if( excludePattern == null ) {
            excludePattern = "^$";
        }

        final String columns = params.get( PARAM_COLUMNS );
        if( columns != null ) {
            items = TextUtil.parseIntParameter( columns, 0 );
        }

        LOG.debug( "Fetching referred pages for "+ rootname +
                   " with a depth of "+ m_depth +
                   " with include pattern of "+ includePattern +
                   " with exclude pattern of "+ excludePattern +
                   " with " + columns + " items" );

        //
        // do the actual work
        //
        final String href  = context.getViewURL( rootname );
        final String title = "ReferredPagesPlugin: depth[" + m_depth +
                             "] include[" + includePattern + "] exclude[" + excludePattern +
                             "] format[" + ( m_formatCompact ? "compact" : "full" ) +
                             ( m_formatSort ? " sort" : "" ) + "]";

        if( items > 1 ) {
            m_result.append( "<div class=\"ReferredPagesPlugin\" style=\"" )
                    .append( "columns:" ).append( columns ).append( ";" )
                    .append( "moz-columns:" ).append( columns ).append( ";" )
                    .append( "webkit-columns:" ).append( columns ).append( ";" )
                    .append( "\">\n" );
        } else {
            m_result.append( "<div class=\"ReferredPagesPlugin\">\n" );
        }
        m_result.append( "<a class=\"wikipage\" href=\"" )
                .append( href ).append( "\" title=\"" )
                .append( TextUtil.replaceEntities( title ) )
                .append( "\">" )
                .append( TextUtil.replaceEntities( rootname ) )
                .append( "</a>\n" );
        m_exists.add( rootname );

        // pre compile all needed patterns
        // glob compiler :  * is 0..n instance of any char  -- more convenient as input
        // perl5 compiler : .* is 0..n instances of any char -- more powerful
        //PatternCompiler g_compiler = new GlobCompiler();
        final PatternCompiler compiler = new Perl5Compiler();

        try {
            m_includePattern = compiler.compile( includePattern );
            m_excludePattern = compiler.compile( excludePattern );
        } catch( final MalformedPatternException e ) {
            if( m_includePattern == null ) {
                throw new PluginException( "Illegal include pattern detected." );
            } else if( m_excludePattern == null ) {
                throw new PluginException( "Illegal exclude pattern detected." );
            } else {
                throw new PluginException( "Illegal internal pattern detected." );
            }
        }

        // go get all referred links
        getReferredPages(context,rootname, 0);

        // close and finish
        m_result.append ("</div>\n" ) ;

        return m_result.toString() ;
    }

    /**
     * Retrieves a list of all referred pages. Is called recursively depending on the depth parameter.
     */
    private void getReferredPages( final Context context, final String pagename, int depth ) {
        if( depth >= m_depth ) {
            return;  // end of recursion
        }
        if( pagename == null ) {
            return;
        }
        if( !m_engine.getManager( PageManager.class ).wikiPageExists(pagename) ) {
            return;
        }

        final ReferenceManager mgr = m_engine.getManager( ReferenceManager.class );
        final Collection< String > allPages = mgr.findRefersTo( pagename );
        handleLinks( context, allPages, ++depth, pagename );
    }

    private void handleLinks( final Context context, final Collection<String> links, final int depth, final String pagename ) {
        boolean isUL = false;
        final HashSet< String > localLinkSet = new HashSet<>();  // needed to skip multiple links to the same page
        localLinkSet.add( pagename );

        final ArrayList< String > allLinks = new ArrayList<>();

        if( links != null )
            allLinks.addAll( links );

        if( m_formatSort ) context.getEngine().getManager( PageManager.class ).getPageSorter().sort( allLinks );

        for( final String link : allLinks ) {
            if( localLinkSet.contains( link ) ) {
                continue; // skip multiple links to the same page
            }
            localLinkSet.add( link );

            if( !m_engine.getManager( PageManager.class ).wikiPageExists( link ) ) {
                continue; // hide links to non-existing pages
            }
            if(  m_matcher.matches( link , m_excludePattern ) ) {
                continue;
            }
            if( !m_matcher.matches( link , m_includePattern ) ) {
                continue;
            }

            if( m_exists.contains( link ) ) {
                if( !m_formatCompact ) {
                    if( !isUL ) {
                        isUL = true;
                        m_result.append("<ul>\n");
                    }

                    //See https://www.w3.org/wiki/HTML_lists  for proper nesting of UL and LI
                    m_result.append( "<li> " ).append( TextUtil.replaceEntities( link ) ).append( "\n" );
                    getReferredPages( context, link, depth );  // added recursive call - on general request
                    m_result.append( "\n</li>\n" );
                }
            } else {
                if( !isUL ) {
                    isUL = true;
                    m_result.append("<ul>\n");
                }

                final String href = context.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), link );
                m_result.append( "<li><a class=\"wikipage\" href=\"" ).append( href ).append( "\">" ).append( TextUtil.replaceEntities( link ) ).append( "</a>\n" );
                m_exists.add( link );
                getReferredPages( context, link, depth );
                m_result.append( "\n</li>\n" );
            }
        }

        if( isUL ) {
            m_result.append("</ul>\n");
        }
    }

}