package com.disnodeteam.dogecv.math;

import com.disnodeteam.dogecv.math.Line;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.imgproc.CLAHE;
import org.opencv.imgproc.Imgproc;
import org.opencv.imgproc.LineSegmentDetector;


public class Lines {

    //Individual Lines
    /**
     * Returns the angular distance, in degrees, between lines 1 and 2.
     * @param line1
     * @param line2
     * @return
     */
    public static double getAngularDistance(Line line1, Line line2) {
        double ang1 = line1.angle();
        double ang2 = line2.angle();
        double ang_diff = 57.296*(ang1 - ang2);
        ArrayList<Double> list = new ArrayList<Double>();
        list.add(Math.abs(ang_diff));
        list.add(Math.abs(180-Math.abs(ang_diff)));
        list.add(Math.abs(360-Math.abs(ang_diff)));
        return Collections.min(list);
    }
    /**
     * Returns whether three points are clockwise or counter-clockwise
     * @param A
     * @param B
     * @param C
     * @return
     */
    private static boolean ccw(Point A, Point B, Point C) {
        return (C.y-A.y)*(B.x-A.x) > (B.y-A.y)*(C.x-A.x);
    }
    /**
     * Returns a boolean corresponding to whether the two lines intersect
     * @param line1
     * @param line2
     * @return
     */
    public static boolean intersect(Line line1, Line line2) {
        return ccw(line1.point1,line2.point1,line2.point2) != ccw(line1.point2,line2.point1,line2.point2) && ccw(line1.point1,line1.point2,line2.point1) != ccw(line1.point1,line1.point2,line2.point2);
    }
    /**
     * Returns the signed cross product of a line and a point, such that the relative position of a point to a line can be determined.
     * @param line The line forming the first vector, taken from Point 1 to Point 2
     * @param point The point to form the second vector, taken from line1.point1 to this point.
     * @return 1 if the point is on one side, -1 if on the other, 0 if exactly on the line.
     */
    public static double crossSign(Line line, Point point) {
        double ABx = line.x2 - line.x1;
        double ACx = point.x - line.x1;
        double ABy = line.y2 - line.y1;
        double ACy = point.y - line.y1;
        return Math.signum((ABx*ACy - ABy*ACx));
    }
    /**
     * Extends a line as a vector, increasing the Euclidean distance of point 2 from point 1 by moving only point 2.
     * @param line The line to be modified.
     * @param lengthFinal The final desired Euclidean length of the line
     * @param size The size of the working image
     * @return A new Line object of the appropriate length
     */
    public static Line vectorExtend(Line line, double lengthFinal, Size size) {
        double scalar = lengthFinal/line.length();
        line.x2 = (int) (MathFTC.clip(scalar*(line.x2 - line.x1) + line.x1, 0, size.width - 1));
        line.y2 = (int) (MathFTC.clip(scalar*(line.y2 - line.y1) + line.y1, 0, size.height - 1));
        line.point2 = new Point(line.x2,line.y2);
        return line;
    }

    /**
     * Extends a list of lines as vector, increasing the Euclidean distance of point 2 from point 1 by moving only point 2.
     * @param lines The lines to be modified
     * @param lengthFinal The final desired Euclidean length of the line
     * @param size The size of the working image
     * @return A new list of lines object of the appropriate length
     */
    public static List<Line> vectorExtend(List<Line> lines, double lengthFinal, Size size) {
        List<Line> newLines = new ArrayList<Line>();
        for (Line line : lines) {
            newLines.add(vectorExtend(line, lengthFinal, size));
        }
        return newLines;
    }

    /**
     * Linearly extends a line in both directions by the given portion of its length
     * @param line The line to be modified
     * @param scale The length of the line will be extended by the line's length divided by twice this value
     * @param size The size of the image
     * @return
     */
    public static Line linearExtend(Line line, double scale, Size size) {
        scale *= 2;
        double xN1 = line.x1 + (line.x1 - line.x2)/scale;
        double yN1 = line.y1 + (line.y1 - line.y2)/scale;
        double xN2 = line.x2 + (line.x2 - line.x1)/scale;
        double yN2 = line.y2 + (line.y2 - line.y1)/scale;
        Point p1 = new Point(MathFTC.clip((int)xN1, 0, size.width -1),MathFTC.clip((int)yN1, 0, size.height-1));
        Point p2 = new Point(MathFTC.clip((int)xN2, 0, size.width -1),MathFTC.clip((int)yN2, 0, size.height-1));
        return new Line(p1, p2);
    }

