# SSAS - Simple Smart Automotive Software
# Copyright (C) 2015 ~ 2023 Parai Wang <parai@foxmail.com>

import logging
import re
import sys

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

__all__ = ["JsonModule", "JsonAction", "JsonBase", "logging"]

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s",
)

C_CHAR_WIDTH = 14
C_ACTION_NUMBER = 8

BASIC_TYPES = ["string", "integer", "bool", "number"]

RE_S_FIELD = re.compile(r"\$\{([^\s\{\}]+)\}")


def prompt(msg):
    QMessageBox(QMessageBox.Information, "Info", msg).exec_()


class JsonString(QLineEdit):
    def __init__(self, title, obj, root):
        assert isinstance(root, JsonModule)
        self.title = title
        self.obj = obj
        self.root = root
        super(QLineEdit, self).__init__(str(self.obj.get(self.title)))
        self.textChanged.connect(self.onTextChanged)
        desc = obj.getDesc(title)
        if desc != None:
            self.setToolTip(desc)

    def onTextChanged(self):
        text = str(self.text())
        self.obj.set(self.title, text)
        vs = self.obj.get(self.title)
        if text != vs:
            self.setText(vs)


class JsonNumber(JsonString):
    def __init__(self, title, obj, root):
        JsonString.__init__(self, title, obj, root)
        prop = obj.getProp(title)
        self.min = prop.get("minimum", -sys.maxsize - 1)
        self.max = prop.get("maximum", sys.maxsize)
        self.format = prop.get("format", "dec")
        self.fresh()

    def timerEvent(self, event):
        self.fresh()
        self.killTimer(event.timerId())

    def fresh(self):
        vs = self.obj.get(self.title)
        if self.format == "hex":
            if type(vs) == str:
                vs = eval(vs)
            self.setText("0x%x" % (vs))
        else:
            self.setText(str(vs))

    def isValid(self, value):
        if (value < self.min) or (value > self.max):
            logging.error("invalid range: %s < %s < %s" % (self.min, value, self.max))
            if value < self.min:
                self.obj.set(self.title, self.min)
            if value > self.max:
                self.obj.set(self.title, self.max)
            return False
        if type(value) not in [int, float]:
            logging.error("invalid value type: %s type %s not in [int, float]" % (value, type(value)))
            return False
        return True

    def onTextChanged(self, value):
        text = str(self.text())
        try:
            value = eval(text)
        except Exception as e:
            if str(text) in ["0x", ""]:
                return
            logging.error("invalid number: %s, value=%s, text=%s" % (e, value, text))
            QMessageBox(
                QMessageBox.Information,
                "Info",
                "invalid number: %s, value=%s, text=%s, restore to default in 3s" % (e, value, text),
            ).exec_()
            self.startTimer(3000)
            return
        ret = self.isValid(value)
        if True != ret:
            logging.error("invalid value: %s" % (value))
            self.startTimer(2000)
            return
        self.obj.set(self.title, value)
        if self.format == "hex":
            self.setText("0x%x" % (self.obj.get(self.title)))


class JsonInteger(JsonNumber):
    def __init__(self, title, obj, root):
        JsonNumber.__init__(self, title, obj, root)

    def isValid(self, value):
        valid = JsonNumber.isValid(self, value)
        if True != valid:
            return valid
        if type(value) != int:
            logging.error("invalid value type: %s type %s != int" % (value, type(value)))
            return False
        return True


