import collections
import itertools
import math
import scipy
import cv2
import pyclipper
import numpy as np
import matplotlib.path
import sklearn.cluster

from image_intersections import find_all_intersections
na = np.array

def llr_normalize(points):
    return [[int(a), int(b)] for a, b in points]

def llr_correctness(points, shape):
    __points = []
    for pt in points:
        if pt[0] < 0 or pt[1] < 0 or \
            pt[0] > shape[1] or \
            pt[1] > shape[0]: continue
        __points += [pt]
    return __points

def llr_unique(a):
    indices = sorted(range(len(a)), key=a.__getitem__)
    indices = set(next(it) for k, it in
        itertools.groupby(indices, key=a.__getitem__))
    return [x for i, x in enumerate(a) if i in indices]

def llr_polysort(pts):
    """sort points clockwise"""
    mlat = sum(x[0] for x in pts) / len(pts)
    mlng = sum(x[1] for x in pts) / len(pts)
    def __sort(x):
        return (math.atan2(x[0]-mlat, x[1]-mlng) + \
                2*math.pi)%(2*math.pi)
    pts.sort(key=__sort)
    return pts

def llr_polyscore(cnt, pts, cen, alfa=5, beta=2):
    a = cnt[0]; b = cnt[1]
    c = cnt[2]; d = cnt[3]

    # (1) # za mala powierzchnia
    area = cv2.contourArea(cnt)
    t2 = area < (4 * alfa * alfa) * 5
    if t2: return 0

    gamma = alfa/1.5

    # (2) # za malo punktow
    pco = pyclipper.PyclipperOffset()
    pco.AddPath(cnt, pyclipper.JT_MITER, pyclipper.ET_CLOSEDPOLYGON)
    pcnt = matplotlib.path.Path(pco.Execute(gamma)[0])
    wtfs = pcnt.contains_points(pts)
    pts_in = min(np.count_nonzero(wtfs), 49)
    t1 = pts_in < min(len(pts), 49) - 2 * beta - 1
    if t1: return 0

    A = pts_in
    B = area

    # (3)
    # punkty za kwadratowosci
    nln = lambda l1, x, dx: \
        np.linalg.norm(np.cross(na(l1[1])-na(l1[0]),
                                na(l1[0])-na(   x)))/dx
    pcnt_in = []; i = 0
    for pt in wtfs:
        if pt: pcnt_in += [pts[i]]
        i += 1

    def __convex_approx(points, alfa=0.001):
        hull = scipy.spatial.ConvexHull(na(points)).vertices
        cnt = na([points[pt] for pt in hull])
        return cnt


    cnt_in = __convex_approx(na(pcnt_in))

    points = cnt_in
    x = [p[0] for p in points]          # szukamy punktu
    y = [p[1] for p in points]          # centralnego skupiska
    cen2 = (sum(x) / len(points), \
            sum(y) / len(points))

    G = np.linalg.norm(na(cen)-na(cen2))


    a = [cnt[0], cnt[1]]
    b = [cnt[1], cnt[2]]
    c = [cnt[2], cnt[3]]
    d = [cnt[3], cnt[0]]
    lns = [a, b, c, d]
    E = 0; F = 0
    for l in lns:
        d = np.linalg.norm(na(l[0])-na(l[1]))
        for p in cnt_in:
            r = nln(l,p,d)
            if r < gamma:
                E += r
                F += 1
    if F == 0:
        return 0

    E /= F

    if B == 0 or A == 0:
        return 0

    C = 1+(E/A)**(1/3)  # rownosc
    D = 1+(G/A)**(1/5)  # centroid
    R = (A**4)/((B**2) * C * D)

    #print(R*(10**12), A, "|", B, C, D, "|", E, G)

    return R

################################################################################


