/*
 * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
 * Copyright (C) 2011, 2013-2020 The JavaParser Team.
 *
 * This file is part of JavaParser.
 *
 * JavaParser can be used either under the terms of
 * a) the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * b) the terms of the Apache License
 *
 * You should have received a copy of both licenses in LICENCE.LGPL and
 * LICENCE.APACHE. Please refer to those files for details.
 *
 * JavaParser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 */

package com.github.javaparser.printer;

import com.github.javaparser.Position;
import com.github.javaparser.printer.PrettyPrinterConfiguration.IndentType;
import com.github.javaparser.utils.Utils;

import java.util.Deque;
import java.util.LinkedList;

/**
 * A support class for code that outputs formatted source code.
 */
public class SourcePrinter {
    private final String endOfLineCharacter;
    private final String indentation;
    private final int tabWidth;
    private final IndentType indentType;

    private final Deque<String> indents = new LinkedList<>();
    private final Deque<String> reindentedIndents = new LinkedList<>();
    private String lastPrintedIndent = "";
    private final StringBuilder buf = new StringBuilder();
    private Position cursor = new Position(Position.FIRST_LINE, Position.FIRST_COLUMN - 1); // Start before the first column
    private boolean indented = false;

    SourcePrinter() {
        this(new PrettyPrinterConfiguration());
    }

    SourcePrinter(final PrettyPrinterConfiguration configuration) {
        indentation = configuration.getIndent();
        endOfLineCharacter = configuration.getEndOfLineCharacter();
        tabWidth = configuration.getTabWidth();
        indentType = configuration.getIndentType();
        indents.push("");
    }

    /**
     * Add the default indentation to the current indentation and push it on the indentation stack.
     * Does not actually output anything.
     */
    public SourcePrinter indent() {
        String currentIndent = indents.peek();
        switch (indentType) {
            case SPACES:
            case TABS_WITH_SPACE_ALIGN:
                indents.push(currentIndent + indentation);
                break;

            case TABS:
                indents.push(indentation + currentIndent);
                break;

            default:
                throw new AssertionError("Unhandled indent type");
        }
        return this;
    }

    /**
     * Add to the current indentation until it is reaches "column" and push it on the indentation stack.
     * Does not actually output anything.
     */
    public SourcePrinter indentWithAlignTo(int column) {
        indents.push(calculateIndentWithAlignTo(column));
        return this;
    }

    private String calculateIndentWithAlignTo(int column) {
        if (column < lastPrintedIndent.length()){
            throw new IllegalStateException("Attempt to indent less than the previous indent.");
        }

        StringBuilder newIndent = new StringBuilder(lastPrintedIndent);
        switch (indentType) {
            case SPACES:
            case TABS_WITH_SPACE_ALIGN:
                while (newIndent.length() < column) {
                    newIndent.append(' ');
                }
                break;

            case TABS:
                int logicalIndentLength = newIndent.length();
                while ((logicalIndentLength + tabWidth) <= column) {
                    newIndent.insert(0, '\t');
                    logicalIndentLength += tabWidth;
                }
                while (logicalIndentLength < column) {
                    newIndent.append(' ');
                    logicalIndentLength++;
                }
                StringBuilder fullTab = new StringBuilder();
                for(int i=0; i<tabWidth; i++){
                    fullTab.append(' ');
                }
                String fullTabString = fullTab.toString();
                if ((newIndent.length() >= tabWidth)
                        && newIndent.substring(newIndent.length() - tabWidth).equals(fullTabString)) {
                    int i = newIndent.indexOf(fullTabString);
                    newIndent.replace(i, i + tabWidth, "\t");
                }
                break;

            default:
                throw new AssertionError("Unhandled indent type");
        }

        return newIndent.toString();
    }

    /**
     * Pop the last indentation of the indentation stack.
     * Does not actually output anything.
     */
    public SourcePrinter unindent() {
        if (indents.isEmpty()) {
            // Since we start out with an empty indent on the stack, this will only occur
            // the second time we over-unindent.
            throw new IllegalStateException("Indent/unindent calls are not well-balanced.");
        }
        indents.pop();
        return this;
    }

