import sys

import numpy as np

import pyqtgraph as pg
import pyqtgraph.parametertree as pt
from pyqtgraph.functions import eq
from pyqtgraph.Qt import QtCore, QtGui

app = pg.mkQApp()

def _getWidget(param):
    return list(param.items.keys())[0].widget


def test_opts():
    paramSpec = [
        dict(name='bool', type='bool', readonly=True),
        dict(name='color', type='color', readonly=True),
    ]

    param = pt.Parameter.create(name='params', type='group', children=paramSpec)
    tree = pt.ParameterTree()
    tree.setParameters(param)

    assert _getWidget(param.param('bool')).isEnabled() is False
    assert _getWidget(param.param('bool')).isEnabled() is False


def test_types():
    paramSpec = [
        dict(name='float', type='float'),
        dict(name='int', type='int'),
        dict(name='str', type='str'),
        dict(name='list', type='list', limits=['x','y','z']),
        dict(name='dict', type='list', limits={'x':1, 'y':3, 'z':7}),
        dict(name='bool', type='bool'),
        dict(name='color', type='color'),
    ]
    
    param = pt.Parameter.create(name='params', type='group', children=paramSpec)
    tree = pt.ParameterTree()
    tree.setParameters(param)

    all_objs = {
        'int0': 0, 'int':7, 'float': -0.35, 'bigfloat': 1e129, 'npfloat': np.float64(5), 
        'npint': np.int64(5),'npinf': np.inf, 'npnan': np.nan, 'bool': True, 
        'complex': 5+3j, 'str': '#xxx', 'unicode': 'µ', 
        'list': [1,2,3], 'dict': {'1': 2}, 'color': pg.mkColor('k'), 
        'brush': pg.mkBrush('k'), 'pen': pg.mkPen('k'), 'none': None
    }
    if hasattr(QtCore, 'QString'):
        all_objs['qstring'] = QtCore.QString('xxxµ')

    # float
    types = ['int0', 'int', 'float', 'bigfloat', 'npfloat', 'npint', 'npinf', 'npnan', 'bool']
    check_param_types(param.child('float'), float, float, 0.0, all_objs, types)

    # int
    types = ['int0', 'int', 'float', 'bigfloat', 'npfloat', 'npint', 'bool']
    check_param_types(param.child('int'), int, int, 0, all_objs, types)
    
    # str  (should be able to make a string out of any type)
    types = all_objs.keys()
    check_param_types(param.child('str'), str, str, '', all_objs, types)
    
    # bool  (should be able to make a boolean out of any type?)
    types = all_objs.keys()
    check_param_types(param.child('bool'), bool, bool, False, all_objs, types)

    # color
    types = ['color', 'int0', 'int', 'float', 'npfloat', 'npint', 'list']
    init = QtGui.QColor(128, 128, 128, 255)
    check_param_types(param.child('color'), QtGui.QColor, pg.mkColor, init, all_objs, types)

    
def check_param_types(param, types, map_func, init, objs, keys):
    """Check that parameter setValue() accepts or rejects the correct types and
    that value() returns the correct type.
    
    Parameters
    ----------
        param : Parameter instance
        types : type or tuple of types
            The allowed types for this parameter to return from value().
        map_func : function
            Converts an input value to the expected output value.
        init : object
            The expected initial value of the parameter
        objs : dict
            Contains a variety of objects that will be tested as arguments to
            param.setValue().
        keys : list
            The list of keys indicating the valid objects in *objs*. When
            param.setValue() is teasted with each value from *objs*, we expect
            an exception to be raised if the associated key is not in *keys*.
    """
    val = param.value()
    if not isinstance(types, tuple):
        types = (types,)
    assert val == init and type(val) in types
    
    # test valid input types
    good_inputs = [objs[k] for k in keys if k in objs]
    good_outputs = map(map_func, good_inputs)
    for x,y in zip(good_inputs, good_outputs):
        param.setValue(x)
        val = param.value()
        if not (eq(val, y) and type(val) in types):
            raise Exception("Setting parameter %s with value %r should have resulted in %r (types: %r), "
                "but resulted in %r (type: %r) instead." % (param, x, y, types, val, type(val)))
        
    # test invalid input types
    for k,v in objs.items():
        if k in keys:
            continue
        try:
            param.setValue(v)
        except (TypeError, ValueError, OverflowError):
            continue
        except Exception as exc:
            raise Exception("Setting %s parameter value to %r raised %r." % (param, v, exc))
        
        raise Exception("Setting %s parameter value to %r should have raised an exception." % (param, v))
        
        
def test_limits_enforcement():
    p = pt.Parameter.create(name='params', type='group', children=[
        dict(name='float', type='float', limits=[0, 1]),
        dict(name='int', type='int', bounds=[0, 1]),
        dict(name='list', type='list', limits=['x', 'y']),
        dict(name='dict', type='list', limits={'x': 1, 'y': 2}),
    ])
    t = pt.ParameterTree()
    t.setParameters(p)
    for k, vin, vout in [('float', -1, 0),
                         ('float',  2, 1),
                         ('int',   -1, 0),
                         ('int',    2, 1),
                         ('list',   'w', 'x'),
                         ('dict',   'w', 1)]:
        p[k] = vin
        assert p[k] == vout


def test_data_race():
    # Ensure widgets don't override user setting of param values whether
    # they connect the signal before or after it's added to a tree
    p = pt.Parameter.create(name='int', type='int', value=0)
    t = pt.ParameterTree()

    def override():
        p.setValue(1)

    p.sigValueChanged.connect(override)
    t.setParameters(p)
    pi = next(iter(p.items))
    assert pi.param is p
    pi.widget.setValue(2)
    assert p.value() == pi.widget.value() == 1
    p.sigValueChanged.disconnect(override)
    p.sigValueChanged.connect(override)
    pi.widget.setValue(2)
    assert p.value() == pi.widget.value() == 1
