/*
 * Copyright 2011 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed 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.drools.drl.parser.lang;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.antlr.runtime.BitSet;
import org.antlr.runtime.EarlyExitException;
import org.antlr.runtime.FailedPredicateException;
import org.antlr.runtime.MismatchedNotSetException;
import org.antlr.runtime.MismatchedSetException;
import org.antlr.runtime.MismatchedTokenException;
import org.antlr.runtime.MismatchedTreeNodeException;
import org.antlr.runtime.NoViableAltException;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.Token;
import org.drools.drl.parser.DRLFactory;
import org.drools.drl.parser.DroolsParserException;
import org.kie.internal.builder.conf.LanguageLevelOption;

/**
 * Helper class that generates DroolsParserException with user friendly error
 * messages.
 * 
 * @see DroolsParserException
 */
public class DroolsParserExceptionFactory {
    public final static String                        MISMATCHED_TOKEN_MESSAGE_COMPLETE       = "Line %1$d:%2$d mismatched input '%3$s' expecting '%4$s'%5$s";
    public final static String                        MISMATCHED_TOKEN_MESSAGE_PART           = "Line %1$d:%2$d mismatched input '%3$s'%4$s";
    public final static String                        MISMATCHED_TREE_NODE_MESSAGE_COMPLETE   = "Line %1$d:%2$d mismatched tree node '%3$s' expecting '%4$s'%5$s";
    public final static String                        MISMATCHED_TREE_NODE_MESSAGE_PART       = "Line %1$d:%2$d mismatched tree node '%3$s'%4$s";
    public final static String                        NO_VIABLE_ALT_MESSAGE                   = "Line %1$d:%2$d no viable alternative at input '%3$s'%4$s";
    public final static String                        EARLY_EXIT_MESSAGE                      = "Line %1$d:%2$d required (...)+ loop did not match anything at input '%3$s'%4$s";
    public final static String                        MISMATCHED_SET_MESSAGE                  = "Line %1$d:%2$d mismatched input '%3$s' expecting one of the following tokens: '%4$s'%5$s.";
    public final static String                        MISMATCHED_NOT_SET_MESSAGE              = "Line %1$d:%2$d mismatched input '%3$s' not expecting any of the following tokens: '%4$s'%5$s";
    public final static String                        FAILED_PREDICATE_MESSAGE                = "Line %1$d:%2$d rule '%3$s' failed predicate: {%4$s}?%5$s";
    public final static String                        TRAILING_SEMI_COLON_NOT_ALLOWED_MESSAGE = "Line %1$d:%2$d trailing semi-colon not allowed%3$s";
    public final static String                        PARSER_LOCATION_MESSAGE_COMPLETE        = " in %1$s %2$s";
    public final static String                        PARSER_LOCATION_MESSAGE_PART            = " in %1$s";
    public final static String                        UNEXPECTED_EXCEPTION                    = "Line %1$d:%2$d unexpected exception at input '%3$s'. Exception: %4$s. Stack trace:\n %5$s";

    private final Collection<Map<DroolsParaphraseTypes, String>> paraphrases;
    
    // TODO: need to deal with this array
    private String[]                                  tokenNames                              = null;

    private final LanguageLevelOption languageLevel;

    /**
     * DroolsParserErrorMessages constructor.
     * 
     * @param tokenNames
     *            tokenNames generated by ANTLR
     * @param paraphrases
     *            paraphrases parser structure
     */
    public DroolsParserExceptionFactory(Collection<Map<DroolsParaphraseTypes, String>> paraphrases,
            LanguageLevelOption languageLevel) {
        this.paraphrases = paraphrases;
        this.languageLevel = languageLevel;
    }

    /**
     * This method creates a DroolsParserException for trailing semicolon
     * exception, full of information.
     * 
     * @param line
     *            line number
     * @param column
     *            column position
     * @param offset
     *            char offset
     * @return DroolsParserException filled.
     */
    public DroolsParserException createTrailingSemicolonException( int line,
                                                                   int column,
                                                                   int offset ) {
        String message = String
                .format(
                         TRAILING_SEMI_COLON_NOT_ALLOWED_MESSAGE,
                         line,
                         column,
                         formatParserLocation() );

        return new DroolsParserException( "ERR 104",
                                          message,
                                          line,
                                          column,
                                          offset,
                                          null );
    }

