try:
    import holoviews as hv
except ImportError:
    hv = None

import param
import pytest

from bokeh.plotting import figure

from panel.custom import JSComponent
from panel.layout import Row
from panel.links import Link
from panel.pane import Bokeh, HoloViews
from panel.tests.util import hv_available
from panel.widgets import (
    ColorPicker, DatetimeInput, FloatInput, FloatSlider, RangeSlider,
    TextInput,
)


class MockJSComponent(JSComponent):

    value = param.Parameter()


def test_widget_link_bidirectional():
    t1 = TextInput()
    t2 = TextInput()

    t1.link(t2, value='value', bidirectional=True)

    t1.value = 'ABC'
    assert t1.value == 'ABC'
    assert t2.value == 'ABC'

    t2.value = 'DEF'
    assert t1.value == 'DEF'
    assert t2.value == 'DEF'

def test_widget_jslink_bidirectional(document, comm):
    t1 = TextInput()
    t2 = TextInput()

    t1.jslink(t2, value='value', bidirectional=True)

    row = Row(t1, t2)

    model = row.get_root(document, comm)

    tm1, tm2 = model.children

    callbacks1 = tm1.js_property_callbacks['change:value']
    callbacks2 = tm2.js_property_callbacks['change:value']
    assert len(callbacks1) == 1
    assert len(callbacks2) == 1

    link1_customjs = callbacks1[0]
    link2_customjs = callbacks2[0]

    assert link1_customjs.args['source'] is tm1
    assert link2_customjs.args['source'] is tm2
    assert link1_customjs.args['target'] is tm2
    assert link2_customjs.args['target'] is tm1

    row[:] = row[::-1]

    assert len(callbacks1) == 1
    assert len(callbacks2) == 1

def test_widget_link_bidirectional_esm(document, comm):
    t1 = TextInput()
    t2 = MockJSComponent()

    t1.jslink(t2, value='value', bidirectional=True)

    row = Row(t1, t2)

    model = row.get_root(document, comm)

    tm1, tm2 = model.children

    callbacks1 = tm1.js_property_callbacks['change:value']
    callbacks2 = tm2.data.js_property_callbacks['change:value']
    assert len(callbacks1) == 1
    assert len(callbacks2) == 1

    link1_customjs = callbacks1[0]
    link2_customjs = callbacks2[0]

    assert link1_customjs.args['source'] is tm1
    assert link2_customjs.args['source'] is tm2.data
    assert link1_customjs.args['target'] is tm2.data
    assert link2_customjs.args['target'] is tm1

    row[:] = row[::-1]

    assert len(callbacks1) == 1
    assert len(callbacks2) == 1

def test_widget_link_source_param_not_found():
    t1 = TextInput()
    t2 = TextInput()

    with pytest.raises(ValueError) as excinfo:
        t1.jslink(t2, value1='value')
    assert "Could not jslink \'value1\' parameter" in str(excinfo)


def test_widget_link_target_param_not_found():
    t1 = TextInput()
    t2 = TextInput()

    with pytest.raises(ValueError) as excinfo:
        t1.jslink(t2, value='value1')
    assert "Could not jslink \'value1\' parameter" in str(excinfo)


def test_widget_link_no_transform_error():
    t1 = DatetimeInput()
    t2 = TextInput()

    with pytest.raises(ValueError) as excinfo:
        t1.jslink(t2, value='value')
    assert "Cannot jslink \'value\' parameter on DatetimeInput object" in str(excinfo)


def test_widget_link_no_target_transform_error():
    t1 = DatetimeInput()
    t2 = TextInput()

    with pytest.raises(ValueError) as excinfo:
        t2.jslink(t1, value='value')
    assert ("Cannot jslink 'value' parameter on TextInput object to 'value' parameter on DatetimeInput") in str(excinfo)


