"""Implement common widgets layouts as reusable components"""

import re
from collections import defaultdict

from traitlets import Instance, Bool, Unicode, CUnicode, CaselessStrEnum, Tuple
from traitlets import Integer
from traitlets import HasTraits, TraitError
from traitlets import observe, validate

from .widget import Widget
from .widget_box import GridBox

from .docutils import doc_subst


_doc_snippets = {
    'style_params' : """

    grid_gap : str
        CSS attribute used to set the gap between the grid cells

    justify_content : str, in ['flex-start', 'flex-end', 'center', 'space-between', 'space-around']
        CSS attribute used to align widgets vertically

    align_items : str, in ['top', 'bottom', 'center', 'flex-start', 'flex-end', 'baseline', 'stretch']
        CSS attribute used to align widgets horizontally

    width : str
    height : str
        width and height"""
    }

@doc_subst(_doc_snippets)
class LayoutProperties(HasTraits):
    """Mixin class for layout templates

    This class handles mainly style attributes (height, grid_gap etc.)

    Parameters
    ----------

    {style_params}


    Note
    ----

    This class is only meant to be used in inheritance as mixin with other
    classes. It will not work, unless `self.layout` attribute is defined.

    """

    # style attributes (passed to Layout)
    grid_gap = Unicode(
        None,
        allow_none=True,
        help="The grid-gap CSS attribute.")
    justify_content = CaselessStrEnum(
        ['flex-start', 'flex-end', 'center',
         'space-between', 'space-around'],
        allow_none=True,
        help="The justify-content CSS attribute.")
    align_items = CaselessStrEnum(
        ['top', 'bottom',
         'flex-start', 'flex-end', 'center',
         'baseline', 'stretch'],
        allow_none=True, help="The align-items CSS attribute.")
    width = Unicode(
        None,
        allow_none=True,
        help="The width CSS attribute.")
    height = Unicode(
        None,
        allow_none=True,
        help="The width CSS attribute.")


    def __init__(self, **kwargs):
        super(LayoutProperties, self).__init__(**kwargs)
        self._property_rewrite = defaultdict(dict)
        self._property_rewrite['align_items'] = {'top': 'flex-start',
                                                 'bottom': 'flex-end'}
        self._copy_layout_props()
        self._set_observers()

    def _delegate_to_layout(self, change):
        "delegate the trait types to their counterparts in self.layout"
        value, name = change['new'], change['name']
        value = self._property_rewrite[name].get(value, value)
        setattr(self.layout, name, value) # pylint: disable=no-member

    def _set_observers(self):
        "set observers on all layout properties defined in this class"
        _props = LayoutProperties.class_trait_names()
        self.observe(self._delegate_to_layout, _props)

    def _copy_layout_props(self):

        _props = LayoutProperties.class_trait_names()

        for prop in _props:
            value = getattr(self, prop)
            if value:
                value = self._property_rewrite[prop].get(value, value)
                setattr(self.layout, prop, value) #pylint: disable=no-member

