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

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.wiki.StringTransmutator;
import org.apache.wiki.api.core.Context;
import org.apache.wiki.api.core.Engine;
import org.apache.wiki.util.TextUtil;
import org.jdom2.Element;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
 *   Provides an abstract class for the parser instances.
 *
 *   @since  2.4
 */
public abstract class MarkupParser {

    /** Allow this many characters to be pushed back in the stream.  In effect, this limits the size of a single line.  */
    protected static final int PUSHBACK_BUFFER_SIZE = 10*1024;
    protected PushbackReader m_in;
    private int m_pos = -1; // current position in reader stream

    protected final Engine m_engine;
    protected final Context m_context;

    /** Optionally stores internal wikilinks */
    protected final ArrayList< StringTransmutator > m_localLinkMutatorChain = new ArrayList<>();
    protected final ArrayList< StringTransmutator > m_externalLinkMutatorChain = new ArrayList<>();
    protected final ArrayList< StringTransmutator > m_attachmentLinkMutatorChain = new ArrayList<>();
    protected final ArrayList< StringTransmutator > m_linkMutators = new ArrayList<>();
    protected final ArrayList< HeadingListener > m_headingListenerChain = new ArrayList<>();

    protected boolean m_inlineImages = true;
    protected boolean m_parseAccessRules = true;
    /** Keeps image regexp Patterns */
    protected List< Pattern > m_inlineImagePatterns;
    protected final LinkParsingOperations m_linkParsingOperations;

    private static final Logger log = LogManager.getLogger( MarkupParser.class );

    /** If set to "true", allows using raw HTML within Wiki text.  Be warned, this is a VERY dangerous option to set -
       never turn this on in a publicly allowable Wiki, unless you are absolutely certain of what you're doing. */
    public static final String PROP_ALLOWHTML = "jspwiki.translatorReader.allowHTML";

    /** If set to "true", enables plugins during parsing */
    public static final String PROP_RUNPLUGINS = "jspwiki.translatorReader.runPlugins";

    /** If true, all outward links (external links) have a small link image appended. */
    public static final String PROP_USEOUTLINKIMAGE = "jspwiki.translatorReader.useOutlinkImage";

    /** If set to "true", all external links are tagged with 'rel="nofollow"' */
    public static final String PROP_USERELNOFOLLOW = "jspwiki.translatorReader.useRelNofollow";

    /** If true, consider CamelCase hyperlinks as well. */
    public static final String PROP_CAMELCASELINKS = "jspwiki.translatorReader.camelCaseLinks";

    /** If true, all hyperlinks are translated as well, regardless whether they
     are surrounded by brackets. */
    public static final String PROP_PLAINURIS = "jspwiki.translatorReader.plainUris";

    /** If true, all outward attachment info links have a small link image appended. */
    public static final String PROP_USEATTACHMENTIMAGE = "jspwiki.translatorReader.useAttachmentImage";

    public static final String HASHLINK = "hashlink";

    /** Name of the outlink image; relative path to the JSPWiki directory. */
    public static final String OUTLINK_IMAGE = "images/out.png";
    /** Outlink css class. */
    public static final String OUTLINK = "outlink";

    private static final String INLINE_IMAGE_PATTERNS = "JSPWikiMarkupParser.inlineImagePatterns";

    /** The value for anchor element <tt>class</tt> attributes when used for wiki page (normal) links. The value is "wikipage". */
   public static final String CLASS_WIKIPAGE = "wikipage";

   /** The value for anchor element <tt>class</tt> attributes when used for edit page links. The value is "createpage". */
   public static final String CLASS_EDITPAGE = "createpage";

   /** The value for anchor element <tt>class</tt> attributes when used for interwiki page links. The value is "interwiki". */
   public static final String CLASS_INTERWIKI = "interwiki";

   /** The value for anchor element <tt>class</tt> attributes when used for footnote links. The value is "footnote". */
   public static final String CLASS_FOOTNOTE = "footnote";

