"""
Module for geographic bokeh plot baseclasses.
"""
from distutils.version import LooseVersion

import param
import holoviews as hv

from cartopy.crs import GOOGLE_MERCATOR, PlateCarree, Mercator
from bokeh.models.tools import BoxZoomTool, WheelZoomTool
from bokeh.models import MercatorTickFormatter, MercatorTicker
from holoviews.plotting.bokeh.element import ElementPlot, OverlayPlot as HvOverlayPlot
from holoviews.plotting.bokeh.util import bokeh_version
from holoviews.core.util import dimension_sanitizer, basestring

from ...element import is_geographic, _Element, Shape
from ..plot import ProjectionPlot


class GeoPlot(ProjectionPlot, ElementPlot):
    """
    Plotting baseclass for geographic plots with a cartopy projection.
    """

    default_tools = param.List(default=['save', 'pan',
                                        WheelZoomTool(**({} if bokeh_version < '0.12.16' else
                                                         {'zoom_on_axis': False})),
                                        BoxZoomTool(match_aspect=True), 'reset'],
        doc="A list of plugin tools to use on the plot.")

    global_extent = param.Boolean(default=False, doc="""
        Whether the plot should display the whole globe.""")

    infer_projection = param.Boolean(default=False, doc="""
        Whether the projection should be inferred from the element crs.""")

    show_grid = param.Boolean(default=False, doc="""
        Whether to show gridlines on the plot.""")

    show_bounds = param.Boolean(default=False, doc="""
        Whether to show gridlines on the plot.""")

    projection = param.Parameter(default=GOOGLE_MERCATOR, doc="""
        Allows supplying a custom projection to transform the axis
        coordinates during display. Defaults to GOOGLE_MERCATOR.""")

    # Project operation to apply to the element
    _project_operation = None

    _hover_code = """
        var projections = require("core/util/projections");
        var x = special_vars.data_x
        var y = special_vars.data_y
        var coords = projections.wgs84_mercator.inverse([x, y])
        return "" + (coords[%d]).toFixed(4)
    """

    def __init__(self, element, **params):
        super(GeoPlot, self).__init__(element, **params)
        self.geographic = is_geographic(self.hmap.last)
        if self.geographic and not isinstance(self.projection, (PlateCarree, Mercator)):
            self.xaxis = None
            self.yaxis = None
            self.show_frame = False
            show_bounds = self._traverse_options(element, 'plot', ['show_bounds'],
                                                 defaults=False)
            self.show_bounds = not any(not sb for sb in show_bounds['show_bounds'])
            if self.show_grid:
                param.main.warning(
                    'Grid lines do not reflect {0}; to do so '
                    'multiply the current element by gv.feature.grid() '
                    'and disable the show_grid option.'.format(self.projection)
                )

    def _axis_properties(self, axis, key, plot, dimension=None,
                         ax_mapping={'x': 0, 'y': 1}):
        axis_props = super(GeoPlot, self)._axis_properties(axis, key, plot,
                                                           dimension, ax_mapping)
        proj = self.projection
        if self.geographic and proj is GOOGLE_MERCATOR:
            dimension = 'lon' if axis == 'x' else 'lat'
            axis_props['ticker'] = MercatorTicker(dimension=dimension)
            axis_props['formatter'] = MercatorTickFormatter(dimension=dimension)
        return axis_props

    def initialize_plot(self, ranges=None, plot=None, plots=None, source=None):
        opts = {} if isinstance(self, HvOverlayPlot) else {'source': source}
        fig = super(GeoPlot, self).initialize_plot(ranges, plot, plots, **opts)
        if self.geographic and self.show_bounds and not self.overlaid:
            from . import GeoShapePlot
            shape = Shape(self.projection.boundary, crs=self.projection).options(fill_alpha=0)
            shapeplot = GeoShapePlot(shape, projection=self.projection,
                                     overlaid=True, renderer=self.renderer)
            shapeplot.geographic = False
            shapeplot.initialize_plot(plot=fig)
        return fig

    def _postprocess_hover(self, renderer, source):
        super(GeoPlot, self)._postprocess_hover(renderer, source)
        hover = self.handles.get('hover')
        try:
            from bokeh.models import CustomJSHover
        except:
            CustomJSHover = None
        if (not self.geographic or None in (hover, CustomJSHover) or
            isinstance(hover.tooltips, basestring) or self.projection is not GOOGLE_MERCATOR
            or hover.tooltips is None):
            return
        element = self.current_frame
        xdim, ydim = [dimension_sanitizer(kd.name) for kd in element.kdims]
        formatters, tooltips = {}, []
        xhover = CustomJSHover(code=self._hover_code % 0)
        yhover = CustomJSHover(code=self._hover_code % 1)
        for name, formatter in hover.tooltips:
            customjs = None
            if formatter in ('@{%s}' % xdim, '$x'):
                dim = xdim
                customjs = xhover
            elif formatter in ('@{%s}' % ydim, '$y'):
                dim = ydim
                customjs = yhover
            if customjs:
                key = formatter if formatter in ('$x', '$y') else dim
                formatters[key] = customjs
                formatter += '{custom}'
            tooltips.append((name, formatter))
        hover.tooltips = tooltips
        hover.formatters = formatters

    def get_data(self, element, ranges, style):
        proj = self.projection
        if self._project_operation and self.geographic:
            element = self._project_operation(element, projection=proj)
        return super(GeoPlot, self).get_data(element, ranges, style)


class GeoOverlayPlot(GeoPlot, HvOverlayPlot):
    """
    Subclasses the HoloViews OverlayPlot to add custom behavior
    for geographic plots.
    """

    global_extent = param.Boolean(default=False, doc="""
        Whether the plot should display the whole globe.""")

    _propagate_options = (HvOverlayPlot._propagate_options +
                          ['global_extent', 'show_bounds', 'infer_projection'])

    def __init__(self, element, **params):
        super(GeoOverlayPlot, self).__init__(element, **params)
        self.geographic = any(element.traverse(is_geographic, [_Element]))
        if self.geographic:
            self.show_grid = False
        if LooseVersion(hv.__version__) < '1.10.4':
            projection = self._get_projection(element)
            self.projection = projection
            for p in self.subplots.values():
                p.projection = projection
