from orangecanvas.localization import Translator  # pylint: disable=wrong-import-order
_tr = Translator("Orange", "biolab.si", "Orange")
del Translator
from typing import Optional, Callable, List, Union, Dict, Tuple
from collections import namedtuple
from functools import singledispatch

import numpy as np

from AnyQt.QtCore import QTimer, Qt, QSortFilterProxyModel, QSize, QDateTime, \
    QModelIndex, Signal, QPoint, QRect, QEvent
from AnyQt.QtGui import QStandardItemModel, QStandardItem, QIcon, QPainter, \
    QColor, QValidator
from AnyQt.QtWidgets import QLineEdit, QTableView, QSlider, \
    QComboBox, QStyledItemDelegate, QWidget, QDateTimeEdit, QHBoxLayout, \
    QDoubleSpinBox, QSizePolicy, QStyleOptionViewItem, QLabel, QMenu, QAction

from orangewidget.gui import Slider

from Orange.data import DiscreteVariable, ContinuousVariable, \
    TimeVariable, Table, StringVariable, Variable, Domain
from Orange.data.util import get_unique_names
from Orange.widgets import gui
from Orange.widgets.utils.itemmodels import TableModel
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.widget import OWWidget, Input, Output, Msg
from Orange.widgets.settings import Setting
import os
if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
    from Orange.widgets.orangecontrib.AAIT.utils import widget_positioning, help_management
else:
    from orangecontrib.AAIT.utils import widget_positioning, help_management

VariableRole = next(gui.OrangeUserRole)
ValuesRole = next(gui.OrangeUserRole)
ValueRole = next(gui.OrangeUserRole)


class VariableEditor(QWidget):
    valueChanged = Signal(float)

    def __init__(self, parent: QWidget, callback: Callable):
        super().__init__(parent)
        layout = QHBoxLayout()
        layout.setContentsMargins(6, 0, 6, 0)
        layout.setAlignment(Qt.AlignLeft)
        self.setLayout(layout)
        self.valueChanged.connect(callback)

    @property
    def value(self) -> Union[int, float, str]:
        return NotImplemented

    @value.setter
    def value(self, value: Union[float, str]):
        raise NotImplementedError

    def sizeHint(self):
        return QSize(super().sizeHint().width(), 40)


class DiscreteVariableEditor(VariableEditor):
    def __init__(self, parent: QWidget, items: Tuple[str], callback: Callable):
        super().__init__(parent, callback)
        self._combo = QComboBox(
            parent,
            maximumWidth=180,
            sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        )
        self._combo.addItems(items + ("?",))
        self._combo.currentIndexChanged.connect(self.__on_index_changed)
        self.layout().addWidget(self._combo)

    @property
    def value(self) -> Union[int, float]:
        return self._map_to_var_values()

    @value.setter
    def value(self, value: float):
        if np.isnan(value):
            value = self._combo.model().rowCount() - 1
        assert value == int(value)
        self._combo.setCurrentIndex(int(value))

    def __on_index_changed(self):
        self.valueChanged.emit(self._map_to_var_values())

    def _map_to_var_values(self) -> Union[int, float]:
        n_values = self._combo.model().rowCount() - 1
        current = self._combo.currentIndex()
        return current if current < n_values else np.nan


