from .pack_algo import PackingAlgorithm
from .geometry import Rectangle
import itertools
import operator


class Guillotine(PackingAlgorithm):
    """Implementation of several variants of Guillotine packing algorithm
    
    For a more detailed explanation of the algorithm used, see:
    Jukka Jylanki - A Thousand Ways to Pack the Bin (February 27, 2010)
    """
    def __init__(self, width, height, rot=True, merge=True, *args, **kwargs):
        """
        Arguments:
            width (int, float):
            height (int, float):
            merge (bool): Optional keyword argument
        """
        self._merge = merge
        super(Guillotine, self).__init__(width, height, rot, *args, **kwargs)
        

    def _add_section(self, section):
        """Adds a new section to the free section list, but before that and if 
        section merge is enabled, tries to join the rectangle with all existing 
        sections, if successful the resulting section is again merged with the 
        remaining sections until the operation fails. The result is then 
        appended to the list.

        Arguments:
            section (Rectangle): New free section.
        """
        section.rid = 0     
        plen = 0

        while self._merge and self._sections and plen != len(self._sections):
            plen = len(self._sections)
            self._sections = [s for s in self._sections if not section.join(s)]
        self._sections.append(section)


    def _split_horizontal(self, section, width, height):
        """For an horizontal split the rectangle is placed in the lower
        left corner of the section (section's xy coordinates), the top
        most side of the rectangle and its horizontal continuation,
        marks the line of division for the split.
        +-----------------+
        |                 |
        |                 |
        |                 |
        |                 |
        +-------+---------+
        |#######|         |
        |#######|         |
        |#######|         |
        +-------+---------+
        If the rectangle width is equal to the the section width, only one
        section is created over the rectangle. If the rectangle height is
        equal to the section height, only one section to the right of the
        rectangle is created. If both width and height are equal, no sections
        are created.
        """
        # First remove the section we are splitting so it doesn't 
        # interfere when later we try to merge the resulting split
        # rectangles, with the rest of free sections.
        #self._sections.remove(section)

        # Creates two new empty sections, and returns the new rectangle.
        if height < section.height:
            self._add_section(Rectangle(section.x, section.y+height,
                section.width, section.height-height))

        if width < section.width:
            self._add_section(Rectangle(section.x+width, section.y,
                section.width-width, height))


    def _split_vertical(self, section, width, height):
        """For a vertical split the rectangle is placed in the lower
        left corner of the section (section's xy coordinates), the
        right most side of the rectangle and its vertical continuation,
        marks the line of division for the split.
        +-------+---------+
        |       |         |
        |       |         |
        |       |         |
        |       |         |
        +-------+         |
        |#######|         |
        |#######|         |
        |#######|         |
        +-------+---------+
        If the rectangle width is equal to the the section width, only one
        section is created over the rectangle. If the rectangle height is
        equal to the section height, only one section to the right of the
        rectangle is created. If both width and height are equal, no sections
        are created.
        """
        # When a section is split, depending on the rectangle size 
        # two, one, or no new sections will be created. 
        if height < section.height:
            self._add_section(Rectangle(section.x, section.y+height,
                width, section.height-height))
        
        if width < section.width:
            self._add_section(Rectangle(section.x+width, section.y,
                section.width-width, section.height))
        

    def _split(self, section, width, height):
        """
        Selects the best split for a section, given a rectangle of dimmensions
        width and height, then calls _split_vertical or _split_horizontal, 
        to do the dirty work.
       
        Arguments:
            section (Rectangle): Section to split
            width (int, float): Rectangle width
            height (int, float): Rectangle height
        """
        raise NotImplementedError


    def _section_fitness(self, section, width, height):
        """The subclass for each one of the Guillotine selection methods,
        BAF, BLSF.... will override this method, this is here only
        to asure a valid value return if the worst happens.
        """
        raise NotImplementedError

    def _select_fittest_section(self, w, h):
        """Calls _section_fitness for each of the sections in free section 
        list. Returns the section with the minimal fitness value, all the rest 
        is boilerplate to make the fitness comparison, to rotatate the rectangles,
        and to take into account when _section_fitness returns None because 
        the rectangle couldn't be placed.

        Arguments:
            w (int, float): Rectangle width
            h (int, float): Rectangle height

        Returns:
            (section, was_rotated): Returns the tuple 
                section (Rectangle): Section with best fitness
                was_rotated (bool): The rectangle was rotated 
        """
        fitn = ((self._section_fitness(s, w, h), s, False) for s in self._sections 
                if self._section_fitness(s, w, h) is not None)
        fitr = ((self._section_fitness(s, h, w), s, True) for s in self._sections 
                if self._section_fitness(s, h, w) is not None)

        if not self.rot:
            fitr = []

        fit = itertools.chain(fitn, fitr)
        
        try:
            _, sec, rot = min(fit, key=operator.itemgetter(0))
        except ValueError:
            return None, None

        return sec, rot


    def add_rect(self, width, height, rid=None):     
        """
        Add rectangle of widthxheight dimensions.

        Arguments:
            width (int, float): Rectangle width
            height (int, float): Rectangle height
            rid: Optional rectangle user id

        Returns:
            Rectangle: Rectangle with placemente coordinates
            None: If the rectangle couldn be placed.
        """
        assert(width > 0 and height >0)

        # Obtain the best section to place the rectangle.
        section, rotated = self._select_fittest_section(width, height)
        if not section:
            return None
        
        if rotated:
            width, height = height, width
        
        # Remove section, split and store results
        self._sections.remove(section)
        self._split(section, width, height)
       
        # Store rectangle in the selected position
        rect = Rectangle(section.x, section.y, width, height, rid)
        self.rectangles.append(rect)
        return rect

    def fitness(self, width, height):
        """
        In guillotine algorithm case, returns the min of the fitness of all 
        free sections, for the given dimension, both normal and rotated
        (if rotation enabled.)
        """
        assert(width > 0 and height > 0)

        # Get best fitness section.
        section, rotated = self._select_fittest_section(width, height)
        if not section:
            return None
        
        # Return fitness of returned section, with correct dimmensions if the
        # the rectangle was rotated.
        if rotated:
            return self._section_fitness(section, height, width)
        else:
            return self._section_fitness(section, width, height)

    def reset(self):
        super(Guillotine, self).reset()
        self._sections = []
        self._add_section(Rectangle(0, 0, self.width, self.height))



