package org.jboss.windup.decompiler.procyon;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;

import com.strobel.decompiler.languages.LineNumberPosition;

/**
 * FIXME - This source file requires its original license and credits for original authors.
 * <p>
 * A <code>LineNumberFormatter</code> is used to rewrite an existing .java file, introducing line number information. It
 * can handle either, or both, of the following jobs:
 *
 * <ul>
 * <li>Introduce line numbers as leading comments.
 * <li>Stretch the file so that the line number comments match the physical lines.
 * </ul>
 */
class LineNumberFormatter {
    private final List<LineNumberPosition> _positions;
    private final File _file;
    private final EnumSet<LineNumberOption> _options;

    public enum LineNumberOption {
        LEADING_COMMENTS,
        STRETCHED,
    }

    /**
     * Constructs an instance.
     *
     * @param file                the file whose line numbers should be fixed
     * @param lineNumberPositions a recipe for how to fix the line numbers in 'file'.
     * @param options             controls how 'this' represents line numbers in the resulting file
     */
    public LineNumberFormatter(File file, List<LineNumberPosition> lineNumberPositions,
                               EnumSet<LineNumberOption> options) {
        _file = file;
        _positions = lineNumberPositions;
        _options = (options == null ? EnumSet.noneOf(LineNumberOption.class) : options);
    }

    /**
     * Rewrites the file passed to 'this' constructor so that the actual line numbers match the recipe passed to 'this'
     * constructor.
     */
    public void reformatFile() throws IOException {
        List<LineNumberPosition> lineBrokenPositions = new ArrayList<>();
        List<String> brokenLines = breakLines(lineBrokenPositions);
        emitFormatted(brokenLines, lineBrokenPositions);
    }

    /**
     * Processes {@link #_file}, breaking apart any lines on which multiple line-number markers appear in different
     * columns.
     *
     * @return the list of broken lines
     */
    private List<String> breakLines(List<LineNumberPosition> o_LineBrokenPositions) throws IOException {
        int numLinesRead = 0;
        int lineOffset = 0;
        List<String> brokenLines = new ArrayList<>();

        try (BufferedReader r = new BufferedReader(new FileReader(_file))) {
            for (int posIndex = 0; posIndex < _positions.size(); posIndex++) {
                LineNumberPosition pos = _positions.get(posIndex);
                o_LineBrokenPositions.add(new LineNumberPosition(
                        pos.getOriginalLine(), pos.getEmittedLine() + lineOffset, pos.getEmittedColumn()));

                // Copy the input file up to but not including the emitted line # in "pos".
                while (numLinesRead < pos.getEmittedLine() - 1) {
                    brokenLines.add(r.readLine());
                    numLinesRead++;
                }

                // Read the line that contains the next line number annotations, but don't write it yet.
                String line = r.readLine();
                numLinesRead++;

                // See if there are two original line annotations on the same emitted line.
                LineNumberPosition nextPos;
                int prevPartLen = 0;
                char[] indent = {};
                do {
                    nextPos = (posIndex < _positions.size() - 1) ? _positions.get(posIndex + 1) : null;
                    if (nextPos != null
                            && nextPos.getEmittedLine() == pos.getEmittedLine()
                            && nextPos.getOriginalLine() > pos.getOriginalLine()) {
                        // Two different source line numbers on the same emitted line!
                        posIndex++;
                        lineOffset++;
                        String firstPart = line.substring(0, nextPos.getEmittedColumn() - prevPartLen - 1);
                        brokenLines.add(new String(indent) + firstPart);
                        prevPartLen += firstPart.length();
                        indent = new char[prevPartLen];
                        Arrays.fill(indent, ' ');
                        line = line.substring(firstPart.length(), line.length());

                        // Alter the position while adding it.
                        o_LineBrokenPositions.add(new LineNumberPosition(
                                nextPos.getOriginalLine(), nextPos.getEmittedLine() + lineOffset, nextPos
                                .getEmittedColumn()));
                    } else {
                        nextPos = null;
                    }
                }
                while (nextPos != null);

                // Nothing special here-- just emit the line.
                brokenLines.add(new String(indent) + line);
            }

            // Copy out the remainder of the file.
            String line;
            while ((line = r.readLine()) != null) {
                brokenLines.add(line);
            }
        }
        return brokenLines;
    }

