/*
 * Copyright MapStruct Authors.
 *
 * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
 */
package org.mapstruct.ap.internal.writer;

import java.io.IOException;
import java.io.Writer;
import java.util.Arrays;

/**
 * EXPERIMENTAL: A writer used to write processed templates which corrects line indentation based on the nesting level
 * as implied by (normal and curly) braces.
 * <p>
 * This writer discards any leading whitespace characters following to a line break character. When the first
 * non-whitespace character is written after a line break, the correct indentation characters are added, which is four
 * whitespace characters per indentation level.
 *
 * <p>
 * The state pattern is line oriented. It starts by writing text. Indentation is increased if a brace '('or
 * brace '{' is encountered in the code to be generated and written out in state: IN_TEXT_START_OF_LINE. Whenever
 * a line end occurs (PC or Linux style) the amount of enters is checked and at max set to 2.
 *
 * Whenever a string definition is encountered in the code that should be generated, increasing the indentation is
 * stopped (so `{` and '(' are ignored) until the end of the string is encountered ('"'). To avoid writing a new
 * indentation, the state then returns to IN_TEXT.
 *
 * <p>
 * This is a very basic implementation which does not take into account comments, escaping etc.
 *
 * @author Gunnar Morling
 */
class IndentationCorrectingWriter extends Writer {

    /**
     * Set to true to enable output of written characters on the console.
     */
    private static final boolean DEBUG = false;
    private static final String LINE_SEPARATOR = System.lineSeparator( );
    private static final boolean IS_WINDOWS = System.getProperty( "os.name" ).startsWith( "Windows" );

    private State currentState = State.START_OF_LINE;
    private final StateContext context;

    IndentationCorrectingWriter(Writer out) {
        super( out );
        this.context = new StateContext( out );
    }

    @Override
    public void write(char[] cbuf, int off, int len) throws IOException {
        context.reset( cbuf, off );

        for ( int i = off; i < len; i++ ) {
            char c = cbuf[i];

            State newState = currentState.handleCharacter( c, context );

            if ( newState != currentState ) {
                currentState.onExit( context, newState );
                newState.onEntry( context );
                currentState = newState;
            }

            context.currentIndex++;
        }

        currentState.onBufferFinished( context );
    }

    @Override
    public void flush() throws IOException {
        context.writer.flush();
    }

    @Override
    public void close() throws IOException {
        currentState.onExit( context, null );
        context.writer.close();
    }

    private static boolean isWindows() {
        return IS_WINDOWS;
    }

    private static char[] getIndentation(int indentationLevel) {
        char[] indentation = new char[indentationLevel * 4];
        Arrays.fill( indentation, ' ' );
        return indentation;
    }

    /**
     * A state of parsing a given character buffer.
     */
    private enum State {

        /**
         * Within any text, before encountering a String definition.
         */
        START_OF_LINE {
            @Override
            State doHandleCharacter(char c, StateContext context) {
                switch ( c ) {
                    case '{':
                    case '(':
                        context.incrementIndentationLevel();
                        return IN_TEXT;
                    case '}':
                    case ')':
                        context.decrementIndentationLevel();
                        return IN_TEXT;
                    case '\"':
                        return IN_STRING;
                    case '\r':
                        return isWindows() ? IN_LINE_BREAK : AFTER_LINE_BREAK;
                    case '\n':
                        return AFTER_LINE_BREAK;
                    default:
                        return IN_TEXT;
                }
            }

            /**
             * Writes out leading whitespace as per the current indentation level.
             */
            @Override
            void doOnEntry(StateContext context) throws IOException {
                context.writer.write( getIndentation( context.getIndentationLevel() ) );

                if ( DEBUG ) {
                    System.out.print( new String( getIndentation( context.getIndentationLevel() ) )
                            .replace( " ", "_" ) );
                }
            }

            /**
             * Writes out the current text.
             */
            @Override
            void onExit(StateContext context, State nextState) throws IOException {
                flush( context );
            }

            /**
             * Writes out the current text.
             */
            @Override
            void onBufferFinished(StateContext context) throws IOException {
                flush( context );
            }

        },

        /**
         * Within any text, but after a String (" ").
         */
        IN_TEXT {
            @Override
            State doHandleCharacter(char c, StateContext context) {
                switch ( c ) {
                    case '{':
                    case '(':
                        context.incrementIndentationLevel();
                        return IN_TEXT;
                    case '}':
                    case ')':
                        context.decrementIndentationLevel();
                        return IN_TEXT;
                    case '\"':
                        return IN_STRING;
                    case '\r':
                        return isWindows() ? IN_LINE_BREAK : AFTER_LINE_BREAK;
                    case '\n':
                        return AFTER_LINE_BREAK;
                    default:
                        return IN_TEXT;
                }
            }

            /**
             * Writes out the current text.
             */
            @Override
            void onExit(StateContext context, State nextState) throws IOException {
                flush( context );
            }

            /**
             * Writes out the current text.
             */
            @Override
            void onBufferFinished(StateContext context) throws IOException {
                flush( context );
            }

        },

