/*
    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.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.oro.text.GlobCompiler;
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.Perl5Matcher;
import org.apache.wiki.StringTransmutator;
import org.apache.wiki.api.core.Context;
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.pages.PageSorter;
import org.apache.wiki.parser.MarkupParser;
import org.apache.wiki.parser.WikiDocument;
import org.apache.wiki.preferences.Preferences;
import org.apache.wiki.preferences.Preferences.TimeFormat;
import org.apache.wiki.render.RenderingManager;
import org.apache.wiki.util.TextUtil;
import org.apache.wiki.util.comparators.CollatorComparator;
import org.apache.wiki.util.comparators.HumanComparator;
import org.apache.wiki.util.comparators.JavaNaturalComparator;
import org.apache.wiki.util.comparators.LocaleComparator;

import java.io.IOException;
import java.text.Collator;
import java.text.ParseException;
import java.text.RuleBasedCollator;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 *  This is a base class for all plugins using referral things.
 *
 *  <p>Parameters (also valid for all subclasses of this class) : </p>
 *  <ul>
 *  <li><b>maxwidth</b> - maximum width of generated links</li>
 *  <li><b>separator</b> - separator between generated links (wikitext)</li>
 *  <li><b>after</b> - output after the link</li>
 *  <li><b>before</b> - output before the link</li>
 *  <li><b>exclude</b> -  a regular expression of pages to exclude from the list. </li>
 *  <li><b>include</b> -  a regular expression of pages to include in the list. </li>
 *  <li><b>show</b> - value is either "pages" (default) or "count".  When "count" is specified, shows only the count
 *      of pages which match. (since 2.8)</li>
 *  <li><b>columns</b> - How many columns should the output be displayed on.</li>
 *  <li><b>showLastModified</b> - When show=count, shows also the last modified date. (since 2.8)</li>
 *  <li><b>sortOrder</b> - specifies the sort order for the resulting list.  Options are
 *  'human', 'java', 'locale' or a <code>RuleBasedCollator</code> rule string. (since 2.8.3)</li>
 *  </ul>
 *
 */
public abstract class AbstractReferralPlugin implements Plugin {

    private static final Logger LOG = LogManager.getLogger( AbstractReferralPlugin.class );

    /** Magic value for rendering all items. */
    public static final int    ALL_ITEMS              = -1;

    /** Parameter name for setting the maximum width.  Value is <tt>{@value}</tt>. */
    public static final String PARAM_MAXWIDTH         = "maxwidth";

    /** Parameter name for the separator string.  Value is <tt>{@value}</tt>. */
    public static final String PARAM_SEPARATOR        = "separator";

    /** Parameter name for the output after the link.  Value is <tt>{@value}</tt>. */
    public static final String PARAM_AFTER            = "after";

    /** Parameter name for the output before the link.  Value is <tt>{@value}</tt>. */
    public static final String PARAM_BEFORE           = "before";

    /** Parameter name for setting the list of excluded patterns.  Value is <tt>{@value}</tt>. */
    public static final String PARAM_EXCLUDE          = "exclude";

    /** Parameter name for setting the list of included patterns.  Value is <tt>{@value}</tt>. */
    public static final String PARAM_INCLUDE          = "include";

    /** Parameter name for the show parameter.  Value is <tt>{@value}</tt>. */
    public static final String PARAM_SHOW             = "show";

    /** Parameter name for setting show to "pages".  Value is <tt>{@value}</tt>. */
    public static final String PARAM_SHOW_VALUE_PAGES = "pages";

    /** Parameter name for setting show to "count".  Value is <tt>{@value}</tt>. */
    public static final String PARAM_SHOW_VALUE_COUNT = "count";

    /** Parameter name for showing the last modification count.  Value is <tt>{@value}</tt>. */
    public static final String PARAM_LASTMODIFIED     = "showLastModified";

    /** 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";

    /** Parameter name for specifying the sort order.  Value is <tt>{@value}</tt>. */
    protected static final String PARAM_SORTORDER        = "sortOrder";
    protected static final String PARAM_SORTORDER_HUMAN  = "human";
    protected static final String PARAM_SORTORDER_JAVA   = "java";
    protected static final String PARAM_SORTORDER_LOCALE = "locale";

    protected int m_maxwidth = Integer.MAX_VALUE;
    protected String m_before = ""; // null not blank
    protected String m_separator = ""; // null not blank
    protected String m_after = "\\\\";
    protected int items;

    protected Pattern[]  m_exclude;
    protected Pattern[]  m_include;
    protected PageSorter m_sorter;

    protected String m_show = "pages";
    protected boolean m_lastModified;
    // the last modified date of the page that has been last modified:
    protected Date m_dateLastModified = new Date(0);
    protected SimpleDateFormat m_dateFormat;