class ContinuousVariableEditor(VariableEditor):
    MAX_FLOAT = 2147483647

    def __init__(self, parent: QWidget, variable: ContinuousVariable,
                 min_value: float, max_value: float, callback: Callable):
        super().__init__(parent, callback)

        if np.isnan(min_value) or np.isnan(max_value):
            raise ValueError("Min/Max cannot be NaN.")

        n_decimals = variable.number_of_decimals
        abs_max = max(abs(min_value), max_value)
        if abs_max * 10 ** n_decimals > self.MAX_FLOAT:
            n_decimals = int(np.log10(self.MAX_FLOAT / abs_max))

        self._value: float = min_value
        self._n_decimals: int = n_decimals
        self._min_value: float = self.__round_value(min_value)
        self._max_value: float = self.__round_value(max_value)

        sp_spin = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        sp_spin.setHorizontalStretch(1)
        sp_slider = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        sp_slider.setHorizontalStretch(5)
        sp_edit = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        sp_edit.setHorizontalStretch(1)

        class DoubleSpinBox(QDoubleSpinBox):
            def sizeHint(self) -> QSize:
                size: QSize = super().sizeHint()
                return QSize(size.width(), size.height() + 2)

            def validate(self, text: str, pos: int) -> Tuple[int, str, int]:
                state, text, pos = super().validate(text, pos)
                if text == "":
                    state = QValidator.Acceptable
                return state, text, pos

            def textFromValue(self, value):
                if not np.isfinite(value):
                    return "?"
                return super().textFromValue(value)

        self._spin = DoubleSpinBox(
            parent,
            value=self._min_value,
            minimum=-np.inf,
            maximum=np.inf,
            singleStep=10 ** (-self._n_decimals),
            decimals=self._n_decimals,
            minimumWidth=70,
            sizePolicy=sp_spin,
        )
        self._slider = Slider(
            parent,
            minimum=self.__map_to_slider(self._min_value),
            maximum=self.__map_to_slider(self._max_value),
            singleStep=1,
            orientation=Qt.Horizontal,
            sizePolicy=sp_slider,
        )
        self._label_min = QLabel(
            parent,
            text=variable.repr_val(min_value),
            alignment=Qt.AlignRight,
            minimumWidth=60,
            sizePolicy=sp_edit,
        )
        self._label_max = QLabel(
            parent,
            text=variable.repr_val(max_value),
            alignment=Qt.AlignLeft,
            minimumWidth=60,
            sizePolicy=sp_edit,
        )

        self._slider.valueChanged.connect(self._apply_slider_value)
        self._spin.valueChanged.connect(self._apply_spin_value)

        self.layout().addWidget(self._spin)
        self.layout().addWidget(self._label_min)
        self.layout().addWidget(self._slider)
        self.layout().addWidget(self._label_max)

        self.setFocusProxy(self._spin)

        def deselect():
            self._spin.lineEdit().deselect()
            try:
                self._spin.lineEdit().selectionChanged.disconnect(deselect)
            except TypeError:
                pass

        self._spin.lineEdit().selectionChanged.connect(deselect)
        self._slider.installEventFilter(self)
        self._spin.installEventFilter(self)

    @property
    def value(self) -> float:
        return self.__round_value(self._value)

    @value.setter
    def value(self, value: float):
        if self._value is None or self.__round_value(value) != self.value:
            self._value = value
            self.valueChanged.emit(self.value)
            self._spin.setValue(self.value)
            slider_value = self.__map_to_slider(self.value)
            self._value = self.__map_from_slider(slider_value)
            self._slider.setValue(slider_value)
            self._value = value

    def _apply_slider_value(self):
        self.value = self.__map_from_slider(self._slider.value())

    def _apply_spin_value(self):
        value = self._spin.value()
        self.value = value if np.isfinite(value) else np.nan

    def __round_value(self, value):
        return round(value, self._n_decimals)

    def __map_to_slider(self, value: float) -> int:
        value = min(self._max_value, max(self._min_value, value))
        return round(value * 10 ** self._n_decimals)

    def __map_from_slider(self, value: int) -> float:
        return value * 10 ** (-self._n_decimals)

    def eventFilter(self, obj: Union[QSlider, QDoubleSpinBox], event: QEvent) -> bool:
        if event.type() == QEvent.Wheel:
            return True
        return super().eventFilter(obj, event)


class StringVariableEditor(VariableEditor):
    valueChanged = Signal()

    def __init__(self, parent: QWidget, callback: Callable):
        super().__init__(parent, callback)
        self._edit = QLineEdit(
            parent,
            sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        )
        self._edit.textChanged.connect(self.valueChanged)
        self.layout().addWidget(self._edit)
        self.setFocusProxy(self._edit)

    @property
    def value(self) -> str:
        return self._edit.text()

    @value.setter
    def value(self, value: str):
        self._edit.setText(value)


