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

import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;

import com.knecon.fforesight.service.layoutparser.processor.utils.CohenSutherlandClipping;
import com.knecon.fforesight.service.layoutparser.processor.utils.DoubleComparisons;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@SuppressWarnings("all")
public class Ruling extends Line2D.Float {

    public static final int PERPENDICULAR_UNIT_EXPAND_AMOUNT = 2;
    public static final int COLINEAR_OR_PARALLEL_UNIT_EXPAND_AMOUNT = 2;

    public enum Classification {
        TABLE_LINE,
        UNDERLINE,
        STRIKETROUGH,
        HEADER_SEPARATOR,
        FOOTER_SEPARATOR,
        OTHER
    }

    @Getter
    @Setter
    private Classification classification;


    public Ruling(Point2D p1, Point2D p2) {

        super(p1, p2);
        this.classification = Classification.OTHER;
    }


    public Ruling straightenVertical() {

        double y1 = Math.min(getY1(), getY2());
        double y2 = Math.max(getY1(), getY2());
        double x = (getX1() + getX2()) / 2;
        return new Ruling(new Point2D.Double(x, y1), new Point2D.Double(x, y2));
    }


    public Ruling straightenHorizontal() {

        double x1 = Math.min(getX1(), getX2());
        double x2 = Math.max(getX1(), getX2());
        double y = (getY1() + getY2()) / 2;
        return new Ruling(new Point2D.Double(x1, y), new Point2D.Double(x2, y));
    }


    public static List<Ruling> cropRulingsToArea(List<Ruling> rulings, Rectangle2D area) {

        ArrayList<Ruling> rv = new ArrayList<>();
        for (Ruling r : rulings) {
            if (r.intersects(area)) {
                rv.add(r.intersect(area));
            }
        }
        return rv;
    }


    public void assertHorizontal() {

        if (isHorizontal()) {
            return;
        }
        throw new IllegalArgumentException("Ruling " + this + " is not horizontal");

    }


    public void assertVertical() {

        if (isVertical()) {
            return;
        }
        throw new IllegalArgumentException("Ruling " + this + " is not vertical");
    }


    public boolean isVertical() {

        return this.length() > 0 && DoubleComparisons.feq(this.x1, this.x2); //diff < ORIENTATION_CHECK_THRESHOLD;
    }


    public boolean isHorizontal() {

        return this.length() > 0 && DoubleComparisons.feq(this.y1, this.y2); //diff < ORIENTATION_CHECK_THRESHOLD;
    }

    // attributes that make sense only for non-oblique lines
    // these are used to have a single collapse method (in page, currently)


    public boolean isOblique() {

        return !(this.isVertical() || this.isHorizontal());
    }


    public float getPosition() {

        if (this.isOblique()) {
            throw new UnsupportedOperationException();
        }
        return this.isVertical() ? this.getLeft() : this.getTop();
    }


    public float getStart() {

        if (this.isOblique()) {
            throw new UnsupportedOperationException();
        }
        return this.isVertical() ? this.getTop() : this.getLeft();
    }


    public void setStart(float v) {

        if (this.isOblique()) {
            throw new UnsupportedOperationException();
        }
        if (this.isVertical()) {
            this.setTop(v);
        } else {
            this.setLeft(v);
        }
    }


    public float getEnd() {

        if (this.isOblique()) {
            throw new UnsupportedOperationException();
        }
        return this.isVertical() ? this.getBottom() : this.getRight();
    }


    public void setEnd(float v) {

        if (this.isOblique()) {
            throw new UnsupportedOperationException();
        }
        if (this.isVertical()) {
            this.setBottom(v);
        } else {
            this.setRight(v);
        }
    }


    public void setStartEnd(float start, float end) {

        if (this.isOblique()) {
            throw new UnsupportedOperationException();
        }
        if (this.isVertical()) {
            this.setTop(start);
            this.setBottom(end);
        } else {
            this.setLeft(start);
            this.setRight(end);
        }
    }


    public boolean perpendicularTo(Ruling other) {

        return this.isVertical() == other.isHorizontal();
    }


    public boolean nearlyIntersects(Ruling another) {

        if (this.intersectsLine(another)) {
            return true;
        }

        boolean rv = false;

        if (this.perpendicularTo(another)) {
            rv = this.expand(PERPENDICULAR_UNIT_EXPAND_AMOUNT).intersectsLine(another);
        } else {
            rv = this.expand(COLINEAR_OR_PARALLEL_UNIT_EXPAND_AMOUNT).intersectsLine(another.expand(COLINEAR_OR_PARALLEL_UNIT_EXPAND_AMOUNT));
        }

        return rv;
    }


    public double length() {

        return Math.sqrt(Math.pow(this.x1 - this.x2, 2) + Math.pow(this.y1 - this.y2, 2));
    }


    public Ruling intersect(Rectangle2D clip) {

        Float clipee = (Float) this.clone();
        boolean clipped = new CohenSutherlandClipping(clip).clip(clipee);

        if (clipped) {
            return new Ruling(clipee.getP1(), clipee.getP2());
        } else {
            return this;
        }
    }


    public Ruling expand(float amount) {

        Ruling r = (Ruling) this.clone();
        try {
            r.setStart(this.getStart() - amount);
            r.setEnd(this.getEnd() + amount);
        } catch (UnsupportedOperationException e) {
            log.warn("Could not expand ruling!");
        }
        return r;
    }


    @Override
    public boolean equals(Object other) {

        if (this == other) {
            return true;
        }

        if (!(other instanceof Ruling)) {
            return false;
        }

        Ruling o = (Ruling) other;
        return this.getP1().equals(o.getP1()) && this.getP2().equals(o.getP2());
    }


    @Override
    public int hashCode() {

        return super.hashCode();
    }


    public float getTop() {

        return this.y1;
    }


    public void setTop(float v) {

        setLine(this.getLeft(), v, this.getRight(), this.getBottom());
    }


    public float getLeft() {

        return this.x1;
    }


    public void setLeft(float v) {

        setLine(v, this.getTop(), this.getRight(), this.getBottom());
    }


    public float getBottom() {

        return this.y2;
    }


    public void setBottom(float v) {

        setLine(this.getLeft(), this.getTop(), this.getRight(), v);
    }


    public float getRight() {

        return this.x2;
    }


    public void setRight(float v) {

        setLine(this.getLeft(), this.getTop(), v, this.getBottom());
    }


    public float getWidth() {

        return this.getRight() - this.getLeft();
    }


    public float getHeight() {

        return this.getBottom() - this.getTop();
    }


    public double getAngle() {

        double angle = Math.toDegrees(Math.atan2(this.getP2().getY() - this.getP1().getY(), this.getP2().getX() - this.getP1().getX()));

        if (angle < 0) {
            angle += 360;
        }
        return angle;
    }


    @Override
    public String toString() {

        StringBuilder sb = new StringBuilder();
        Formatter formatter = new Formatter(sb);
        String rv = formatter.format("%s[minX=%f minY=%f maxX=%f maxY=%f]", this.getClass().toString(), this.x1, this.y1, this.x2, this.y2).toString();
        formatter.close();
        return rv;
    }


    public boolean almostMatches(Ruling ruling) {

        final float TOLERANCE = 1;
        return Math.abs(ruling.getX1() - x1) < TOLERANCE &&//
               Math.abs(ruling.getY1() - y1) < TOLERANCE &&//
               Math.abs(ruling.getX2() - x2) < TOLERANCE &&//
               Math.abs(ruling.getY2() - y2) < TOLERANCE;
    }

}
