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

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;

import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Character;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Line;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.UnionFind;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Zone;
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;

@Service
public class ZoneBuilderService {

    private static final double MIN_HORIZONTAL_DISTANCE_MULTIPLIER = -0.5;
    private static final double MAX_VERTICAL_DISTANCE_MULTIPLIER = 1.2;

    private static final double MIN_HORIZONTAL_MERGE_DISTANCE_MULTIPLIER = -3.0;

    private static final double MAX_VERTICAL_MERGE_DISTANCE_MULTIPLIER = 0.5;

    private static final double MIN_LINE_SIZE_SCALE = 0.9;

    private static final double MAX_LINE_SIZE_SCALE = 2.5;

    private static final double ANGLE_TOLERANCE = Math.PI / 6;

    private static final double MAX_VERTICAL_MERGE_DISTANCE = 0.5;


    public List<Zone> buildZones(List<Line> lines, double characterSpacing, double lineSpacing, CleanRulings rulings) {

        double minHorizontalDistance = characterSpacing * MIN_HORIZONTAL_DISTANCE_MULTIPLIER;
        double maxVerticalDistance = lineSpacing * MAX_VERTICAL_DISTANCE_MULTIPLIER;
        double minHorizontalMergeDistance = characterSpacing * MIN_HORIZONTAL_MERGE_DISTANCE_MULTIPLIER;
        double maxVerticalMergeDistance = lineSpacing * MAX_VERTICAL_MERGE_DISTANCE_MULTIPLIER;

        UnionFind<Line> unionFind = new UnionFind<>(new HashSet<>(lines));

        double meanHeight = calculateMeanHeight(lines);

        lines.forEach(outerLine -> {
            lines.forEach(innerLine -> {

                if (innerLine == outerLine //
                    || unionFind.inSameSet(outerLine, innerLine)//
                    || outerLine.angularDifference(innerLine) > ANGLE_TOLERANCE) {
                    return;
                }

                double scale = Math.min(outerLine.getHeight(), innerLine.getHeight()) / meanHeight;
                scale = Math.max(MIN_LINE_SIZE_SCALE, Math.min(scale, MAX_LINE_SIZE_SCALE));

                double horizontalDistance = outerLine.horizontalDistance(innerLine) / scale;
                double verticalDistance = outerLine.verticalDistance(innerLine) / scale;

                if ((!(minHorizontalDistance <= horizontalDistance) || !(verticalDistance <= maxVerticalDistance)) //
                    && (!(minHorizontalMergeDistance <= horizontalDistance) || !(verticalDistance <= maxVerticalMergeDistance))) {
                    return;
                }

                if (rulings.lineBetween(outerLine.getBBox(), innerLine.getBBox())) {
                    return;
                }

                unionFind.union(outerLine, innerLine);

            });
        });

        return unionFind.getGroups()
                .stream()
                .map(group -> mergeLinesInZone(new ArrayList<>(group), characterSpacing, lineSpacing))
                .toList();
    }


    private double calculateMeanHeight(List<Line> lines) {

        double meanHeight = 0.0;
        double weights = 0.0;
        for (Line line : lines) {
            double weight = line.getLength();
            meanHeight += line.getHeight() * weight;
            weights += weight;
        }
        meanHeight /= weights;
        return meanHeight;
    }


    private Zone mergeLinesInZone(List<Line> lines, double characterSpacing, double lineSpacing) {

        double maxHorizontalDistance = 0;
        double minVerticalDistance = 0;
        double maxVerticalDistance = lineSpacing * MAX_VERTICAL_MERGE_DISTANCE;

        UnionFind<Line> unionFind = new UnionFind<>(new HashSet<>(lines));

        lines.forEach(outer -> {
            lines.forEach(inner -> {
                if (inner == outer) {
                    return;
                }

                double horizontalDistance = outer.horizontalDistance(inner);
                double verticalDistance = outer.verticalDistance(inner);

                if (horizontalDistance <= maxHorizontalDistance && minVerticalDistance <= verticalDistance && verticalDistance <= maxVerticalDistance) {

                    unionFind.union(outer, inner);

                } else if (minVerticalDistance <= verticalDistance
                           && verticalDistance <= maxVerticalDistance
                           && Math.abs(horizontalDistance - Math.min(outer.getLength(), inner.getLength())) < 0.1) {

                    boolean characterOverlap = false;
                    int overlappingCount = 0;
                    for (Character outerCharacter : outer.getCharacters()) {
                        for (Character innerCharacter : inner.getCharacters()) {
                            double characterOverlapDistance = outerCharacter.overlappingDistance(innerCharacter);
                            if (characterOverlapDistance > 2) {
                                characterOverlap = true;
                            }
                            if (characterOverlapDistance > 0) {
                                overlappingCount++;
                            }
                        }
                    }
                    if (!characterOverlap && overlappingCount <= 2) {
                        unionFind.union(outer, inner);
                    }
                }

            });
        });

        List<Line> outputZone = new ArrayList<>();
        for (Set<Line> group : unionFind.getGroups()) {
            List<Character> characters = new ArrayList<>();
            for (Line line : group) {
                characters.addAll(line.getCharacters());
            }
            characters.sort(Comparator.comparingDouble(Character::getX));

            outputZone.add(new Line(characters, characterSpacing));
        }

        return new Zone(outputZone.stream()
                                .sorted(Comparator.comparing(Line::getY0))
                                .collect(Collectors.toList()));
    }

}