class TimeVariableEditor(VariableEditor):
    DATE_FORMAT = "yyyy-MM-dd"
    TIME_FORMAT = "hh:mm:ss"

    def __init__(self, parent: QWidget, variable: TimeVariable, callback: Callable):
        super().__init__(parent, callback)
        self._value: float = 0
        self._variable: TimeVariable = variable

        if variable.have_date and not variable.have_time:
            self._format = TimeVariableEditor.DATE_FORMAT
        elif not variable.have_date and variable.have_time:
            self._format = TimeVariableEditor.TIME_FORMAT
        else:
            self._format = (f"{TimeVariableEditor.DATE_FORMAT} "
                            f"{TimeVariableEditor.TIME_FORMAT}")

        class DateTimeEdit(QDateTimeEdit):
            def sizeHint(self) -> QSize:
                size: QSize = super().sizeHint()
                return QSize(size.width(), size.height() + 2)

        self._edit = DateTimeEdit(
            parent,
            dateTime=self.__map_to_datetime(self._value),
            displayFormat=self._format,
            sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
        )
        self._edit.dateTimeChanged.connect(self._apply_edit_value)
        self.layout().addWidget(self._edit)
        self.setFocusProxy(self._edit)
        self._edit.installEventFilter(self)

    @property
    def value(self) -> float:
        return self._value

    @value.setter
    def value(self, value: float):
        if value != self.value:
            self._value = value
            self.valueChanged.emit(self.value)
            self._edit.setDateTime(self.__map_to_datetime(self.value))

    def _apply_edit_value(self):
        self.value = self.__map_from_datetime(self._edit.dateTime())

    def __map_from_datetime(self, date_time: QDateTime) -> float:
        return self._variable.to_val(date_time.toString(self._format))

    def __map_to_datetime(self, value: float) -> QDateTime:
        return QDateTime.fromString(self._variable.repr_val(value), self._format)

    def eventFilter(self, obj: QDateTimeEdit, event: QEvent) -> bool:
        if event.type() == QEvent.Wheel:
            return True
        return super().eventFilter(obj, event)


class VariableDelegate(QStyledItemDelegate):
    def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex):
        self.parent().view.openPersistentEditor(index)
        super().paint(painter, option, index)

    def createEditor(self, parent: QWidget, _: QStyleOptionViewItem,
                     index: QModelIndex) -> VariableEditor:
        variable = index.data(VariableRole)
        values = index.data(ValuesRole)
        return _create_editor(variable, values, parent, self._commit_data)

    def _commit_data(self):
        editor = self.sender()
        assert isinstance(editor, VariableEditor)
        self.commitData.emit(editor)

    # pylint: disable=no-self-use
    def setEditorData(self, editor: VariableEditor, index: QModelIndex):
        editor.value = index.model().data(index, ValueRole)

    # pylint: disable=no-self-use
    def setModelData(self, editor: VariableEditor,
                     model: QSortFilterProxyModel, index: QModelIndex):
        model.setData(index, editor.value, ValueRole)

    # pylint: disable=no-self-use
    def updateEditorGeometry(self, editor: VariableEditor,
                             option: QStyleOptionViewItem, _: QModelIndex):
        rect: QRect = option.rect
        if isinstance(editor, ContinuousVariableEditor):
            width = editor.sizeHint().width()
            if width > rect.width():
                rect.setWidth(width)
        editor.setGeometry(rect)

    # pylint: disable=no-self-use
    def sizeHint(self, _: QStyleOptionViewItem, index: QModelIndex) -> QSize:
        return _create_editor(index.data(role=VariableRole), np.array([0]),
                              None, lambda: 1).sizeHint()


@singledispatch
def _create_editor(*_) -> VariableEditor:
    raise NotImplementedError


@_create_editor.register(DiscreteVariable)
def _(variable: DiscreteVariable, _: np.ndarray,
      parent: QWidget, callback: Callable) -> DiscreteVariableEditor:
    return DiscreteVariableEditor(parent, variable.values, callback)