    /**
     * This method creates a DroolsParserException full of information.
     * 
     * @param e
     *            original exception
     * @return DroolsParserException filled.
     */
    public DroolsParserException createDroolsException( RecognitionException e ) {
        List<String> codeAndMessage = createErrorMessage( e );
        return new DroolsParserException( codeAndMessage.get( 1 ),
                                          codeAndMessage
                                                  .get( 0 ),
                                          e.line,
                                          e.charPositionInLine,
                                          e.index,
                                          e );
    }

    /**
     * This will take a RecognitionException, and create a sensible error
     * message out of it
     */
    private List<String> createErrorMessage( RecognitionException e ) {
        List<String> codeAndMessage = new ArrayList<>( 2 );
        String message;
        if ( e instanceof MismatchedTokenException ) {
            MismatchedTokenException mte = (MismatchedTokenException) e;
            String expecting = mte instanceof DroolsMismatchedTokenException ? ((DroolsMismatchedTokenException)mte).getTokenText() : getBetterToken( mte.expecting );
            if ( tokenNames != null && mte.expecting >= 0 && mte.expecting < tokenNames.length ) {
                message = String
                        .format(
                                 MISMATCHED_TOKEN_MESSAGE_COMPLETE,
                                 e.line,
                                 e.charPositionInLine,
                                 getBetterToken( e.token ),
                                 expecting,
                                 formatParserLocation() );
                codeAndMessage.add( message );
                codeAndMessage.add( "ERR 102" );
            } else {
                message = String
                        .format(
                                 MISMATCHED_TOKEN_MESSAGE_PART,
                                 e.line,
                                 e.charPositionInLine,
                                 getBetterToken( e.token ),
                                 formatParserLocation() );
                codeAndMessage.add( message );
                codeAndMessage.add( "ERR 102" );
            }
        } else if ( e instanceof MismatchedTreeNodeException ) {
            MismatchedTreeNodeException mtne = (MismatchedTreeNodeException) e;
            if ( mtne.expecting >= 0 && mtne.expecting < tokenNames.length ) {
                message = String
                        .format(
                                 MISMATCHED_TREE_NODE_MESSAGE_COMPLETE,
                                 e.line,
                                 e.charPositionInLine,
                                 getBetterToken( e.token ),
                                 getBetterToken( mtne.expecting ),
                                 formatParserLocation() );
                codeAndMessage.add( message );
                codeAndMessage.add( "ERR 106" );
            } else {
                message = String
                        .format(
                                 MISMATCHED_TREE_NODE_MESSAGE_PART,
                                 e.line,
                                 e.charPositionInLine,
                                 getBetterToken( e.token ),
                                 formatParserLocation() );
                codeAndMessage.add( message );
                codeAndMessage.add( "ERR 106" );
            }
        } else if ( e instanceof NoViableAltException ) {
            // NoViableAltException nvae = (NoViableAltException) e;
            message = String.format(
                                     NO_VIABLE_ALT_MESSAGE,
                                     e.line,
                                     e.charPositionInLine,
                                     getBetterToken( e.token ),
                                     formatParserLocation() );
            codeAndMessage.add( message );
            codeAndMessage.add( "ERR 101" );
        } else if ( e instanceof EarlyExitException ) {
            // EarlyExitException eee = (EarlyExitException) e;
            message = String.format(
                                     EARLY_EXIT_MESSAGE,
                                     e.line,
                                     e.charPositionInLine,
                                     getBetterToken( e.token ),
                                     formatParserLocation() );
            codeAndMessage.add( message );
            codeAndMessage.add( "ERR 105" );
        } else if ( e instanceof MismatchedSetException ) {
            MismatchedSetException mse = (MismatchedSetException) e;
            String expected = expectedTokensAsString( mse.expecting );
            message = String.format(
                                     MISMATCHED_SET_MESSAGE,
                                     e.line,
                                     e.charPositionInLine,
                                     getBetterToken( e.token ),
                                     expected,
                                     formatParserLocation() );
            codeAndMessage.add( message );
            codeAndMessage.add( "ERR 107" );
        } else if ( e instanceof DroolsMismatchedSetException ) {
            DroolsMismatchedSetException mse = (DroolsMismatchedSetException) e;
            String expected = Arrays.asList( mse.getTokenText() ).toString();
            message = String.format(
                                     MISMATCHED_SET_MESSAGE,
                                     e.line,
                                     e.charPositionInLine,
                                     getBetterToken( e.token ),
                                     expected,
                                     formatParserLocation() );
            codeAndMessage.add( message );
            codeAndMessage.add( "ERR 107" );
        } else if ( e instanceof MismatchedNotSetException ) {
            MismatchedNotSetException mse = (MismatchedNotSetException) e;
            String expected = expectedTokensAsString( mse.expecting );
            message = String.format(
                                     MISMATCHED_NOT_SET_MESSAGE,
                                     e.line,
                                     e.charPositionInLine,
                                     getBetterToken( e.token ),
                                     expected,
                                     formatParserLocation() );
            codeAndMessage.add( message );
            codeAndMessage.add( "ERR 108" );
        } else if ( e instanceof FailedPredicateException ) {
            FailedPredicateException fpe = (FailedPredicateException) e;
            message = String.format(
                                     FAILED_PREDICATE_MESSAGE,
                                     e.line,
                                     e.charPositionInLine,
                                     fpe.ruleName,
                                     fpe.predicateText,
                                     formatParserLocation() );
            codeAndMessage.add( message );
            codeAndMessage.add( "ERR 103" );
        }
        if ( codeAndMessage.get( 0 ).length() == 0 ) {
            codeAndMessage.add( "?????" );
        }
        return codeAndMessage;
    }