class JsonEnumRefString(QComboBox):
    def __init__(self, title, obj, root):
        assert isinstance(root, JsonModule)
        prop = obj.getProp(title)
        self.enumref = prop["enumref"]
        self.enumitems = prop.get("enum", [])
        self.editable = prop.get("editable", False)
        self.title = title
        self.obj = obj
        self.root = root
        super(QComboBox, self).__init__()
        self.setEditable(self.editable)
        self.initItems()
        self.currentTextChanged.connect(self.onTextChanged)
        desc = obj.getDesc(title)
        if desc != None:
            self.setToolTip(desc)
        self.timerEvent(None)
        self.startTimer(1000)

    def timerEvent(self, event):
        items = self.obj.find(self.enumref)
        vs = str(self.obj.get(self.title))
        if None != items:
            self.clear()
            self.addItems(self.enumitems + [str(i) for i in items])
        if self.editable:
            self.setCurrentText(vs)
        else:
            self.setCurrentIndex(self.findText(vs))

    def initItems(self):
        items = self.obj.find(self.enumref)
        if None != items:
            self.addItems([str(i) for i in items])
        vs = str(self.obj.get(self.title))
        if self.editable:
            self.setCurrentText(vs)
        else:
            self.setCurrentIndex(self.findText(vs))

    def onTextChanged(self, text):
        text = str(text)
        self.obj.set(self.title, text)


class JsonEnumRefInteger(JsonEnumRefString):
    def __init__(self, title, obj, root):
        JsonEnumRefString.__init__(self, title, obj, root)

    def onTextChanged(self, text):
        try:
            value = eval(str(self.text()))
        except Exception as e:
            logging.error("invalid int: %s" % (e))
            return
        self.obj.set(self.title, value)


class JsonEnumString(QComboBox):
    def __init__(self, title, obj, root):
        assert isinstance(root, JsonModule)
        prop = obj.getProp(title)
        self.items = prop["enum"]
        self.editable = prop.get("editable", False)
        self.title = title
        self.obj = obj
        self.root = root
        super(QComboBox, self).__init__()
        self.setEditable(self.editable)
        self.initItems()
        self.currentTextChanged.connect(self.onTextChanged)
        desc = obj.getDesc(title)
        if desc != None:
            self.setToolTip(desc)

    def initItems(self):
        self.addItems([str(i) for i in self.items])
        vs = str(self.obj.get(self.title))
        if self.editable:
            self.setCurrentText(vs)
        else:
            self.setCurrentIndex(self.findText(vs))

    def onTextChanged(self, text):
        text = str(text)
        self.obj.set(self.title, text)


class JsonEnumInteger(JsonEnumString):
    def __init__(self, title, obj, root):
        JsonEnumString.__init__(self, title, obj, root)

    def onTextChanged(self, text):
        try:
            value = eval(str(self.text()))
        except Exception as e:
            logging.error("invalid int: %s" % (e))
            return
        self.obj.set(self.title, value)


class JsonBool(QComboBox):
    def __init__(self, title, obj, root):
        assert isinstance(root, JsonModule)
        self.title = title
        self.obj = obj
        self.root = root
        super(QComboBox, self).__init__()
        self.initItems()
        self.currentTextChanged.connect(self.onTextChanged)
        desc = obj.getDesc(title)
        if desc != None:
            self.setToolTip(desc)

    def initItems(self):
        self.addItems(["True", "False"])
        vs = str(self.obj.get(self.title))
        self.setCurrentIndex(self.findText(vs))

    def onTextChanged(self, text):
        text = str(text)
        if text == "True":
            text = True
        else:
            text = False
        self.obj.set(self.title, text)


class JsonMapString(QComboBox):
    def __init__(self, title, obj, root):
        assert isinstance(root, JsonModule)
        assert isinstance(obj, JsonObject)
        prop = obj.getProp(title)
        self.map = prop["map"]
        self.friends = prop.get("friends", [])
        self.title = title
        self.obj = obj
        self.root = root
        super(QComboBox, self).__init__()
        self.initItems()
        self.currentTextChanged.connect(self.onTextChanged)

    def initItems(self):
        items = self.map.keys()
        self.addItems([str(i) for i in items])
        vs = str(self.obj.get(self.title))
        self.setCurrentIndex(self.findText(vs))

    def onTextChanged(self, text):
        text = str(text)
        self.obj.set(self.title, text)
        mp = self.map[text]
        for f in self.friends:
            v2 = mp[f]
            self.obj.set(f, v2)
        if mp.get("extend", False):
            self.obj.extend(text)
        else:
            self.obj.rmExtend()


