# Copyright 2015 Bloomberg Finance L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

r"""

======
Figure
======

.. currentmodule:: bqplot.figure

.. autosummary::
   :toctree: _generate/

   Figure
"""

from traitlets import (
    Bool, Unicode, Instance, List, Dict, Enum, Float, Int, TraitError, default,
    validate
)
from ipywidgets import DOMWidget, register, widget_serialization

from bqscales import Scale, LinearScale
from .interacts import Interaction
from .marks import Mark
from .axes import Axis
from ._version import __frontend_version__


@register
class Figure(DOMWidget):

    """Main canvas for drawing a chart.

    The Figure object holds the list of Marks and Axes. It also holds an
    optional Interaction object that is responsible for figure-level mouse
    interactions, the "interaction layer".

    Besides, the Figure object has two reference scales, for positioning items
    in an absolute fashion in the figure canvas.

    Style Attributes
    ----------------

    Attributes
    ----------
    title: string (default: '')
        title of the figure
    axes: List of Axes (default: [])
        list containing the instances of the axes for the figure
    marks: List of Marks (default: [])
        list containing the marks which are to be appended to the figure
    interaction: Interaction or None (default: None)
        optional interaction layer for the figure
    scale_x: Scale
        Scale representing the x values of the figure
    scale_y: Scale
        Scale representing the y values of the figure
    padding_x: Float (default: 0.0)
        Padding to be applied in the horizontal direction of the figure
        around the data points, proportion of the horizontal length
    padding_y: Float (default: 0.025)
        Padding to be applied in the vertical direction of the figure
        around the data points, proportion of the vertical length
    legend_location: {'top-right', 'top', 'top-left', 'left',
        'bottom-left', 'bottom', 'bottom-right', 'right'}
        location of the legend relative to the center of the figure
    background_style: Dict (default: {})
        CSS style to be applied to the background of the figure
    legend_style: Dict (default: {})
        CSS style to be applied to the SVG legend e.g, {'fill': 'white'}
    legend_text: Dict (default: {})
        CSS style to be applied to the legend text e.g., {'font-size': 20}
    title_style: Dict (default: {})
        CSS style to be applied to the title of the figure
    animation_duration: nonnegative int (default: 0)
        Duration of transition on change of data attributes, in milliseconds.

    Layout Attributes
    -----------------

    Attributes
    ----------
    pixel_ratio:
        Pixel ratio of the WebGL canvas (2 on retina screens). Set to 1 for better performance,
        but less crisp edges. If set to None it will use the browser's window.devicePixelRatio.
    display_toolbar: boolean (default: True)
        Show or hide the integrated toolbar.
    fig_margin: dict (default: {top=60, bottom=60, left=60, right=60})
        Dictionary containing the top, bottom, left and right margins. The user
        is responsible for making sure that the width and height are greater
        than the sum of the margins.
    auto_layout: boolean (default: False)
        Whether to use the auto-layout solver or not
    min_aspect_ratio: float
        Minimum width / height ratio of the figure
    max_aspect_ratio: float
        Maximum width / height ratio of the figure

    !!! Note

        The aspect ratios stand for width / height ratios.

        - If the available space is within bounds in terms of min and max aspect
        ratio, we use the entire available space.
        - If the available space is too oblong horizontally, we use the client
        height and the width that corresponds max_aspect_ratio (maximize width
        under the constraints).
        - If the available space is too oblong vertically, we use the client width
        and the height that corresponds to min_aspect_ratio (maximize height
        under the constraint).
        This corresponds to maximizing the area under the constraints.

        Default min and max aspect ratio are both equal to 16 / 9.
    """
    title = Unicode().tag(sync=True, display_name='Title')
    axes = List(Instance(Axis)).tag(sync=True, **widget_serialization)
    marks = List(Instance(Mark)).tag(sync=True, **widget_serialization)
    interaction = Instance(Interaction, default_value=None,
                           allow_none=True).tag(sync=True,
                                                **widget_serialization)
    scale_x = Instance(Scale).tag(sync=True, **widget_serialization)
    scale_y = Instance(Scale).tag(sync=True, **widget_serialization)
    title_style = Dict(value_trait=Unicode()).tag(sync=True)
    background_style = Dict().tag(sync=True)
    legend_style = Dict().tag(sync=True)
    legend_text = Dict().tag(sync=True)
    theme = Enum(['classic', 'gg'], default_value='classic').tag(sync=True)

    auto_layout = Bool(False).tag(sync=True)
    min_aspect_ratio = Float(0.01).tag(sync=True)
    max_aspect_ratio = Float(100).tag(sync=True)
    pixel_ratio = Float(None, allow_none=True).tag(sync=True)

    fig_margin = Dict(dict(top=60, bottom=60, left=60, right=60))\
        .tag(sync=True)
    padding_x = Float(0.0, min=0.0, max=1.0).tag(sync=True)
    padding_y = Float(0.025, min=0.0, max=1.0).tag(sync=True)
    legend_location = Enum(['top-right', 'top', 'top-left', 'left',
                            'bottom-left', 'bottom', 'bottom-right', 'right'],
                           default_value='top-right')\
        .tag(sync=True, display_name='Legend position')
    animation_duration = Int().tag(sync=True,
                                   display_name='Animation duration')
    display_toolbar = Bool(default_value=True).tag(sync=True)

    def __init__(self, **kwargs):
        super(Figure, self).__init__(**kwargs)

        self._upload_png_callback = None
        self._upload_svg_callback = None
        self.on_msg(self._handle_custom_msgs)

    @default('scale_x')
    def _default_scale_x(self):
        return LinearScale(min=0, max=1, allow_padding=False)

    @default('scale_y')
    def _default_scale_y(self):
        return LinearScale(min=0, max=1, allow_padding=False)

    def save_png(self, filename='bqplot.png', scale=None):
        '''
        Saves the Figure as a PNG file

        Parameters
        ----------
        filename: str (default: 'bqplot.png')
            name of the saved file
        scale: float (default: None)
            Scale up the png resolution when scale > 1, when not given base this on the screen pixel ratio.
        '''
        self.send({'type': 'save_png', 'filename': filename, 'scale': scale})

    def save_svg(self, filename='bqplot.svg'):
        '''
        Saves the Figure as an SVG file

        Parameters
        ----------
        filename: str (default: 'bqplot.svg')
            name of the saved file
        '''
        self.send({"type": "save_svg", "filename": filename})

    def get_png_data(self, callback, scale=None):
        '''
        Gets the Figure as a PNG memory view

        Parameters
        ----------
        callback: callable
            Called with the PNG data as the only positional argument.
        scale: float (default: None)
            Scale up the png resolution when scale > 1, when not given base this on the screen pixel ratio.
        '''
        if self._upload_png_callback:
            raise Exception('get_png_data already in progress')
        self._upload_png_callback = callback
        self.send({'type': 'upload_png', 'scale': scale})

    def get_svg_data(self, callback):
        '''
        Gets the Figure as an SVG memory view (utf-8 encoded bytes).

        Parameters
        ----------
        callback: callable
            Called with the SVG data (utf-8 encoded bytes) as the only positional argument.
        '''
        if self._upload_svg_callback:
            raise Exception('get_svg_data already in progress')
        self._upload_svg_callback = callback
        self.send({'type': 'upload_svg'})

    @validate('min_aspect_ratio', 'max_aspect_ratio')
    def _validate_aspect_ratio(self, proposal):
        value = proposal['value']
        if proposal['trait'].name == 'min_aspect_ratio' and \
           value > self.max_aspect_ratio:
            raise TraitError('setting min_aspect_ratio > max_aspect_ratio')
        if proposal['trait'].name == 'max_aspect_ratio' and \
           value < self.min_aspect_ratio:
            raise TraitError('setting max_aspect_ratio < min_aspect_ratio')
        return value

    def _handle_custom_msgs(self, _, content, buffers=None):
        if content.get('event') == 'upload_png':
            try:
                self._upload_png_callback(buffers[0])
            finally:
                self._upload_png_callback = None
        elif content.get('event') == 'upload_svg':
            try:
                self._upload_svg_callback(buffers[0])
            finally:
                self._upload_svg_callback = None

    _view_name = Unicode('Figure').tag(sync=True)
    _model_name = Unicode('FigureModel').tag(sync=True)
    _view_module = Unicode('bqplot').tag(sync=True)
    _model_module = Unicode('bqplot').tag(sync=True)
    _view_module_version = Unicode(__frontend_version__).tag(sync=True)
    _model_module_version = Unicode(__frontend_version__).tag(sync=True)
