package math.bezier;

import math.kombi.KombiUtil;
import math.matrix.Matrix;
import math.matrix.MatrixException;
import math.utils.ArrayUtil;
import math.utils.Tuple2d;

import java.awt.*;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.QuadCurve2D;

public class BezierUtil {
    private BezierUtil() {
    }

    public static double bernsteinTValue(int n, int k, double t) {
        double btv = KombiUtil.npok2(n, k) * Math.pow(t, k)
                * Math.pow(1.0 - t, n - k);
        if (btv < 0) {
            btv = 0.0;
        }
        return btv;
    }

    public static void drawBernstein(Graphics2D g2, int w, int h, int n, int k,
                                     int liczbaIteracji) {
        double t = 0.0;
        int a = KombiUtil.npok2(n, k);
        double b = a * Math.pow(t, k) * Math.pow(1.0 - t, n - k);
        float x0 = 0;
        float y0 = (float) (w - b * liczbaIteracji);
        for (int i = 0; i < liczbaIteracji; i++) {
            t = i * 1.0 / liczbaIteracji;
            b = a * Math.pow(t, k) * Math.pow(1.0 - t, n - k);
            if (b < 0) {
                b = 0.99;
            }
            float x = (float) (t * liczbaIteracji);
            float y = (float) (w - b * liczbaIteracji);
            Line2D.Float line = new Line2D.Float(x0, y0, x, y);
            g2.draw(line);
            x0 = x;
            y0 = y;
        }
    }

    public static Tuple2d qBezierValue1(Tuple2d[] points, double t) {
        double ax = points[0].getX();
        double ay = points[0].getY();
        double bx = points[1].getX();
        double by = points[1].getY();
        double cx = points[2].getX();
        double cy = points[2].getY();
        double x1 = ax - 2 * bx + cx;
        double x2 = -2 * ax + 2 * bx;
        double y1 = ay - 2 * by + cy;
        double y2 = -2 * ay + 2 * by;
        double pt = t * t;
        double x = pt * x1 + t * x2 + ax;
        double y = pt * y1 + t * y2 + ay;
        return new Tuple2d(x, y);
    }

    public static Tuple2d qBezierValue2(Tuple2d[] points, double t) {
        double ax = points[0].getX();
        double ay = points[0].getY();
        double bx = points[1].getX();
        double by = points[1].getY();
        double cx = points[2].getX();
        double cy = points[2].getY();
        double[] vx = {ax, bx, cx};
        double[] vy = {ay, by, cy};
        double[] coeffs = {1, -2, 1, -2, 2, 0, 1, 0, 0};
        double[] ts = {Math.pow(t, 2), t, 1};
        Matrix mvx = new Matrix(vx, 1);
        Matrix mvy = new Matrix(vy, 1);
        Matrix mcoeffs = new Matrix(coeffs, 3);
        Matrix mts = new Matrix(ts, 3);
        double x = 0.0;
        double y = 0.0;
        try {
            x = (mts.multiply2(mcoeffs.multiply2(mvx))).getCell(0, 0);
            y = (mts.multiply2(mcoeffs).multiply2(mvy)).getCell(0, 0);
        } catch (MatrixException e) {
            e.printStackTrace();
        }
        return new Tuple2d(x, y);
    }

    public static Tuple2d qBezierValue3(Tuple2d[] points, double t) {
        double ax = points[0].getX();
        double ay = points[0].getY();
        double bx = points[1].getX();
        double by = points[1].getY();
        double cx = points[2].getX();
        double cy = points[2].getY();
        double pt1 = 1.0 - t;
        double pt2 = 2 * t * pt1;
        double pt3 = pt1 * pt1;
        double pt4 = t * t;
        double x = pt3 * ax + pt2 * bx + pt4 * cx;
        double y = pt3 * ay + pt2 * by + pt4 * cy;
        return new Tuple2d(x, y);
    }

    public static Tuple2d cBezierValue1(Tuple2d[] points, double t) {
        double ax = points[0].getX();
        double ay = points[0].getY();
        double bx = points[1].getX();
        double by = points[1].getY();
        double cx = points[2].getX();
        double cy = points[2].getY();
        double dx = points[3].getX();
        double dy = points[3].getY();
        double x1 = -ax + 3 * bx - 3 * cx + dx;
        double y1 = -ay + 3 * by - 3 * cy + dy;
        double x2 = 3 * ax - 6 * bx + 3 * cx;
        double y2 = 3 * ay - 6 * by + 3 * cy;
        double x3 = -3 * ax + 3 * bx;
        double y3 = -3 * ay + 3 * by;
        double pt2 = t * t;
        double pt3 = pt2 * t;
        double x = pt3 * x1 + pt2 * x2 + t * x3 + ax;
        double y = pt3 * y1 + pt2 * y2 + t * y3 + ay;
        return new Tuple2d(x, y);
    }