def _applyAnnotation(widget, severity, description):
    styleMap = {
        "ERROR": "border: 2px solid #dc2626; background-color: #fef2f2;",
        "WARNING": "border: 2px solid #d97706; background-color: #fffbeb;",
        "INFO": "border: 2px solid #2563eb; background-color: #eff6ff;",
    }
    widget.setStyleSheet(widget.styleSheet() + styleMap.get(severity, ""))
    tooltip = widget.toolTip()
    if tooltip:
        widget.setToolTip(f"{tooltip}\n\n? Issue: {description}")
    else:
        widget.setToolTip(f"? Issue: {description}")


def JsonValue(title, obj, root):
    prop = obj.getProp(title)
    logging.debug("create widget with property: %s", prop)
    if "enumref" in prop:
        if prop["type"] == "string":
            widget = JsonEnumRefString(title, obj, root)
        elif prop["type"] == "integer":
            widget = JsonEnumRefInteger(title, obj, root)
    elif "enum" in prop:
        if prop["type"] == "string":
            widget = JsonEnumString(title, obj, root)
        elif prop["type"] == "integer":
            widget = JsonEnumInteger(title, obj, root)
    elif "map" in prop:
        if prop["type"] == "string":
            widget = JsonMapString(title, obj, root)
    elif prop["type"] == "string":
        widget = JsonString(title, obj, root)
    elif prop["type"] == "integer":
        widget = JsonInteger(title, obj, root)
    elif prop["type"] == "number":
        widget = JsonNumber(title, obj, root)
    elif prop["type"] == "bool":
        widget = JsonBool(title, obj, root)
    else:
        return QLineEdit("Type %s not supported for %s" % (prop["type"], title))

    # Apply field-level annotation if available (more specific), else node-level
    fieldAnnotation = None
    if hasattr(obj, 'getFieldAnnotation'):
        fieldAnnotation = obj.getFieldAnnotation(title)

    if fieldAnnotation:
        severity, description = fieldAnnotation
        _applyAnnotation(widget, severity, description)
    elif hasattr(obj, '_hasIssue') and obj._hasIssue:
        _applyAnnotation(widget, obj._issueSeverity, obj._issueDescription)

    def clearAnnotation():
        if hasattr(obj, 'clearFieldAnnotation'):
            obj.clearFieldAnnotation(title)
        elif hasattr(obj, 'clearAnnotations'):
            obj.clearAnnotations()

    if hasattr(widget, 'textChanged'):
        widget.textChanged.connect(clearAnnotation)
    elif hasattr(widget, 'currentTextChanged'):
        widget.currentTextChanged.connect(clearAnnotation)

    return widget


class JsonAction(QAction):
    def __init__(self, text, parent):
        super(QAction, self).__init__(text, parent)
        self.root = parent
        self.triggered.connect(self.onAction)

    def onAction(self):
        self.root.onAction(str(self.text()))


class UIGroup(QScrollArea):
    def __init__(self, wd, parent=None):
        super(QScrollArea, self).__init__(parent)
        self.setWidget(wd)
        self.timerEvent(0)
        self.startTimer(1000)

    def timerEvent(self, event):
        frame = self.widget()
        grid = frame.layout()
        for row in range(grid.rowCount()):
            ko = grid.itemAtPosition(row, 0)
            if ko is None:
                continue
            K = ko.widget()
            V = grid.itemAtPosition(row, 1).widget()
            enabled = V.obj.isEnabled() and V.obj.isEnabled(V.title)
            K.setVisible(enabled)
            V.setVisible(enabled)
            V.setEnabled(enabled)


