package com.uddernetworks.mspaint.code.highlighter;

import com.uddernetworks.mspaint.code.ImageClass;
import com.uddernetworks.mspaint.main.StartupLogic;
import com.uddernetworks.newocr.character.ImageLetter;
import com.uddernetworks.newocr.recognition.ScannedImage;
import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
import org.apache.batik.transcoder.TranscoderException;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class AngrySquiggleHighlighter {

    private static Logger LOGGER = LoggerFactory.getLogger(AngrySquiggleHighlighter.class);

    private StartupLogic startupLogic;
    private ImageClass imageClass;
    private BufferedImage image;
    private double extraSquigglePaddingRatio;
    private File highlightedFile;
    private ScannedImage scannedImage;
    private List<Diagnostic> diagnostics;
    private BufferedImage squiggleImage; // The highlighter will use the size of the first character to calculate the persistent angry squiggle image

    public AngrySquiggleHighlighter(StartupLogic startupLogic, ImageClass imageClass, double extraSquigglePaddingRatio, File highlightedFile, ScannedImage scannedImage, List<Diagnostic> diagnostics) {
        this.startupLogic = startupLogic;
        this.imageClass = imageClass;
        this.image = imageClass.getImage();
        this.extraSquigglePaddingRatio = extraSquigglePaddingRatio;
        this.highlightedFile = highlightedFile;
        this.scannedImage = scannedImage;
        this.diagnostics = diagnostics;
    }

    public void highlightAngrySquiggles() throws IOException, TranscoderException {
        LOGGER.info("Starting highlight");

        // TODO: Make this configurable?
        this.diagnostics.removeIf(diagnostic -> diagnostic.getSeverity() != DiagnosticSeverity.Error);

        for (Diagnostic diagnostic : this.diagnostics) {
            var range = diagnostic.getRange();
            var start = range.getStart();
            var end = range.getEnd();
            int startLine = start.getLine();
            int startColumn = start.getCharacter();

            var endLine = end.getLine();
            var endColumn = end.getCharacter();

            if (endLine != startLine) {
                var numOfFullLines = endLine - startLine - 1;
                for (int fullLine = 0; fullLine < numOfFullLines; fullLine++) {
                    getLineAndLength(startLine, startColumn, Integer.MAX_VALUE);
                }

                getLineAndLength(startLine, startColumn, Integer.MAX_VALUE);
                getLineAndLength(endLine, endColumn, Integer.MAX_VALUE);
            } else {
                getLineAndLength(startLine, startColumn, endColumn - startColumn);
            }
        }
    }

    private void drawAngrySquiggle(int squiggleX, int squiggleY, int length) throws IOException {
        for (int y = squiggleY; y < squiggleImage.getHeight() + squiggleY; y++) {
            for (int x = squiggleX; x < squiggleX + length; x++) {
                if (!isInBounds(x, y)) continue;
                int squiggleImageX = (x - squiggleX) % squiggleImage.getWidth();
                int squiggleImageY = y - squiggleY;

                Color squiggleColor = new Color(squiggleImage.getRGB(squiggleImageX, squiggleImageY), true);

                if (squiggleColor.getAlpha() == 255) image.setRGB(x, y, squiggleColor.getRGB());
            }
        }

        ImageIO.write(image, "png", highlightedFile);
    }

    private boolean isInBounds(int x, int y) {
        return this.squiggleImage.getWidth() < x
                && 0 <= x
                && this.squiggleImage.getHeight() < y
                && 0 <= y;
    }

    private void getLineAndLength(int lineNumber, int columnNumber, int length) throws IOException, TranscoderException {
        var actions = this.startupLogic.getOCRManager().getActions();
        int xIndex;
        int yIndex;
        int pixelLength;
        int fontSize = 0;

        List<ImageLetter> line = this.scannedImage.getLine(lineNumber);
        ImageLetter first = line.get(0);
        ImageLetter last = line.get(line.size() - 1);
        ImageLetter calcXY;

        int i = 0;
        Optional<ImageLetter> letterOptional;
        while ((letterOptional = this.scannedImage.letterAt(i++)).isPresent()) {
            var letter = letterOptional.get();
            if (letter.getLetter() == ' ') continue;
            fontSize = (int) actions.getFontSize(letter).getAsDouble();
        }

        int extraSquigglePadding = 0;

        if (columnNumber == -1) {
            calcXY = first;
            pixelLength = last.getX() + last.getWidth() - first.getX();
        } else {
            var startIndex = Math.min(columnNumber, line.size() - 1); // Need to get BEFORE
            ImageLetter firstLetter = line.get(startIndex);
            calcXY = firstLetter;

            var lastLetter = line.get(length == Integer.MAX_VALUE ?
                    line.size() - 1 :
                    Math.min(startIndex + length, line.size() - 1));

            pixelLength = lastLetter.getX() - firstLetter.getX() + lastLetter.getWidth();
            extraSquigglePadding = (int) Math.round(this.extraSquigglePaddingRatio * pixelLength);
        }

        xIndex = calcXY.getX() + (int) Math.round(calcXY.getWidth() / 2D);

        yIndex = getBaseline(line, fontSize);

        pixelLength = pixelLength + extraSquigglePadding * 2;

        if (this.squiggleImage == null) {
            AngrySquiggleGenerator angrySquiggleGenerator = new AngrySquiggleGenerator(fontSize);
            this.squiggleImage = angrySquiggleGenerator.getGeneratedPNG();
        }

        pixelLength = getRoundedSquiggleLength(pixelLength);

        drawAngrySquiggle((int) Math.round(xIndex - (pixelLength / 2D)), (int) Math.round(yIndex + this.squiggleImage.getHeight() / 2D), pixelLength);
    }

    private int getBaseline(List<ImageLetter> imageLetters, int fontSize) throws IOException {
        var centerPopulator = this.startupLogic.getCenterPopulator();
        centerPopulator.generateCenters(fontSize);

        var descriptiveStatistics = new DescriptiveStatistics();

        var sizes = imageLetters
                .stream()
                .map(imageLetter -> (double) imageLetter.getHeight() + imageLetter.getY())
                .peek(descriptiveStatistics::addValue)
                .collect(Collectors.toCollection(DoubleArrayList::new));

        var lowerBound = descriptiveStatistics.getPercentile(40);
        var upperBound = descriptiveStatistics.getPercentile(60);

        sizes.removeIf((Predicate<Double>) value -> value > upperBound || value < lowerBound);

        return (int) sizes.stream().mapToDouble(Double::valueOf).average().orElse(0);
    }

    private int getRoundedSquiggleLength(int originalLength) {
        int finalLength = originalLength;
        int imageWidth = this.squiggleImage.getWidth();

        int extra = originalLength % imageWidth;
        finalLength -= extra;
        if (extra > 0) finalLength += imageWidth;

        return finalLength;
    }

}