    public static Tuple2d cBezierValue2(Tuple2d[] points, double t) {
        double ax = points[0].getX();
        double ay = points[0].getY();
        double bx = points[1].getX();
        double by = points[1].getY();
        double cx = points[2].getX();
        double cy = points[2].getY();
        double dx = points[3].getX();
        double dy = points[3].getY();
        double[] vx = {ax, bx, cx, dx};
        double[] vy = {ay, by, cy, dy};
        double[] coeffs = {-1, 3, -3, 1, 3, -6, 3, 0, -3, 3, 0, 0, 1, 0, 0, 0};
        double[] ts = {Math.pow(t, 3), Math.pow(t, 2), t, 1};
        Matrix mvx = new Matrix(vx, 1);
        Matrix mvy = new Matrix(vy, 1);
        Matrix mcoeffs = new Matrix(coeffs, 4);
        Matrix mts = new Matrix(ts, 4);
        double x = 0.0;
        double y = 0.0;
        try {
            x = (mts.multiply2(mcoeffs.multiply2(mvx))).getCell(0, 0);
            y = (mts.multiply2(mcoeffs).multiply2(mvy)).getCell(0, 0);
        } catch (MatrixException e) {
            e.printStackTrace();
        }
        return new Tuple2d(x, y);
    }

    public static Tuple2d cBezierValue3(Tuple2d[] points, double t) {
        double ax = points[0].getX();
        double ay = points[0].getY();
        double bx = points[1].getX();
        double by = points[1].getY();
        double cx = points[2].getX();
        double cy = points[2].getY();
        double dx = points[3].getX();
        double dy = points[3].getY();
        double pt1 = 1.0 - t;
        double pt2 = pt1 * pt1;
        double pt3 = pt2 * pt1;
        double t2 = t * t;
        double t3 = t2 * t;
        double x = pt3 * ax + 3 * t * pt2 * bx + 3 * t2 * pt1 * cx + t3 * dx;
        double y = pt3 * ay + 3 * t * pt2 * by + 3 * t2 * pt1 * cy + t3 * dy;
        return new Tuple2d(x, y);
    }

    /**
     * Tworzy kwadratowa krzywą Beziera
     *
     * @param points Point2D[] - tablica 3 punktów
     * @return QuadCurve2D.Double - zwracana krzywa
     */
    private static QuadCurve2D.Double makeQuadCurve(Tuple2d[] points) {
        return new QuadCurve2D.Double(points[0].getX(),
                points[0].getY(), points[1].getX(), points[1].getY(),
                points[2].getX(), points[2].getY());
    }

    public static void drawQuadCurve(Graphics2D g2, Tuple2d[] points,
                                     boolean showPoints) {
        QuadCurve2D.Double qc2d = makeQuadCurve(points);
        g2.draw(qc2d);
        if (showPoints) {
            drawPoints(g2, points);
        }
    }

    public static void drawPoints(Graphics2D g2, Tuple2d[] points) {
        Ellipse2D.Double e = null;
        for (Tuple2d point : points) {
            e = new Ellipse2D.Double(point.getX() - 3,
                    point.getY() - 3, 6, 6);
            g2.fill(e);
        }
    }

    /**
     * Tworzy sześcienną krzywą Beziera
     *
     * @param points Point2D[] - tablica 4 punktów
     * @return CubicCurve2D.Double - zwracana krzywa
     */
    public static CubicCurve2D.Double makeCubCurve(Tuple2d[] points) {
        return new CubicCurve2D.Double(points[0].getX(),
                points[0].getY(), points[1].getX(), points[1].getY(),
                points[2].getX(), points[2].getY(), points[3].getX(),
                points[3].getY());
    }

    public static void drawCubCurve(Graphics2D g2, Tuple2d[] points,
                                    boolean showPoints) {
        CubicCurve2D.Double qc2d = makeCubCurve(points);
        g2.draw(qc2d);
        if (showPoints) {
            drawPoints(g2, points);
        }
    }

    public static double tValue(Tuple2d[] points) {
        double maxx = Double.MIN_VALUE;
        double maxy = Double.MIN_VALUE;
        double minx = Double.MAX_VALUE;
        double miny = Double.MAX_VALUE;
        for (Tuple2d val : points) {
            maxx = Math.max(val.getX(), maxx);
            maxy = Math.max(val.getY(), maxy);
            minx = Math.min(val.getX(), minx);
            miny = Math.min(val.getY(), miny);
        }
        return Math.max(Math.abs(maxx) - Math.abs(minx),
                Math.abs(maxy) - Math.abs(miny));
    }