    /**
     * Linearly extends a lines in both directions by the given portion of its length
     * @param lines The lines to be modified
     * @param scale The length of the lines will be extended by the line's length divided by twice this value
     * @param size The size of the image
     * @return
     */
    public static List<Line> linearExtend(List<Line> lines, double scale, Size size) {
        List<Line> newLines = new ArrayList<Line>();
        for (Line line : lines) {
            newLines.add(linearExtend(line, scale, size));
        }
        return newLines;
    }

    /**
     * Constructs a line given a center point, an angle in the plane, and a length
     * @param point The center point of the line
     * @param angle The desired angle of the line, in degrees
     * @param length The desired length of the line, in pixels
     * @return The constructed line
     */
    public static Line constructLine(Point point, double angle, double length) {
        double dx = Math.cos(angle*Math.PI/180);
        double dy = Math.sin(angle*Math.PI/180);
        Point p1 = new Point(point.x + 0.5*length*dx, point.y + 0.5*length*dy);
        Point p2 = new Point(point.x - 0.5*length*dx, point.y - 0.5*length*dy);
        return new Line(p1,p2);
    }

    //Multiple Lines
    static LineSegmentDetector detector = Imgproc.createLineSegmentDetector(Imgproc.LSD_REFINE_STD, 0.8, 0.6,2.0, 22.5, 0, 0.7, 32);

    /**
     * Modern OpenCV line segment detection - far better than Canny, but must be carefully adjusted.
     * @param original The original image to be scanned, as an RGB image
     * @param scale The factor by which the image is to be downscaled
     * @param minLength The minimum line segment length to be returned
     * @return A List of Lines found
     */
    public static List<Line> getOpenCvLines(Mat original, int scale, double minLength) {
        Mat raw = new Mat();
        Imgproc.resize(original.clone(), raw, new Size((int) (original.size().width/scale), (int) (original.size().height/scale)));
        if(raw.channels() > 1) {
            Imgproc.cvtColor(raw, raw, Imgproc.COLOR_RGB2GRAY);
        }
        Imgproc.equalizeHist(raw, raw);
        Imgproc.blur(raw, raw, new Size(3,3));
        //Line Segment Detection 2
        Mat linesM1 = new Mat();

        detector.detect(raw, linesM1);
        ArrayList<Line> lines = new ArrayList<Line>();
        for (int x = 0; x < linesM1.rows(); x++)  {
            double[] vec = linesM1.get(x, 0);
            Point start = new Point(vec[0],vec[1]);
            Point end = new Point(vec[2], vec[3]);
            Line line = new Line(start, end);
            line = new Line(new Point((int)line.x1*scale, (int) line.y1*scale), new Point((int)line.x2*scale, (int)line.y2*scale));
            if(line.length() > minLength) lines.add(line);
        }

        raw.release();
        linesM1.release();

        return lines;
    }

    public static List<Line> resize(List<Line> lines, double scale) {
        List<Line> linesN = new ArrayList<Line>();
        for (Line line : lines) {
            line.resize(scale);
            linesN.add(line);
        }
        return linesN;
    }

    public static Line findRightMost(List<Line> lines, Size size) {
        double maxX = 0;
        int maxXi = -1;
        for (int i = 0; i < lines.size(); i++) {
            if(lines.get(i).center().x > maxX && Points.inBounds(lines.get(i).point1, size) && Points.inBounds(lines.get(i).point2, size)) {
                maxX = lines.get(i).center().x;
                maxXi = i;
            }
        }
        return lines.get(maxXi);
    }

    public static Line findLeftMost(List<Line> lines, Size size) {
        double minX = 1000000;
        int minXi = -1;
        for (int i = 0; i < lines.size(); i++) {
            if(lines.get(i).center().x < minX) {
                minX = lines.get(i).center().x;
                minXi = i;
            }
        }
        return lines.get(minXi);
    }

    public static Line getPerpindicular(Line line, double sign) {
        double angle = Lines.getAngularDistance(line, new Line(new Point(0,0), new Point(100,0)));
        angle += 90;
        double x = line.center().x + 50*Math.cos(angle*Math.PI/180);
        double y = line.center().y + 50*Math.sin(angle*Math.PI/180);
        if(Lines.crossSign(line, new Point(x,y)) != sign) {
            x = line.center().x - 50*Math.cos(angle*Math.PI/180);
            y = line.center().y - 50*Math.sin(angle*Math.PI/180);
        }
        Line perp = new Line(line.center(), new Point(x,y));
        return perp;
    }

    public static double getPerpindicularDistance(Line line1, Line line2) {
        Line perp = Lines.getPerpindicular(line1, Lines.crossSign(line1, line2.center()));
        Line joint = new Line(line1.center(), line2.center());
        return Math.cos(Lines.getAngularDistance(perp, joint)*Math.PI/180)*joint.length();
    }