@_create_editor.register(ContinuousVariable)
def _(variable: ContinuousVariable, values: np.ndarray,
      parent: QWidget, callback: Callable) -> ContinuousVariableEditor:
    return ContinuousVariableEditor(parent, variable, np.nanmin(values),
                                    np.nanmax(values), callback)


@_create_editor.register(StringVariable)
def _(_: StringVariable, __: np.ndarray, parent: QWidget,
      callback: Callable) -> StringVariableEditor:
    return StringVariableEditor(parent, callback)


@_create_editor.register(TimeVariable)
def _(variable: TimeVariable, _: np.ndarray,
      parent: QWidget, callback: Callable) -> TimeVariableEditor:
    return TimeVariableEditor(parent, variable, callback)


def majority(values: np.ndarray) -> Union[int, float]:
    if all(np.isnan(values)):
        return np.nan
    return np.bincount(values[~np.isnan(values)].astype(int)).argmax()


def disc_random(values: np.ndarray) -> int:
    return np.random.randint(low=np.nanmin(values), high=np.nanmax(values) + 1)


def cont_random(values: np.ndarray) -> float:
    return np.random.uniform(low=np.nanmin(values), high=np.nanmax(values))


class VariableItemModel(QStandardItemModel):
    dataHasNanColumn = Signal()

    # pylint: disable=dangerous-default-value
    def set_data(self, data: Table, saved_values={}):
        domain = data.domain
        variables = [(TableModel.Attribute, a) for a in domain.attributes] + \
                    [(TableModel.ClassVar, c) for c in domain.class_vars] + \
                    [(TableModel.Meta, m) for m in domain.metas]
        for place, variable in variables:
            if variable.is_primitive():
                values = data.get_column(variable)
                if all(np.isnan(values)):
                    self.dataHasNanColumn.emit()
                    continue
            else:
                values = np.array([])
            color = TableModel.ColorForRole.get(place)
            self._add_row(variable, values, color, saved_values.get(variable.name))

    def _add_row(self, variable: Variable, values: np.ndarray, color: QColor,
                 saved_value: Optional[Union[int, float, str]]):
        var_item = QStandardItem()
        var_item.setData(variable.name, Qt.DisplayRole)
        var_item.setToolTip(variable.name)
        var_item.setIcon(self._variable_icon(variable))
        var_item.setEditable(False)
        if color:
            var_item.setBackground(color)

        control_item = QStandardItem()
        control_item.setData(variable, VariableRole)
        control_item.setData(values, ValuesRole)
        if color:
            control_item.setBackground(color)

        value = self._default_for_variable(variable, values)
        if saved_value is not None and not \
                (variable.is_discrete and saved_value >= len(variable.values)):
            value = saved_value
        control_item.setData(value, ValueRole)

        self.appendRow([var_item, control_item])

    @staticmethod
    def _default_for_variable(variable: Variable, values: np.ndarray) \
            -> Union[float, int, str]:
        if variable.is_continuous:
            return round(np.nanmedian(values), variable.number_of_decimals)
        elif variable.is_discrete:
            return majority(values)
        elif variable.is_string:
            return ""
        else:
            raise NotImplementedError

    @staticmethod
    def _variable_icon(variable: Variable) -> QIcon:
        if variable.is_discrete:
            return gui.attributeIconDict[1]
        elif variable.is_time:
            return gui.attributeIconDict[4]
        elif variable.is_continuous:
            return gui.attributeIconDict[2]
        elif variable.is_string:
            return gui.attributeIconDict[3]
        else:
            return gui.attributeIconDict[-1]