@doc_subst(_doc_snippets)
class AppLayout(GridBox, LayoutProperties):
    """ Define an application like layout of widgets.

    Parameters
    ----------

    header: instance of Widget
    left_sidebar: instance of Widget
    center: instance of Widget
    right_sidebar: instance of Widget
    footer: instance of Widget
        widgets to fill the positions in the layout

    merge: bool
        flag to say whether the empty positions should be automatically merged

    pane_widths: list of numbers/strings
        the fraction of the total layout width each of the central panes should occupy
        (left_sidebar,
        center, right_sidebar)

    pane_heights: list of numbers/strings
        the fraction of the width the vertical space that the panes should occupy
         (left_sidebar, center, right_sidebar)

    {style_params}

    Examples
    --------

    """

    # widget positions
    header = Instance(Widget, allow_none=True)
    footer = Instance(Widget, allow_none=True)
    left_sidebar = Instance(Widget, allow_none=True)
    right_sidebar = Instance(Widget, allow_none=True)
    center = Instance(Widget, allow_none=True)

    # extra args
    pane_widths = Tuple(CUnicode(), CUnicode(), CUnicode(),
                        default_value=['1fr', '2fr', '1fr'])
    pane_heights = Tuple(CUnicode(), CUnicode(), CUnicode(),
                         default_value=['1fr', '3fr', '1fr'])

    merge = Bool(default_value=True)

    def __init__(self, **kwargs):
        super(AppLayout, self).__init__(**kwargs)
        self._update_layout()

    @staticmethod
    def _size_to_css(size):
        if re.match(r'\d+\.?\d*(px|fr|%)$', size):
            return size
        if re.match(r'\d+\.?\d*$', size):
            return size + 'fr'

        raise TypeError("the pane sizes must be in one of the following formats: "
                        "'10px', '10fr', 10 (will be converted to '10fr')."
                        "Got '{}'".format(size))

    def _convert_sizes(self, size_list):
        return list(map(self._size_to_css, size_list))

    def _update_layout(self):

        grid_template_areas = [["header", "header", "header"],
                               ["left-sidebar", "center", "right-sidebar"],
                               ["footer", "footer", "footer"]]

        grid_template_columns = self._convert_sizes(self.pane_widths)
        grid_template_rows = self._convert_sizes(self.pane_heights)

        all_children = {'header': self.header,
                        'footer': self.footer,
                        'left-sidebar': self.left_sidebar,
                        'right-sidebar': self.right_sidebar,
                        'center': self.center}

        children = {position : child
                    for position, child in all_children.items()
                    if child is not None}

        if not children:
            return

        for position, child in children.items():
            child.layout.grid_area = position

        if self.merge:

            if len(children) == 1:
                position = list(children.keys())[0]
                grid_template_areas = [[position, position, position],
                                       [position, position, position],
                                       [position, position, position]]

            else:
                if self.center is None:
                    for row in grid_template_areas:
                        del row[1]
                    del grid_template_columns[1]

                if self.left_sidebar is None:
                    grid_template_areas[1][0] = grid_template_areas[1][1]

                if self.right_sidebar is None:
                    grid_template_areas[1][-1] = grid_template_areas[1][-2]

                if (self.left_sidebar is None and
                        self.right_sidebar is None and
                        self.center is None):
                    grid_template_areas = [['header'], ['footer']]
                    grid_template_columns = ['1fr']
                    grid_template_rows = ['1fr', '1fr']

                if self.header is None:
                    del grid_template_areas[0]
                    del grid_template_rows[0]

                if self.footer is None:
                    del grid_template_areas[-1]
                    del grid_template_rows[-1]


        grid_template_areas_css = "\n".join('"{}"'.format(" ".join(line))
                                            for line in grid_template_areas)

        self.layout.grid_template_columns = " ".join(grid_template_columns)
        self.layout.grid_template_rows = " ".join(grid_template_rows)
        self.layout.grid_template_areas = grid_template_areas_css

        self.children = tuple(children.values())

    @observe("footer", "header", "center", "left_sidebar", "right_sidebar", "merge",
             "pane_widths", "pane_heights")
    def _child_changed(self, change): #pylint: disable=unused-argument
        self._update_layout()


@doc_subst(_doc_snippets)
class GridspecLayout(GridBox, LayoutProperties):
    """ Define a N by M grid layout

    Parameters
    ----------

    n_rows : int
        number of rows in the grid

    n_columns : int
        number of columns in the grid

    {style_params}

    Examples
    --------

    >>> from ipywidgets import GridspecLayout, Button, Layout
    >>> layout = GridspecLayout(n_rows=4, n_columns=2, height='200px')
    >>> layout[:3, 0] = Button(layout=Layout(height='auto', width='auto'))
    >>> layout[1:, 1] = Button(layout=Layout(height='auto', width='auto'))
    >>> layout[-1, 0] = Button(layout=Layout(height='auto', width='auto'))
    >>> layout[0, 1] = Button(layout=Layout(height='auto', width='auto'))
    >>> layout
    """

    n_rows = Integer()
    n_columns = Integer()

    def __init__(self, n_rows=None, n_columns=None, **kwargs):
        super(GridspecLayout, self).__init__(**kwargs)
        self.n_rows = n_rows
        self.n_columns = n_columns
        self._grid_template_areas = [['.'] * self.n_columns for i in range(self.n_rows)]

        self._grid_template_rows = 'repeat(%d, 1fr)' % (self.n_rows,)
        self._grid_template_columns = 'repeat(%d, 1fr)' % (self.n_columns,)
        self._children = {}
        self._id_count = 0

    @validate('n_rows', 'n_columns')
    def _validate_integer(self, proposal):
        if proposal['value'] > 0:
            return proposal['value']
        raise TraitError('n_rows and n_columns must be positive integer')

    def _get_indices_from_slice(self, row, column):
        "convert a two-dimensional slice to a list of rows and column indices"

        if isinstance(row, slice):
            start, stop, stride = row.indices(self.n_rows)
            rows = range(start, stop, stride)
        else:
            rows = [row]

        if isinstance(column, slice):
            start, stop, stride = column.indices(self.n_columns)
            columns = range(start, stop, stride)
        else:
            columns = [column]

        return rows, columns

    def __setitem__(self, key, value):
        row, column = key
        self._id_count += 1
        obj_id = 'widget%03d' % self._id_count
        value.layout.grid_area = obj_id

        rows, columns = self._get_indices_from_slice(row, column)

        for row in rows:
            for column in columns:
                current_value = self._grid_template_areas[row][column]
                if current_value != '.' and current_value in self._children:
                    del self._children[current_value]
                self._grid_template_areas[row][column] = obj_id

        self._children[obj_id] = value
        self._update_layout()

    def __getitem__(self, key):
        rows, columns = self._get_indices_from_slice(*key)

        obj_id = None
        for row in rows:
            for column in columns:
                new_obj_id = self._grid_template_areas[row][column]
                obj_id = obj_id or new_obj_id
                if obj_id != new_obj_id:
                    raise TypeError('The slice spans several widgets, but '
                                    'only a single widget can be retrieved '
                                    'at a time')

        return self._children[obj_id]

    def _update_layout(self):

        grid_template_areas_css = "\n".join('"{}"'.format(" ".join(line))
                                            for line in self._grid_template_areas)

        self.layout.grid_template_columns = self._grid_template_columns
        self.layout.grid_template_rows = self._grid_template_rows
        self.layout.grid_template_areas = grid_template_areas_css
        self.children = tuple(self._children.values())


