import requests
from jsonschema import validate
from six import text_type, string_types

from pykechain.enums import ComponentXType, Category, SortTable, PropertyType, NavigationBarAlignment, WidgetNames, \
    ShowColumnTypes
from pykechain.exceptions import APIError, IllegalArgumentError, _DeprecationMixin
from pykechain.models import Part2, Activity2
from pykechain.models.activity import Activity
from pykechain.models.part import Part
from pykechain.models.property import Property
from pykechain.models.service import Service
from pykechain.utils import is_uuid

uuid_pattern = r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
uuid_string = {"type": "string", "pattern": uuid_pattern}

# deprecated as we are moving towards a 'meta' definition for the widgets in KE-chain 3
# only for checking the jsonwidget
component_jsonwidget_schema = {
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "Component JSON Schema",
    "type": "object",
    "properties": {
        "xtype": {
            "type": "string",
            "enum": ComponentXType.values()
        },
        "filter": {
            "type": "object",
            "properties": {
                "part": uuid_string,
                "model": uuid_string,
                "parent": uuid_string,
                "part_id": uuid_string,
                "model_id": uuid_string,
                "parent_id": uuid_string
            }
        },
        "title": {"type": ["string", "null"]},
        "viewModel": {"type": "object"},
        "model": uuid_string,
        "parent": uuid_string
    },
    "required": ["xtype"]
}

widget_json_stub = {
    "title": "Widget JSON schema",
    "type": "object",
    "required": ["name", "meta"],
    "properties": {
        "name": {"type": "string", "enum": WidgetNames.values()},
        "meta": {"type": "object"},
        "config": {"type": "object"}
    }
}

widgetconfig_json_schema = {
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "WidgetConfig JSON",
    "type": "object",
    "properties": {
        "ext": {
            "type": "object",
            "widgets": {
                "type": "array",
                "items": widget_json_stub
            }
        }
    }
}


class CustomizationBase(object):
    """Base class for customization objects.

    :cvar activity: an instance of :class:`pykechain.models.Activity`
    """

    def __init__(self, activity, client):
        """Initialize the Base class for customization objects."""
        self._client = client
        self.activity = activity