   /** The value for anchor element <tt>class</tt> attributes when used for footnote links. The value is "footnote". */
   public static final String CLASS_FOOTNOTE_REF = "footnoteref";

   /** The value for anchor element <tt>class</tt> attributes when used for external links. The value is "external". */
   public static final String CLASS_EXTERNAL = "external";

   /** The value for anchor element <tt>class</tt> attributes when used for attachments. The value is "attachment". */
   public static final String CLASS_ATTACHMENT = "attachment";

   public static final String[] CLASS_TYPES = {
      CLASS_WIKIPAGE,
      CLASS_EDITPAGE,
      "",
      CLASS_FOOTNOTE,
      CLASS_FOOTNOTE_REF,
      "",
      CLASS_EXTERNAL,
      CLASS_INTERWIKI,
      CLASS_EXTERNAL,
      CLASS_WIKIPAGE,
      CLASS_ATTACHMENT
   };

    /**
     *  Constructs a MarkupParser.  The subclass must call this constructor to set up the necessary bits and pieces.
     *
     *  @param context The WikiContext.
     *  @param in The reader from which we are reading the bytes from.
     */
    protected MarkupParser( final Context context, final Reader in ) {
        m_engine = context.getEngine();
        m_context = context;
        m_linkParsingOperations = new LinkParsingOperations( m_context );
        setInputReader( in );
    }

    /**
     *  Replaces the current input character stream with a new one.
     *
     *  @param in New source for input.  If null, this method does nothing.
     *  @return the old stream
     */
    public Reader setInputReader( final Reader in ) {
        final Reader old = m_in;
        if( in != null ) {
            m_in = new PushbackReader( new BufferedReader( in ), PUSHBACK_BUFFER_SIZE );
        }

        return old;
    }

    /**
     *  Adds a hook for processing link texts.  This hook is called when the link text is written into the output stream, and
     *  you may use it to modify the text.  It does not affect the actual link, only the user-visible text.
     *
     *  @param mutator The hook to call.  Null is safe.
     */
    public void addLinkTransmutator( final StringTransmutator mutator ) {
        addLinkHook( m_linkMutators, mutator );
    }

    /**
     *  Adds a hook for processing local links.  The engine transforms both non-existing and existing page links.
     *
     *  @param mutator The hook to call.  Null is safe.
     */
    public void addLocalLinkHook( final StringTransmutator mutator ) {
        addLinkHook( m_localLinkMutatorChain, mutator );
    }

    /**
     *  Adds a hook for processing external links.  This includes all http:// ftp://, etc. links, including inlined images.
     *
     *  @param mutator The hook to call.  Null is safe.
     */
    public void addExternalLinkHook( final StringTransmutator mutator ) {
        addLinkHook( m_externalLinkMutatorChain, mutator );
    }

    /**
     *  Adds a hook for processing attachment links.
     *
     *  @param mutator The hook to call.  Null is safe.
     */
    public void addAttachmentLinkHook( final StringTransmutator mutator ) {
        addLinkHook( m_attachmentLinkMutatorChain, mutator );
    }

    void addLinkHook( final List< StringTransmutator > mutatorChain, final StringTransmutator mutator ) {
        if( mutator != null ) {
            mutatorChain.add( mutator );
        }
    }

    /**
     *  Adds a HeadingListener to the parser chain.  It will be called whenever a parsed header is found.
     *
     *  @param listener The listener to add.
     */
    public void addHeadingListener( final HeadingListener listener ) {
        if( listener != null ) {
            m_headingListenerChain.add( listener );
        }
    }

    /**
     *  Disables access rule parsing.
     */
    public void disableAccessRules()
    {
        m_parseAccessRules = false;
    }

    public boolean isParseAccessRules()
    {
        return m_parseAccessRules;
    }

    /**
     *  Use this to turn on or off image inlining.
     *
     *  @param toggle If true, images are inlined (as per set in jspwiki.properties)
     *                If false, then images won't be inlined; instead, they will be
     *                treated as standard hyperlinks.
     *  @since 2.2.9
     */
    public void enableImageInlining( final boolean toggle )
    {
        m_inlineImages = toggle;
    }