    private void append(String arg) {
        buf.append(arg);
        cursor = cursor.withColumn(cursor.column + arg.length());
    }

    /**
     * Append the source string passed as argument to the buffer.
     * If this is being appended at the beginning of a line, performs indentation first.
     * <p>
     * The source line to be printed should not contain newline/carriage-return characters;
     * use {@link #println(String)} to automatically append a newline at the end of the source string.
     * If the source line passed as argument contains newline/carriage-return characters would
     * impredictably affect a correct computation of the current {@link #getCursor()} position.
     *
     * @param arg source line to be printed (should not contain newline/carriage-return characters)
     * @return this instance, for nesting calls to method as fluent interface
     * @see SourcePrinter#println(String)
     */
    public SourcePrinter print(final String arg) {
        if (!indented) {
            lastPrintedIndent = indents.peek();
            append(lastPrintedIndent);
            indented = true;
        }
        append(arg);
        return this;
    }

    /**
     * Append the source string passed as argument to the buffer, then append a newline.
     * If this is being appended at the beginning of a line, performs indentation first.
     * <p>
     * The source line to be printed should not contain newline/carriage-return characters.
     * If the source line passed as argument contains newline/carriage-return characters would
     * impredictably affect a correct computation of the current {@link #getCursor()} position.
     *
     * @param arg source line to be printed (should not contain newline/carriage-return characters)
     * @return this instance, for nesting calls to method as fluent interface
     */
    public SourcePrinter println(final String arg) {
        print(arg);
        println();
        return this;
    }

    /**
     * Append a newline to the buffer.
     *
     * @return this instance, for nesting calls to method as fluent interface
     */
    public SourcePrinter println() {
        buf.append(endOfLineCharacter);
        cursor = new Position(cursor.line + 1, Position.FIRST_COLUMN - 1); // Start before the first column
        indented = false;
        return this;
    }

    /**
     * Return the current cursor position (line, column) in the source printer buffer.
     * <p>
     * Please notice in order to guarantee a correct computation of the cursor position,
     * this printer expect the contracts of the methods {@link #print(String)} and {@link #println(String)}
     * has been respected through all method calls, meaning the source string passed as argument to those method
     * calls did not contain newline/carriage-return characters.
     *
     * @return the current cursor position (line, column).
     */
    public Position getCursor() {
        return cursor;
    }

    /**
     * @return the currently printed source code.
     * @deprecated use toString()
     */
    @Deprecated
    public String getSource() {
        return toString();
    }

    /**
     * @return the currently printed source code.
     */
    @Override
    public String toString() {
        return buf.toString();
    }

    /**
     * Changes all EOL characters in "content" to the EOL character this SourcePrinter is using.
     */
    public String normalizeEolInTextBlock(String content) {
        return Utils.normalizeEolInTextBlock(content, endOfLineCharacter);
    }

    /**
     * Set the top-most indent to the column the cursor is currently in, can be undone with
     * {@link #reindentToPreviousLevel()}. Does not actually output anything.
     */
    public void reindentWithAlignToCursor() {
        String newIndent = calculateIndentWithAlignTo(cursor.column);
        reindentedIndents.push(indents.pop());
        indents.push(newIndent);
    }

    /**
     * Set the top-most indent to the column the cursor was before the last {@link #reindentWithAlignToCursor()} call.
     * Does not actually output anything.
     */
    public void reindentToPreviousLevel() {
        if (reindentedIndents.isEmpty()) {
            throw new IllegalStateException("Reindent calls are not well-balanced.");
        }
        indents.pop();
        indents.push(reindentedIndents.pop());
    }

    /**
     * Adds an indent to the top of the stack that is a copy of the current top indent.
     * With this you announce "I'm going to indent the next line(s)" but not how far yet.
     * Once you do know, you can pop this indent ("unindent") and indent to the right column.
     * (Does not actually output anything.)
     */
    public void duplicateIndent() {
        indents.push(indents.peek());
    }
}