    public static void drawMyQBezier(Graphics2D g2, Tuple2d[] points,
                                     boolean showPoints) {
        double x1 = points[0].getX() - 2 * points[1].getX() + points[2].getX();
        double y1 = points[0].getY() - 2 * points[1].getY() + points[2].getY();
        double x2 = -2 * points[0].getX() + 2 * points[1].getX();
        double y2 = -2 * points[0].getY() + 2 * points[1].getY();
        double extr = tValue(points);
        double te = 1.0 / extr;
        double t = 0;
        double x0 = points[0].getX();
        double y0 = points[0].getY();
        for (int i = 0; i < (int) extr + 1; i++) {
            t = i * te;
            double pt = t * t;
            double ptx = pt * x1;
            double pty = pt * y1;
            double x = ptx + t * x2 + points[0].getX();
            double y = pty + t * y2 + points[0].getY();
            Line2D.Double l2d = new Line2D.Double(x0, y0, x, y);
            g2.draw(l2d);
            x0 = x;
            y0 = y;
        }
        if (showPoints) {
            BezierUtil.drawPoints(g2, points);
        }
    }

    public static void drawMyCBezier(Graphics2D g2, Tuple2d[] points,
                                     boolean showPoints) {
        double x1 = -points[0].getX() + 3 * points[1].getX() - 3
                * points[2].getX() + points[3].getX();
        double x2 = 3 * points[0].getX() - 6 * points[1].getX() + 3
                * points[2].getX();
        double y1 = -points[0].getY() + 3 * points[1].getY() - 3
                * points[2].getY() + points[3].getY();
        double y2 = 3 * points[0].getY() - 6 * points[1].getY() + 3
                * points[2].getY();
        double x3 = -3 * points[0].getX() + 3 * points[1].getX();
        double y3 = -3 * points[0].getY() + 3 * points[1].getY();
        double extr = tValue(points);
        double te = 1.0 / extr;
        double t = 0;
        double x0 = points[0].getX();
        double y0 = points[0].getY();
        for (int i = 0; i < extr; i++) {
            t = i * te;
            double pt2 = t * t;
            double pt3 = pt2 * t;
            double x = pt3 * x1 + pt2 * x2 + t * x3 + points[0].getX();
            double y = pt3 * y1 + pt2 * y2 + t * y3 + points[0].getY();
            Line2D.Double l2d = new Line2D.Double(x0, y0, x, y);
            g2.draw(l2d);
            x0 = x;
            y0 = y;
        }
        if (showPoints) {
            drawPoints(g2, points);
        }
    }

    public static void drawNBezier(Graphics2D g2, Tuple2d[] points,
                                   boolean showPoints) {
        int n = points.length - 1;
        double extr = tValue(points);
        double te = 1.0 / extr;
        double t = 0;
        double x0 = points[0].getX();
        double y0 = points[0].getY();
        for (int i = 0; i < extr + 1; i++) {
            t = i * te;
            double x = 0;
            double y = 0;
            for (int k = 0; k < points.length; k++) {
                double np = KombiUtil.npok2(n, k) * Math.pow(t, k)
                        * Math.pow(1 - t, n - k);
                x += np * points[k].getX();
                y += np * points[k].getY();
            }
            Line2D.Double l2d = new Line2D.Double(x0, y0, x, y);
            x0 = x;
            y0 = y;
            g2.draw(l2d);
        }
        if (showPoints) {
            drawPoints(g2, points);
        }
    }

    public static void drawNBezier2(Graphics2D g2, Tuple2d[] points,
                                    boolean showPoints) {
        int n = points.length - 1;
        double extr = tValue(points);
        double te = 1.0 / extr;
        double t = 0;
        double x0 = points[0].getX();
        double y0 = points[0].getY();
        for (int i = 0; i < extr; i++) {
            t = i * te;
            double x = 0;
            double y = 0;
            for (int k = 0; k < points.length; k++) {
                double np = KombiUtil.npok2(n, k) * Math.pow(t, k)
                        * Math.pow(1 - t, n - k);
                x += np * points[k].getX();
                y += np * points[k].getY();
            }
            Line2D.Double l2d = new Line2D.Double(x0, y0, x, y);
            x0 = x;
            y0 = y;
            System.out.println(x + " " + y);
            g2.draw(l2d);
        }
        if (showPoints) {
            drawPoints(g2, points);
        }
    }