        /**
         * In a String definition, Between un-escaped quotes " "
         */
        IN_STRING {

            @Override
            State doHandleCharacter(char c, StateContext context) {
                switch ( c ) {
                    case '\"':
                        return IN_TEXT;
                    case '\\':
                        return IN_STRING_ESCAPED_CHAR;
                    default:
                        return IN_STRING;
                }
            }

            /**
             * Writes out the current text.
             */
            @Override
            void onExit(StateContext context, State nextState) throws IOException {
                flush( context );
            }

            /**
             * Writes out the current text.
             */
            @Override
            void onBufferFinished(StateContext context) throws IOException {
                flush( context );
            }

        },

        /**
         * In a String, character following an escape character '\', should be ignored, can also be '"' that
         * should be ignored.
         */
        IN_STRING_ESCAPED_CHAR {

            @Override
            State doHandleCharacter(char c, StateContext context) {
                // ignore escaped character
                return IN_STRING;
            }

            /**
             * Writes out the current text.
             */
            @Override
            void onExit(StateContext context, State nextState) throws IOException {
                flush( context );
            }

            /**
             * Writes out the current text.
             */
            @Override
            void onBufferFinished(StateContext context) throws IOException {
                flush( context );
            }

        },

        /**
         * Between \r and \n of a Windows line-break.
         */
        IN_LINE_BREAK {
            @Override
            State doHandleCharacter(char c, StateContext context) {
                if ( c == '\n' ) {
                    return AFTER_LINE_BREAK;
                }
                else {
                    throw new IllegalArgumentException( "Unexpected character: " + c );
                }
            }
        },

        /**
         * Directly after a line-break, or within leading whitespace following to a line-break.
         */
        AFTER_LINE_BREAK {
            @Override
            State doHandleCharacter(char c, StateContext context) {

                switch ( c ) {
                    case '{':
                    case '(':
                        context.incrementIndentationLevel();
                        return START_OF_LINE;
                    case '}':
                        if ( context.consecutiveLineBreaks > 0 ) {
                            context.consecutiveLineBreaks = 0; // remove previous blank lines
                        }
                    case ')':
                        context.decrementIndentationLevel();
                        return START_OF_LINE;
                    case '\r':
                        return isWindows() ? IN_LINE_BREAK : AFTER_LINE_BREAK;
                    case ' ':
                        return AFTER_LINE_BREAK;
                    case '\n':
                        context.consecutiveLineBreaks++;
                        return AFTER_LINE_BREAK;
                    default:
                        return START_OF_LINE;
                }
            }

            /**
             * Writes out the current line-breaks, avoiding more than one consecutive empty line
             */
            @Override
            void onExit(StateContext context, State nextState) throws IOException {
                context.consecutiveLineBreaks++;
                if ( nextState != IN_LINE_BREAK ) {
                    int lineBreaks = Math.min( context.consecutiveLineBreaks, 2 );

                    for ( int i = 0; i < lineBreaks; i++ ) {
                        context.writer.append( LINE_SEPARATOR );

                        if ( DEBUG ) {
                            System.out.print( "\\n" + LINE_SEPARATOR );
                        }
                    }

                    context.consecutiveLineBreaks = 0;
                }
            }
        };

        final State handleCharacter(char c, StateContext context) throws IOException {


            return doHandleCharacter( c, context );
        }

        abstract State doHandleCharacter(char c, StateContext context) throws IOException;

        final void onEntry(StateContext context) throws IOException {
            context.lastStateChange = context.currentIndex;
            doOnEntry( context );
        }

        void doOnEntry(StateContext context) throws IOException {
        }

        void onExit(StateContext context, State nextState) throws IOException {
        }

        void onBufferFinished(StateContext context) throws IOException {
        }

        protected void flush(StateContext context) throws IOException {
            if ( null != context.characters && context.currentIndex - context.lastStateChange > 0 ) {
                context.writer.write(
                    context.characters,
                    context.lastStateChange,
                    context.currentIndex - context.lastStateChange
                );

                if ( DEBUG ) {
                    System.out.print(
                        new String(
                            java.util.Arrays.copyOfRange(
                                context.characters,
                                context.lastStateChange,
                                context.currentIndex
                            )
                        )
                    );
                }
            }
        }
    }

    /**
     * Keeps the current context of parsing the given character buffer.
     */
    private static class StateContext {
        final Writer writer;

        char[] characters;

        /**
         * The position at which when the current state was entered.
         */
        int lastStateChange;

        /**
         * The current position within the buffer.
         */
        int currentIndex;

        /**
         * Keeps track of the current indentation level, as implied by brace characters.
         */
        private int indentationLevel;

        /**
         * The number of consecutive line-breaks when within {@link State#AFTER_LINE_BREAK}.
         */
        int consecutiveLineBreaks;

        StateContext(Writer writer) {
            this.writer = writer;
        }

        void reset(char[] characters, int off) {
            this.characters = characters;
            this.lastStateChange = off;
            this.currentIndex = 0;
        }

        void incrementIndentationLevel() {
            indentationLevel++;
        }

        void decrementIndentationLevel() {
            // decrementing below 0 indicates misbalanced braces in the code, typically because too many closing braces
            // are given in an expression; we let that code pass through, the compiler will complain eventually about
            // the malformed source file
            if ( indentationLevel > 0 ) {
                indentationLevel--;
            }
        }

        int getIndentationLevel() {
            return indentationLevel;
        }
    }
}