    protected Engine m_engine;

    /**
     * @param context the wiki context
     * @param params parameters for initializing the plugin
     * @throws PluginException if any of the plugin parameters are malformed
     */
    // FIXME: The compiled pattern strings should really be cached somehow.
    public void initialize( final Context context, final Map< String, String > params ) throws PluginException {
        m_dateFormat = Preferences.getDateFormat( context, TimeFormat.DATETIME );
        m_engine = context.getEngine();
        m_maxwidth = TextUtil.parseIntParameter( params.get( PARAM_MAXWIDTH ), Integer.MAX_VALUE );
        if( m_maxwidth < 0 ) {
            m_maxwidth = 0;
        }

        String s = params.get( PARAM_SEPARATOR );
        if( s != null ) {
            m_separator = TextUtil.replaceEntities( s );
            // pre-2.1.145 there was a separator at the end of the list
            // if they set the parameters, we use the new format of
            // before Item1 after separator before Item2 after separator before Item3 after
            m_after = "";
        }

        s = params.get( PARAM_BEFORE );
        if( s != null ) {
            m_before = s;
        }

        s = params.get( PARAM_AFTER );
        if( s != null ) {
            m_after = s;
        }

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

        s = params.get( PARAM_EXCLUDE );
        if ( s != null ) {
            try {
                final PatternCompiler pc = new GlobCompiler();
                final String[] ptrns = StringUtils.split( s, "," );
                m_exclude = new Pattern[ ptrns.length ];
                for ( int i = 0; i < ptrns.length; i++ ) {
                    m_exclude[ i ] = pc.compile( ptrns[ i ] );
                }
            } catch ( final MalformedPatternException e ) {
                throw new PluginException( "Exclude-parameter has a malformed pattern: " + e.getMessage() );
            }
        }

        // TODO: Cut-n-paste, refactor
        s = params.get( PARAM_INCLUDE );
        if ( s != null ) {
            try {
                final PatternCompiler pc = new GlobCompiler();
                final String[] ptrns = StringUtils.split( s, "," );
                m_include = new Pattern[ ptrns.length ];
                for ( int i = 0; i < ptrns.length; i++ ) {
                    m_include[ i ] = pc.compile( ptrns[ i ] );
                }
            } catch ( final MalformedPatternException e ) {
                throw new PluginException( "Include-parameter has a malformed pattern: " + e.getMessage() );
            }
        }

        // LOG.debug( "Requested maximum width is "+m_maxwidth );
        s = params.get(PARAM_SHOW);
        if ( s != null ) {
            if ( s.equalsIgnoreCase( "count" ) ) {
                m_show = "count";
            }
        }

        s = params.get( PARAM_LASTMODIFIED );
        if ( s != null ) {
            if ( s.equalsIgnoreCase( "true" ) ) {
                if ( m_show.equals( "count" ) ) {
                    m_lastModified = true;
                } else {
                    throw new PluginException( "showLastModified=true is only valid if show=count is also specified" );
                }
            }
        }

        initSorter( context, params );
    }

    protected List< Page > filterWikiPageCollection( final Collection< Page > pages ) {
        final List< String > pageNames = filterCollection( pages.stream()
                                                                .map( Page::getName )
                                                                .collect( Collectors.toList() ) );
        return pages.stream()
                    .filter( wikiPage -> pageNames.contains( wikiPage.getName() ) )
                    .collect( Collectors.toList() );
    }

    /**
     *  Filters a collection according to the include and exclude parameters.
     *
     *  @param c The collection to filter.
     *  @return A filtered collection.
     */
    protected List< String > filterCollection( final Collection< String > c ) {
        final ArrayList< String > result = new ArrayList<>();
        final PatternMatcher pm = new Perl5Matcher();
        for( final String pageName : c ) {
            //
            //  If include parameter exists, then by default we include only those
            //  pages in it (excluding the ones in the exclude pattern list).
            //
            //  include='*' means the same as no include.
            //
            boolean includeThis = m_include == null;

            if( m_include != null ) {
                includeThis = Arrays.stream(m_include).anyMatch(pattern -> pm.matches(pageName, pattern)) ? true : m_include == null;
            }

            if( m_exclude != null ) {
                // The inner loop, continue on the next item
                if (Arrays.stream(m_exclude).anyMatch(pattern -> pm.matches(pageName, pattern))) {
                    includeThis = false;
                }
            }

            if( includeThis ) {
                result.add( pageName );
                //  if we want to show the last modified date of the most recently change page, we keep a "high watermark" here:
                final Page page;
                if( m_lastModified ) {
                    page = m_engine.getManager( PageManager.class ).getPage( pageName );
                    if( page != null ) {
                        final Date lastModPage = page.getLastModified();
                        LOG.debug( "lastModified Date of page {} : {}", pageName, m_dateLastModified );
                        if( lastModPage.after( m_dateLastModified ) ) {
                            m_dateLastModified = lastModPage;
                        }
                    }
                }
            }
        }

        return result;
    }

