package com.knecon.fforesight.service.layoutparser.processor.docstrum.model;

import static com.knecon.fforesight.service.layoutparser.processor.model.text.Word.BOLD;
import static com.knecon.fforesight.service.layoutparser.processor.model.text.Word.BOLD_ITALIC;
import static com.knecon.fforesight.service.layoutparser.processor.model.text.Word.ITALIC;
import static com.knecon.fforesight.service.layoutparser.processor.model.text.Word.STANDARD;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import com.knecon.fforesight.service.layoutparser.processor.docstrum.utils.FastAtan2;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.model.text.FontStyle;

import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class Line extends TextBoundingBox {

    private static final double WORD_DISTANCE_MULTIPLIER = 0.17;

    @EqualsAndHashCode.Include
    private final double x0;
    @EqualsAndHashCode.Include
    private final double y0;

    @EqualsAndHashCode.Include
    private final double x1;
    @EqualsAndHashCode.Include
    private final double y1;

    private FontStyle fontStyle;

    private final List<Word> words;


    public Line(List<Character> characters, double wordSpacing) {

        if (characters.size() >= 2) {
            // linear regression
            double sx = 0.0;
            double sxx = 0.0;
            double sxy = 0.0;
            double sy = 0.0;
            for (Character character : characters) {
                sx += character.getX();
                sxx += character.getX() * character.getX();
                sxy += character.getX() * character.getY();
                sy += character.getY();
            }
            double b = (characters.size() * sxy - sx * sy) / (characters.size() * sxx - sx * sx);
            double a = (sy - b * sx) / characters.size();

            this.x0 = characters.get(0).getX();
            this.y0 = a + b * this.x0;
            this.x1 = characters.get(characters.size() - 1).getX();
            this.y1 = a + b * this.x1;
        } else {
            Character character = characters.get(0);
            double dx = character.getTextPosition().getWidthDirAdj() / 3;
            double dy = dx * Math.tan(0);
            this.x0 = character.getX() - dx;
            this.x1 = character.getX() + dx;
            this.y0 = character.getY() - dy;
            this.y1 = character.getY() + dy;
        }
        this.words = new ArrayList<>();
        computeWords(characters, wordSpacing * WORD_DISTANCE_MULTIPLIER);
        buildBBox();
        computeFontStyle();
    }


    public Line(List<Word> words) {

        this.words = words;
        buildBBox();
        x0 = getMinX();
        y0 = getMinY();
        x1 = getMaxX();
        y1 = getMaxY();
        computeFontStyle();
    }


    private void computeFontStyle() {

        EnumMap<FontStyle, AtomicInteger> fontStyleCounter = new EnumMap<>(FontStyle.class);
        for (FontStyle fontStyle : FontStyle.values()) {
            fontStyleCounter.put(fontStyle, new AtomicInteger(0));
        }
        for (Word word : words) {
            switch (word.getFontStyle()) {
                case STANDARD -> fontStyleCounter.get(FontStyle.REGULAR).getAndIncrement();
                case BOLD -> fontStyleCounter.get(FontStyle.BOLD).getAndIncrement();
                case ITALIC -> fontStyleCounter.get(FontStyle.ITALIC).getAndIncrement();
                case BOLD_ITALIC -> fontStyleCounter.get(FontStyle.BOLD_ITALIC).getAndIncrement();
            }
        }
        fontStyle = fontStyleCounter.entrySet()
                .stream()
                .max(Comparator.comparing(entry -> entry.getValue().get()))
                .map(Map.Entry::getKey).orElse(FontStyle.REGULAR);
    }


    public double getAngle() {

        return FastAtan2.fastAtan2(y1 - y0, x1 - x0);
    }


    public double getLength() {

        return Math.sqrt((x0 - x1) * (x0 - x1) + (y0 - y1) * (y0 - y1));
    }


    public double angularDifference(Line j) {

        double diff = Math.abs(getAngle() - j.getAngle());
        if (diff <= Math.PI / 2) {
            return diff;
        } else {
            return Math.PI - diff;
        }
    }


    public double horizontalDistance(Line other) {

        double[] xs = new double[4];
        xs[0] = x0;
        xs[1] = x1;
        xs[2] = other.x0;
        xs[3] = other.x1;
        boolean overlapping = xs[1] >= xs[2] && xs[3] >= xs[0];
        Arrays.sort(xs);
        return Math.abs(xs[2] - xs[1]) * (overlapping ? 1 : -1);
    }


    public double verticalDistance(Line other) {

        double ym = (y0 + y1) / 2;
        double yn = (other.y0 + other.y1) / 2;
        return Math.abs(ym - yn);
    }


    private void computeWords(List<Character> characters, double wordSpacing) {

        // Imo, the width of space should be scaled with the font size, but it only depends on the median distance between horizontal neighbours.
        // If there are large differences in fontsize on a page, this might lead to missing spaces for the smaller fonts and too many for larger fonts.
        // I've just now changed the scaling factor. If you come across this comment with missing whitespaces again, try scaling the fontsize instead of simply changing the factor again.
        Word word = new Word();
        Character previous = null;
        for (Character current : characters) {
            if (previous != null) {
                double dist = current.getTextPosition().getXDirAdj() - previous.getTextPosition().getXDirAdj() - previous.getTextPosition().getWidthDirAdj();
                if (dist > wordSpacing) {
                    words.add(word);
                    word = new Word();
                }
            }
            word.add(current);
            previous = current;
        }
        words.add(word);
    }


    private void buildBBox() {

        this.setToBBoxOfComponents(words);
    }


    public String toString() {

        StringBuilder sb = new StringBuilder();
        words.forEach(word -> sb.append(word.toString()).append(" "));
        return sb.toString().trim();
    }

}

