# This file is part of Sympathy for Data.
# Copyright (c) 2017, Combine Control Systems AB
#
# SYMPATHY FOR DATA COMMERCIAL LICENSE
# You should have received a link to the License with Sympathy for Data.
import json
import numpy as np
import sys
from sympathy.api import node as synode
from sympathy.api import node_helper
from sympathy.api.nodeconfig import Port, Ports, Tag, Tags
from sympathy.api import exceptions
from sylib.util import base_eval
def _update_group(conf_group, data_group):
    old = list(conf_group)
    new = list(data_group)
    add = set(new).difference(old)
    remove = set(old).difference(new)
    for key in remove:
        del conf_group[key]
    for key in new:
        if key in add:
            conf_group[key] = data_group[key]
            continue
        conf_item = conf_group[key]
        data_item = data_group[key]
        if conf_item.type != data_item.type:
            # Replace.
            conf_group[key] = data_group[key]
        else:
            for k in ('order', 'label', 'description', 'editor'):
                setattr(conf_item, k, getattr(data_item, k))
            if conf_item.type in ['group', 'page']:
                _update_group(conf_item, data_item)
            elif conf_item.type == 'list':
                list_ = data_item.list
                conf_item.list = list_
    # Change the order of parameters based on input port:
    for key in new:
        conf_group[key] = conf_group.pop(key)
def _prune(param):
    def prune_names(param):
        return set(param).difference(
            ['editor', 'order', 'description'])
    if not isinstance(param, dict):
        return param
    elif param['type'] in ['group', 'page']:
        return {k: _prune(param[k]) for k in prune_names(param.keys())}
    else:
        return {k: param[k] for k in prune_names(param.keys())}
[docs]
class CreateParameters(synode.Node):
    """
    Manually Create a Sympathy parameter structure by writing a python
    expression which modifies the parameters variable using the sympathy
    parameter api (same class as nodes use to create their parameters).
    Example:
    .. code-block:: python
        parameters.set_integer(
            'number',
            label='Numbers',
            description='Description of the number parameter.',
            value=1)
        parameters.set_string(
            'string',
            label='String',
            description='Description of the string parameter.',
            value="Hello")
        parameters.set_integer(
            'bounded_number',
            label='Bounded number',
            description='Description of the bounded_nummber parameter.',
            value=0,
            editor=synode.editors.bounded_lineedit_editor(0, 4))
    In order to create editors and doing some other operations, synode
    is defined when the code is evaluated.
    Optional input port, named arg, can be used in the code. Have a look
    at the :ref:`Data type APIs<datatypeapis>` to see what methods and
    attributes are available on the data type that you are working with.
    The evaluation context contains ``parameters`` of type
    :class:`ParameterRoot`, ``synode`` - which is the module obtained from
    :mod:`sympathy.api.node`, and optionally ``arg`` which is an instance of
    some sympathy :ref:`datatype<datatypeapis>` corresponding to the data type
    connected to the input port.
    """
    name = 'Create Json Parameters'
    author = 'Erik der Hagopian'
    icon = 'create_json.svg'
    tags = Tags(Tag.Generic.Configuration)
    nodeid = 'org.sysess.sympathy.create.createparameters'
    related = ['org.sysess.sympathy.create.configureparameters']
    inputs = Ports([Port.Custom('<a>', 'Input',
                                name='arg', n=(0, 1))])
    outputs = Ports([Port.Json('Output', name='output')])
    parameters = synode.parameters()
    parameters.set_string(
        'code',
        label='Parameters:',
        description='Python code that modifies the parameter structure.',
        value='',
        editor=synode.editors.code_editor())
    def execute(self, node_context):
        inputs = node_context.input.group('arg')
        arg = inputs[0] if inputs else None
        parameters = synode.parameters()
        env = {'arg': arg, 'synode': synode, 'parameters': parameters}
        try:
            base_eval(node_context.parameters['code'].value, env, mode='exec')
        except Exception as exc:
            raise exceptions.SyUserCodeError(sys.exc_info()) from exc
        node_context.output[0].set(parameters.to_dict()) 
[docs]
class CreateJSON(synode.Node):
    """
    Create Json by writing a python expression which evaluates to normal python
    values, that is, dictionaries, lists, floats, integers, strings and
    booleans.
    Optional input port, named arg, can be used in the expression. Have a look
    at the :ref:`Data type APIs<datatypeapis>` to see what methods and
    attributes are available on the data type that you are working with.
    """
    name = 'Create Json'
    author = 'Erik der Hagopian'
    icon = 'create_json.svg'
    tags = Tags(Tag.Generic.Configuration)
    related = ['org.sysess.sympathy.jsoncalculator']
    nodeid = 'org.sysess.sympathy.create.createjson'
    inputs = Ports([Port.Custom('<a>', 'Input',
                                name='arg', n=(0, 1))])
    outputs = Ports([
        Port.Custom('json', 'Output', name='output', preview=True)])
    parameters = synode.parameters()
    parameters.set_string(
        'code',
        description='Python expression that evaluates to a '
                    'json-serilizable object.',
        value='{}  # Empty dictionary.',
        editor=synode.editors.code_editor())
    def execute(self, node_context):
        inputs = node_context.input.group('arg')
        arg = inputs[0] if inputs else None
        env = {'arg': arg}
        try:
            dict_ = base_eval(node_context.parameters['code'].value, env)
            node_context.output[0].set(dict_)
        except Exception as exc:
            raise exceptions.SyUserCodeError(sys.exc_info()) from exc 
[docs]
class JSONtoText(synode.Node):
    name = 'Json to Text'
    description = 'Convert Json to Text using standard Json encoding.'
    author = 'Erik der Hagopian'
    icon = 'json_to_text.svg'
    tags = Tags(Tag.DataProcessing.Convert)
    related = [
        'org.sysess.sympathy.convert.jsonstotexts',
        'org.sysess.sympathy.convert.texttojson',
    ]
    nodeid = 'org.sysess.sympathy.convert.jsontotext'
    inputs = Ports([Port.Json('Input', name='input')])
    outputs = Ports([Port.Text('Output', name='output')])
    parameters = synode.parameters()
    def execute(self, node_context):
        node_context.output[0].set(
            json.dumps(node_context.input[0].get())) 
[docs]
@node_helper.list_node_decorator(['input'], ['output'])
class JSONstoTexts(JSONtoText):
    name = 'Jsons to Texts'
    nodeid = 'org.sysess.sympathy.convert.jsonstotexts' 
[docs]
class TexttoJSON(synode.Node):
    name = 'Text to Json'
    author = 'Erik der Hagopian'
    icon = 'text_to_json.svg'
    description = 'Convert Json-compliant Text to Json.'
    tags = Tags(Tag.DataProcessing.Convert)
    related = [
        'org.sysess.sympathy.convert.textstojsons',
        'org.sysess.sympathy.convert.jsontotext',
    ]
    nodeid = 'org.sysess.sympathy.convert.texttojson'
    inputs = Ports([Port.Text('Input', name='input')])
    outputs = Ports([Port.Json('Output', name='output')])
    parameters = synode.parameters()
    def execute(self, node_context):
        text = node_context.input[0].get()
        try:
            data = json.loads(text)
        except json.decoder.JSONDecodeError as e:
            raise exceptions.SyDataError(
                f"Unsupported json content in text. {e}."
            ) from e
        node_context.output[0].set(data) 
[docs]
@node_helper.list_node_decorator(['input'], ['output'])
class TextstoJSONs(TexttoJSON):
    name = 'Texts to Jsons'
    nodeid = 'org.sysess.sympathy.convert.textstojsons'