    public static double getPerpindicularDistance(Line line1, Point point) {
        Line perp = Lines.getPerpindicular(line1, Lines.crossSign(line1, point));
        Line joint = new Line(line1.center(), point);
        return Math.cos(Lines.getAngularDistance(perp, joint)*Math.PI/180)*joint.length();
    }

    public static Line getPerpindicularConnector(Line left, Line right, Size size) {
        double angle = Lines.getAngularDistance(left, new Line(new Point(0,0), new Point(100,0)));
        angle += 90;
        double dx = 3*Math.cos(angle*Math.PI/180);
        double dy = 3*Math.sin(angle*Math.PI/180);
        double x = left.center().x + dx;
        double y = left.center().y + dy;
        if(Lines.crossSign(left, new Point(x,y)) !=  Lines.crossSign(left, right.center())) {
            dx = -dx;
            dy = -dy;
            x -= 2*dx;
            y -= 2*dy;
        }
        while(Points.inBounds(new Point(x,y), size) && !Lines.intersect(new Line(left.center(),new Point(x,y)), right)) {
            x += dx;
            y += dy;
        }
        return new Line(left.center(), new Point(x,y));
    }

    public static List<List<Line>> groupLines(ArrayList<Line> lines, double seperation) {
        //Merge close and parallel lines
        //Find parallel lines and create list of joints to be merged
        ArrayList<Joint> joints = new ArrayList<Joint>();
        for (List<Line> b : MathFTC.combinations(lines, 2)) {
            Line jointLine = new Line(b.get(0).center(), b.get(1).center());
            if (Lines.getPerpindicularDistance(b.get(0),b.get(1)) < seperation) {
                joints.add(new Joint(b.get(0),b.get(1),jointLine.center()));
            }
        }
        //Create a dictionary of mergers close together (i.e. same edge)
        ArrayList<MergeFocus> mergeCollections = new ArrayList<MergeFocus>();
        for (Joint joint : joints) {
            int close = -1;
            for (int i = 0; i < mergeCollections.size(); i++) {
                if(Lines.getPerpindicularDistance(joint.line1, mergeCollections.get(i).center) < seperation) {
                    close = i;
                    break;
                }
            }
            if (close >= 0) {
                MergeFocus focus = mergeCollections.get(close);
                if(!focus.lines.contains(joint.line1)) {
                    focus.lines.add(joint.line1);
                }
                if(!focus.lines.contains(joint.line2)) {
                    focus.lines.add(joint.line2);
                }
            } else {
                ArrayList<Line> jointLines = new ArrayList<Line>();
                jointLines.add(joint.line1);
                jointLines.add(joint.line2);
                mergeCollections.add(new MergeFocus(joint.center,jointLines));
            }
        }
        List<List<Line>> mergeCollectionsList= new ArrayList<List<Line>>();
        for (MergeFocus focus : mergeCollections) {
            List<Line> list = new ArrayList<Line>();
            for (Line line : focus.lines) {
                lines.remove(line);
                list.add(line);
            }
            mergeCollectionsList.add(list);
        }
        return mergeCollectionsList;
    }

    private static class Joint {

        Line line1, line2;
        Point center;

        Joint(Line line1, Line line2, Point center) {
            this.center = center;
            this.line1 = line1;
            this.line2 = line2;
        }
    }

    private static class MergeFocus {

        List<Line> lines;
        Point center;

        MergeFocus(Point center, List<Line> lines) {
            this.center = center;
            this.lines = lines;
        }
    }

    public static Point getMeanPoint(List<Line> lines) {
        if (lines.size() == 0) return null;
        double x = 0;
        double y = 0;
        for(Line line : lines) {
            x += line.center().x;
            y += line.center().y;
        }
        return new Point(x/lines.size(), y/lines.size());
    }

    public static double getMeanAngle(List<Line> lines) {
        if(lines.size() == 0) return Double.NaN;
        double angle = 0;
        for (Line line : lines) {
            angle += MathFTC.normalizeAngle(line.angle()*180/Math.PI);
        }
        return angle/lines.size();
    }

    public static double stdDevX(List<Line> lines) {
        if (lines.size() == 0) return Double.NaN;
        List<Double> data = new ArrayList<Double>();
        for(Line line : lines) {
            data.add(line.center().x);
        }
        return MathFTC.getStdDev(data);
    }

    public static double stdDevY(List<Line> lines) {
        List<Double> data = new ArrayList<Double>();
        for(Line line : lines) {
            data.add(line.center().y);
        }
        return MathFTC.getStdDev(data);
    }

}