    private void emitFormatted(List<String> brokenLines, List<LineNumberPosition> lineBrokenPositions)
            throws IOException {
        File tempFile = new File(_file.getAbsolutePath() + ".fixed");
        int globalOffset = 0;
        int numLinesRead = 0;
        Iterator<String> lines = brokenLines.iterator();

        int maxLineNo = LineNumberPosition.computeMaxLineNumber(lineBrokenPositions);
        try (LineNumberPrintWriter w = new LineNumberPrintWriter(
                maxLineNo, new BufferedWriter(new FileWriter(tempFile)))) {

            // Suppress all line numbers if we weren't asked to show them.
            if (!_options.contains(LineNumberOption.LEADING_COMMENTS)) {
                w.suppressLineNumbers();
            }

            // Suppress stretching if we weren't asked to do it.
            boolean doStretching = (_options.contains(LineNumberOption.STRETCHED));

            for (LineNumberPosition pos : lineBrokenPositions) {
                int nextTarget = pos.getOriginalLine();
                int nextActual = pos.getEmittedLine();
                int requiredAdjustment = (nextTarget - nextActual - globalOffset);

                if (doStretching && requiredAdjustment < 0) {
                    // We currently need to remove newlines to squeeze things together.
                    // prefer to remove empty lines,
                    // 1. read all lines before nextActual and remove empty lines as needed
                    List<String> stripped = new ArrayList<>();
                    while (numLinesRead < nextActual - 1) {
                        String line = lines.next();
                        numLinesRead++;
                        if ((requiredAdjustment < 0) && line.trim().isEmpty()) {
                            requiredAdjustment++;
                            globalOffset--;
                        } else {
                            stripped.add(line);
                        }
                    }
                    // 2. print non empty lines while stripping further as needed
                    int lineNoToPrint = (stripped.size() + requiredAdjustment <= 0)
                            ? nextTarget : LineNumberPrintWriter.NO_LINE_NUMBER;
                    for (String line : stripped) {
                        if (requiredAdjustment < 0) {
                            w.print(lineNoToPrint, line);
                            w.print("  ");
                            requiredAdjustment++;
                            globalOffset--;
                        } else {
                            w.println(lineNoToPrint, line);
                        }
                    }
                    // 3. read and print next actual
                    String line = lines.next();
                    numLinesRead++;
                    if (requiredAdjustment < 0) {
                        w.print(nextTarget, line);
                        w.print("  ");
                        globalOffset--;
                    } else {
                        w.println(nextTarget, line);
                    }

                } else {
                    while (numLinesRead < nextActual) {
                        String line = lines.next();
                        numLinesRead++;
                        boolean isLast = (numLinesRead >= nextActual);
                        int lineNoToPrint = isLast ? nextTarget : LineNumberPrintWriter.NO_LINE_NUMBER;

                        if (requiredAdjustment > 0 && doStretching) {
                            // We currently need to inject newlines to space things out.
                            do {
                                w.println("");
                                requiredAdjustment--;
                                globalOffset++;
                            }
                            while (isLast && requiredAdjustment > 0);
                            w.println(lineNoToPrint, line);
                        } else {
                            // No tweaks needed-- we are on the ball.
                            w.println(lineNoToPrint, line);
                        }
                    }
                }
            }

            // Finish out the file.
            String line;
            while (lines.hasNext()) {
                line = lines.next();
                w.println(line);
            }
        }

        // Delete the original file and rename the formatted temp file over the original.
        _file.delete();
        tempFile.renameTo(_file);
    }

}