    public boolean isImageInlining() {
        return m_inlineImages;
    }

    protected final void initInlineImagePatterns() {
		final PatternCompiler compiler = new GlobCompiler();

        //  We cache compiled patterns in the engine, since their creation is really expensive
        List< Pattern > compiledpatterns = m_engine.getAttribute( INLINE_IMAGE_PATTERNS );

        if( compiledpatterns == null ) {
            compiledpatterns = new ArrayList< >( 20 );
            final Collection< String > ptrns = m_engine.getAllInlinedImagePatterns();

            //  Make them into Regexp Patterns.  Unknown patterns are ignored.
            for( final String pattern : ptrns ) {
                try {
                    compiledpatterns.add( compiler.compile( pattern, GlobCompiler.DEFAULT_MASK | GlobCompiler.READ_ONLY_MASK ) );
                } catch( final MalformedPatternException e ) {
                    log.error( "Malformed pattern [" + pattern + "] in properties: ", e );
                }
            }

            m_engine.setAttribute( INLINE_IMAGE_PATTERNS, compiledpatterns );
        }

        m_inlineImagePatterns = Collections.unmodifiableList( compiledpatterns );
	}

    public List< Pattern > getInlineImagePatterns() {
    	if( m_inlineImagePatterns == null ) {
    		initInlineImagePatterns();
    	}
    	return m_inlineImagePatterns;
    }

    /**
     *  Parses the document.
     *
     *  @return the parsed document, as a WikiDocument
     *  @throws IOException If something goes wrong.
     */
    public abstract WikiDocument parse() throws IOException;

    /**
     *  Return the current position in the reader stream. The value will be -1 prior to reading.
     *
     * @return the reader position as an int.
     */
    public int getPosition()
    {
        return m_pos;
    }

    /**
     * Returns the next token in the stream.  This is the most called method in the entire parser, so it needs to be lean and mean.
     *
     * @return The next token in the stream; or, if the stream is ended, -1.
     * @throws IOException If something bad happens
     * @throws NullPointerException If you have not yet created an input document.
     */
    protected final int nextToken() throws IOException, NullPointerException {
        // if( m_in == null ) return -1;
        m_pos++;
        return m_in.read();
    }

    /**
     *  Push back any character to the current input.  Does not push back a read EOF, though.
     *
     *  @param c Character to push back.
     *  @throws IOException In case the character cannot be pushed back.
     */
    protected void pushBack( final int c ) throws IOException {
        if( c != -1 && m_in != null ) {
            m_pos--;
            m_in.unread( c );
        }
    }

    /**
     *  Writes HTML for error message.  Does not add it to the document, you have to do it yourself.
     *
     *  @param error The error string.
     *  @return An Element containing the error.
     */
    public static Element makeError( final String error ) {
        return new Element( "span" ).setAttribute( "class", "error" ).addContent( error );
    }

    /**
     *  Cleans a Wiki name.  The functionality of this method was changed in 2.6 so that the list of allowed characters is much larger.
     *  Use {@link #wikifyLink(String)} to get the legacy behaviour.
     *  <P>
     *  [ This is a link ] -&gt; This is a link
     *
     *  @param link Link to be cleared. Null is safe, and causes this to return null.
     *  @return A cleaned link.
     *
     *  @since 2.0
     */
    public static String cleanLink( final String link ) {
        return TextUtil.cleanString( link, TextUtil.PUNCTUATION_CHARS_ALLOWED );
    }

    /**
     *  Cleans away extra legacy characters.  This method functions exactly like pre-2.6 cleanLink()
     *  <P>
     *  [ This is a link ] -&gt; ThisIsALink
     *
     *  @param link Link to be cleared. Null is safe, and causes this to return null.
     *  @return A cleaned link.
     *  @since 2.6
     */
    public static String wikifyLink( final String link ) {
        return TextUtil.cleanString( link, TextUtil.LEGACY_CHARS_ALLOWED );
    }

}
