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

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.springframework.stereotype.Service;

import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.BoundingBox;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Zone;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.utils.DoubleUtils;

@Service
public class ReadingOrderService {

    private static final double THRESHOLD = 5;
    public static final double MULTI_COLUMN_DETECTION_THRESHOLD = 1.5;


    public List<Zone> resolve(List<Zone> zones, boolean xyReadingOrder) {

        if (zones.isEmpty() || zones.size() == 1) {
            return zones;
        }

        if (xyReadingOrder) {
            return resolveSingleColumnReadingOrder(zones);
        }

        Map<Long, Integer> histogram = new HashMap<>();
        for (Zone zone : zones) {
            long minY = Math.round(zone.getBBox().getMinY());
            long maxY = Math.round(zone.getBBox().getMaxY());
            for (long i = minY; i <= maxY; i++) {
                histogram.put(i, histogram.getOrDefault(i, 0) + 1);
            }
        }

        if (histogram.values().stream().mapToInt(Integer::intValue).average().orElse(1) < MULTI_COLUMN_DETECTION_THRESHOLD) {
            return resolveSingleColumnReadingOrder(zones);
        } else {

            return resolveMultiColumnReadingOder(zones);
        }

    }


    private static List<Zone> resolveSingleColumnReadingOrder(List<Zone> zones) {

        zones.sort(Comparator.comparing(BoundingBox::getY, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD))
                .thenComparing(BoundingBox::getX, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD)));
        return zones;
    }


    private List<Zone> resolveMultiColumnReadingOder(List<Zone> zones) {

        // Simple reading order resolver for multi column page layout as described here : https://pub.towardsai.net/advanced-rag-02-unveiling-pdf-parsing-b84ae866344e
        // TODO implement a more fancy reading order resolver see https://github.com/BobLd/DocumentLayoutAnalysis/blob/master/README.md#reading-order

        double minX = Double.POSITIVE_INFINITY;
        double maxX = Double.NEGATIVE_INFINITY;

        for (Zone zone : zones) {
            if (zone.getX() < minX) {
                minX = zone.getX();
            }
            if (zone.getX() + zone.getWidth() > maxX) {
                maxX = zone.getX() + zone.getWidth();
            }
        }

        double midLineXCoordinate = (minX + maxX) / 2;

        List<Zone> leftOf = new ArrayList<>();
        List<Zone> rightOf = new ArrayList<>();
        List<Zone> middle = new ArrayList<>();
        for (Zone zone : zones) {
            if (zone.getX() < midLineXCoordinate && zone.getX() + zone.getWidth() < midLineXCoordinate) {
                leftOf.add(zone);
            } else if (zone.getX() > midLineXCoordinate && zone.getX() + zone.getWidth() > midLineXCoordinate) {
                rightOf.add(zone);
            } else {
                middle.add(zone);
            }
        }

        leftOf.sort(Comparator.comparing(BoundingBox::getY, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD))
                .thenComparing(BoundingBox::getX, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD)));

        rightOf.sort(Comparator.comparing(BoundingBox::getY, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD))
                .thenComparing(BoundingBox::getX, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD)));

        middle.sort(Comparator.comparing(BoundingBox::getY, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD))
                .thenComparing(BoundingBox::getX, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD)));

        List<Zone> leftNotIntersecting = new ArrayList<>();
        for (Zone leftZone : leftOf) {
            boolean intersects = false;
            for (Zone rightZone : rightOf) {
                if (leftZone.intersectsY(rightZone)) {
                    intersects = true;
                    break;
                }
                // early stopping
                if (rightZone.getBBox().getMinY() > leftZone.getBBox().getMaxY()) {
                    break;
                }
            }
            if (!intersects) {
                leftNotIntersecting.add(leftZone);
            }
        }

        List<Zone> rightNotIntersecting = new ArrayList<>();
        for (Zone rightZone : rightOf) {
            boolean intersects = false;
            for (Zone leftZone : leftOf) {
                if (rightZone.intersectsY(leftZone)) {
                    intersects = true;
                    break;
                }
                // early stopping
                if (leftZone.getBBox().getMinY() > rightZone.getBBox().getMaxY()) {
                    break;
                }
            }
            if (!intersects) {
                rightNotIntersecting.add(rightZone);
            }
        }

        leftOf.removeAll(leftNotIntersecting);
        rightOf.removeAll(rightNotIntersecting);

        middle.addAll(leftNotIntersecting);
        middle.addAll(rightNotIntersecting);

        List<Zone> sortedZones = new ArrayList<>();
        sortedZones.addAll(leftOf);
        sortedZones.addAll(rightOf);

        ListIterator<Zone> itty = middle.listIterator();

        while (itty.hasNext()) {
            Zone current = itty.next();
            for (int i = 0; i < sortedZones.size(); i++) {
                if (current.getY() < sortedZones.get(i).getY()) {
                    sortedZones.add(i, current);
                    itty.remove();
                    break;
                }
            }
        }

        sortedZones.addAll(middle);

        return sortedZones;
    }

}
