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

import java.awt.geom.Rectangle2D;
import java.util.Comparator;
import java.util.List;

import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;

import lombok.Data;

@Data
public abstract class BoundingBox {

    // Java coordinate system: (0, 0) is always upper left, x is increasing left to right and y is increasing from top to bottom.
    // should be used when determining reading order or other tasks which require coordinates in a harmonized system.
    protected Rectangle2D bBox; // I would not trust this coordinate when comparing rulings and text, due to the text positions being slightly off.

    // PDF coordinate system: depends on page rotation, (0, 0) is lower left corner, x is increasing left to right and y from bottom to top.
    // This rotates completely in 90 degree steps with page rotation.
    // Needs to be used when writing to a PDF.
    // Also, these are definitely correct and should be used whenever possible.
    protected Rectangle2D bBoxInitialUserSpace;

    protected static final float VERTICAL_COMPARISON_THRESHOLD = 0.4f;


    public double getX() {

        return bBox.getX();
    }


    public double getY() {

        return bBox.getY();
    }


    public double getMinX() {

        return bBox.getMinX();
    }


    public double getMinY() {

        return bBox.getMinY();
    }


    public double getPdfMinX() {

        return bBoxInitialUserSpace.getMinX();
    }


    public double getPdfMaxX() {

        return bBoxInitialUserSpace.getMaxX();
    }


    public double getPdfMinY() {

        return bBoxInitialUserSpace.getMinY();
    }


    public double getPdfMaxY() {

        return bBoxInitialUserSpace.getMaxY();
    }


    public double getWidth() {

        return bBox.getWidth();
    }


    public double getHeight() {

        return bBox.getHeight();
    }


    public double getMaxX() {

        return bBox.getMaxX();
    }


    public double getMaxY() {

        return bBox.getMaxY();
    }


    public double getArea() {

        return (bBox.getHeight() * bBox.getWidth());
    }


    public boolean contains(BoundingBox contained) {

        return contains(contained, 0);
    }


    public boolean contains(BoundingBox contained, double tolerance) {

        return getPdfMinX() <= contained.getPdfMinX() + tolerance
               && getPdfMinY() <= contained.getPdfMinY() + tolerance
               && getPdfMaxX() >= contained.getPdfMaxX() - tolerance
               && getPdfMaxY() >= contained.getPdfMaxY() - tolerance;
    }


    public boolean intersects(BoundingBox other) {

        return this.intersectsX(other) && this.intersectsY(other);
    }


    public boolean intersects(BoundingBox other, float yThreshold, float xThreshold) {

        return this.intersectsX(other, xThreshold) && this.intersectsY(other, yThreshold);
    }


    public boolean intersectsY(BoundingBox other) {

        return this.getPdfMinY() <= other.getPdfMaxY() && this.getPdfMaxY() >= other.getPdfMinY();
    }


    public boolean intersectsYJava(BoundingBox other) {

        return this.getY() <= other.getMaxY() && this.getMaxY() >= other.getY();
    }


    public boolean intersectsY(BoundingBox other, float threshold) {

        return this.getPdfMinY() - threshold <= other.getPdfMaxY() && this.getPdfMaxY() + threshold >= other.getPdfMinY();
    }


    public boolean intersectsX(BoundingBox other) {

        return this.getPdfMinX() <= other.getPdfMaxX() && this.getPdfMaxX() >= other.getPdfMinX();
    }


    public boolean intersectsXJava(BoundingBox other) {

        return this.getX() <= other.getMaxX() && this.getMaxX() >= other.getMinX();
    }


    public boolean intersectsX(BoundingBox other, float threshold) {

        return this.getPdfMinX() - threshold <= other.getMaxX() && this.getMaxX() + threshold >= other.getPdfMinX();
    }


    public void setToBBoxOfComponents(List<? extends BoundingBox> components) {

        this.bBox = components.stream()
                .map(BoundingBox::getBBox)
                .collect(RectangleTransformations.collectBBox());
        this.bBoxInitialUserSpace = components.stream()
                .map(BoundingBox::getBBoxInitialUserSpace)
                .collect(RectangleTransformations.collectBBox());
    }


    public double verticalOverlap(BoundingBox other) {

        return Math.max(0, Math.min(this.getPdfMaxY(), other.getPdfMaxY()) - Math.max(this.getPdfMinY(), other.getPdfMinY()));
    }


    public static final Comparator<BoundingBox> ILL_DEFINED_ORDER = (o1, o2) -> {

        if (o1.equals(o2)) {
            return 0;
        }
        if (o1.verticalOverlap(o2) > VERTICAL_COMPARISON_THRESHOLD * ((o1.getHeight() + o2.getHeight()) / 2)) {
            return Double.compare(o1.getPdfMinX(), o2.getPdfMinX());
        } else {
            return Double.compare(o1.getPdfMaxY(), o2.getPdfMaxY());
        }
    };


    public double horizontalDistance(BoundingBox other) {

        Rectangle2D left;
        Rectangle2D right;
        if (this.leftOf(other)) {
            left = this.getBBox();
            right = other.getBBox();
        } else {
            left = other.getBBox();
            right = this.getBBox();
        }

        return Math.max(0, right.getMinX() - left.getMaxX());
    }


    public double verticalDistance(BoundingBox other) {

        Rectangle2D bottom;
        Rectangle2D top;
        if (this.isAbove(other)) {
            top = this.getBBox();
            bottom = other.getBBox();
        } else {
            bottom = this.getBBox();
            top = other.getBBox();
        }

        return Math.max(0, bottom.getMinY() - top.getMaxY());
    }


    public boolean rightOf(BoundingBox other) {

        return this.intersectsYJava(other) && other.getMaxX() <= this.getMinX();
    }


    public boolean leftOf(BoundingBox other) {

        return this.intersectsYJava(other) && other.getMinX() >= this.getMaxX();
    }


    public boolean isAbove(BoundingBox other) {

        return this.intersectsXJava(other) && other.getMinY() >= this.getMaxY();
    }


    public boolean isBelow(BoundingBox other) {

        return this.intersectsXJava(other) && this.getMinY() >= other.getMaxY();
    }

}