class JsonBase(QTreeWidgetItem):
    def __init__(self, title, schema, root, parent=None):
        assert isinstance(root, JsonModule)
        QTreeWidgetItem.__init__(self, parent)
        self.root = root
        self.title = title
        self.schema = dict(schema)
        self._hasIssue = False
        self._issueSeverity = None
        self._issueDescription = None
        self._fieldAnnotations = {}
        logging.debug("%s schema: %s", schema["type"], schema)
        self.reload()
        self.setText(0, self.name())

    def preproc(self, url):
        rsts = RE_S_FIELD.findall(url)
        if rsts:
            for field in rsts:
                v = self.find(field)
                logging.debug("%s: replace field: %s = %s", url, field, v)
                url = url.replace("${%s}" % (field), str(v))
        return url

    def isEnabled(self, attr=None):
        prop = self.getProp(attr)
        es = prop.get("enabled", "True")
        es = self.preproc(es)
        logging.debug("attr %s of prop %s condition: %s", attr, prop, es)
        return eval(es)

    def find(self, url):
        url = self.preproc(url)
        logging.debug("find url: %s", url)
        if url[0] == "/":
            return self.root.find(url[1:])
        which = self
        urls = url.split("/")
        for u in urls[:-1]:
            if u == "..":
                which = which.parent()
            elif u == "./":
                pass
            else:
                raise
        attr = urls[-1]
        if ":" in attr:
            cfg = which.toJSON(bFind=True)
            title, field = attr.split(":")
            logging.debug("which cfg: %s, title: %s, field: %s", cfg, title, field)
            return [x[field] for x in cfg[title]]
        return which.get(attr)

    def reload(self):
        raise

    def reloadUI(self, wConfig):
        raise

    def toJSON(self, bFind=False):
        raise

    def isList(self):
        return False

    def isObj(self):
        return False

    def clear(self):
        for i in range(self.childCount()):
            self.takeChild(0)

    def getDesc(self, attr):
        prop = self.getProp(attr)
        return prop.get("description", None)

    def getProp(self, attr):
        if None == attr:
            return self.schema
        logging.debug("get attr %s prop from schema %s", attr, self.schema)
        if "properties" in self.schema:
            prop = self.schema["properties"][attr]
        else:
            prop = self.schema
        return prop

    def autoField(self, attr):
        updated = False
        value = self.get(attr)
        if type(value) == str:
            rsts = RE_S_FIELD.findall(value)
            if rsts:
                for field in rsts:
                    v = self.find(field)
                    logging.debug("auto field: %s = %s", field, v)
                    value = value.replace("${%s}" % (field), str(v))
                self.schema[attr] = value
                updated = True
        return updated

    def get(self, attr):
        logging.debug("get %s of schema: %s", attr, self.schema)
        if attr not in self.schema:
            try:
                prop = self.getProp(attr)
            except KeyError:
                if attr == "name":
                    prop = {"default": self.title}
                else:
                    raise Exception("no attribute %s for %s" % (attr, self.schema))
            if "default" in prop:
                self.schema[attr] = prop["default"]
            elif prop["type"] == "string":
                self.schema[attr] = "TBD"
            elif prop["type"] == "integer":
                self.schema[attr] = 0
            else:
                self.schema[attr] = "unknown"
        return self.schema[attr]

    def set(self, attr, value):
        """return True if has some automatical updates"""
        updated = self.autoField(attr)
        if updated:
            return True
        self.schema[attr] = value
        if attr == "name":
            self.setText(0, value)
        return False

    def name(self):
        if "name" not in self.schema:
            if self.parent() != None:
                logging.debug("new %s for %s, idx=%s" % (self.title, self.parent().title, self.parent().childCount()))
                vs = self.get("name")
                if vs in ["TBD", 0]:
                    vs = "%s%s" % (self.title, self.parent().childCount())
                self.schema["name"] = vs
            else:
                self.schema["name"] = self.title
        return self.schema["name"]

    def onItemSelectionChanged(self):
        logging.debug(
            "object %s ononItemSelectionChanged: isList=%s, schema: %s"
            % (self.title, self.isList(), self.schema)
        )
        index = 0
        if self.isList():
            items = self.schema["items"]
            if self.listMaxAllowed() > self.childCount():
                if self.isEnabled(self.title):
                    self.root.actions[index].setText("Add %s" % (items["title"]))
                    self.root.actions[index].setDisabled(False)
                    index += 1
                else:
                    self.clear()
        if self.parent() != None and self.parent().isList():  # if parent is None, then it is top obj, cannot be deleted
            self.root.actions[index].setText("Delete %s" % (self.title))
            self.root.actions[index].setDisabled(False)
            index += 1
        for i in range(index, C_ACTION_NUMBER):
            self.root.actions[i].setDisabled(True)
            self.root.actions[i].setText("")

        expand = True
        for i in range(self.childCount()):
            obj = self.child(i)
            if not obj.isEnabled():
                expand = False
        self.setExpanded(expand)
        self.root.showConfig(self)

    def addChildObj(self, obj):
        self.addChild(obj)
        self.onItemSelectionChanged()  # trigger refresh

    def annotateIssue(self, issue: dict) -> None:
        self._hasIssue = True
        self._issueSeverity = issue.get("severity", "INFO")
        self._issueDescription = issue.get("description", "")
        self._updateTreeIcon()

    def annotateField(self, fieldName: str, issue: dict) -> None:
        self._fieldAnnotations[fieldName] = (
            issue.get("severity", "INFO"),
            issue.get("description", ""),
        )
        self._hasIssue = True
        self._updateTreeIcon()

    def getFieldAnnotation(self, fieldName: str):
        return self._fieldAnnotations.get(fieldName, None)

    def clearFieldAnnotation(self, fieldName: str):
        if fieldName in self._fieldAnnotations:
            del self._fieldAnnotations[fieldName]
        if not self._fieldAnnotations:
            self._hasIssue = False
            self._issueSeverity = None
            self._issueDescription = None
        self._updateTreeIcon()

    def clearAnnotations(self) -> None:
        self._hasIssue = False
        self._issueSeverity = None
        self._issueDescription = None
        self._fieldAnnotations.clear()
        self._updateTreeIcon()
        for i in range(self.childCount()):
            child = self.child(i)
            if isinstance(child, JsonBase):
                child.clearAnnotations()

    def _updateTreeIcon(self):
        if self._hasIssue:
            iconMap = {
                "ERROR": QStyle.SP_MessageBoxCritical,
                "WARNING": QStyle.SP_MessageBoxWarning,
                "INFO": QStyle.SP_MessageBoxInformation,
            }
            icon = self.root.style().standardIcon(iconMap.get(self._issueSeverity, QStyle.SP_MessageBoxCritical))
            self.setIcon(0, icon)
        else:
            self.setIcon(0, QIcon())

    def getTreePath(self) -> str:
        parts = []
        node = self
        while node.parent() and isinstance(node.parent(), JsonBase):
            if node.parent().isList():
                idx = node.parent().indexOfChild(node)
                parts.append(str(idx))
            parts.append(node.title)
            node = node.parent()
        parts.reverse()
        return "/".join(parts)

    @staticmethod
    def resolvePath(rootObj, path: list, partial_ok: bool = False):
        """Resolve path to a tree node. Returns (node, fieldName) tuple.
        fieldName is None if path resolves to a tree item.
        fieldName is set if the leaf is a basic-typed field (not in tree).
        When partial_ok=True, returns the deepest successfully resolved node
        even if the full path cannot be resolved."""
        if not path or len(path) < 2:
            return None, None
        current = rootObj
        for segment in path[1:]:
            if isinstance(segment, int):
                if isinstance(current, JsonArray) and segment < current.childCount():
                    current = current.child(segment)
                else:
                    if partial_ok:
                        return current, None
                    return None, None
            else:
                found = None
                if isinstance(current, JsonBase):
                    for i in range(current.childCount()):
                        child = current.child(i)
                        if child.title == segment:
                            found = child
                            break
                    if found is None and hasattr(current, 'objMap'):
                        found = current.objMap.get(segment)
                    # Check if segment is a basic property (leaf field, not tree item)
                    if found is None and hasattr(current, 'schema') and 'properties' in current.schema:
                        prop = current.schema['properties'].get(segment)
                        if prop and prop.get('type') in BASIC_TYPES:
                            return current, segment
                elif isinstance(current, JsonObjectTree):
                    found = current.objMap.get(segment)
                if found is None:
                    if partial_ok:
                        return current, None
                    return None, None
                current = found
        return current, None

    def applySetFix(self, path: list, value) -> bool:
        if len(path) >= 2 and path[-1] == self.title:
            self.schema[self.title] = value
            if hasattr(self, 'display'):
                self.display(value)
            self.clearAnnotations()
            return True
        return False

    def applyFieldFix(self, fieldName: str, value) -> bool:
        """Apply a fix to a basic-typed field on this node in-place."""
        prop = self.getProp(fieldName)
        if prop and prop.get('type') in BASIC_TYPES:
            self.schema[fieldName] = value
            hadAnnotation = fieldName in self._fieldAnnotations
            self.clearFieldAnnotation(fieldName)
            if hadAnnotation:
                self.root.showConfig(self)
            return True
        return False

    def applyDeletionFix(self) -> bool:
        parent = self.parent()
        if parent and isinstance(parent, JsonBase):
            parent.takeChild(parent.indexOfChild(self))
            return True
        return False

    def reloadUI(self):
        raise