class ExtCustomization(CustomizationBase, _DeprecationMixin):
    """A class to represent the activity customization for Ext Js."""

    def __str__(self):  # pragma: no cover
        return "<pyke ExtCustomization '{}' id {} ({} widgets)>".format(
            self.activity.name, str(self.activity.id)[-8:], len(self.widgets()))

    def __repr__(self):  # pragma: no cover
        return self.__str__()

    def _save_customization(self, widgets):
        """
        Save the complete customization to the activity.

        :param widgets: The complete set of widgets to be customized
        """
        if len(widgets) > 0:
            # Get the current customization and only replace the 'ext' part of it
            customization = self.activity._json_data.get('customization', dict())
            if customization:
                customization['ext'] = dict(widgets=widgets)
            else:
                customization = dict(ext=dict(widgets=widgets))

        # Empty the customization if if the widgets list is empty
        else:
            customization = None

        # perform validation
        if customization:
            validate(customization, widgetconfig_json_schema)

        # Save to the activity and store the saved activity to self
        response = self._client._request("PUT",
                                         self._client._build_url("activity", activity_id=str(self.activity.id)),
                                         json=dict(customization=customization))
        if response.status_code != requests.codes.ok:  # pragma: no cover
            raise APIError("Could not save customization ({})".format(response))
        else:
            # refresh the activity json
            self.activity = self._client.activity(pk=self.activity.id)

    def _add_widget(self, widget):
        """
        Add a widget to the customization.

        Will save the widget to KE-chain.

        :param widget: The widget (specific json dict) to be added
        :type widget: dict
        """
        widgets = self.widgets()
        widgets += [widget]
        self._save_customization(widgets)

    def widgets(self):
        """
        Get the Ext JS specific customization from the activity.

        :return: The Ext JS specific customization in `list(dict)` form
        """
        customization = self.activity._json_data.get('customization')

        if customization and "ext" in customization.keys():
            return customization['ext']['widgets']
        else:
            return []

    def delete_widget(self, index):
        """
        Delete widgets by index.

        The widgets are saved to KE-chain.

        :param index: The index of the widget to be deleted in the self.widgets
        :type index: int
        :raises ValueError: if the customization has no widgets
        """
        widgets = self.widgets()
        if len(widgets) == 0:
            raise ValueError("This customization has no widgets")
        widgets.pop(index)
        self._save_customization(widgets)

    def delete_all_widgets(self):
        """Delete all widgets.

        :raises APIError: if the widgets could not be deleted.
        """
        self._save_customization([])

    def add_json_widget(self, config):
        """
        Add an Ext Json Widget to the customization.

        The configuration json provided must be interpretable by KE-chain. The json will be validated
        against the widget json schema.

        The widget will be saved to KE-chain.

        :param config: The json configuration of the widget
        :type config: dict
        """
        validate(config, component_jsonwidget_schema)
        self._add_widget(dict(config=config, name=WidgetNames.JSONWIDGET))

    def add_super_grid_widget(self, part_model, delete=False, edit=True, export=True, incomplete_rows=True,
                              new_instance=False, parent_part_instance=None, max_height=None, custom_title=False,
                              emphasize_edit=False, emphasize_new_instance=True, sort_property=None,
                              sort_direction=SortTable.ASCENDING):
        """
        Add a KE-chain superGrid (e.g. basic table widget) to the customization.

        The widget will be saved to KE-chain.

        :param emphasize_new_instance: Emphasize the New instance button (default True)
        :type emphasize_new_instance: bool
        :param emphasize_edit: Emphasize the Edit button (default False)
        :type emphasize_edit: bool
        :param new_instance: Show or hide the New instance button (default False). You need to provide a
            `parent_part_instance` in order for this to work.
        :type new_instance: bool
        :param incomplete_rows: Show or hide the Incomplete Rows filter button (default True)
        :type incomplete_rows: bool
        :param export: Show or hide the Export Grid button (default True)
        :type export: bool
        :param edit: Show or hide the Edit button (default True)
        :type edit: bool
        :param delete: Show or hide the Delete button (default False)
        :type delete: bool
        :param part_model: The part model based on which all instances will be shown.
        :type parent_part_instance: :class:`Part` or UUID
        :param parent_part_instance: The parent part instance for which the instances will be shown or to which new
            instances will be added.
        :type parent_part_instance: :class:`Part` or UUID
        :param max_height: The max height of the supergrid in pixels
        :type max_height: int or None
        :param custom_title: A custom title for the supergrid::
            * False (default): Part instance name
            * String value: Custom title
            * None: No title
        :type custom_title: bool or basestring or None
        :param sort_property: The property model on which the part instances are being sorted on
        :type sort_property: :class:`Property` or UUID
        :param sort_direction: The direction on which the values of property instances are being sorted on:
            * ASC (default): Sort in ascending order
            * DESC: Sort in descending order
        :type sort_direction: basestring (see :class:`enums.SortTable`)
        :raises IllegalArgumentError: When unknown or illegal arguments are passed.

        """
        # Check whether the part_model is uuid type or class `Part`
        if isinstance(part_model, (Part, Part2)):
            part_model_id = part_model.id
        elif isinstance(part_model, (string_types, text_type)) and is_uuid(part_model):
            part_model_id = part_model
            part_model = self._client.model(id=part_model_id)
        else:
            raise IllegalArgumentError("When using the add_supergrid_widget, part_model must be a Part or Part id. "
                                       "Type is: {}".format(type(part_model)))

        # Check whether the parent_part_instance is uuid type or class `Part`
        if isinstance(parent_part_instance, (Part, Part2)):
            parent_part_instance_id = parent_part_instance.id
        elif isinstance(parent_part_instance, (string_types, text_type)) and is_uuid(parent_part_instance):
            parent_part_instance_id = parent_part_instance
            parent_part_instance = self._client.part(id=parent_part_instance_id)
        elif isinstance(parent_part_instance, type(None)):
            parent_part_instance_id = None
        else:
            raise IllegalArgumentError("When using the add_supergrid_widget, parent_part_instance must be a "
                                       "Part, Part id or None. Type is: {}".format(type(parent_part_instance)))

        # Check whether the sort_property is uuid type or class `Property`
        if isinstance(sort_property, Property):
            sort_property_id = sort_property.id
        elif isinstance(sort_property, (string_types, text_type)) and is_uuid(sort_property):
            sort_property_id = sort_property
            sort_property = self._client.property(id=sort_property_id, category=Category.MODEL)
        elif isinstance(sort_property, type(None)):
            sort_property_id = None
        else:
            raise IllegalArgumentError("When using the add_supergrid_widget, sort_property must be a "
                                       "Property, Property id or None. Type is: {}".format(type(sort_property)))

        # Assertions
        if not parent_part_instance and new_instance:
            raise IllegalArgumentError("If you want to allow the creation of new part instances, you must specify a "
                                       "parent_part_instance")
        if sort_property and sort_property.part.id != part_model.id:
            raise IllegalArgumentError("If you want to sort on a property, then sort_property must be located under "
                                       "part_model")
        # Declare superGrid config
        if self._client.match_app_version(label='pim', version='>=3.0.0'):
            config = {
                "xtype": ComponentXType.SUPERGRID,
                "filter": {
                    "activity_id": str(self.activity.id),
                    "model_id": part_model_id
                },
                "viewModel": {
                    "data": {
                        "actions": {
                            "delete": delete,
                            "edit": edit,
                            "export": export,
                            "incompleteRows": incomplete_rows,
                            "newInstance": new_instance
                        },
                        "sorters": [{
                            "direction": sort_direction,
                            "property": sort_property_id
                        }] if sort_property_id else [],
                        "ui": {
                            "newInstance": "primary-action" if emphasize_new_instance else "default-toolbar",
                            "edit": "primary-action" if emphasize_edit else "default-toolbar"
                        }
                    }
                }
            }
            # Add parent to filter if added.
            config['filter']["parent_id"] = parent_part_instance_id
        else:
            config = {
                "xtype": ComponentXType.SUPERGRID,
                "filter": {
                    "activity_id": str(self.activity.id),
                    "model": part_model_id
                },
                "viewModel": {
                    "data": {
                        "actions": {
                            "delete": delete,
                            "edit": edit,
                            "export": export,
                            "incompleteRows": incomplete_rows,
                            "newInstance": new_instance
                        },
                        "sorters": [{
                            "direction": sort_direction,
                            "property": sort_property_id
                        }] if sort_property_id else [],
                        "ui": {
                            "newInstance": "primary-action" if emphasize_new_instance else "default-toolbar",
                            "edit": "primary-action" if emphasize_edit else "default-toolbar"
                        }
                    }
                }
            }
            config['filter']["parent"] = parent_part_instance_id

        # Add max height and custom title
        if max_height:
            config['maxHeight'] = max_height

        if custom_title is False:
            show_title_value = "Default"
            title = part_model.name
        elif custom_title is None:
            show_title_value = "No title"
            title = str()
        else:
            show_title_value = "Custom Title"
            title = str(custom_title)

        config["title"] = title
        config["showTitleValue"] = show_title_value

        # Declare the meta info for the superGrid
        meta = {
            "parentInstanceId": parent_part_instance_id,
            "editButtonUi": "primary-action" if emphasize_edit else "default-toolbar",
            "customHeight": max_height if max_height else 500,
            "primaryAddUiValue": emphasize_new_instance,
            "activityId": str(self.activity.id),
            "customTitle": title,
            "primaryEditUiValue": emphasize_edit,
            "downloadButtonVisible": export,
            "addButtonUi": "primary-action" if emphasize_new_instance else "default-toolbar",
            "deleteButtonVisible": delete,
            "incompleteRowsButtonVisible": incomplete_rows,
            "addButtonVisible": new_instance,
            "showTitleValue": show_title_value,
            "partModelId": part_model_id,
            "editButtonVisible": edit,
            "showHeightValue": "Custom max height" if max_height else "Auto",
            "sortDirection": sort_direction,
            "sortedColumn": sort_property_id if sort_property_id else None
        }

        self._add_widget(dict(config=config, meta=meta, name=WidgetNames.SUPERGRIDWIDGET))

    def add_property_grid_widget(self, part_instance, max_height=None, custom_title=False, show_headers=True,
                                 show_columns=None):
        """
        Add a KE-chain Property Grid widget to the customization.

        The widget will be saved to KE-chain.

        :param part_instance: The part instance on which the property grid will be based
        :type part_instance: :class:`Part` or UUID
        :param max_height: The max height of the property grid in pixels
        :type max_height: int or None
        :param custom_title: A custom title for the property grid::
            * False (default): Part instance name
            * String value: Custom title
            * None: No title
        :type custom_title: bool or basestring or None
        :param show_headers: Show or hide the headers in the grid (default True)
        :type show_headers: bool
        :param show_columns: Columns to be hidden or shown (default to 'unit' and 'description')
        :type show_columns: list
        :raises IllegalArgumentError: When unknown or illegal arguments are passed.
        """
        # Check whether the parent_part_instance is uuid type or class `Part`
        if isinstance(part_instance, (Part, Part2)):
            part_instance_id = part_instance.id
        elif isinstance(part_instance, (string_types, text_type)) and is_uuid(part_instance):
            part_instance_id = part_instance
            part_instance = self._client.part(id=part_instance_id)
        else:
            raise IllegalArgumentError("When using the add_property_grid_widget, part_instance must be a "
                                       "Part or Part id. Type is: {}".format(type(part_instance)))
        if not show_columns:
            show_columns = list()

        # Set the display_columns for the config
        possible_columns = [ShowColumnTypes.DESCRIPTION, ShowColumnTypes.UNIT]
        display_columns = dict()

        for possible_column in possible_columns:
            if possible_column in show_columns:
                display_columns[possible_column] = True
            else:
                display_columns[possible_column] = False

        # Declare property grid config
        if self._client.match_app_version(label='pim', version='>=3.0.0'):
            config = {
                "xtype": ComponentXType.PROPERTYGRID,
                "category": Category.INSTANCE,
                "filter": {
                    "activity_id": str(self.activity.id),
                    "part_id": part_instance_id  # gpim
                },
                "hideHeaders": not show_headers,
                "viewModel": {
                    "data": {
                        "displayColumns": display_columns
                    }
                }
            }
        else:
            config = {
                "xtype": ComponentXType.PROPERTYGRID,
                "category": Category.INSTANCE,
                "filter": {
                    "activity_id": str(self.activity.id),
                    "part": part_instance_id
                },
                "hideHeaders": not show_headers,
                "viewModel": {
                    "data": {
                        "displayColumns": display_columns
                    }
                }
            }

        # Add max height and custom title
        if max_height:
            config['maxHeight'] = max_height

        if custom_title is False:
            show_title_value = "Default"
            title = part_instance.name
        elif custom_title is None:
            show_title_value = "No title"
            title = str()
        else:
            show_title_value = "Custom Title"
            title = str(custom_title)

        config["title"] = title

        # Declare the meta info for the property grid
        meta = {
            "activityId": str(self.activity.id),
            "customHeight": max_height if max_height else None,
            "customTitle": title,
            "partInstanceId": part_instance_id,
            "showColumns": show_columns,
            "showHeaders": show_headers,
            "showHeightValue": "Custom max height" if max_height else "Auto",
            "showTitleValue": show_title_value
        }

        self._add_widget(dict(config=config, meta=meta, name=WidgetNames.PROPERTYGRIDWIDGET))

    def add_text_widget(self, text=None, custom_title=None, collapsible=True, collapsed=False):
        """
        Add a KE-chain Text widget to the customization.

        The widget will be saved to KE-chain.

        :param text: The text that will be shown by the widget.
        :type text: basestring or None
        :param custom_title: A custom title for the text panel::
            * None (default): No title
            * String value: Custom title
        :type custom_title: basestring or None
        :param collapsible: A boolean to decide whether the panel is collapsible or not (default True)
        :type collapsible: bool
        :param collapsed: A boolean to decide whether the panel is collapsed or not (default False)
        :type collapsible: bool
        :raises IllegalArgumentError: When unknown or illegal arguments are passed.
        """
        # Declare text widget config
        config = {
            "xtype": ComponentXType.HTMLPANEL,
            "filter": {
                "activity_id": str(self.activity.id),
            }
        }

        # Add text and custom title
        if text:
            config['html'] = text
        if custom_title:
            show_title_value = "Custom Title"
            title = custom_title
        else:
            show_title_value = "No Title"
            title = None
        config['collapsible'] = collapsible
        # A widget can only be collapsed if it is collapsible in the first place
        if collapsible:
            config['collapsed'] = collapsed
        else:
            config['collapsed'] = False
        config['title'] = title
        # Declare the meta info for the property grid
        meta = {
            "activityId": str(self.activity.id),
            "customTitle": title,
            "collapsible": collapsible,
            "collapsed": collapsed,
            "html": text,
            "showTitleValue": show_title_value
        }

        self._add_widget(dict(config=config, meta=meta, name=WidgetNames.HTMLWIDGET))

    def add_paginated_grid_widget(self, part_model, delete=False, edit=True, export=True,
                                  new_instance=False, clone_instance=False, parent_part_instance=None, max_height=None,
                                  custom_title=False, emphasize_edit=False, emphasize_new_instance=True,
                                  emphasize_clone_instance=False, sort_property=None,
                                  sort_direction=SortTable.ASCENDING, page_size=25, collapse_filters=False):
        """
        Add a KE-chain paginatedGrid (e.g. paginated table widget) to the customization.

        The widget will be saved to KE-chain.

        :param new_instance: Show or hide the New instance button (default False). You need to provide a
            `parent_part_instance` in order for this to work.
        :type new_instance: bool
        :param clone_instance: Show or hide the Clone instance button (default False). You need to provide a
            `parent_part_instance` in order for this to work.
        :type clone_instance: bool
        :param edit: Show or hide the Edit button (default True)
        :type edit: bool
        :param delete: Show or hide the Delete button (default False)
        :type delete: bool

        :param collapse_filters: Hide or show the filters pane (default False)
        :type collapse_filters: bool
        :param incomplete_rows: Show or hide the Incomplete Rows filter button (default True)
        :type incomplete_rows: bool
        :param export: Show or hide the Export Grid button (default True)
        :type export: bool

        :param part_model: The part model based on which all instances will be shown.
        :param part_model: :class:`Part` or UUID
        :param parent_part_instance: The parent part instance for which the instances will be shown or to which new
            instances will be added.
        :type parent_part_instance: :class:`Part` or UUID

        :param emphasize_new_instance: Emphasize the New instance button (default True)
        :type emphasize_new_instance: bool
        :param emphasize_edit: Emphasize the Edit button (default False)
        :type emphasize_edit: bool
        :param emphasize_clone_instance:  emphasize the Clone instance button (default False)
        :type emphasize_clone_instance: bool

        :param page_size: Number of parts that will be shown per page in the grid.
        :type page_size: int
        :param max_height: The max height of the paginated grid in pixels
        :type max_height: int or None
        :param custom_title: A custom title for the paginated grid::
            * False (default): Part instance name
            * String value: Custom title
            * None: No title
        :type custom_title: bool or basestring or None
        :param sort_property: The property model on which the part instances are being sorted on
        :type sort_property: :class:`Property` or UUID
        :param sort_direction: The direction on which the values of property instances are being sorted on::
            * ASC (default): Sort in ascending order
            * DESC: Sort in descending order
        :type sort_direction: basestring (see :class:`enums.SortTable`)
        :raises IllegalArgumentError: When unknown or illegal arguments are passed.
        """
        # Check whether the part_model is uuid type or class `Part`
        if isinstance(part_model, (Part, Part2)):
            part_model_id = part_model.id
        elif isinstance(part_model, (string_types, text_type)) and is_uuid(part_model):
            part_model_id = part_model
            part_model = self._client.model(id=part_model_id)
        else:
            raise IllegalArgumentError("When using the add_paginated_grid_widget, part_model must be a Part or Part id."
                                       " Type is: {}".format(type(part_model)))

        # Check whether the parent_part_instance is uuid type or class `Part`
        if isinstance(parent_part_instance, (Part, Part2)):
            parent_part_instance_id = parent_part_instance.id
        elif isinstance(parent_part_instance, (string_types, text_type)) and is_uuid(parent_part_instance):
            parent_part_instance_id = parent_part_instance
            parent_part_instance = self._client.part(id=parent_part_instance_id)
        elif isinstance(parent_part_instance, type(None)):
            parent_part_instance_id = None
        else:
            raise IllegalArgumentError("When using the add_paginated_grid_widget, parent_part_instance must be a "
                                       "Part, Part id or None. Type is: {}".format(type(parent_part_instance)))

        # Check whether the sort_property is uuid type or class `Property`
        if isinstance(sort_property, Property):
            sort_property_id = sort_property.id
        elif isinstance(sort_property, (string_types, text_type)) and is_uuid(sort_property):
            sort_property_id = sort_property
            sort_property = self._client.property(id=sort_property_id, category=Category.MODEL)
        elif isinstance(sort_property, type(None)):
            sort_property_id = None
        else:
            raise IllegalArgumentError("When using the add_paginated_grid_widget, sort_property must be a "
                                       "Property, Property id or None. Type is: {}".format(type(sort_property)))

        # Assertions
        if not parent_part_instance and (new_instance or clone_instance):
            raise IllegalArgumentError("If you want to allow the creation (or cloning) of new part instances, "
                                       "you must specify a `parent_part_instance`")
        if sort_property and sort_property.part.id != part_model.id:
            raise IllegalArgumentError("If you want to sort on a property, then sort_property must be located under "
                                       "`part_model`")

        # Add custom title
        if custom_title is False:
            show_title_value = "Default"
            title = part_model.name
        elif custom_title is None:
            show_title_value = "No title"
            title = ' '
        else:
            show_title_value = "Custom Title"
            title = str(custom_title)

        # Set the collapse filters value
        if collapse_filters:
            collapse_filters_value = "Collapsed"
        else:
            collapse_filters_value = "Expanded"

        config = {
            "xtype": ComponentXType.FILTEREDGRID,
            "filter": {
                "activity_id": str(self.activity.id),
            },
            "grid": {
                "viewModel": {
                    "data": {
                        "actions": {
                            "delete": delete,
                            "edit": edit,
                            "export": export,
                            "newInstance": new_instance,
                            "cloneInstance": clone_instance
                        },
                        "sorters": [{
                            "direction": sort_direction,
                            "property": sort_property_id
                        }] if sort_property_id else [],
                        "ui": {
                            "newInstance": "primary-action" if emphasize_new_instance else "default-toolbar",
                            "edit": "primary-action" if emphasize_edit else "default-toolbar",
                            "cloneInstance": "primary-action" if emphasize_clone_instance else "default-toolbar"
                        },
                        "pageSize": page_size
                    }
                },
                "xtype": ComponentXType.PAGINATEDSUPERGRID,
                "title": title,
                "showTitleValue": show_title_value,
            },
            "maxHeight": max_height if max_height else None,
            "parentInstanceId": parent_part_instance_id,
            "partModelId": part_model_id,
            "collapseFilters": collapse_filters
        }

        # Declare the meta info for the paginatedGrid
        meta = {
            "parentInstanceId": parent_part_instance_id,
            "editButtonUi": "primary-action" if emphasize_edit else "default-toolbar",
            "editButtonVisible": edit,
            "customHeight": max_height if max_height else 500,
            "primaryAddUiValue": emphasize_new_instance,
            "activityId": str(self.activity.id),
            "customTitle": title,
            "primaryEditUiValue": emphasize_edit,
            "downloadButtonVisible": export,
            "addButtonUi": "primary-action" if emphasize_new_instance else "default-toolbar",
            "deleteButtonVisible": delete,
            "addButtonVisible": new_instance,
            "showTitleValue": show_title_value,
            "partModelId": str(part_model_id),
            "showHeightValue": "Custom max height" if max_height else "Auto",
            "sortDirection": sort_direction,
            "sortedColumn": sort_property_id if sort_property_id else None,
            "collapseFilters": collapse_filters,
            "showCollapseFiltersValue": collapse_filters_value,
            "customPageSize": page_size
        }

        self._add_widget(dict(config=config, meta=meta, name=WidgetNames.FILTEREDGRIDWIDGET))

    def add_script_widget(self, script, custom_title=False, custom_button_text=False, emphasize_run=True):
        """
        Add a KE-chain Script (e.g. script widget) to the customization.

        The widget will be saved to KE-chain.

        :param script: The Script to which the button will be coupled and will be ran when the button is pressed.
        :type script: :class:`Service` or UUID
        :param custom_title: A custom title for the script widget
            * False (default): Script name
            * String value: Custom title
            * None: No title
        :type custom_title: bool or basestring or None
        :param custom_button_text: A custom text for the button linked to the script
            * False (default): Script name
            * String value: Custom title
            * None: No title
        :type custom_button_text: bool or basestring or None
        :param emphasize_run: Emphasize the run button (default True)
        :type emphasize_run: bool
        :raises IllegalArgumentError: When unknown or illegal arguments are passed.
        """
        # Check whether the script is uuid type or class `Service`
        if isinstance(script, Service):
            script_id = script.id
        elif isinstance(script, (string_types, text_type)) and is_uuid(script):
            script_id = script
            script = self._client.service(id=script_id)
        else:
            raise IllegalArgumentError("When using the add_script_widget, script must be a Service or Service id. "
                                       "Type is: {}".format(type(script)))
        # Add custom title
        if custom_title is False:
            show_title_value = "Default"
            title = script.name
        elif custom_title is None:
            show_title_value = "No title"
            title = ''
        else:
            show_title_value = "Custom Title"
            title = str(custom_title)

        # Add custom button text
        if custom_button_text is False:
            show_button_value = "Default"
            button_text = script.name
        elif custom_button_text is None:
            show_button_value = "No title"
            button_text = ''
        else:
            show_button_value = "Custom Title"
            button_text = str(custom_button_text)

        # Declare script widget config
        config = {
            'showTitleValue': show_title_value,
            'serviceId': script_id,
            'viewModel': {
                'data': {
                    'buttonUI': 'primary-action' if emphasize_run else "default-toolbar",
                }
            },
            'title': title,
            'xtype': ComponentXType.EXECUTESERVICE,
            'customButtonText': button_text
        }

        # Declare script widget meta
        meta = {
            'showButtonValue': show_button_value,
            'customText': button_text,
            'customTitle': title,
            'serviceId': script_id,
            'emphasizeButton': emphasize_run,
            'showTitleValue': show_title_value
        }

        self._add_widget(dict(config=config, meta=meta, name=WidgetNames.SERVICEWIDGET))

    def add_notebook_widget(self, notebook, custom_title=False, height=None):
        """
        Add a KE-chain Notebook (e.g. notebook widget) to the customization.

        The widget will be saved to KE-chain.

        :param notebook: The Notebook to which the button will be coupled and will start when the button is pressed.
        :type notebook: :class:`Service` or UUID
        :param custom_title: A custom title for the notebook widget
            * False (default): Notebook name
            * String value: Custom title
            * None: No title
        :type custom_title: bool or basestring or None
        :param height: The height of the Notebook in pixels
        :type height: int or None
        :raises IllegalArgumentError: When unknown or illegal arguments are passed.
        """
        # Check whether the script is uuid type or class `Service`
        if isinstance(notebook, Service):
            notebook_id = notebook.id
        elif isinstance(notebook, (string_types, text_type)) and is_uuid(notebook):
            notebook_id = notebook
            notebook = self._client.service(id=notebook_id)
        else:
            raise IllegalArgumentError("When using the add_notebook_widget, notebook must be a Service or Service id. "
                                       "Type is: {}".format(type(notebook)))

        # Add custom title
        if custom_title is False:
            show_title_value = "Default"
            title = notebook.name
        elif custom_title is None:
            show_title_value = "No title"
            title = ''
        else:
            show_title_value = "Custom Title"
            title = str(custom_title)

        # Declare notebook widget config
        config = {
            'title': title,
            'showTitleValue': show_title_value,
            'height': height if height else 800,
            'xtype': ComponentXType.NOTEBOOKPANEL,
            'serviceId': notebook_id
        }

        # Declare notebook widget meta
        meta = {
            'showTitleValue': show_title_value,
            'customHeight': height if height else 800,
            'customTitle': title,
            'serviceId': notebook_id
        }

        self._add_widget(dict(config=config, meta=meta, name=WidgetNames.NOTEBOOKWIDGET))

    def add_attachment_viewer_widget(self, attachment_property, custom_title=False, height=None):
        """
        Add a KE-chain Attachment Viewer (e.g. attachment viewer widget) to the customization.

        The widget will be saved to KE-chain.

        :param attachment_property: The Attachment Property to which the Viewer will be connected to.
        :type attachment_property: :class:`Property` or UUID
        :param custom_title: A custom title for the attachment viewer widget
            * False (default): Notebook name
            * String value: Custom title
            * None: No title
        :type custom_title: bool or basestring or None
        :param height: The height of the Notebook in pixels
        :type height: int or None
        :raises IllegalArgumentError: When unknown or illegal arguments are passed.
        """
        # Check whether the attachment property is uuid type or class `Property`
        if isinstance(attachment_property, Property):
            attachment_property_id = attachment_property.id
        elif isinstance(attachment_property, (string_types, text_type)) and is_uuid(attachment_property):
            attachment_property_id = attachment_property
            attachment_property = self._client.property(id=attachment_property_id)
        else:
            raise IllegalArgumentError("When using the add_attachment_viewer_widget, attachment_property must be a "
                                       "Property or Property id. Type is: {}".format(type(attachment_property)))

        # Check whether the `Property` has type `Attachment`
        property_type = attachment_property.type
        if property_type != PropertyType.ATTACHMENT_VALUE:
            raise IllegalArgumentError("When using the add_attachment_viewer_widget, attachment_property must have "
                                       "type {}. Type found: {}".format(PropertyType.ATTACHMENT_VALUE, property_type))

        # Check also whether `Property` has category `Instance`
        property_category = attachment_property._json_data['category']
        if property_category != Category.INSTANCE:
            raise IllegalArgumentError("When using the add_attachment_viewer_widget, attachment_property must have "
                                       "category {}. Category found: {}".format(Category.INSTANCE, property_category))

        # Add custom title
        if custom_title is False:
            show_title_value = "Default"
            title = attachment_property.name
        elif custom_title is None:
            show_title_value = "No title"
            title = ''
        else:
            show_title_value = "Custom Title"
            title = str(custom_title)

        # Declare attachment viewer widget config
        config = {
            'propertyId': attachment_property_id,
            'showTitleValue': show_title_value,
            'xtype': ComponentXType.PROPERTYATTACHMENTPREVIEWER,
            'title': title,
            'filter': {
                'activity_id': str(self.activity.id)
            },
            'height': height if height else 500
        }

        # Declare attachment viewer widget meta
        meta = {
            'propertyInstanceId': attachment_property_id,
            'activityId': str(self.activity.id),
            'customHeight': height if height else 500,
            'showTitleValue': show_title_value,
            'customTitle': title
        }

        self._add_widget(dict(config=config, meta=meta, name=WidgetNames.ATTACHMENTVIEWERWIDGET))

    def add_navigation_bar_widget(self, activities, alignment=NavigationBarAlignment.CENTER):
        """
        Add a KE-chain Navigation Bar (e.g. navigation bar widget) to the customization.

        The widget will be saved to KE-chain.

        :param activities: List of activities. Each activity must be a Python dict(), with the following keys:
            * customText: A custom text for each button in the attachment viewer widget: None (default): Task name;
            a String value: Custom text
            * emphasize: bool which determines if the button should stand-out or not - default(False)
            * activityId: class `Activity` or UUID
        :type activities: list
        :param alignment: The alignment of the buttons inside navigation bar
            * center (default): Center aligned
            * start: left aligned
        :type alignment: basestring (see :class:`enums.NavigationBarAlignment`)

        :raises IllegalArgumentError: When unknown or illegal arguments are passed.
        """
        # Loop through the list of activities
        set_of_expected_keys = {'activityId', 'customText', 'emphasize'}
        for activity_dict in activities:
            if set(activity_dict.keys()).issubset(set_of_expected_keys) and 'activityId' in set_of_expected_keys:
                # Check whether the activityId is class `Activity` or UUID
                activity = activity_dict['activityId']
                if isinstance(activity, (Activity, Activity2)):
                    activity_dict['activityId'] = activity.id
                elif isinstance(activity, (string_types, text_type)) and is_uuid(activity):
                    pass
                else:
                    raise IllegalArgumentError("When using the add_navigation_bar_widget, activityId must be an "
                                               "Activity or Activity id. Type is: {}".format(type(activity)))
                if 'customText' not in activity_dict.keys() or not activity_dict['customText']:
                    activity_dict['customText'] = str()
                if 'emphasize' not in activity_dict.keys():
                    activity_dict['emphasize'] = False

            else:
                raise IllegalArgumentError("Found unexpected key in activities. Only keys allowed are: {}".
                                           format(set_of_expected_keys))

        # Declare navigation bar widget config
        config = {
            'alignment': alignment,
            'xtype': ComponentXType.ACTIVITYNAVIGATIONBAR,
            'filter': {
                'activity_id': str(self.activity.id)
            },
            'taskButtons': activities
        }

        for activity_dict in activities:
            activity_id = activity_dict['activityId']
            activity_dict['name'] = self._client.activity(id=activity_id).name

        # Declare navigation bar widget meta
        meta = {
            'alignment': alignment,
            'activityId': str(self.activity.id),
            'taskButtons': activities
        }

        self._add_widget(dict(config=config, meta=meta, name=WidgetNames.TASKNAVIGATIONBARWIDGET))