class OWCreateInstance(OWWidget):
    name = "Auto show Create Instance"
    description = "Interactively create a data instance from sample dataset."
    icon = "icons/CreateInstance.svg"
    if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
        icon = "icons_dev/CreateInstance.svg"
    category = "AAIT - TOOLBOX"
    keywords = "create instance, simulator"
    priority = 2310

    class Inputs:
        data = Input("Data", Table)
        reference = Input("Reference", Table)
        input_autoshow = Input("AutoShowConfiguration", str, auto_summary=False)

    class Outputs:
        data = Output("Data", Table)

    class Information(OWWidget.Information):
        nans_removed = Msg("Variables with only missing values were removed from the list.")

    str_WidgetPositionning: str = Setting("None")

    want_main_area = False
    BUTTONS = ["Median", "Mean", "Random", "Input"]
    ACTIONS = ["median", "mean", "random", "input"]
    HEADER = [["name", "Variable"],
              ["variable", "Value"]]
    Header = namedtuple(
        "header", [tag for tag, _ in HEADER]
    )(*range(len(HEADER)))

    values: Dict[str, Union[float, str]] = Setting({}, schema_only=True)
    append_to_data = Setting(True)
    auto_commit = Setting(True)
    auto_apply_reference = Setting(True)

    def __init__(self):
        super().__init__()
        self.data: Optional[Table] = None
        self.reference: Optional[Table] = None

        # When True, dataChanged from the model does NOT trigger _do_commit.
        # Set before every programmatic model update, cleared in finally.
        self._silent_update = False

        self.filter_edit = QLineEdit(textChanged=self.__filter_edit_changed,
                                     placeholderText="Filter...")
        self.view = QTableView(sortingEnabled=True,
                               contextMenuPolicy=Qt.CustomContextMenu,
                               selectionMode=QTableView.NoSelection)
        self.view.customContextMenuRequested.connect(self.__menu_requested)
        self.view.setItemDelegateForColumn(
            self.Header.variable, VariableDelegate(self)
        )
        self.view.verticalHeader().hide()
        self.view.horizontalHeader().setStretchLastSection(True)
        self.view.horizontalHeader().setMaximumSectionSize(350)

        self.model = VariableItemModel(self)
        self.model.setHorizontalHeaderLabels([x for _, x in self.HEADER])
        self.model.dataChanged.connect(self.__table_data_changed)
        self.model.dataHasNanColumn.connect(self.Information.nans_removed)
        self.proxy_model = QSortFilterProxyModel()
        self.proxy_model.setFilterKeyColumn(-1)
        self.proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.proxy_model.setSourceModel(self.model)
        self.view.setModel(self.proxy_model)

        vbox = gui.vBox(self.controlArea, box=True)
        vbox.layout().addWidget(self.filter_edit)
        vbox.layout().addWidget(self.view)

        box = gui.hBox(vbox, objectName="buttonBox")
        gui.rubber(box)
        for name, action in zip(self.BUTTONS, self.ACTIONS):
            gui.button(
                box, self, name,
                lambda *args, fun=action: self._initialize_values(fun),
                autoDefault=False
            )
        gui.rubber(box)

        gui.checkBox(self.buttonsArea, self, "append_to_data",
                     "Append this instance to input data",
                     callback=self._do_commit_if_auto)

        gui.checkBox(
            self.buttonsArea, self, "auto_apply_reference",
            self.tr("Auto-apply Reference to UI"),
            callback=self._on_auto_apply_reference_changed
        )

        gui.rubber(self.buttonsArea)

        # Apply button (always enabled) + "Apply automatically" checkbox
        apply_box = gui.hBox(self.buttonsArea)
        self._apply_button = gui.button(
            apply_box, self, "Apply",
            callback=self._do_commit,   # appel direct, pas via gui.deferred
            autoDefault=False
        )
        gui.checkBox(
            apply_box, self, "auto_commit", "Apply automatically",
            callback=self._on_auto_commit_changed
        )

        self.settingsAboutToBePacked.connect(self.pack_settings)
        widget_positioning.show_and_adjust_at_opening(self, str(self.str_WidgetPositionning))
        QTimer.singleShot(0, lambda: help_management.override_help_action(self))

    # ------------------------------------------------------------------
    # Core output method — plain Python, no Orange deferred machinery
    # ------------------------------------------------------------------

    def _do_commit(self):
        """Build and send the output. Always executes immediately."""
        output_data = None
        if self.data:
            output_data = self._create_data_from_values()
            if self.append_to_data:
                output_data = self._append_to_data(output_data)
        self.Outputs.data.send(output_data)

    def _do_commit_if_auto(self):
        """Send only when 'Apply automatically' is checked."""
        if self.auto_commit:
            self._do_commit()

    # ------------------------------------------------------------------
    # UI callbacks
    # ------------------------------------------------------------------

    def _on_auto_commit_changed(self):
        if self.auto_commit:
            self._do_commit()

    def _on_auto_apply_reference_changed(self):
        if self.auto_apply_reference:
            self._apply_reference_to_ui()

    def __filter_edit_changed(self):
        self.proxy_model.setFilterFixedString(self.filter_edit.text().strip())
        if self.auto_apply_reference and self.reference is not None:
            self._apply_reference_to_ui()

    def __table_data_changed(self):
        """Fired when the user manually moves a slider / combo.
        Suppressed during all programmatic updates via _silent_update."""
        if not self._silent_update and self.auto_commit:
            self._do_commit()

    def __menu_requested(self, point: QPoint):
        index = self.view.indexAt(point)
        model: QSortFilterProxyModel = index.model()
        source_index = model.mapToSource(index)
        menu = QMenu(self)
        for action in self._create_actions(source_index):
            menu.addAction(action)
        menu.popup(self.view.viewport().mapToGlobal(point))

    def _create_actions(self, index: QModelIndex) -> List[QAction]:
        actions = []
        for name in self.ACTIONS:
            action = QAction(name.capitalize(), self)
            action.triggered.connect(
                lambda *args, fun=name: self._initialize_values(fun, [index])
            )
            actions.append(action)
        return actions

    # ------------------------------------------------------------------
    # Reference application — always silent, never commits
    # ------------------------------------------------------------------

    def _apply_reference_to_ui(self, indices: List[QModelIndex] = None):
        """Update UI values from reference row. Never triggers a commit."""
        if not self.data or self.reference is None or len(self.reference) < 1:
            return

        ref_table = self.reference
        self._silent_update = True
        try:
            if indices is not None:
                source_indexes = indices
            else:
                source_indexes = [
                    self.proxy_model.mapToSource(
                        self.proxy_model.index(prow, self.Header.variable)
                    )
                    for prow in range(self.proxy_model.rowCount())
                ]

            for sindex in source_indexes:
                var = self.model.data(sindex, VariableRole)
                if var is None or var.name not in ref_table.domain:
                    continue
                ref_var = ref_table.domain[var.name]
                if (var.is_discrete != ref_var.is_discrete) or \
                        (var.is_continuous != ref_var.is_continuous) or \
                        (var.is_string != ref_var.is_string) or \
                        (var.is_time != ref_var.is_time):
                    continue
                col = ref_table.get_column(ref_var)
                value = col[0] if len(col) else np.nan
                self.model.setData(sindex, value, ValueRole)
        finally:
            self._silent_update = False

    # ------------------------------------------------------------------
    # Button actions — silent UI update, no automatic commit
    # ------------------------------------------------------------------

    def _initialize_values(self, fun: str, indices: List[QModelIndex] = None):
        cont_fun = {"median": np.nanmedian,
                    "mean": np.nanmean,
                    "random": cont_random,
                    "input": np.nanmean}.get(fun, NotImplemented)
        disc_fun = {"median": majority,
                    "mean": majority,
                    "random": disc_random,
                    "input": majority}.get(fun, NotImplemented)

        if not self.data or (fun == "input" and not self.reference):
            return

        if fun == "input":
            self._apply_reference_to_ui(indices)
            return

        self._silent_update = True
        try:
            if indices is None:
                source_indexes = [
                    self.proxy_model.mapToSource(
                        self.proxy_model.index(r, self.Header.variable)
                    )
                    for r in range(self.proxy_model.rowCount())
                ]
            else:
                source_indexes = indices

            for sindex in source_indexes:
                variable = self.model.data(sindex, VariableRole)
                values = self.model.data(sindex, ValuesRole)

                if variable.is_continuous:
                    value = cont_fun(values)
                    value = round(value, variable.number_of_decimals)
                elif variable.is_discrete:
                    value = disc_fun(values)
                elif variable.is_string:
                    value = ""
                else:
                    raise NotImplementedError

                self.model.setData(sindex, value, ValueRole)
        finally:
            self._silent_update = False

    # ------------------------------------------------------------------
    # Inputs — always silent, no automatic commit on arrival
    # ------------------------------------------------------------------

    @Inputs.data
    def set_data(self, data: Table):
        self.data = data
        self._silent_update = True
        try:
            self._set_model_data()
            if self.auto_apply_reference and self.reference is not None:
                self._apply_reference_to_ui()
        finally:
            self._silent_update = False

    def _set_model_data(self):
        self.Information.nans_removed.clear()
        self.model.removeRows(0, self.model.rowCount())
        if not self.data:
            return
        self.model.set_data(self.data, self.values)
        self.values = {}
        self.view.horizontalHeader().setStretchLastSection(False)
        self.view.resizeColumnsToContents()
        self.view.resizeRowsToContents()
        self.view.horizontalHeader().setStretchLastSection(True)

    @Inputs.reference
    def set_reference(self, data: Table):
        self.reference = data
        if self.auto_apply_reference:
            self._apply_reference_to_ui()

    @Inputs.input_autoshow
    def set_input_autoshow(self, le_str):
        if le_str is not None:
            self.str_WidgetPositionning = str(le_str)

    # ------------------------------------------------------------------
    # Output builders
    # ------------------------------------------------------------------

    def _create_data_from_values(self) -> Table:
        data = Table.from_domain(self.data.domain, 1)
        with data.unlocked():
            data.name = "created"
            if data.X.size:
                data.X[:] = np.nan
            if data.Y.size:
                data.Y[:] = np.nan
            for i, m in enumerate(self.data.domain.metas):
                data.metas[:, i] = "" if m.is_string else np.nan
            values = self._get_values()
            for var_name, value in values.items():
                data[:, var_name] = value
        return data

    def _append_to_data(self, instance: Table) -> Table:
        assert self.data
        assert len(instance) == 1
        source_label = "__source_widget"

        data = Table.concatenate([self.data, instance], axis=0)
        domain = self.data.domain
        with data.unlocked():
            for attrs, part in ((domain.attributes, data.X),
                                (domain.class_vars, data.Y.reshape(len(data), -1)),
                                (domain.metas, data.metas)):
                for idx, var in enumerate(attrs):
                    if var.attributes.get(source_label) == OWCreateInstance:
                        part[-1, idx] = 1
                        return data

        name = get_unique_names(self.data.domain, "Source ID")
        var = DiscreteVariable(name, values=(self.data.name, instance.name))
        var.attributes[source_label] = OWCreateInstance
        domain = Domain(domain.attributes, domain.class_vars,
                        domain.metas + (var,))
        data = data.transform(domain)
        with data.unlocked(data.metas):
            data.metas[: len(self.data), -1] = 0
            data.metas[len(self.data):, -1] = 1
        return data

    def _get_values(self) -> Dict[str, Union[str, float]]:
        values = {}
        for row in range(self.model.rowCount()):
            index = self.model.index(row, self.Header.variable)
            values[self.model.data(index, VariableRole).name] = \
                self.model.data(index, ValueRole)
        return values

    # ------------------------------------------------------------------
    # Misc
    # ------------------------------------------------------------------

    def send_report(self):
        if not self.data:
            return
        self.report_domain("Input", self.data.domain)
        self.report_domain("Output", self.data.domain)
        items = []
        values: Dict = self._get_values()
        for var in self.data.domain.variables + self.data.domain.metas:
            val = values.get(var.name, np.nan)
            if var.is_primitive():
                val = var.repr_val(val)
            items.append([f"{var.name}:", val])
        self.report_table("Values", items)

    @staticmethod
    def sizeHint():
        return QSize(600, 500)

    def pack_settings(self):
        self.values: Dict[str, Union[str, float]] = self._get_values()


if __name__ == "__main__":  # pragma: no cover
    table = Table("housing")
    WidgetPreview(OWCreateInstance).run(set_data=table,
                                        set_reference=table[:1])