@doc_subst(_doc_snippets)
class TwoByTwoLayout(GridBox, LayoutProperties):
    """ Define a layout with 2x2 regular grid.

    Parameters
    ----------

    top_left: instance of Widget
    top_right: instance of Widget
    bottom_left: instance of Widget
    bottom_right: instance of Widget
        widgets to fill the positions in the layout

    merge: bool
        flag to say whether the empty positions should be automatically merged

    {style_params}

    Examples
    --------

    >>> from ipywidgets import TwoByTwoLayout, Button
    >>> TwoByTwoLayout(top_left=Button(description="Top left"),
    ...                top_right=Button(description="Top right"),
    ...                bottom_left=Button(description="Bottom left"),
    ...                bottom_right=Button(description="Bottom right"))

    """

    # widget positions
    top_left = Instance(Widget, allow_none=True)
    top_right = Instance(Widget, allow_none=True)
    bottom_left = Instance(Widget, allow_none=True)
    bottom_right = Instance(Widget, allow_none=True)

    # extra args
    merge = Bool(default_value=True)

    def __init__(self, **kwargs):
        super(TwoByTwoLayout, self).__init__(**kwargs)
        self._update_layout()

    def _update_layout(self):


        grid_template_areas = [["top-left", "top-right"],
                               ["bottom-left", "bottom-right"]]

        all_children = {'top-left' : self.top_left,
                        'top-right' : self.top_right,
                        'bottom-left' : self.bottom_left,
                        'bottom-right' : self.bottom_right}

        children = {position : child
                    for position, child in all_children.items()
                    if child is not None}

        if not children:
            return

        for position, child in children.items():
            child.layout.grid_area = position

        if self.merge:

            if len(children) == 1:
                position = list(children.keys())[0]
                grid_template_areas = [[position, position],
                                       [position, position]]
            else:
                columns = ['left', 'right']
                for i, column in enumerate(columns):
                    top, bottom = children.get('top-' + column), children.get('bottom-' + column)
                    i_neighbour = (i + 1) % 2
                    if top is None and bottom is None:
                        # merge each cell in this column with the neighbour on the same row
                        grid_template_areas[0][i] = grid_template_areas[0][i_neighbour]
                        grid_template_areas[1][i] = grid_template_areas[1][i_neighbour]
                    elif top is None:
                        # merge with the cell below
                        grid_template_areas[0][i] = grid_template_areas[1][i]
                    elif bottom is None:
                        # merge with the cell above
                        grid_template_areas[1][i] = grid_template_areas[0][i]

        grid_template_areas_css = "\n".join('"{}"'.format(" ".join(line))
                                            for line in grid_template_areas)

        self.layout.grid_template_columns = '1fr 1fr'
        self.layout.grid_template_rows = '1fr 1fr'
        self.layout.grid_template_areas = grid_template_areas_css

        self.children = tuple(children.values())

    @observe("top_left", "bottom_left", "top_right", "bottom_right", "merge")
    def _child_changed(self, change): #pylint: disable=unused-argument
        self._update_layout()
