# -*- coding: utf-8 -*-

from itertools import chain
import datetime
import logging

from IPython.display import display
import ipywidgets as widgets

logger = logging.getLogger(__name__)


class ExpectationExplorer(object):
    def __init__(self):
        self.state = {
            "data_assets": {

            }
        }
        self.expectation_kwarg_field_names = {
            'expect_column_values_to_be_unique': ['mostly'],
            'expect_column_unique_value_count_to_be_between': ['min_value', 'max_value'],
            'expect_column_value_lengths_to_be_between': ['min_value', 'max_value', 'mostly'],
            'expect_table_row_count_to_be_between': ['min_value', 'max_value'],
            'expect_table_column_count_to_be_between': ['min_value', 'max_value'],
            'expect_column_proportion_of_unique_values_to_be_between': ['min_value', 'max_value'],
            'expect_column_median_to_be_between': ['min_value', 'max_value'],
            'expect_column_mean_to_be_between': ['min_value', 'max_value'],
            'expect_column_stdev_to_be_between': ['min_value', 'max_value'],
            'expect_column_quantile_values_to_be_between': ['quantile_ranges'],
            'expect_column_sum_to_be_between': ['min_value', 'max_value'],
            'expect_column_values_to_not_be_null': ['mostly'],
            'expect_column_values_to_be_null': ['mostly'],
            'expect_column_values_to_be_json_parseable': ['mostly'],
            'expect_column_values_to_be_dateutil_parseable': ['mostly'],
            'expect_column_values_to_be_increasing': ['strictly', 'parse_strings_as_datetimes', 'mostly'],
            'expect_column_values_to_be_decreasing': ['strictly', 'parse_strings_as_datetimes', 'mostly'],
            'expect_column_values_to_match_regex': ['regex', 'mostly'],
            'expect_column_values_to_not_match_regex': ['regex', 'mostly'],
            'expect_column_values_to_match_json_schema': ['json_schema', 'mostly'],
            'expect_column_values_to_match_regex_list': ['regex_list', 'match_on', 'mostly'],
            'expect_column_values_to_not_match_regex_list': ['regex_list', 'mostly'],
            'expect_column_values_to_be_in_set': ['value_set', 'parse_strings_as_datetimes', 'mostly'],
            'expect_column_values_to_not_be_in_set': ['value_set', 'parse_strings_as_datetimes', 'mostly'],
            'expect_column_distinct_values_to_be_in_set': ['value_set', 'parse_strings_as_datetimes'],
            'expect_column_distinct_values_to_equal_set': ['value_set', 'parse_strings_as_datetimes'],
            'expect_column_distinct_values_to_contain_set': ['value_set', 'parse_strings_as_datetimes'],
            'expect_column_most_common_value_to_be_in_set': ['value_set', 'ties_okay'],
            'expect_column_to_exist': ['column_index'],
            'expect_column_value_lengths_to_equal': ['value', 'mostly'],
            'expect_table_row_count_to_equal': ['value'],
            'expect_table_column_count_to_equal': ['value'],
            'expect_column_values_to_match_strftime_format': ['strftime_format', 'mostly'],
            'expect_column_values_to_be_between': ['min_max_type', 'parse_strings_as_datetimes', 'output_strftime_format', 'min_value', 'max_value', 'mostly', 'allow_cross_type_comparisons'],
            'expect_column_max_to_be_between': ['min_max_type', 'parse_strings_as_datetimes', 'output_strftime_format', 'min_value', 'max_value'],
            'expect_column_min_to_be_between': ['min_max_type', 'parse_strings_as_datetimes', 'output_strftime_format', 'min_value', 'max_value'],
            'expect_table_columns_to_match_ordered_list': ['column_list'],
            ####
            'expect_column_pair_values_to_be_equal': ['ignore_row_if'],
            'expect_column_pair_values_A_to_be_greater_than_B': ['or_equal', 'ignore_row_if', 'allow_cross_type_comparisons'],
            'expect_column_pair_values_to_be_in_set': ['value_pairs_set', 'ignore_row_if'],
            'expect_multicolumn_values_to_be_unique': ['ignore_row_if'],
            'expect_column_values_to_be_of_type': ['type_', 'mostly'],
            'expect_column_values_to_be_in_type_list': ['type_list', 'mostly'],
            'expect_column_kl_divergence_to_be_less_than': ['partition_object', 'threshold', 'internal_weight_holdout', 'tail_weight_holdout'],
            'expect_column_chisquare_test_p_value_to_be_greater_than': ['partition_object', 'p'],
            'expect_column_bootstrapped_ks_test_p_value_to_be_greater_than': ['partition_object', 'p', 'bootstrap_samples', 'bootstrap_sample_size'],
            'expect_column_parameterized_distribution_ks_test_p_value_to_be_greater_than': ['distribution', 'p_value', 'params'],
            '_expect_column_values_to_be_in_type_list__aggregate': ['type_list'],
            '_expect_column_values_to_be_in_type_list__map': ['type_list']
        }
        self.kwarg_widget_exclusions = [
            'column', 'result_format', 'include_config']
        self.min_max_subtypes = {
            'integer': {
                'positive': [
                    'expect_column_unique_value_count_to_be_between',
                    'expect_column_value_lengths_to_be_between',
                    'expect_table_row_count_to_be_between'
                ],
                'unbounded': ['expect_column_median_to_be_between'],
            },
            'float': {
                'unit_interval': ['expect_column_proportion_of_unique_values_to_be_between'],
                'unbounded': [
                    'expect_column_mean_to_be_between',
                    'expect_column_stdev_to_be_between',
                    'expect_column_sum_to_be_between'
                ]
            },
            'mixed': {
                'unbounded': [
                    'expect_column_values_to_be_between',
                    'expect_column_min_to_be_between',
                    'expect_column_max_to_be_between'
                ]
            }
        }
        self.debug_view = widgets.Output()
        self.styles = {
            'description_width': {'description_width': '150px'}
        }

    def update_result(self, data_asset_name, new_result, column=None):
        new_success_value = new_result.get('success')
        expectation_type = new_result['expectation_config'].get('expectation_type')
        new_result_widgets = self.generate_expectation_result_detail_widgets(result=new_result.get('result', {}))
        new_border_color = 'green' if new_success_value else 'red'
        data_asset_expectations = self.state['data_assets'][data_asset_name]['expectations']

        if column:
            data_asset_expectations[column][expectation_type][
                'success'].value = "<span><strong>Success: </strong>{new_success_value}</span>".format(new_success_value=str(new_success_value))
            data_asset_expectations[column][expectation_type]['result_detail_widget']\
                .children = new_result_widgets
            data_asset_expectations[column][expectation_type]['editor_widget']\
                .layout\
                .border = "2px solid {new_border_color}".format(new_border_color=new_border_color)
        else:
            data_asset_expectations['non_column_expectations'][expectation_type][
                'success'].value = "<span><strong>Success: </strong>{new_success_value}</span>".format(new_success_value=str(new_success_value))
            data_asset_expectations['non_column_expectations'][expectation_type]['result_detail_widget']\
                .children = new_result_widgets
            data_asset_expectations['non_column_expectations'][expectation_type]['editor_widget']\
                .layout\
                .border = "2px solid {new_border_color}".format(new_border_color=new_border_color)

    def get_expectation_state(self, data_asset_name, expectation_type, column=None):
        data_asset_state = self.state['data_assets'].get(data_asset_name, {})
        data_asset_expectations = data_asset_state.get(
            'expectations', {})

        if column:
            column_expectations = data_asset_expectations.get(column)
            if not column_expectations:
                return None
            return column_expectations.get(expectation_type)
        else:
            non_column_expectations = data_asset_expectations.get(
                'non_column_expectations')
            if not non_column_expectations:
                return None
            return non_column_expectations.get(expectation_type)

    def initialize_data_asset_state(self, data_asset):
        data_asset_name = data_asset.get_data_asset_name()

        self.state['data_assets'][data_asset_name] = {
            "data_asset": data_asset,
            "expectations": {}
        }

    def set_expectation_state(self, data_asset, expectation_state, column=None):
        data_asset_name = data_asset.get_data_asset_name()
        expectation_type = expectation_state.get('expectation_type')
        data_asset_state = self.state['data_assets'].get(data_asset_name)

        data_asset_expectations = data_asset_state.get('expectations')

        if column:
            column_expectations = data_asset_expectations.get(column, {})
            column_expectations[expectation_type] = expectation_state
            data_asset_expectations[column] = column_expectations
        else:
            non_column_expectations = data_asset_expectations.get(
                'non_column_expectations', {})
            non_column_expectations[expectation_type] = expectation_state
            data_asset_expectations['non_column_expectations'] = non_column_expectations

    # interconvert expectation kwargs
    def expectation_kwarg_dict_to_ge_kwargs(self, kwarg_dict):
        # kwarg_exclusions are non-ge keys which contain ancillary widgets required for proper parsing
        # for e.g., a widget for setting the date type of min/max
        ancillary_kwargs = ['min_max_type']
        expectation_kwargs = {}

        for kwarg_name, widget_dict in kwarg_dict.items():
            if kwarg_name == 'column':
                expectation_kwargs['column'] = widget_dict
                continue
            if kwarg_name in ancillary_kwargs:
                continue
            elif not hasattr(self, "generate_{kwarg_name}_widget_dict".format(kwarg_name=kwarg_name)):
                expectation_kwargs[kwarg_name] = widget_dict.get('ge_kwarg_value')
            else:
                expectation_kwargs[kwarg_name] = widget_dict.get(
                    'ge_kwarg_value') if 'ge_kwarg_value' in widget_dict else widget_dict['kwarg_widget'].value

        return expectation_kwargs

    def update_kwarg_widget_dict(self, expectation_state, current_widget_dict, ge_kwarg_name, new_ge_kwarg_value):
        def update_tag_list_widget_dict(widget_dict, new_list):
            widget_dict['ge_kwarg_value'] = new_list
            widget_display = widget_dict['widget_display']
            widget_display.children = self.generate_tag_button_list(
                expectation_state=expectation_state,
                tag_list=new_list,
                widget_display=widget_display
            )
        
        def ge_number_to_widget_string(widget_dict, number_kwarg):
            if hasattr(widget_dict['kwarg_widget'], 'value'):
                widget_dict['kwarg_widget'].value = str(number_kwarg) if number_kwarg or number_kwarg == 0 else ''
            if widget_dict.get('ge_kwarg_value'):
                widget_dict['ge_kwarg_value'] = str(number_kwarg) if number_kwarg or number_kwarg == 0 else ''

        def min_max_value_to_string(widget_dict, number_kwarg):
            # expectations with min/max kwargs where widget expects a string
            number_to_string_expectations = [
                'expect_column_values_to_be_between',
                'expect_column_max_to_be_between',
                'expect_column_min_to_be_between',
            ]

            if expectation_state.get('expectation_type') in number_to_string_expectations:
                ge_number_to_widget_string(widget_dict, number_kwarg)

        updater_functions = {
            'regex_list': update_tag_list_widget_dict,
            'value_set': update_tag_list_widget_dict,
            'column_list': update_tag_list_widget_dict,
            'column_index': ge_number_to_widget_string,
            'min_value': min_max_value_to_string,
            'max_value': min_max_value_to_string
        }
        updater_functions.get(
            ge_kwarg_name,
            lambda current_widget_dict, ge_kwarg_value: setattr(current_widget_dict['kwarg_widget'], 'value', ge_kwarg_value)
        )(current_widget_dict, new_ge_kwarg_value)

    def update_expectation_state(self, existing_expectation_state, expectation_validation_result, validation_time):
        expectation_editor_widget = existing_expectation_state.get(
            'editor_widget')
        new_ge_expectation_kwargs = expectation_validation_result['expectation_config']['kwargs']
        current_expectation_kwarg_dict = existing_expectation_state['kwargs']
        column = current_expectation_kwarg_dict.get('column')
        data_asset_name = existing_expectation_state.get('data_asset_name')

        for ge_kwarg_name, ge_kwarg_value in new_ge_expectation_kwargs.items():
            if ge_kwarg_name in self.kwarg_widget_exclusions:
                continue
            
            current_widget_dict = current_expectation_kwarg_dict.get(ge_kwarg_name)

            if current_widget_dict:
                if not hasattr(self, "generate_{ge_kwarg_name}_widget_dict".format(ge_kwarg_name=ge_kwarg_name)):
                    current_expectation_kwarg_dict[ge_kwarg_name] = self.generate_expectation_kwarg_fallback_widget_dict(
                        expectation_kwarg_name=ge_kwarg_name, **new_ge_expectation_kwargs)
                else:
                    self.update_kwarg_widget_dict(
                        expectation_state=existing_expectation_state,
                        current_widget_dict=current_widget_dict,
                        ge_kwarg_name=ge_kwarg_name,
                        new_ge_kwarg_value=ge_kwarg_value
                    )
            else:
                widget_dict_generator = getattr(
                    self, "generate_{ge_kwarg_name}_widget_dict".format(ge_kwarg_name=ge_kwarg_name), None)
                widget_dict = widget_dict_generator(expectation_state=existing_expectation_state, **new_ge_expectation_kwargs) if widget_dict_generator \
                    else self.generate_expectation_kwarg_fallback_widget_dict(expectation_kwarg_name=ge_kwarg_name, **new_ge_expectation_kwargs)
                current_expectation_kwarg_dict[ge_kwarg_name] = widget_dict
                expectation_editor_widget.children[0].children[1].children += (
                    widget_dict['kwarg_widget'],)

        self.update_result(data_asset_name=data_asset_name, new_result=expectation_validation_result, column=column)
        # TODO: This is messy. Figure out better way of storing/accessing UI elements
        validation_time_widget = expectation_editor_widget.children[0].children[0].children[0].children[-1]
        validation_time_widget.value = "<div><strong>Date/Time Validated (UTC): </strong>{validation_time}</div>".format(validation_time=validation_time)
        return expectation_editor_widget

    # widget generators for general input fields
    def generate_boolean_checkbox_widget(self, value, description='', description_tooltip='', disabled=False):
        return widgets.Checkbox(
            value=value,
            description=description,
            style=self.styles.get('description_width'),
            layout={'width': '400px'},
            description_tooltip=description_tooltip,
            disabled=disabled
        )

    def generate_text_area_widget(self, value, description='', description_tooltip='', continuous_update=False, placeholder=''):
        return widgets.Textarea(
            value=value,
            placeholder=placeholder, 
            description="<strong>{description}: </strong>".format(description=description),
            style=self.styles.get('description_width'),
            layout={'width': '400px'},
            description_tooltip=description_tooltip,
            continuous_update=continuous_update
        )

    def generate_text_widget(self, value, description='', description_tooltip='', continuous_update=False, placeholder='', disabled=False):
        return widgets.Text(
            value=value,
            placeholder=placeholder,
            description="<strong>{description}: </strong>".format(description=description),
            style=self.styles.get('description_width'),
            layout={'width': '400px'},
            description_tooltip=description_tooltip,
            continuous_update=continuous_update,
            disabled=disabled
        )

    def generate_radio_buttons_widget(self, value, options=[], description='', description_tooltip=''):
        return widgets.RadioButtons(
            options=options,
            value=value,
            description="<strong>{description}: </strong>".format(description=description),
            layout={'width': '400px'},
            style=self.styles.get('description_width'),
            description_tooltip=description_tooltip
        )

    def generate_remove_expectation_button(self, expectation_state):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)['data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']

        column = expectation_state['kwargs'].get('column')

        remove_expectation_button = widgets.Button(
            description='Remove Expectation',
            button_style='danger',
            tooltip='click to remove expectation',
            icon='trash',
            layout={'width': 'auto'}
        )

        @expectation_feedback_widget.capture(clear_output=True)
        def on_click(button):
            editor_widget = expectation_state.get('editor_widget')
            expectation = data_asset.remove_expectation(
                expectation_type=expectation_type, column=column)
            data_asset_state = self.state['data_assets'].get(data_asset_name, {})
            data_asset_expectations = data_asset_state.get('expectations')

            if column:
                column_expectations = data_asset_expectations.get(column)
                column_expectations.pop(expectation_type)
            else:
                non_column_expectations = data_asset_expectations.get(
                    'non_column_expectations')
                non_column_expectations.pop(expectation_type)

            editor_widget.close()

        remove_expectation_button.on_click(on_click)

        return remove_expectation_button

    def generate_tag_button(self, expectation_state, tag, tag_list, widget_display):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']
        tag_button = widgets.Button(
            description=str(tag),
            button_style='info',
            tooltip='click to delete',
            icon='trash',
            layout={'width': 'auto'}
        )

        @expectation_feedback_widget.capture(clear_output=True)
        def on_click(button):
            if len(tag_list) == 1:
                with expectation_feedback_widget:
                    print('Warning: cannot remove - set must have at least one member')
                return
            tag_list.remove(tag)

            display_children = list(widget_display.children)
            display_children.remove(tag_button)
            widget_display.children = display_children

            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)

        tag_button.on_click(on_click)

        return tag_button

    def generate_tag_button_list(self, expectation_state, tag_list, widget_display):
        return [
            self.generate_tag_button(
                expectation_state=expectation_state,
                tag=tag,
                tag_list=tag_list,
                widget_display=widget_display
            ) for tag in tag_list
        ] if tag_list else []

    def generate_zero_or_positive_integer_widget(self, value, max=int(9e300), description='', continuous_update=False):
        return widgets.BoundedIntText(
            value=value,
            min=0,
            max=max,
            description="<strong>{description}: </strong>".format(description=description),
            style=self.styles.get('description_width'),
            layout={'width': '400px'},
            continuous_update=continuous_update
        )

    # widget dict generators for kwarg input fields
    def generate_output_strftime_format_widget_dict(self, expectation_state, output_strftime_format='', column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']

        parse_strings_as_datetimes = expectation_kwargs.get('parse_strings_as_datetimes', False)

        output_strftime_format_widget = self.generate_text_widget(
            value=output_strftime_format,
            description='output strftime format',
            placeholder='press enter to confirm...',
            description_tooltip='Enter a valid strfime format for datetime output.',
            disabled=not parse_strings_as_datetimes
        )

        widget_dict = {
            'kwarg_widget': output_strftime_format_widget
        }

        @expectation_feedback_widget.capture(clear_output=True)
        def on_output_strftime_format_submit(widget):
            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)

        output_strftime_format_widget.on_submit(on_output_strftime_format_submit)

        return widget_dict

    def generate_strftime_format_widget_dict(self, expectation_state, strftime_format='', column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']

        strftime_format_widget = self.generate_text_widget(
            value=strftime_format,
            description='strftime format',
            placeholder='press enter to confirm...'
        )

        widget_dict = {
            'kwarg_widget': strftime_format_widget
        }

        @expectation_feedback_widget.capture(clear_output=True)
        def on_strftime_format_submit(widget):
            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)

        strftime_format_widget.on_submit(on_strftime_format_submit)

        return widget_dict

    def generate_value_widget_dict(self, expectation_state, value=None, column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']

        value_widget = self.generate_zero_or_positive_integer_widget(
            value=value, description='value')

        @expectation_feedback_widget.capture(clear_output=True)
        def on_value_change(change):
            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)

        value_widget.observe(on_value_change, names='value')

        widget_dict = {
            'kwarg_widget': value_widget
        }

        return widget_dict

    def generate_json_schema_widget_dict(self, expectation_state, json_schema='', column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']

        json_schema_widget = self.generate_text_area_widget(value=json_schema, description='json schema', placeholder='enter json schema')

        @expectation_feedback_widget.capture(clear_output=True)
        def on_json_schema_change(change):
            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)

        json_schema_widget.observe(
            on_json_schema_change, names='value')

        widget_dict = {
            'kwarg_widget': json_schema_widget
        }

        return widget_dict

    def generate_ties_okay_widget_dict(self, expectation_state, ties_okay=None, column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']
        ties_okay_widget = self.generate_boolean_checkbox_widget(
            value=ties_okay,
            description='ties_okay',
            description_tooltip='If True, then the expectation will still succeed if values outside the designated set are as common (but not more common) than designated values.'
        )

        @expectation_feedback_widget.capture(clear_output=True)
        def on_ties_okay_change(change):
            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)

        ties_okay_widget.observe(
            on_ties_okay_change, names='value')

        widget_dict = {
            'kwarg_widget': ties_okay_widget
        }

        return widget_dict

    def generate_match_on_widget_dict(self, expectation_state, match_on='any', column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']
        match_on_widget = self.generate_radio_buttons_widget(
            value=match_on,
            options=['any', 'all'],
            description='match_on',
            description_tooltip='Use “any” if the value should match at least one regular expression in the list. Use “all” if it should match each regular expression in the list.'
        )

        @expectation_feedback_widget.capture(clear_output=True)
        def on_match_on_change(change):
            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)

        match_on_widget.observe(
            on_match_on_change, names='value')

        widget_dict = {
            'kwarg_widget': match_on_widget
        }

        return widget_dict

    def generate_regex_list_widget_dict(self, expectation_state, regex_list=[], column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']
        regex_list_widget_display = widgets.Box(
            layout={
                'display': 'flex',
                'flex_flow': 'row wrap'
            }
        )
        regex_list_widget_input = self.generate_text_widget(value=None, description='add regex', placeholder='press enter to add...', continuous_update=True)
        regex_list_label = widgets.HTML(
            value='<div style="text-align:right"><strong>regex list: </strong></div>', layout={'width': '150px'})
        regex_list_widget = widgets.VBox([widgets.HBox([regex_list_label, regex_list_widget_display]), regex_list_widget_input])

        widget_dict = {
            'kwarg_widget': regex_list_widget,
            'ge_kwarg_value': regex_list,
            'widget_display': regex_list_widget_display,
            'widget_input': regex_list_widget_input
        }

        regex_list_widget_display.children = self.generate_tag_button_list(
            expectation_state=expectation_state,
            tag_list=regex_list,
            widget_display=regex_list_widget_display
        )

        @expectation_feedback_widget.capture(clear_output=True)
        def on_regex_list_input_submit(widget):
            widget_display = expectation_state['kwargs']['regex_list']['widget_display']
            regex_list = expectation_state['kwargs']['regex_list']['ge_kwarg_value']
            new_regex = widget.value

            if new_regex in regex_list:
                with expectation_feedback_widget:
                    print('Warning: regex already exists in set')
                return

            regex_list.append(new_regex)
            widget_display.children += (self.generate_tag_button(
                tag=new_regex,
                expectation_state=expectation_state,
                tag_list=regex_list,
                widget_display=widget_display), 
            )
            widget.value = ''

            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)
        regex_list_widget_input.on_submit(on_regex_list_input_submit)

        return widget_dict

    def generate_column_list_widget_dict(self, expectation_state, column_list=[], column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']
        column_list_widget_display = widgets.Box(
            layout={
                'display': 'flex',
                'flex_flow': 'row wrap'
            }
        )
        column_list_widget_input = self.generate_text_widget(
            value=None, description='add column', placeholder='press enter to add...', continuous_update=True)
        column_list_label = widgets.HTML(
            value='<div style="text-align:right"><strong>column list: </strong></div>', layout={'width': '150px'})
        column_list_widget = widgets.VBox([widgets.HBox(
            [column_list_label, column_list_widget_display]), column_list_widget_input])

        widget_dict = {
            'kwarg_widget': column_list_widget,
            'ge_kwarg_value': column_list,
            'widget_display': column_list_widget_display,
            'widget_input': column_list_widget_input
        }

        column_list_widget_display.children = self.generate_tag_button_list(
            expectation_state=expectation_state,
            tag_list=column_list,
            widget_display=column_list_widget_display
        )

        @expectation_feedback_widget.capture(clear_output=True)
        def on_column_list_input_submit(widget):
            widget_display = expectation_state['kwargs']['column_list']['widget_display']
            column_list = expectation_state['kwargs']['column_list']['ge_kwarg_value']
            new_column = widget.value

            if new_column in column_list:
                with expectation_feedback_widget:
                    print('Warning: column already exists in list')
                return

            column_list.append(new_column)
            widget_display.children += (self.generate_tag_button(
                tag=new_column,
                expectation_state=expectation_state,
                tag_list=column_list,
                widget_display=widget_display),
            )
            widget.value = ''

            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)
        column_list_widget_input.on_submit(on_column_list_input_submit)

        return widget_dict

    def generate_column_index_widget_dict(self, expectation_state, column, column_index='', **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']
        
        column_index_widget = self.generate_text_widget(value=str(column_index), description='column_index', continuous_update=True, placeholder='press enter to confirm...')

        widget_dict = {
            'kwarg_widget': column_index_widget,
            'ge_kwarg_value': column_index
        }

        @expectation_feedback_widget.capture(clear_output=True)
        def on_column_index_submit(widget):
            new_column_index = widget.value if widget.value else None
            if new_column_index:
                if '.' in new_column_index or '-' in new_column_index:
                    with expectation_feedback_widget:
                        print('Warning: column_index must be an integer >= 0')
                    return
                try:
                    new_column_index = int(new_column_index)
                except ValueError:
                    with expectation_feedback_widget:
                        print('Warning: column_index must be an integer >= 0')
                    return

            widget_dict['ge_kwarg_value'] = new_column_index

            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)

        column_index_widget.on_submit(on_column_index_submit)

        return widget_dict

    def generate_regex_widget_dict(self, expectation_state, regex='', column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']
        regex_widget = self.generate_text_area_widget(
            value=regex, description='regex', placeholder='e.g. ([A-Z])\w+')

        @expectation_feedback_widget.capture(clear_output=True)
        def on_regex_change(change):
            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)

        regex_widget.observe(
            on_regex_change, names='value')
        
        widget_dict = {
            'kwarg_widget': regex_widget
        }

        return widget_dict

    def generate_parse_strings_as_datetimes_widget_dict(
            self, expectation_state, parse_strings_as_datetimes=None, column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']
        min_max_type_widget_dict = expectation_state['kwargs'].get('min_max_type', {})
        min_max_type_widget = min_max_type_widget_dict.get('kwarg_widget')
        min_max_type = min_max_type_widget.value if min_max_type_widget else None
        parse_strings_as_datetimes_widget = self.generate_boolean_checkbox_widget(
            value=parse_strings_as_datetimes,
            description='parse strings as datetimes',
            disabled=False if min_max_type == 'string' or not min_max_type else True
        )

        @expectation_feedback_widget.capture(clear_output=True)
        def on_parse_strings_as_datetimes_change(change):
            output_strftime_format_widget_dict = expectation_state['kwargs'].get('output_strftime_format', {})
            output_strftime_format_widget = output_strftime_format_widget_dict.get('kwarg_widget')
            if output_strftime_format_widget:
                output_strftime_format_widget.disabled = not change['new']

            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)

        parse_strings_as_datetimes_widget.observe(
            on_parse_strings_as_datetimes_change, names='value')

        widget_dict = {
            'kwarg_widget': parse_strings_as_datetimes_widget
        }

        return widget_dict
 
    def generate_strictly_widget_dict(self, expectation_state, strictly=None, column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']
        inc_dec = expectation_type.split('_')[-1]
        strictly_widget = self.generate_boolean_checkbox_widget(
            value=strictly,
            description="strictly {inc_dec}".format(inc_dec=inc_dec)
        )

        @expectation_feedback_widget.capture(clear_output=True)
        def on_strictly_change(change):
            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)

        strictly_widget.observe(on_strictly_change, names='value')

        widget_dict = {
            'kwarg_widget': strictly_widget
        }

        return widget_dict

    def generate_mostly_widget_dict(self, expectation_state, mostly=1, column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']
        mostly_widget = widgets.FloatSlider(
            value=mostly,
            min=0,
            max=1.0,
            step=0.01,
            description='<strong>mostly: </strong>',
            style=self.styles.get('description_width'),
            layout={'width': '400px'},
            continuous_update=True,
            orientation='horizontal',
            readout=True,
            readout_format='.2f'
        )

        @expectation_feedback_widget.capture(clear_output=True)
        def on_mostly_change(change):
            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)

        mostly_widget.observe(on_mostly_change, names='value')
        widget_dict = {
            'kwarg_widget': mostly_widget
        }

        return widget_dict
    
    def generate_min_max_type_widget_dict(self, expectation_state, type_option=None, column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']

        min_value = expectation_kwargs.get('min_value')
        min_value_type = type(min_value)

        max_value = expectation_kwargs.get('max_value')
        max_value_type = type(max_value)

        number_types = [int, float]
        if min_value_type in number_types or max_value_type in number_types:
            type_option = 'number'
        elif min_value_type is str or max_value_type is str:
            type_option = 'string'

        min_max_type_widget = self.generate_radio_buttons_widget(
            value=type_option, options=['string', 'number'], description='min/max type')

        @expectation_feedback_widget.capture(clear_output=True)
        def on_min_max_type_change(change):
            new_type_selection = change.get('new')

            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])
            min_value = ge_expectation_kwargs.get('min_value')
            max_value = ge_expectation_kwargs.get('max_value')

            parse_strings_as_datetimes_widget_dict = expectation_state['kwargs'].get(
                'parse_strings_as_datetimes', {})
            parse_strings_as_datetimes_widget = parse_strings_as_datetimes_widget_dict.get(
                'kwarg_widget')

            if new_type_selection == 'string':
                ge_expectation_kwargs['min_value'] = str(min_value) if min_value else None
                ge_expectation_kwargs['max_value'] = str(max_value) if max_value else None

                if parse_strings_as_datetimes_widget:
                    parse_strings_as_datetimes_widget.disabled = False
            elif new_type_selection == 'number':
                ge_expectation_kwargs['min_value'] = float(min_value) if min_value else None
                ge_expectation_kwargs['max_value'] = float(max_value) if max_value else None

                if parse_strings_as_datetimes_widget:
                    parse_strings_as_datetimes_widget.value = False
                    parse_strings_as_datetimes_widget.disabled = True

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)

        min_max_type_widget.observe(on_min_max_type_change, names='value')

        return {
            'kwarg_widget': min_max_type_widget
        }

    def generate_min_value_widget_dict(self, expectation_state, min_value=None, column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']
        min_value_widget_dict = expectation_state['kwargs'].get('min_value', {})
        min_value_widget = min_value_widget_dict.get('kwarg_widget') if min_value_widget_dict else None
        max_value_widget_dict = expectation_state['kwargs'].get('max_value', {})
        max_value_widget = max_value_widget_dict.get('kwarg_widget') if max_value_widget_dict else None

        # integer
        if expectation_type in list(chain.from_iterable(self.min_max_subtypes['integer'].values())):
            if expectation_type in self.min_max_subtypes['integer']['positive']:
                default_min_value = 0
                default_max_value = int(9e300)
            elif expectation_type in self.min_max_subtypes['integer']['unbounded']:
                default_min_value = -int(9e300)
                default_max_value = int(9e300)

            if min_value_widget:
                min_value_widget.value = min_value if min_value else default_min_value
            else:
                min_value_widget = widgets.BoundedIntText(
                    value=min_value or default_min_value,
                    min=default_min_value,
                    description='min_value: ',
                    disabled=False
                )
            if not max_value_widget:
                max_value_widget = widgets.BoundedIntText(
                    description='max_value: ',
                    value=default_max_value,
                    max=default_max_value,
                    disabled=False
                )
        # float
        elif expectation_type in list(chain.from_iterable(self.min_max_subtypes['float'].values())):
            if expectation_type in self.min_max_subtypes['float']['unit_interval']:
                default_min_value = 0
                default_max_value = 1
            elif expectation_type in self.min_max_subtypes['float']['unbounded']:
                default_min_value = -int(9e300)
                default_max_value = int(9e300)

            if min_value_widget:
                min_value_widget.value = min_value if min_value else default_min_value
            else:
                min_value_widget = widgets.BoundedFloatText(
                    value=min_value or default_min_value,
                    min=default_min_value,
                    step=0.01,
                    description='min_value: ',
                    disabled=False
                )
            if not max_value_widget:
                max_value_widget = widgets.BoundedFloatText(
                    description='max_value: ',
                    value=default_max_value,
                    max=default_max_value,
                    step=0.01,
                    disabled=False
                )
        # number or string
        elif expectation_type in list(chain.from_iterable(self.min_max_subtypes['mixed'].values())):
            min_value_str = str(min_value) if min_value or min_value == 0 else ''
            if min_value_widget:
                min_value_widget.value = min_value
            else:
                min_value_widget = self.generate_text_widget(
                    value=min_value_str,
                    description='min_value',
                    placeholder='press enter to confirm...'
                )
 
            @expectation_feedback_widget.capture(clear_output=True)
            def on_min_value_change(change):
                min_max_type = expectation_state['kwargs']['min_max_type']['kwarg_widget'].value
                ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                    expectation_state['kwargs'])
                min_value = ge_expectation_kwargs.get('min_value')
                max_value = ge_expectation_kwargs.get('max_value')

                if min_max_type == 'string':
                    min_value = str(min_value) if min_value else None
                    max_value = str(max_value) if max_value else None
                elif min_max_type == 'number':
                    min_value = float(min_value) if min_value else None
                    max_value = float(max_value) if max_value else None

                if min_value and max_value and min_value > max_value:
                    with expectation_feedback_widget:
                        print('Error: min_value must be less than or equal to max_value.')
                    return

                ge_expectation_kwargs['min_value'] = min_value
                ge_expectation_kwargs['max_value'] = max_value

                getattr(data_asset, expectation_type)(include_config=True,
                                                 **ge_expectation_kwargs)

            min_value_widget.observe(on_min_value_change, names='value')

        if min_value_widget and max_value_widget and expectation_type not in list(chain.from_iterable(self.min_max_subtypes['mixed'].values())):
            @expectation_feedback_widget.capture(clear_output=True)
            def on_min_value_change(change):
                ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                    expectation_state['kwargs'])
                getattr(data_asset, expectation_type)(include_config=True,
                    **ge_expectation_kwargs)

            min_value_widget.observe(on_min_value_change, names='value')
            max_dl = widgets.link((max_value_widget, 'value'),
                                (min_value_widget, 'max'))
            expectation_state['kwargs']['max_value'] = {'kwarg_widget': max_value_widget}

        if min_value_widget:
            min_value_widget_dict['kwarg_widget'] = min_value_widget
        else:
            min_value_widget_dict = self.generate_expectation_kwarg_fallback_widget_dict(expectation_kwarg_name='min_value', **{'min_value': min_value})

        expectation_state['kwargs']['min_value'] = min_value_widget_dict

        return min_value_widget_dict

    def generate_max_value_widget_dict(self, expectation_state, max_value=None, column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']
        min_value_widget_dict = expectation_state['kwargs'].get('min_value')
        min_value_widget = min_value_widget_dict.get('kwarg_widget') if min_value_widget_dict else None
        max_value_widget_dict = expectation_state['kwargs'].get('max_value')
        max_value_widget = max_value_widget_dict.get('kwarg_widget') if max_value_widget_dict else None

        # integer
        if expectation_type in list(chain.from_iterable(self.min_max_subtypes['integer'].values())):
            if expectation_type in self.min_max_subtypes['integer']['positive']:
                default_min_value = 0
                default_max_value = int(9e300)
            elif expectation_type in self.min_max_subtypes['integer']['unbounded']:
                default_min_value = -int(9e300)
                default_max_value = int(9e300)

            if max_value_widget:
                max_value_widget.value = max_value if max_value else default_max_value
            else:
                max_value_widget = widgets.BoundedIntText(
                    value=max_value or default_max_value,
                    max=default_max_value,
                    description='max_value: ',
                    disabled=False
                )
            if not min_value_widget:
                min_value_widget = widgets.BoundedIntText(
                    min=default_min_value,
                    value=default_min_value,
                    description='min_value: ',
                    disabled=False
                )
        # float
        elif expectation_type in list(chain.from_iterable(self.min_max_subtypes['float'].values())):
            if expectation_type in self.min_max_subtypes['float']['unit_interval']:
                default_min_value = 0
                default_max_value = 1
            elif expectation_type in self.min_max_subtypes['float']['unbounded']:
                default_min_value = -int(9e300)
                default_max_value = int(9e300)
            
            if max_value_widget:
                max_value_widget.value = max_value if max_value else default_max_value
            else:
                max_value_widget = widgets.BoundedFloatText(
                    value=max_value or default_max_value,
                    max=default_max_value,
                    step=0.01,
                    description='max_value: ',
                    disabled=False
                )
            if not min_value_widget:
                min_value_widget = widgets.BoundedFloatText(
                    min=default_min_value,
                    step=0.01,
                    value=default_min_value,
                    description='min_value: ',
                    disabled=False
                )
        # number or string
        elif expectation_type in list(chain.from_iterable(self.min_max_subtypes['mixed'].values())):
            max_value_str = str(max_value) if max_value or max_value == 0 else ''
            if max_value_widget:
                max_value_widget.value = max_value
            else:
                max_value_widget = self.generate_text_widget(
                    value=max_value_str,
                    description='max_value',
                    placeholder='press enter to confirm...'
                )

            @expectation_feedback_widget.capture(clear_output=True)
            def on_max_value_change(change):
                min_max_type = expectation_state['kwargs']['min_max_type']['kwarg_widget'].value
                ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                    expectation_state['kwargs'])
                min_value = ge_expectation_kwargs.get('min_value')
                max_value = ge_expectation_kwargs.get('max_value')

                if min_max_type == 'string':
                    min_value = str(min_value) if min_value else None
                    max_value = str(max_value) if max_value else None
                elif min_max_type == 'number':
                    min_value = float(min_value) if min_value else None
                    max_value = float(max_value) if max_value else None

                if min_value and max_value and min_value > max_value:
                    with expectation_feedback_widget:
                        print(
                            'Error: min_value must be less than or equal to max_value.')
                    return

                ge_expectation_kwargs['min_value'] = min_value
                ge_expectation_kwargs['max_value'] = max_value

                getattr(data_asset, expectation_type)(include_config=True,
                                                 **ge_expectation_kwargs)

            max_value_widget.observe(on_max_value_change, names='value')

        if min_value_widget and max_value_widget and expectation_type not in list(chain.from_iterable(self.min_max_subtypes['mixed'].values())):
            @expectation_feedback_widget.capture(clear_output=True)
            def on_max_value_change(change):
                ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                    expectation_state['kwargs'])
                getattr(data_asset, expectation_type)(include_config=True,
                    **ge_expectation_kwargs)

            max_value_widget.observe(on_max_value_change, names='value')
            min_dl = widgets.link((min_value_widget, 'value'),
                                (max_value_widget, 'min'))
            expectation_state['kwargs']['min_value'] = {'kwarg_widget': min_value_widget}

        max_value_widget_dict = {'kwarg_widget': max_value_widget} if max_value_widget else self.generate_expectation_kwarg_fallback_widget_dict(
            expectation_kwarg_name='max_value', **{'max_value': max_value}
        )
        expectation_state['kwargs']['max_value'] = max_value_widget_dict

        return max_value_widget_dict

    def generate_value_set_widget_dict(self, expectation_state, value_set=[], column=None, **expectation_kwargs):
        data_asset_name = expectation_state['data_asset_name']
        data_asset = self.state['data_assets'].get(data_asset_name)[
            'data_asset']
        expectation_feedback_widget = expectation_state['expectation_feedback_widget']
        expectation_type = expectation_state['expectation_type']

        value_set_widget_display = widgets.Box()

        value_set_widget_number_input = self.generate_text_widget(
            value=None, description='add number', placeholder='press enter to add number...', continuous_update=True)
        value_set_widget_string_input = self.generate_text_widget(
            value=None, description='add string', placeholder='press enter to add string...', continuous_update=True)
        value_set_widget_input = widgets.VBox([value_set_widget_string_input, value_set_widget_number_input])
        value_set_label = widgets.HTML(value='<div style="text-align:right"><strong>value set: </strong></div>', layout={'width': '150px'})
        value_set_widget = widgets.VBox([widgets.HBox([value_set_label, value_set_widget_display]), value_set_widget_input])

        widget_dict = {
            'kwarg_widget': value_set_widget,
            'ge_kwarg_value': value_set,
            'widget_display': value_set_widget_display,
            'widget_input': value_set_widget_input
        }

        value_set_widget_display.children = self.generate_tag_button_list(
            expectation_state=expectation_state,
            tag_list=value_set,
            widget_display=value_set_widget_display
        )

        @expectation_feedback_widget.capture(clear_output=True)
        def on_value_set_number_input_submit(widget):
            widget_display = expectation_state['kwargs']['value_set']['widget_display']
            value_set = expectation_state['kwargs']['value_set']['ge_kwarg_value']
            new_value = widget.value

            new_value = float(new_value) if '.' in new_value else int(new_value)

            if new_value in value_set:
                with expectation_feedback_widget:
                    print('Warning: value already exists in set')
                return

            value_set.append(new_value)
            widget_display.children += (
                self.generate_tag_button(
                    tag=new_value,
                    expectation_state=expectation_state,
                    tag_list=value_set,
                    widget_display=widget_display
                ),
            )
            widget.value = ''

            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(
                include_config=True, **ge_expectation_kwargs)

        @expectation_feedback_widget.capture(clear_output=True)
        def on_value_set_string_input_submit(widget):
            widget_display = expectation_state['kwargs']['value_set']['widget_display']
            value_set = expectation_state['kwargs']['value_set']['ge_kwarg_value']
            new_value = widget.value

            if new_value in value_set:
                with expectation_feedback_widget:
                    print('Warning: value already exists in set')
                return

            value_set.append(new_value)
            widget_display.children += (
                self.generate_tag_button(
                    tag=new_value,
                    expectation_state=expectation_state,
                    tag_list=value_set,
                    widget_display=widget_display
                ),
            )
            widget.value = ''

            ge_expectation_kwargs = self.expectation_kwarg_dict_to_ge_kwargs(
                expectation_state['kwargs'])

            getattr(data_asset, expectation_type)(include_config=True, **ge_expectation_kwargs)

        value_set_widget_number_input.on_submit(on_value_set_number_input_submit)
        value_set_widget_string_input.on_submit(on_value_set_string_input_submit)

        return widget_dict

    def generate_expectation_kwarg_fallback_widget_dict(self, expectation_kwarg_name, **expectation_kwargs):
        ge_kwarg_value = expectation_kwargs.get(
            expectation_kwarg_name)
        warning_message = widgets.HTML(
            value='<div><strong>Warning: </strong>No input widget for kwarg "{expectation_kwarg_name}". To change value, call expectation again with the modified value.</div>'.format(expectation_kwarg_name=expectation_kwarg_name),
            layout={
                'width': '400px',
                'margin': '0px 0px 0px 10px'
            }
        )
        static_widget = widgets.Textarea(
            value=str(ge_kwarg_value),
            description="<strong>{expectation_kwarg_name}: </strong>".format(expectation_kwarg_name=expectation_kwarg_name),
            style=self.styles.get('description_width'),
            layout={'width': '400px'},
            disabled=True
        )
        widget_dict = {
            'kwarg_widget': widgets.HBox([static_widget, warning_message]),
            'ge_kwarg_value': ge_kwarg_value
        }
        
        return widget_dict

    # widget generators for general info shared between all expectations
    def generate_column_widget(self, column=None):
        return widgets.HTML(value="<div><strong>Column: </strong>{column}</div>".format(column=column)) if column else None

    def generate_expectation_type_widget(self, expectation_type):
        return widgets.HTML(value="<span><strong>Expectation Type: </strong>{expectation_type}</span>".format(expectation_type=expectation_type))

    def generate_basic_expectation_info_box(
        self, data_asset_name, expectation_type, success_widget, validation_time, column=None):
        if column:
            return widgets.VBox(
                [
                    widgets.HTML(value="<div><strong>Data Asset Name: </strong>{data_asset_name}</div>".format(data_asset_name=data_asset_name)),
                    self.generate_column_widget(column=column),
                    self.generate_expectation_type_widget(expectation_type),
                    success_widget,
                    widgets.HTML(value="<div><strong>Date/Time Validated (UTC): </strong>{validation_time}</div>".format(validation_time=validation_time)),
                ],
                layout=widgets.Layout(margin='10px', width='40%')
            )
        else:
            return widgets.VBox(
                [
                    widgets.HTML(
                        value="<div><strong>Data Asset Name: </strong>{data_asset_name}</div>".format(data_asset_name=data_asset_name)),
                    self.generate_expectation_type_widget(expectation_type),
                    success_widget,
                    widgets.HTML(
                        value="<div><strong>Date/Time Validated (UTC): </strong>{validation_time}</div>".format(validation_time=validation_time)),
                ],
                layout=widgets.Layout(margin='10px', width='40%')
            )

    def generate_expectation_result_detail_widgets(self, result={}):
        result_detail_widgets = []

        for result_title, result_value in result.items():
            result_detail_widgets.append(
                widgets.HTML(
                    value="<span><strong>{0}: </strong>{1:.2f}</span>".format(result_title, result_value) if type(result_value) is float
                    else "<span><strong>{result_title}: </strong>{result_value}</span>".format(result_title=result_title, result_value=result_value)

                )
            )

        return result_detail_widgets

    # widget generator for complete expectation widget
    
    def create_expectation_widget(
            self,
            data_asset,
            expectation_validation_result,
            collapsed=False
    ):
        validation_time = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M")
        data_asset_name = data_asset.get_data_asset_name()
        data_asset_state = self.state['data_assets'].get(data_asset_name)
        expectation_type = expectation_validation_result['expectation_config']['expectation_type']
        expectation_kwargs = expectation_validation_result['expectation_config']['kwargs']
        column = expectation_kwargs.get('column')

        if data_asset_state:
            existing_expectation_state = self.get_expectation_state(
                data_asset_name=data_asset_name,
                expectation_type=expectation_type,
                column=column
            )
            if existing_expectation_state:
                return self.update_expectation_state(existing_expectation_state, expectation_validation_result, validation_time)
        else:
            self.initialize_data_asset_state(data_asset)

        # success_widget
        success = expectation_validation_result['success']
        success_widget = widgets.HTML(
            value="<span><strong>Success: </strong>{success}</span>".format(success=str(success)))

        # widget with result details
        result_detail_widget = widgets.VBox(children=self.generate_expectation_result_detail_widgets(
            result=expectation_validation_result.get("result", {})
        ))

        # accordion container for result_detail_widget
        result_detail_accordion = widgets.Accordion(
            children=[result_detail_widget])
        result_detail_accordion.set_title(0, 'Validation Result Details')
        result_detail_accordion.selected_index = None

        # widget for displaying kwarg widget exceptions
        expectation_feedback_widget = widgets.Output()
        exception_accordion = widgets.Accordion(
            children=[expectation_feedback_widget]
        )
        exception_accordion.set_title(0, 'Exceptions/Warnings')

        # create scaffold dict for expectation state
        expectation_state = {
            'success': success_widget,
            'expectation_type': expectation_type,
            'expectation_feedback_widget': expectation_feedback_widget,
            'data_asset_name': data_asset_name,
            'result_detail_widget': result_detail_widget,
            'kwargs': {
            }
        }

        if column:
            expectation_state['kwargs']['column'] = column

        remove_expectation_button = self.generate_remove_expectation_button(
            expectation_state=expectation_state)

        # widget with basic info (column, expectation_type)
        basic_expectation_info_box = self.generate_basic_expectation_info_box(
            data_asset_name, expectation_type, success_widget, validation_time, column)

        # collect widget dicts for kwarg inputs
        expectation_kwarg_input_widget_dicts = []
        for expectation_kwarg_name in self.expectation_kwarg_field_names.get(expectation_type):
            widget_dict_generator = getattr(
                self, "generate_{expectation_kwarg_name}_widget_dict".format(expectation_kwarg_name=expectation_kwarg_name), None)
            widget_dict = widget_dict_generator(
                expectation_state=expectation_state,
                **expectation_kwargs) if widget_dict_generator else \
                    self.generate_expectation_kwarg_fallback_widget_dict(expectation_kwarg_name=expectation_kwarg_name, **expectation_kwargs)
            expectation_state['kwargs'][expectation_kwarg_name] = widget_dict
            expectation_kwarg_input_widget_dicts.append(widget_dict)

        # container for kwarg input widgets
        kwarg_widgets = [dict['kwarg_widget']
                         for dict in expectation_kwarg_input_widget_dicts]
        expectation_kwarg_input_box = widgets.VBox(
            children=kwarg_widgets,
            layout=widgets.Layout(margin='10px', width='60%')
        )

        # top-level widget container
        expectation_editor_widget_content = widgets.VBox(
            [
                widgets.HBox([
                    basic_expectation_info_box,
                    expectation_kwarg_input_box
                ]),
                exception_accordion,
                result_detail_accordion,
                remove_expectation_button
            ]
        )
        expectation_editor_widget = widgets.Accordion(
            children=[expectation_editor_widget_content],
            layout=widgets.Layout(
                border='2px solid {color}'.format(
                    color="green" if success else "red"),
                margin='5px'
            )
        )
        expectation_editor_title = "{column} | {expectation_type}".format(column=column, expectation_type=expectation_type) if column \
            else "non_column_expectation | {expectation_type}".format(expectation_type=expectation_type)
        expectation_editor_widget.set_title(0, expectation_editor_title)

        if collapsed:
            expectation_editor_widget.selected_index=None

        # complete expectation state scaffold
        expectation_state['editor_widget'] = expectation_editor_widget

        # set expectation state
        self.set_expectation_state(data_asset, expectation_state, column)

        # update expectation_suite_editor
        expectation_suite_editor = self.state['data_assets'][data_asset_name].get('expectation_suite_editor')
        if expectation_suite_editor:
            expectation_suite = data_asset.get_expectation_suite(
                discard_failed_expectations=False)
            expectation_suite_editor.children = self.generate_expectation_suite_editor_widgets(data_asset, expectation_suite)

        return expectation_editor_widget

    # expectation_suite_editing
    def get_column_names(self, data_asset_name):
        data_asset_state = self.state['data_assets'].get(data_asset_name)
        if not data_asset_state: return []
        data_asset_expectations = data_asset_state.get('expectations')
        if not data_asset_expectations: return []

        column_names = [column_name for column_name in data_asset_expectations.keys() if column_name != 'non_column_expectations']
        column_names.sort()
        if 'non_column_expectations' in data_asset_expectations.keys():
            column_names.insert(0, 'non_column_expectations')

        return column_names

    def get_expectation_types(self, data_asset_name):
        data_asset_state = self.state['data_assets'].get(data_asset_name)
        data_asset_expectations = data_asset_state.get('expectations')
        if not data_asset_state or not data_asset_expectations:
            return []

        expectation_types = []

        for expectation_group in data_asset_expectations.values():
            expectation_types += list(expectation_group.keys())

        return list(set(expectation_types))

    def generate_expectation_suite_editor_widgets(self, data_asset, expectation_suite):
        data_asset_name = data_asset.get_data_asset_name()
        column_names = self.get_column_names(data_asset_name)
        column_accordions = []
        data_asset_state = self.state['data_assets'].get(data_asset_name, {})
        data_asset_expectations = data_asset_state.get('expectations', {})

        ge_version = expectation_suite.get(
            'meta')['great_expectations.__version__']
        column_count = len(column_names)
        expectation_count = len(expectation_suite['expectations'])

        summary_widget_content = """\
            <div>
                <h1>Expectation Suite Editor</h1>
                <hr>
                <ul>
                    <li><strong>Data Asset Name</strong>: {data_asset_name}</li>
                    <li><strong>Great Expectations Version</strong>: {ge_version}</li>
                    <li><strong>Column Count</strong>: {column_count}</li>
                    <li><strong>Expectation Count</strong>: {expectation_count}</li>
                </ul>
                <hr>
                <h2>Columns:</h2>
            </div>\
        """.format(
            data_asset_name=data_asset_name,
            ge_version=ge_version,
            column_count=column_count,
            expectation_count=expectation_count
        )
        summary_widget = widgets.HTML(
            value=summary_widget_content,
            layout=widgets.Layout(
                margin='10px',
            )
        )

        for column_name in column_names:
            expectation_editor_widgets = []
            column_expectations = data_asset_expectations[column_name]
            expectation_types = list(column_expectations.keys())
            expectation_types.sort()
            for expectation_type in expectation_types:
                editor_widget = column_expectations[expectation_type]['editor_widget']
                editor_widget.selected_index = None
                expectation_editor_widgets.append(editor_widget)
            column_accordion_content = widgets.VBox(
                children=expectation_editor_widgets)
            column_accordion = widgets.Accordion(
                children=[column_accordion_content])
            column_accordion.set_title(0, column_name)
            column_accordion.selected_index = None
            column_accordions.append(column_accordion)

        return [summary_widget] + column_accordions

    def edit_expectation_suite(self, data_asset):
        data_asset_name = data_asset.get_data_asset_name()
        expectation_suite = data_asset.get_expectation_suite(
            discard_failed_expectations=False)
        expectations = expectation_suite.get('expectations')

        ################### editor widgets
        for expectation in expectations:
            expectation_type = expectation.get('expectation_type')
            expectation_kwargs = expectation.get('kwargs')
            editor_widget = getattr(data_asset, expectation_type)(
                include_config=True, **expectation_kwargs)

        expectation_suite_editor_widgets = self.generate_expectation_suite_editor_widgets(data_asset, expectation_suite)
        ###################
        expectation_suite_editor = widgets.VBox(
            children=expectation_suite_editor_widgets,
            layout=widgets.Layout(
                border='2px solid black',
            )
        )

        self.state['data_assets'][data_asset_name]['expectation_suite_editor'] = expectation_suite_editor

        return expectation_suite_editor