@hv_available
def test_pnwidget_hvplot_links(document, comm):
    size_widget = FloatSlider(value=5, start=1, end=10)
    points1 = hv.Points([1, 2, 3])

    size_widget.jslink(points1, value='glyph.size')

    row = Row(HoloViews(points1, backend='bokeh'), size_widget)
    model = row.get_root(document, comm=comm)
    hv_views = row.select(HoloViews)
    widg_views = row.select(FloatSlider)

    assert len(hv_views) == 1
    assert len(widg_views) == 1
    slider = widg_views[0]._models[model.ref['id']][0]
    scatter = hv_views[0]._plots[model.ref['id']][0].handles['glyph']

    link_customjs = slider.js_property_callbacks['change:value'][-1]
    assert link_customjs.args['source'] is slider
    assert link_customjs.args['target'] is scatter

    code = """
    var value = source['value'];
    value = value;
    value = value;
    try {
      var property = target.properties['size'];
      if (property !== undefined) { property.validate(value); }
    } catch(err) {
      console.log('WARNING: Could not set size on target, raised error: ' + err);
      return;
    }
    try {
      target['size'] = value;
    } catch(err) {
      console.log(err)
    }
    """
    assert link_customjs.code == code


@hv_available
def test_bkwidget_hvplot_links(document, comm):
    from bokeh.models import Slider
    bokeh_widget = Slider(value=5, start=1, end=10, step=1e-1)
    points1 = hv.Points([1, 2, 3])

    Link(bokeh_widget, points1, properties={'value': 'glyph.size'})

    row = Row(HoloViews(points1, backend='bokeh'), bokeh_widget)
    model = row.get_root(document, comm=comm)
    hv_views = row.select(HoloViews)

    assert len(hv_views) == 1
    slider = bokeh_widget
    scatter = hv_views[0]._plots[model.ref['id']][0].handles['glyph']

    link_customjs = slider.js_property_callbacks['change:value'][-1]
    assert link_customjs.args['source'] is slider
    assert link_customjs.args['target'] is scatter

    code = """
    var value = source['value'];
    value = value;
    value = value;
    try {
      var property = target.properties['size'];
      if (property !== undefined) { property.validate(value); }
    } catch(err) {
      console.log('WARNING: Could not set size on target, raised error: ' + err);
      return;
    }
    try {
      target['size'] = value;
    } catch(err) {
      console.log(err)
    }
    """
    assert link_customjs.code == code


def test_bkwidget_bkplot_links(document, comm):
    from bokeh.models import Slider
    bokeh_widget = Slider(value=5, start=1, end=10, step=1e-1)
    bokeh_fig = figure()
    scatter = bokeh_fig.scatter([1, 2, 3], [1, 2, 3])

    Link(bokeh_widget, scatter, properties={'value': 'glyph.size'})

    row = Row(bokeh_fig, bokeh_widget)
    row.get_root(document, comm=comm)

    slider = bokeh_widget

    link_customjs = slider.js_property_callbacks['change:value'][-1]
    assert link_customjs.args['source'] is slider
    assert link_customjs.args['target'] is scatter.glyph

    code = """
    var value = source['value'];
    value = value;
    value = value;
    try {
      var property = target.properties['size'];
      if (property !== undefined) { property.validate(value); }
    } catch(err) {
      console.log('WARNING: Could not set size on target, raised error: ' + err);
      return;
    }
    try {
      target['size'] = value;
    } catch(err) {
      console.log(err)
    }
    """
    assert link_customjs.code == code


def test_widget_bkplot_link(document, comm):
    widget = ColorPicker(value='#ff00ff')
    bokeh_fig = figure()
    scatter = bokeh_fig.scatter([1, 2, 3], [1, 2, 3])

    widget.jslink(scatter.glyph, value='fill_color')

    row = Row(bokeh_fig, widget)
    model = row.get_root(document, comm=comm)

    link_customjs = model.children[1].js_property_callbacks['change:color'][-1]
    assert link_customjs.args['source'] is model.children[1]
    assert link_customjs.args['target'] is scatter.glyph
    assert scatter.glyph.fill_color == '#ff00ff'

    code = """
    var value = source['color'];
    value = value;
    value = value;
    try {
      var property = target.properties['fill_color'];
      if (property !== undefined) { property.validate(value); }
    } catch(err) {
      console.log('WARNING: Could not set fill_color on target, raised error: ' + err);
      return;
    }
    try {
      target['fill_color'] = value;
    } catch(err) {
      console.log(err)
    }
    """
    assert link_customjs.code == code