    /**
     *  Filters and sorts a collection according to the include and exclude parameters.
     *
     *  @param c The collection to filter.
     *  @return A filtered and sorted collection.
     */
    protected List< String > filterAndSortCollection( final Collection< String > c ) {
        final List< String > result = filterCollection( c );
        result.sort( m_sorter );
        return result;
    }

    /**
     *  Makes WikiText from a Collection.
     *
     *  @param links Collection to make into WikiText.
     *  @param separator Separator string to use.
     *  @param numItems How many items to show.
     *  @return The WikiText
     */
    protected String wikitizeCollection( final Collection< String > links, final String separator, final int numItems ) {
        if( links == null || links.isEmpty() ) {
            return "";
        }

        final StringBuilder output = new StringBuilder();

        final Iterator< String > it = links.iterator();
        int count = 0;

        //  The output will be B Item[1] A S B Item[2] A S B Item[3] A
        while( it.hasNext() && ( (count < numItems) || ( numItems == ALL_ITEMS ) ) ) {
            final String value = it.next();
            if( count > 0 ) {
                output.append( m_after );
                output.append( separator );
            }

            output.append( m_before );

            // Make a Wiki markup link. See TranslatorReader.
            output.append( "[" )
                  .append( m_engine.getManager( RenderingManager.class ).beautifyTitle( value ) )
                  .append( "|" )
                  .append( value )
                  .append( "]" );
            count++;
        }

        //  Output final item - if there have been none, no "after" is printed
        if( count > 0 ) {
            output.append( m_after );
        }

        return output.toString();
    }

    /**
     *  Makes HTML with common parameters.
     *
     *  @param context The WikiContext
     *  @param wikitext The wikitext to render
     *  @return HTML
     *  @since 1.6.4
     */
    protected String makeHTML( final Context context, final String wikitext ) {
        String result = "";

        final RenderingManager mgr = m_engine.getManager( RenderingManager.class );

        try {
            final MarkupParser parser = mgr.getParser( context, wikitext );
            parser.addLinkTransmutator( new CutMutator( m_maxwidth ) );
            parser.enableImageInlining( false );

            final WikiDocument doc = parser.parse();
            result = mgr.getHTML( context, doc );
        } catch( final IOException e ) {
            LOG.error("Failed to convert page data to HTML", e);
        }

        return result;
    }

    protected String applyColumnsStyle( final String result ) {
        if( items > 1 ) {
            return "<div style=\"columns:" + items + ";" +
                                "-moz-columns:" + items + ";" +
                                "-webkit-columns:" + items + ";" + "\">"
                    + result + "</div>";
        }
        return result;
    }

    /**
     *  A simple class that just cuts a String to a maximum
     *  length, adding three dots after the cutpoint.
     */
    private static class CutMutator implements StringTransmutator {

        private final int m_length;

        public CutMutator( final int length ) {
            m_length = length;
        }

        @Override
        public String mutate( final Context context, final String text ) {
            if( text.length() > m_length ) {
                return text.substring( 0, m_length ) + "...";
            }

            return text;
        }
    }

    /**
     * Helper method to initialize the comparator for this page.
     */
    private void initSorter( final Context context, final Map< String, String > params ) {
        final String order = params.get( PARAM_SORTORDER );
        if( order == null || order.isEmpty() ) {
            // Use the configured comparator
            m_sorter = context.getEngine().getManager( PageManager.class ).getPageSorter();
        } else if( order.equalsIgnoreCase( PARAM_SORTORDER_JAVA ) ) {
            // use Java "natural" ordering
            m_sorter = new PageSorter( JavaNaturalComparator.DEFAULT_JAVA_COMPARATOR );
        } else if( order.equalsIgnoreCase( PARAM_SORTORDER_LOCALE ) ) {
            // use this locale's ordering
            m_sorter = new PageSorter( LocaleComparator.DEFAULT_LOCALE_COMPARATOR );
        } else if( order.equalsIgnoreCase( PARAM_SORTORDER_HUMAN ) ) {
            // use human ordering
            m_sorter = new PageSorter( HumanComparator.DEFAULT_HUMAN_COMPARATOR );
        } else {
            try {
                final Collator collator = new RuleBasedCollator( order );
                collator.setStrength( Collator.PRIMARY );
                m_sorter = new PageSorter( new CollatorComparator( collator ) );
            } catch( final ParseException pe ) {
                LOG.info( "Failed to parse requested collator - using default ordering", pe );
                m_sorter = context.getEngine().getManager( PageManager.class ).getPageSorter();
            }
        }
    }

}