class GuillotineBaf(Guillotine):
    """Implements Best Area Fit (BAF) section selection criteria for 
    Guillotine algorithm.
    """
    def _section_fitness(self, section, width, height):
        if width > section.width or height > section.height:
            return None
        return section.area()-width*height


class GuillotineBlsf(Guillotine):
    """Implements Best Long Side Fit (BLSF) section selection criteria for 
    Guillotine algorithm.
    """
    def _section_fitness(self, section, width, height):
        if width > section.width or height > section.height:
            return None
        return max(section.width-width, section.height-height)


class GuillotineBssf(Guillotine):
    """Implements Best Short Side Fit (BSSF) section selection criteria for 
    Guillotine algorithm.
    """
    def _section_fitness(self, section, width, height):
        if width > section.width or height > section.height:
            return None
        return min(section.width-width, section.height-height)


class GuillotineSas(Guillotine):
    """Implements Short Axis Split (SAS) selection rule for Guillotine 
    algorithm.
    """
    def _split(self, section, width, height):
        if section.width < section.height:
            return self._split_horizontal(section, width, height)
        else:
            return self._split_vertical(section, width, height)
        


class GuillotineLas(Guillotine):
    """Implements Long Axis Split (LAS) selection rule for Guillotine 
    algorithm.
    """
    def _split(self, section, width, height):
        if section.width >= section.height:
            return self._split_horizontal(section, width, height)
        else:
            return self._split_vertical(section, width, height)



class GuillotineSlas(Guillotine):
    """Implements Short Leftover Axis Split (SLAS) selection rule for 
    Guillotine algorithm.
    """
    def _split(self, section, width, height):
        if section.width-width < section.height-height:
            return self._split_horizontal(section, width, height)
        else:
            return self._split_vertical(section, width, height)
        


class GuillotineLlas(Guillotine):
    """Implements Long Leftover Axis Split (LLAS) selection rule for 
    Guillotine algorithm.
    """
    def _split(self, section, width, height):
        if section.width-width >= section.height-height:
            return self._split_horizontal(section, width, height)
        else:
            return self._split_vertical(section, width, height)



class GuillotineMaxas(Guillotine):
    """Implements Max Area Axis Split (MAXAS) selection rule for Guillotine
    algorithm. Maximize the larger area == minimize the smaller area.
    Tries to make the rectangles more even-sized.
    """
    def _split(self, section, width, height):
        if width*(section.height-height) <= height*(section.width-width):
            return self._split_horizontal(section, width, height)
        else:
            return self._split_vertical(section, width, height)
        


class GuillotineMinas(Guillotine):
    """Implements Min Area Axis Split (MINAS) selection rule for Guillotine 
    algorithm. 
    """
    def _split(self, section, width, height):
        if width*(section.height-height) >= height*(section.width-width):
            return self._split_horizontal(section, width, height)
        else:
            return self._split_vertical(section, width, height)
       


# Guillotine algorithms GUILLOTINE-RECT-SPLIT, Selecting one
# Axis split, and one selection criteria.
class GuillotineBssfSas(GuillotineBssf, GuillotineSas):
    pass
class GuillotineBssfLas(GuillotineBssf, GuillotineLas):
    pass
class GuillotineBssfSlas(GuillotineBssf, GuillotineSlas):
    pass
class GuillotineBssfLlas(GuillotineBssf, GuillotineLlas):
    pass
class GuillotineBssfMaxas(GuillotineBssf, GuillotineMaxas):
    pass
class GuillotineBssfMinas(GuillotineBssf, GuillotineMinas):
    pass
class GuillotineBlsfSas(GuillotineBlsf, GuillotineSas):
    pass
class GuillotineBlsfLas(GuillotineBlsf, GuillotineLas):
    pass
class GuillotineBlsfSlas(GuillotineBlsf, GuillotineSlas):
    pass
class GuillotineBlsfLlas(GuillotineBlsf, GuillotineLlas):
    pass
class GuillotineBlsfMaxas(GuillotineBlsf, GuillotineMaxas):
    pass
class GuillotineBlsfMinas(GuillotineBlsf, GuillotineMinas):
    pass
class GuillotineBafSas(GuillotineBaf, GuillotineSas):
    pass
class GuillotineBafLas(GuillotineBaf, GuillotineLas):
    pass
class GuillotineBafSlas(GuillotineBaf, GuillotineSlas):
    pass
class GuillotineBafLlas(GuillotineBaf, GuillotineLlas):
    pass
class GuillotineBafMaxas(GuillotineBaf, GuillotineMaxas):
    pass
class GuillotineBafMinas(GuillotineBaf, GuillotineMinas):
    pass