def find_inner_corners(img, points, lines):

    old = points

    # --- otoczka
    def __convex_approx(points, alfa=0.01):
        hull = scipy.spatial.ConvexHull(na(points)).vertices
        cnt = na([points[pt] for pt in hull])
        approx = cv2.approxPolyDP(cnt,alfa*\
                cv2.arcLength(cnt,True),True)
        return llr_normalize(itertools.chain(*approx))
    # ---

    # --- geometria
    __cache = {}
    def __dis(a, b):
        idx = hash("__dis" + str(a) + str(b))
        if idx in __cache: return __cache[idx]
        __cache[idx] = np.linalg.norm(na(a)-na(b))
        return __cache[idx]

    nln = lambda l1, x, dx: \
        np.linalg.norm(np.cross(na(l1[1])-na(l1[0]),
                        na(l1[0])-na(   x)))/dx
    # ---

    pregroup = [[], []]                   # podzial na 2 grupy (dla ramki)
    S = {}                                # ranking ramek // wraz z wynikiem

    points = llr_correctness(llr_normalize(points), img.shape) # popraw punkty

    # --- clustrowanie

    __points = {}
    points = llr_polysort(points)
    __max, __points_max = 0, []
    alfa = math.sqrt(cv2.contourArea(na(points))/49)
    X = sklearn.cluster.DBSCAN(eps=alfa*4).fit(points)
    for i in range(len(points)):
        __points[i] = []
    for i in range(len(points)):
        if X.labels_[i] != -1:
            __points[X.labels_[i]] += [points[i]]
    for i in range(len(points)):
        if len(__points[i]) > __max:
            __max = len(__points[i])
            __points_max = __points[i]
    if len(__points) > 0 and len(points) > 49/2:
        points = __points_max
    #print(X.labels_)
    # ---

    # tworzymy zewnetrzny pierscien
    ring = __convex_approx(llr_polysort(points))

    n = len(points)
    beta = n*(5/100) # beta=n*(100-(skutecznosc LAPS))
    alfa = math.sqrt(cv2.contourArea(na(points))/49) # srednia otoczka siatki

    x = [p[0] for p in points]          # szukamy punktu
    y = [p[1] for p in points]          # centralnego skupiska
    centroid = (sum(x) / len(points), \
                sum(y) / len(points))

    #print(alfa, beta, centroid)

    def __v(l):
        y_0, x_0 = l[0][0], l[0][1]
        y_1, x_1 = l[1][0], l[1][1]

        x_2 = 0;            t=(x_0-x_2)/(x_0-x_1+0.0001)
        a = [int((1-t)*x_0+t*x_1), int((1-t)*y_0+t*y_1)][::-1]

        x_2 = img.shape[0]; t=(x_0-x_2)/(x_0-x_1+0.0001)
        b = [int((1-t)*x_0+t*x_1), int((1-t)*y_0+t*y_1)][::-1]

        poly1 = llr_polysort([[0,0], [0, img.shape[0]], a, b])
        s1 = llr_polyscore(na(poly1), points, centroid, beta=beta, alfa=alfa/2)
        poly2 = llr_polysort([a, b, \
                [img.shape[1],0], [img.shape[1],img.shape[0]]])
        s2 = llr_polyscore(na(poly2), points, centroid, beta=beta, alfa=alfa/2)

        return [a, b], s1, s2

    def __h(l):
        x_0, y_0 = l[0][0], l[0][1]
        x_1, y_1 = l[1][0], l[1][1]

        x_2 = 0;            t=(x_0-x_2)/(x_0-x_1+0.0001)
        a = [int((1-t)*x_0+t*x_1), int((1-t)*y_0+t*y_1)]

        x_2 = img.shape[1]; t=(x_0-x_2)/(x_0-x_1+0.0001)
        b = [int((1-t)*x_0+t*x_1), int((1-t)*y_0+t*y_1)]

        poly1 = llr_polysort([[0,0], [img.shape[1], 0], a, b])
        s1 = llr_polyscore(na(poly1), points, centroid, beta=beta, alfa=alfa/2)
        poly2 = llr_polysort([a, b, \
                [0, img.shape[0]], [img.shape[1], img.shape[0]]])
        s2 = llr_polyscore(na(poly2), points, centroid, beta=beta, alfa=alfa/2)

        return [a, b], s1, s2

    for l in lines: # bedziemy wszystkie przegladac
        for p in points: # odrzucamy linie ktore nie pasuja
            # (1) linia przechodzi blisko dobrego punktu
            t1 = nln(l, p, __dis(*l)) < alfa
            # (2) linia przechodzi przez srodek skupiska
            t2 = nln(l, centroid, __dis(*l)) > alfa * 2.5 # 3
            # (3) linia nalezy do pierscienia
            # t3 = True if p in ring else False
            if t1 and t2:
                tx, ty = l[0][0]-l[1][0], l[0][1]-l[1][1]
                if abs(tx) < abs(ty):
                    ll, s1, s2 = __v(l); o = 0
                else:
                    ll, s1, s2 = __h(l); o = 1
                if s1 == 0 and s2 == 0:
                    continue
                pregroup[o] += [ll]

    pregroup[0] = llr_unique(pregroup[0])
    pregroup[1] = llr_unique(pregroup[1])

    print("---------------------")
    for v in itertools.combinations(pregroup[0], 2):            # poziome
        for h in itertools.combinations(pregroup[1], 2):        # pionowe
            poly = find_all_intersections([v[0], v[1], h[0], h[1]]) # przeciecia
            poly = llr_correctness(poly, img.shape)             # w obrazku
            if len(poly) != 4: continue                         # jesl. nie ma
            poly = na(llr_polysort(llr_normalize(poly)))        # sortuj
            if not cv2.isContourConvex(poly): continue          # wypukly?
            S[-llr_polyscore(poly, points, centroid, \
                beta=beta, alfa=alfa/2)] = poly                 # dodaj

    S = collections.OrderedDict(sorted(S.items()))              # max
    K = next(iter(S))
    print("key --", K)
    four_points = llr_normalize(S[K])               # score



    print("POINTS:", len(points))
    print("LINES:", len(lines))

    return four_points

def find_outer_corners(four_points, img):
    # print(utils.call("llr_pad(four_points)"));
    pco = pyclipper.PyclipperOffset()
    pco.AddPath(four_points, pyclipper.JT_MITER, pyclipper.ET_CLOSEDPOLYGON)

    padded = pco.Execute(60)[0]

    return pco.Execute(60)[0] # 60,70/75 is best (with buffer/for debug purpose)