    public static Tuple2d[] bezierDegreeUp(Tuple2d[] points) {
        int len = points.length;// 4
        Tuple2d[] arr = new Tuple2d[len + 1];
        arr[0] = new Tuple2d(points[0].getX(), points[0].getY());
        for (int i = 1; i < len; i++) {
            double a = (double) i / (double) len;
            double b = 1.0 - a;
            arr[i] = new Tuple2d(a * points[i - 1].getX() + b
                    * points[i].getX(), a * points[i - 1].getY() + b
                    * points[i].getY());
        }
        arr[len] = new Tuple2d(points[len - 1].getX(), points[len - 1].getY());
        return arr;
    }

    public static Tuple2d[] rotateBezier(Tuple2d[] points, double angle) {
        Tuple2d[] points1 = new Tuple2d[points.length];
        for (int i = 0; i < points.length; i++) {
            points1[i] = new Tuple2d(points[i].getX() * Math.cos(angle)
                    + points[i].getY() * Math.sin(angle), -points[i].getX()
                    * Math.sin(angle) + points[i].getY() * Math.cos(angle));
        }
        return points1;
    }

    public static Tuple2d casteljauTValue(Tuple2d[] points, double t) {
        Tuple2d[] temp = ArrayUtil.clone(points);
        int len = points.length;
        double t1 = 1.0 - t;
        for (int i = 0; i < len - 1; i++) {
            for (int j = 0; j < len - 1 - i; j++) {
                temp[j].setX(temp[j].getX() * t1 + t * temp[j + 1].getX());
                temp[j].setY(temp[j].getY() * t1 + t * temp[j + 1].getY());
            }
        }
        return temp[0];
    }

    public static Tuple2d[][] divideCasteljau(Tuple2d[] points) {
        Tuple2d[] temp = ArrayUtil.clone(points);
        Tuple2d[] temp1 = ArrayUtil.reverte(ArrayUtil.clone(points));
        int len = points.length;
        double t = 0.3;
        double t1 = 1.0 - t;
        for (int i = 0; i < len - 1; i++) {
            for (int j = 0; j < len - 1 - i; j++) {
                temp[j].setX(temp[j].getX() * t1 + t * temp[j + 1].getX());
                temp[j].setY(temp[j].getY() * t1 + t * temp[j + 1].getY());
                temp1[j].setX(temp1[j + 1].getX() * t1 + t * temp1[j].getX());
                temp1[j].setY(temp1[j + 1].getY() * t1 + t * temp1[j].getY());
            }
        }
        Tuple2d[][] arr = new Tuple2d[2][];
        arr[0] = ArrayUtil.reverte(temp1);
        arr[1] = temp;
        return arr;
    }

    public static void connectBezier(Tuple2d[] points, Tuple2d[] qoints) {
        int n = points.length - 1;
        int m = qoints.length - 1;
        points[n - 1].setX(((m + n) * qoints[0].getX() - m * qoints[1].getX())
                / n);
        points[n - 1].setY(((m + n) * qoints[0].getY() - m * qoints[1].getY())
                / n);
    }

    public static double ratBezierTValue(int n, int k, double t,
                                         double[] weights) {
        double[] arr = new double[weights.length];
        double mian = 0.0;
        double sum = 0.0;
        for (int i = 0; i < n + 1; i++) {
            double a = KombiUtil.npok2(n, i) * Math.pow(t, i)
                    * Math.pow(1.0 - t, n - i) * weights[i];
            if (a < 0) {
                a = 0;
            }
            arr[i] = a;
            mian += a;
        }
        sum = arr[k] / mian;
        return sum;
    }

    public static double ratBezierTValue2(double np, int n, int k, double t,
                                          double[] weights) {
        double[] arr = new double[weights.length];
        double mian = 0.0;
        double sum = 0.0;
        for (int i = 0; i < n + 1; i++) {
            double a = np * Math.pow(t, i) * Math.pow(1.0 - t, n - i)
                    * weights[i];
            if (a < 0) {
                a = 0;
            }
            arr[i] = a;
            mian += a;
        }
        sum = arr[k] / mian;
        return sum;
    }

