import random 
from unittest import TestCase
from collections import defaultdict
from timeit import default_timer as timer
import rectpack


from  rectpack import GuillotineBssfSas, GuillotineBssfLas, \
    GuillotineBssfSlas, GuillotineBssfLlas, GuillotineBssfMaxas, \
    GuillotineBssfMinas, GuillotineBlsfSas, GuillotineBlsfLas, \
    GuillotineBlsfSlas, GuillotineBlsfLlas, GuillotineBlsfMaxas, \
    GuillotineBlsfMinas, GuillotineBafSas, GuillotineBafLas, \
    GuillotineBafSlas, GuillotineBafLlas, GuillotineBafMaxas, \
    GuillotineBafMinas

from rectpack import MaxRectsBl, MaxRectsBssf, MaxRectsBaf, MaxRectsBlsf

from rectpack import SkylineMwf, SkylineMwfl, SkylineBl, \
    SkylineBlWm, SkylineMwfWm, SkylineMwflWm

from rectpack import PackingMode, PackingBin


# For repeatable rectangle generation
random.seed(33)


def coherce_to(maxn, minn, n):
    assert maxn >= minn
    return max(min(maxn, n), minn)


def random_rectangle(max_side, min_side, sigma=0.5, ratio=1.0, coherce=True):

    assert min_side <= max_side

    #
    half_side = (max_side-min_side)/2
    center = max_side-half_side
    width  = random.normalvariate(0, sigma)*half_side
    height = random.normalvariate(0, sigma)*half_side

    #
    if ratio > 1:
        height = height/ratio
    else:
        width = width*ratio
   
    # Coherce value to max
    if coherce:
        width  = coherce_to(max_side, min_side, width+center)
        height = coherce_to(max_side, min_side, height+center)
    
    return width, height


RECTANGLES = [random_rectangle(40, 20, ratio=1.2) for _ in range(50)]\
        +[random_rectangle(20, 15, ratio=1.2) for _ in range(50)]
BINS = [random_rectangle(150, 150, ratio=1.5) for _ in range(3)]


class TestWastedSpace(TestCase):

    def setUp(self):
        self.rectangles = [(int(w), int(h)) for w, h in RECTANGLES]
        self.bins = [(int(w), int(h)) for w, h in BINS]
        self.packer = None
        self.algos = [
            GuillotineBssfSas, GuillotineBssfLas, GuillotineBssfSlas, \
            GuillotineBssfLlas, GuillotineBssfMaxas, GuillotineBssfMinas, \
            GuillotineBlsfSas, GuillotineBlsfLas, GuillotineBlsfSlas, \
            GuillotineBlsfLlas, GuillotineBlsfMaxas, GuillotineBlsfMinas, \
            GuillotineBafSas, GuillotineBafLas, GuillotineBafSlas, \
            MaxRectsBl, MaxRectsBssf, MaxRectsBaf, MaxRectsBlsf, \
            SkylineBl, SkylineBlWm, SkylineMwfl, SkylineMwf]

        self.bin_algos = [
            (rectpack.PackingBin.BNF, "BNF"),
            (rectpack.PackingBin.BFF, "BFF"),
            (rectpack.PackingBin.BBF, "BBF"),
            (rectpack.PackingBin.Global, "GLOBAL"),
        ] 
        self.bin_algos_online = [
            (rectpack.PackingBin.BNF, "BNF"),
            (rectpack.PackingBin.BFF, "BFF"),
            (rectpack.PackingBin.BBF, "BBF"),
        ] 
        self.sort_algo = rectpack.SORT_AREA
        self.log=False

    @staticmethod
    def first_bin_wasted_space(packer):
        bin1 = packer[1] 
        bin_area = bin1.width*bin1.height
        rect_area = sum((r.width*r.height for r in bin1))    
        return ((bin_area-rect_area)/bin_area)*100


    @staticmethod
    def packing_time(packer):
        start = timer()
        packer.pack()
        end = timer()
        return end-start

    @staticmethod
    def setup_packer(packer, bins, rectangles):
        for b in bins:
            packer.add_bin(*b)
        for r in rectangles:
            packer.add_rect(*r)
        return packer

    def test_offline_modes(self):
        for bin_algo, algo_name in self.bin_algos:
            for algo in self.algos:
                packer = rectpack.newPacker(pack_algo=algo, 
                        mode=PackingMode.Offline, 
                        bin_algo=bin_algo,
                        sort_algo=self.sort_algo)
                self.setup_packer(packer, self.bins, self.rectangles)
                time = self.packing_time(packer)
                self.first_bin_wasted_space(packer)
                wasted = self.first_bin_wasted_space(packer)

                # Test wasted spaced threshold
                if self.log:
                    print("Offline {0} {1:<20s} {2:>10.3f}s {3:>10.3f}% {4:>10} bins".format(
                        algo_name, algo.__name__, time, wasted, len(packer)))
                self.assertTrue(wasted<50)

                # Validate rectangle packing
                for b in packer:
                    b.validate_packing()

    def test_online_modes(self):
        for bin_algo, algo_name in self.bin_algos_online:
            for algo in self.algos:
                packer = rectpack.newPacker(pack_algo=algo, 
                        mode=PackingMode.Online, 
                        bin_algo=bin_algo,
                        sort_algo=self.sort_algo)
                start = timer()
                self.setup_packer(packer, self.bins, self.rectangles)
                end = timer()
                time = end-start
                self.first_bin_wasted_space(packer)
                wasted = self.first_bin_wasted_space(packer)

                # Test wasted spaced threshold
                if self.log:
                    print("Online {0} {1:<20s} {2:>10.3f}s {3:>10.3f}% {4:>10} bins".format(
                        algo_name, algo.__name__, time, wasted, len(packer)))
                self.assertTrue(wasted<90)

                # Validate rectangle packing
                for b in packer:
                    b.validate_packing()