def test_bokeh_figure_jslink(document, comm):
    fig = figure()
    fig.line(0, 0)

    pane = Bokeh(fig)
    t1 = FloatInput()

    pane.jslink(t1, **{'x_range.start': 'value'})
    row = Row(pane, t1)

    model = row.get_root(document, comm)

    link_customjs = fig.x_range.js_property_callbacks['change:start'][-1]
    assert link_customjs.args['source'] == fig.x_range
    assert link_customjs.args['target'] == model.children[1]
    assert link_customjs.code == """
    var value = source['start'];
    value = value;
    value = value;
    try {
      var property = target.properties['value'];
      if (property !== undefined) { property.validate(value); }
    } catch(err) {
      console.log('WARNING: Could not set value on target, raised error: ' + err);
      return;
    }
    try {
      target['value'] = value;
    } catch(err) {
      console.log(err)
    }
    """

def test_widget_jscallback(document, comm):
    widget = ColorPicker(value='#ff00ff')

    widget.jscallback(value='some_code')

    model = widget.get_root(document, comm=comm)

    customjs = model.js_property_callbacks['change:color'][-1]
    assert customjs.args['source'] is model
    assert customjs.code == "try { some_code } catch(err) { console.log(err) }"


def test_widget_jscallback_args_scalar(document, comm):
    widget = ColorPicker(value='#ff00ff')

    widget.jscallback(value='some_code', args={'scalar': 1})

    model = widget.get_root(document, comm=comm)

    customjs = model.js_property_callbacks['change:color'][-1]
    assert customjs.args['scalar'] == 1


def test_widget_jscallback_args_model(document, comm):
    widget = ColorPicker(value='#ff00ff')
    widget2 = ColorPicker(value='#ff00ff')

    widget.jscallback(value='some_code', args={'widget': widget2})

    model = Row(widget, widget2).get_root(document, comm=comm)

    customjs = model.children[0].js_property_callbacks['change:color'][-1]
    assert customjs.args['source'] is model.children[0]
    assert customjs.args['widget'] is model.children[1]
    assert customjs.code == "try { some_code } catch(err) { console.log(err) }"


@hv_available
def test_hvplot_jscallback(document, comm):
    points1 = hv.Points([1, 2, 3])

    hvplot = HoloViews(points1, backend='bokeh')

    hvplot.jscallback(**{'x_range.start': "some_code"})

    model = hvplot.get_root(document, comm=comm)
    x_range = hvplot._plots[model.ref['id']][0].handles['x_range']

    customjs = x_range.js_property_callbacks['change:start'][-1]
    assert customjs.args['source'] is x_range
    assert customjs.code == "try { some_code } catch(err) { console.log(err) }"


@hv_available
def test_link_with_customcode(document, comm):
    range_widget = RangeSlider(start=0., end=1.)
    curve = hv.Curve([])
    code = """
      x_range.start = source.value[0]
      x_range.end = source.value[1]
    """
    range_widget.jslink(curve, code={'value': code})
    row = Row(HoloViews(curve, backend='bokeh'), range_widget)

    range_widget.value = (0.5, 0.7)
    model = row.get_root(document, comm=comm)
    hv_views = row.select(HoloViews)
    widg_views = row.select(RangeSlider)

    assert len(hv_views) == 1
    assert len(widg_views) == 1
    range_slider = widg_views[0]._models[model.ref['id']][0]
    x_range = hv_views[0]._plots[model.ref['id']][0].handles['x_range']

    link_customjs = range_slider.js_property_callbacks['change:value'][-1]
    assert link_customjs.args['source'] is range_slider
    assert link_customjs.args['x_range'] is x_range
    assert link_customjs.code == f"try {{ {code} }} catch(err) {{ console.log(err) }}"