    public DroolsParserException createDroolsException( Exception e,
                                                        Token token ) {
        StringWriter sw = new StringWriter();
        e.printStackTrace( new PrintWriter(sw) );
        return new DroolsParserException( String.format(
                                                         DroolsParserExceptionFactory.UNEXPECTED_EXCEPTION,
                                                         token.getLine(),
                                                         token.getCharPositionInLine(),
                                                         getBetterToken( token ),
                                                         e.toString(),
                                                         sw.toString() ),
                                          e );

    }

    private String expectedTokensAsString( BitSet set ) {
        StringBuilder buf = new StringBuilder();
        buf.append( "{ " );
        int i = 0;
        for ( int token : set.toArray() ) {
            if ( i > 0 ) buf.append( ", " );
            buf.append( getBetterToken( token ) );
            i++;
        }
        buf.append( " }" );
        return buf.toString();
    }

    /**
     * This will take Paraphrases stack, and create a sensible location
     */
    private String formatParserLocation() {
        StringBuilder sb = new StringBuilder();
        if ( paraphrases != null ) {
            for ( Map<DroolsParaphraseTypes, String> map : paraphrases ) {
                for ( Entry<DroolsParaphraseTypes, String> activeEntry : map.entrySet() ) {
                    if ( activeEntry.getValue().length() == 0 ) {
                        String kStr = getLocationName( activeEntry.getKey() );
                        if( kStr.length() > 0 ){
                            sb.append( String.format( PARSER_LOCATION_MESSAGE_PART, kStr ) );
                        }
                    } else {
                        sb.append( String.format( PARSER_LOCATION_MESSAGE_COMPLETE,
                                                  getLocationName( activeEntry.getKey() ),
                                                  activeEntry.getValue() ) );
                    }
                }
            }
        }
        return sb.toString();
    }

    /**
     * Returns a string based on Paraphrase Type
     * 
     * @param type
     *            Paraphrase Type
     * @return a string representing the
     */
    private String getLocationName( DroolsParaphraseTypes type ) {
        switch ( type ) {
            case PACKAGE :
                return "package";
            case IMPORT :
                return "import";
            case FUNCTION_IMPORT :
                return "function import";
            case ACCUMULATE_IMPORT :
                return "accumulate import";
            case GLOBAL :
                return "global";
            case FUNCTION :
                return "function";
            case QUERY :
                return "query";
            case TEMPLATE :
                return "template";
            case RULE :
                return "rule";
            case RULE_ATTRIBUTE :
                return "rule attribute";
            case PATTERN :
                return "pattern";
            case EVAL :
                return "eval";
            default :
                return "";
        }
    }

    /**
     * Helper method that creates a user friendly token definition
     * 
     * @param token
     *            token
     * @return user friendly token definition
     */
    private String getBetterToken( Token token ) {
        if ( token == null ) {
            return "";
        }
        return DRLFactory.getBetterToken(token.getType(), token.getText(), languageLevel);
    }

    /**
     * Helper method that creates a user friendly token definition
     * 
     * @param tokenType
     *            token type
     * @return user friendly token definition
     */
    private String getBetterToken( int tokenType ) {
        return DRLFactory.getBetterToken( tokenType, null, languageLevel );
    }


}