class JsonBasic(JsonBase):
    def __init__(self, title, schema, root, parent=None):
        JsonBase.__init__(self, title, schema, root, parent)
        self.display(self.get(self.title))

    def reload(self):
        if "__init__" in self.schema:
            self.schema[self.title] = self.schema["__init__"]

    def display(self, value):
        if "hex" == self.schema.get("format", None):
            if type(value) == int:
                value = "0x%x" % (value)
        self.setText(0, "%s: %s" % (self.title, str(value)))

    def set(self, attr, value):
        self.schema[attr] = value
        self.display(value)

    def toJSON(self, bFind=False):
        return self.get(self.title)

    def createUI(self):
        K = QLabel(self.title)
        V = JsonValue(self.title, self, self.root)
        return [(K, V)]

    def reloadUI(self):
        frame = QFrame()
        frame.setMinimumWidth(self.root.width() * 3 // 5)
        grid = QGridLayout()
        frame.setLayout(grid)
        K = QLabel(self.title)
        V = JsonValue(self.title, self, self.root)
        grid.addWidget(K, 0, 0)
        grid.addWidget(V, 0, 1)
        return UIGroup(frame)


class JsonObject(JsonBase):
    def __init__(self, title, schema, root, parent=None):
        JsonBase.__init__(self, title, schema, root, parent)
        for title, schema in self.schema["properties"].items():
            if "map" in schema:
                value = self.get(title)
                if schema["map"].get(value, {}).get("extend", False):
                    self.extend(value, True)

    def extend(self, ext, slient=False):
        if ext in self.schema.get("extends", {}):
            properties = dict(self.schema["extends"][ext])
            if not slient:
                self.schema["__init__"] = self.toJSON()
            logging.debug('extend schema %s with new properties "%s":%s' % (self.schema, ext, properties))
            if "__properties__" not in self.schema:
                self.schema["__properties__"] = dict(self.schema["properties"])
            else:
                for title, schema in self.schema["properties"].items():
                    if title in properties:
                        if title in self.schema["__init__"]:
                            del self.schema["__init__"][title]
                        if title in self.schema:
                            del self.schema[title]
            properties.update(self.schema["__properties__"])
            self.schema["properties"] = properties
            self.clear()
            self.reload()
            if not slient:
                self.root.showConfig(self)
        else:
            logging.error("extend %s properties not found for %s" % (ext, self.schema))

    def rmExtend(self):
        if "__properties__" in self.schema:
            self.schema["__init__"] = self.toJSON()
            self.schema["properties"] = self.schema["__properties__"]
            self.clear()
            self.reload()
            self.root.showConfig(self)

    def reload(self):
        cfg = self.schema.get("__init__", {})
        for title, schema in self.schema["properties"].items():
            if schema["type"] in ["object", "array"]:
                schema = dict(schema)
                if title in cfg:
                    schema["__init__"] = cfg[title]
                schema["name"] = title
                self.addChild(JsonItem(title, schema, self.root, self))
            else:
                if title in cfg:
                    self.schema[title] = cfg[title]
                    logging.debug("reload object property:%s" % (self.schema))

    def isObj(self):
        return True

    def toJSON(self, bFind=False):
        # for find mode, always do toJSON without isEnabled checks
        cfg = {}
        for title, prop in self.schema["properties"].items():
            if prop["type"] not in ["object", "array"]:
                if self.isEnabled(title):
                    cfg[title] = self.get(title)
        for i in range(self.childCount()):
            obj = self.child(i)
            if bFind or obj.isEnabled():
                cfg[obj.name()] = obj.toJSON(bFind=bFind)
        return cfg

    def createUI(self, prefix=""):
        UIs = []
        orders = list(self.schema.get("orders", []))
        for title, schema in self.schema["properties"].items():
            if title not in orders:
                orders.append(title)
        logging.debug("create UI: orders = %s, schema = %s" % (orders, self.schema))
        for title in orders:
            schema = self.schema["properties"][title]
            if schema["type"] in BASIC_TYPES:
                K = QLabel("%s%s" % (prefix, title))
                V = JsonValue(title, self, self.root)
                UIs.append((K, V))
        return UIs

    def reloadUI(self):
        frame = QFrame()
        frame.setMinimumWidth(self.root.width() * 3 // 5)
        grid = QGridLayout()
        frame.setLayout(grid)
        UIs = []
        UIs += self.createUI()
        for i in range(0, self.childCount()):
            obj1 = self.child(i)
            if obj1.isObj():
                UIs += obj1.createUI(prefix="%s: " % (obj1.title))
            elif obj1.isList() and self.schema.get("display_with_list", True):
                num = obj1.childCount()
                if num > 8:
                    continue
                lUIs = obj1.createUI()
                if num > 0:
                    num = len(lUIs) // num
                else:
                    num = 0
                for i, (K, V) in enumerate(lUIs):
                    K.setText("%s %s: %s" % (obj1.title, i // num, K.text()))
                UIs += lUIs
        for column, (K, V) in enumerate(UIs):
            grid.addWidget(K, column, 0)
            grid.addWidget(V, column, 1)
        return UIGroup(frame)


class JsonArray(JsonBase):
    def __init__(self, title, schema, root, parent=None):
        JsonBase.__init__(self, title, schema, root, parent)

    def name(self):
        return self.title

    def isList(self):
        return True

    def reload(self):
        if "__init__" in self.schema:
            items = dict(self.schema["items"])
            for cfg in self.schema["__init__"]:
                schema = dict(items)
                schema["__init__"] = cfg
                logging.debug("reload list %s:%s" % (schema["type"], schema))
                self.addChild(JsonItem(items["title"], schema, self.root, self))

    def listMaxAllowed(self):
        return self.schema.get("max", 65535)

    def toJSON(self, bFind=False):
        cfg = []
        for i in range(self.childCount()):
            obj = self.child(i)
            cfg.append(obj.toJSON(bFind=bFind))
        return cfg

    def onActionAdd(self, what):
        items = self.schema["items"]
        if items["title"] == what:
            if self.isList():
                if self.listMaxAllowed() > self.childCount():
                    self.addChildObj(JsonItem(items["title"], items, self.root, self))
                else:
                    logging.error("Error:Maximum %s for %s is %s!" % (what, items["title"], self.listMaxAllowed()))
            self.setExpanded(True)
        else:
            logging.error("add with what=%s != %s" % (what, items["title"]))

    def createUI(self):
        UIs = []
        for i in range(0, self.childCount()):
            obj1 = self.child(i)
            if not obj1.isList():
                UIs += obj1.createUI()
        return UIs

    def reloadUI(self):
        table = QTableWidget()
        headers = []
        widths = []
        UIs = []
        for i in range(0, self.childCount()):
            obj1 = self.child(i)
            if not obj1.isList():
                uis = obj1.createUI()
                uis_ = []
                for pos, (K, V) in enumerate(uis):
                    if not V.obj.isEnabled(V.title):
                        continue
                    uis_.append([K, V])
                    title = str(K.text())
                    if title not in headers:
                        headers.insert(pos, title)
                        widths.insert(pos, len(title) * C_CHAR_WIDTH)
                UIs.append(uis_)
        table.setColumnCount(len(headers))
        table.setHorizontalHeaderLabels(headers)
        for uis in UIs:
            index = table.rowCount()
            table.setRowCount(index + 1)
            for K, V in uis:
                title = str(K.text())
                column = headers.index(title)
                table.setCellWidget(index, column, V)
                if (len(str(V.obj.get(title))) + 1) * C_CHAR_WIDTH > widths[column]:
                    widths[column] = (len(str(V.obj.get(title))) + 1) * C_CHAR_WIDTH
        for column in range(len(widths)):
            table.setColumnWidth(column, widths[column])
        table.setMinimumWidth(self.root.width() * 3 // 4)
        return table


def JsonItem(title, schema, root, parent=None):
    if schema["type"] == "object":
        return JsonObject(title, schema, root, parent)
    elif schema["type"] == "array":
        return JsonArray(title, schema, root, parent)
    elif schema["type"] in BASIC_TYPES:
        return JsonBasic(title, schema, root, parent)
    else:
        raise


class JsonObjectTree(QTreeWidget):
    def __init__(self, schema, parent=None):
        assert isinstance(parent, JsonModule)
        super(QTreeWidget, self).__init__(parent)
        self.root = parent
        self.title = schema["title"]
        self.schema = schema
        self.objMap = {}
        logging.debug("tree schema: %s", schema)
        self.setHeaderLabel("%s" % (self.schema["title"]))
        for title, schema in self.schema["properties"].items():
            if title in self.schema.get("__init__", {}):
                schema = dict(schema)
                schema["__init__"] = self.schema["__init__"][title]
            obj = JsonItem(title, schema, self.root)
            self.objMap[title] = obj
            self.addTopLevelItem(obj)
        self.itemSelectionChanged.connect(self.onItemSelectionChanged)
        self.setMaximumWidth(600)

    def toJSON(self, bFind=False):
        cfg = {"class": self.title}
        for title, schema in self.schema["properties"].items():
            jse = self.objMap[title].toJSON(bFind=bFind)
            logging.debug("tree toJSON %s %s: %s" % (schema["type"], title, jse))
            cfg[title] = jse
        return cfg

    def onItemSelectionChanged(self):
        obj = self.currentItem()
        if isinstance(obj, JsonBase):
            obj.onItemSelectionChanged()

    def onActionDelete(self, what):
        obj = self.currentItem()
        assert isinstance(obj, JsonBase)
        assert obj.title == what
        if self.indexOfTopLevelItem(obj) != -1:
            self.takeTopLevelItem(self.indexOfTopLevelItem(obj))
        else:
            pObj = obj.parent()
            pObj.takeChild(pObj.indexOfChild(obj))

    def onAction(self, text):
        reAction = re.compile(r"(Add|Delete) ([^\s]+)")
        action = reAction.search(text).groups()
        if action[0] == "Add":
            obj = self.currentItem()
            assert isinstance(obj, JsonArray)
            obj.onActionAdd(action[1])
        elif action[0] == "Delete":
            self.onActionDelete(action[1])


class JsonModule(QMainWindow):
    def __init__(self, schema, main):
        self.actions = []
        super(QMainWindow, self).__init__()
        self.main = main
        self.title = schema["title"]
        self.qSplitter = QSplitter(Qt.Horizontal, self)

        self.objTree = JsonObjectTree(schema, self)
        self.wConfig = QMainWindow()
        self.qSplitter.insertWidget(0, self.objTree)
        self.qSplitter.insertWidget(1, self.wConfig)

        self.setCentralWidget(self.qSplitter)
        self.creActions()

    def creActions(self):
        # create C_ACTION_NUMBER action
        self.actionBar = QToolBar()
        self.addToolBar(Qt.TopToolBarArea, self.actionBar)
        for i in range(0, C_ACTION_NUMBER):
            qAction = JsonAction(self.tr(""), self)
            self.actionBar.addAction(qAction)
            qAction.setDisabled(True)
            self.actions.append(qAction)

    def toJSON(self, bFind=False):
        return self.objTree.toJSON(bFind=bFind)

    def find(self, url):
        return self.main.find(url)

    def onAction(self, text):
        self.objTree.onAction(text)

    def showConfig(self, obj):
        assert isinstance(obj, JsonBase)
        self.wConfig.setCentralWidget(obj.reloadUI())