    public static void drawRatBezier(Graphics2D g2, int w, int h, int n, int k,
                                     double[] weights, int liczbaPunktow) {
        double t;
        int np = KombiUtil.npok2(n, k);
        float x0 = 0.0f;
        float y0 = (float) (h - ratBezierTValue2(np, n, k, 0, weights)
                * liczbaPunktow);
        for (int i = 0; i < liczbaPunktow; i++) {
            t = i * 1.0 / liczbaPunktow;
            double b = ratBezierTValue2(np, n, k, t, weights);
            if (b < 0) {
                b = 0;
            }
            float x = (float) (t * liczbaPunktow);
            float y = (float) (h - b * liczbaPunktow);
            Line2D.Float line = new Line2D.Float(x0, y0, x, y);
            g2.draw(line);
            x0 = x;
            y0 = y;
        }
    }

    public static void drawRatBezier2(Graphics2D g2, Tuple2d[] points,
                                      double[] weights, boolean showPoints) {
        double extr = tValue(points);
        double te = 1.0 / extr;
        double x0 = points[0].getX();
        double y0 = points[0].getY();
        for (int i = 0; i < extr + 1; i++) {
            double t = i * te;
            double t1 = 1.0 - t;
            double m0 = t1 * t1 * weights[0];
            double m1 = 2 * t * t1 * weights[1];
            double m2 = t * t * weights[2];
            double mian = m0 + m1 + m2;
            double w0 = m0 / mian;
            double w1 = m1 / mian;
            double w2 = m2 / mian;
            double x = w0 * points[0].getX() + w1 * points[1].getX() + w2
                    * points[2].getX();
            double y = w0 * points[0].getY() + w1 * points[1].getY() + w2
                    * points[2].getY();
            Line2D.Double line = new Line2D.Double(x0, y0, x, y);
            g2.draw(line);
            x0 = x;
            y0 = y;
        }
        Line2D.Double line1 = new Line2D.Double(x0, y0, points[2].getX(),
                points[2].getY());
        g2.draw(line1);
        if (showPoints) {
            drawPoints(g2, points);
        }
    }

    public static void drawRatBezier3(Graphics2D g2, Tuple2d[] points,
                                      double[] weights, boolean showPoints) {
        double extr = tValue(points);
        double te = 1.0 / extr;
        double x0 = points[0].getX();
        double y0 = points[0].getY();
        for (int i = 0; i < extr; i++) {
            double t = i * te;
            double t1 = 1.0 - t;
            double m0 = t1 * t1 * t1 * weights[0];
            double m1 = 3 * t * t1 * t1 * weights[1];
            double m2 = 3 * t * t * t1 * weights[2];
            double m3 = t * t * t * weights[3];
            double mian = m0 + m1 + m2 + m3;
            double w0 = m0 / mian;
            double w1 = m1 / mian;
            double w2 = m2 / mian;
            double w3 = m3 / mian;
            double x = w0 * points[0].getX() + w1 * points[1].getX() + w2
                    * points[2].getX() + w3 * points[3].getX();
            double y = w0 * points[0].getY() + w1 * points[1].getY() + w2
                    * points[2].getY() + w3 * points[3].getY();
            Line2D.Double line = new Line2D.Double(x0, y0, x, y);
            g2.draw(line);
            x0 = x;
            y0 = y;
        }
        Line2D.Double line1 = new Line2D.Double(x0, y0, points[2].getX(),
                points[2].getY());
        g2.draw(line1);
        if (showPoints) {
            drawPoints(g2, points);
        }
    }

    public static void drawRatBezierN(Graphics2D g2, Tuple2d[] points,
                                      double[] weights, boolean showPoints) {
        double extr = tValue(points);
        double te = 1.0 / extr;
        int n = points.length - 1;
        double x0 = points[0].getX();
        double y0 = points[0].getY();
        for (int i = 0; i < extr + 1; i++) {
            double t = i * te;
            double t1 = 1.0 - t;
            double[] ms = new double[points.length];
            for (int j = 0; j < points.length; j++) {
                ms[j] = KombiUtil.npok2(n, j) * Math.pow(t, j)
                        * Math.pow(t1, n - j) * weights[j];
            }
            double mian = 0.0;
            for (double m1 : ms) {
                mian += m1;
            }
            double x = 0.0;
            double y = 0.0;
            for (int m = 0; m < points.length; m++) {
                x += ms[m] * points[m].getX();
                y += ms[m] * points[m].getY();
            }
            x = x / mian;
            y = y / mian;
            Line2D.Double line = new Line2D.Double(x0, y0, x, y);
            g2.draw(line);
            x0 = x;
            y0 = y;
        }
        Line2D.Double line1 = new Line2D.Double(x0, y0,
                points[points.length - 1].getX(),
                points[points.length - 1].getY());
        g2.draw(line1);
        if (showPoints) {
            drawPoints(g2, points);
        }
    }

}
