# coding: utf-8
# -------------------------------------------------------------------
# YakPanel
# -------------------------------------------------------------------
# Copyright (c) 2014-2099 YakPanel(www.yakpanel.com) All rights reserved.
# -------------------------------------------------------------------
# Author: yakpanel
# -------------------------------------------------------------------

# ------------------------------
# config app
# ------------------------------

import copy
import os
import threading

try:
    import ujson as json
except ImportError:
    import json

__all__ = [
    "DictConfig",
    "ListConfig",
]


class _Ctx:
    """轻量锁+加载"""
    __slots__ = ("_mgr", "_save")

    def __init__(self, mgr: "SimpleConfig", save: bool = False):
        self._mgr = mgr
        self._save = save

    def __enter__(self):
        mgr = self._mgr
        mgr._lock.acquire()
        try:
            if not mgr._loaded:
                mgr._do_load()
        except:
            mgr._lock.release()
            raise
        return mgr._cache

    def __exit__(self, exc_type, exc_val, exc_tb):
        try:
            if self._save and exc_type is None:
                self._mgr._save()
        finally:
            self._mgr._lock.release()


class SimpleConfig(object):
    """json"""
    __slots__ = ("_path", "_tmp_path", "_lock", "_cache", "_loaded", "_default")

    def __init__(self, path: str, default=None):
        """配置文件的绝对路径"""
        self._path: str = path
        self._default = default
        self._tmp_path: str = path + ".tmp"
        self._lock = threading.RLock()
        self._cache = None
        self._loaded: bool = False

        if default is not None and not os.path.exists(path):
            with self._lock:
                self._cache = copy.deepcopy(default)
                self._loaded = True
                self._save()

    def __bool__(self) -> bool:
        """判断当前数据是否非空"""
        with self._ctx() as c:
            return bool(c)

    def _ctx(self, save: bool = False) -> _Ctx:
        return _Ctx(self, save=save)

    def _do_load(self):
        """需持锁"""
        if os.path.exists(self._path):
            try:
                with open(self._path, "r", encoding="utf-8") as f:
                    self._cache = json.load(f)
                self._loaded = True
                return
            except (ValueError, OSError):
                pass
        self._cache = copy.deepcopy(self._default) if self._default is not None else self._default_data()
        self._loaded = True

    def _save(self):
        """需持锁"""
        dirname = os.path.dirname(self._path)
        if dirname:
            os.makedirs(dirname, exist_ok=True)
        try:
            with open(self._tmp_path, "w", encoding="utf-8") as f:
                json.dump(self._cache, f, ensure_ascii=False)
                f.flush()
            os.replace(self._tmp_path, self._path)
        except OSError:
            self._loaded = False
            try:
                os.remove(self._tmp_path)
            except FileNotFoundError:
                pass
            raise

    def _default_data(self):
        raise NotImplementedError

    # ----------- public -----------

    def reload(self):
        """强制重新加载"""
        with self._lock:
            self._loaded = False
            self._do_load()

    def clear(self):
        """清空并持久化"""
        with self._ctx(save=True):
            self._cache = copy.deepcopy(self._default) if self._default is not None else self._default_data()

    def save(self):
        """手动持久化"""
        with self._ctx():
            if self._loaded:
                self._save()

    def atomic(self) -> _Ctx:
        """原子操作上下文, 事务"""
        return self._ctx(save=True)

    @property
    def path(self) -> str:
        return self._path

    def exists(self) -> bool:
        """配置文件是否存在于磁盘"""
        return os.path.exists(self._path)

    def delete_config(self):
        with self._lock:
            for p in (self._path, self._tmp_path):
                try:
                    os.remove(p)
                except FileNotFoundError:
                    pass
            self._cache = copy.deepcopy(self._default) if self._default is not None else self._default_data()
            self._loaded = True


# ---------------------------------------------------------------------------


class DictConfig(SimpleConfig):
    """
        cfg = DictConfig("/path/to/config.json", default={"a": 1})
        cfg.set("key", "value")
        cfg.get("key")            # "value"
        cfg.get("missing", 0)     # 0
        cfg["key"] = "new_value"
        del cfg["key"]
        cfg.update({"a": 1, "b": 2})      # 浅合并，整体覆盖同名 key
        cfg.merge({"a": {"x": 1}})        # 深合并，更新嵌套中若干字段而非整体覆盖
        cfg.keys() / cfg.values() / cfg.items()
        "key" in cfg
        len(cfg)
        cfg.pop("key", None)
        cfg.setdefault("key", default_val)
        cfg.as_dict()
    """

    def _default_data(self) -> dict:
        return {}

    def get(self, key: str, default=None):
        with self._ctx() as c:
            return c.get(key, default)

    def __getitem__(self, key: str):
        with self._ctx() as c:
            return c[key]

    def __contains__(self, key: str) -> bool:
        with self._ctx() as c:
            return key in c

    def __len__(self) -> int:
        with self._ctx() as c:
            return len(c)

    def __iter__(self):
        with self._ctx() as c:
            return iter(list(c.keys()))

    def keys(self):
        with self._ctx() as c:
            return list(c.keys())

    def values(self):
        with self._ctx() as c:
            return list(c.values())

    def items(self):
        with self._ctx() as c:
            return list(c.items())

    def as_dict(self) -> dict:
        with self._ctx() as c:
            return dict(c)

    def __repr__(self) -> str:
        with self._ctx() as c:
            return f"DictConfig({self._path!r}, {c!r})"

    def set(self, key: str, value) -> None:
        with self._ctx(save=True) as c:
            c[key] = value

    def __setitem__(self, key: str, value) -> None:
        with self._ctx(save=True) as c:
            c[key] = value

    def update(self, data: dict) -> None:
        if not data:
            return
        with self._ctx(save=True) as c:
            c.update(data)

    def merge(self, data: dict) -> None:
        """合并data到配置, 对嵌套dict递归合并, 非update覆盖"""
        if not data:
            return

        def _deep_merge(base: dict, patch: dict) -> None:
            for k, v in patch.items():
                if k in base and isinstance(base[k], dict) and isinstance(v, dict):
                    _deep_merge(base[k], v)
                else:
                    base[k] = v

        with self._ctx(save=True) as c:
            _deep_merge(c, data)

    def setdefault(self, key: str, default=None):
        with self._ctx(save=True) as c:
            if key not in c:
                c[key] = default
            return c[key]

    def delete(self, key: str) -> None:
        with self._ctx(save=True) as c:
            if key in c:
                del c[key]

    def __delitem__(self, key: str) -> None:
        with self._ctx(save=True) as c:
            del c[key]

    def pop(self, key: str, *args):
        with self._ctx(save=True) as c:
            return c.pop(key, *args) if args else c.pop(key)


# ---------------------------------------------------------------------------


class ListConfig(SimpleConfig):
    """
        cfg = ListConfig("/path/to/list.json", default=[1,2,3])
        cfg.append("item") / cfg.insert(0, "x") / cfg.extend([...])
        cfg.get(0) / cfg[0] / cfg[0] = "v"
        cfg.remove("x") / cfg.pop(0) / del cfg[0]
        cfg.index("x") / cfg.count("x")
        "x" in cfg / len(cfg) / iter(cfg)
        cfg.sort() / cfg.reverse()
        cfg.unique()
        cfg.as_list()
    """

    def _default_data(self) -> list:
        return []

    def get(self, index: int, default=None):
        with self._ctx() as c:
            try:
                return c[index]
            except IndexError:
                return default

    def __getitem__(self, index):
        with self._ctx() as c:
            return c[index]

    def __contains__(self, item) -> bool:
        with self._ctx() as c:
            return item in c

    def __len__(self) -> int:
        with self._ctx() as c:
            return len(c)

    def __iter__(self):
        with self._ctx() as c:
            return iter(list(c))

    def count(self, item) -> int:
        with self._ctx() as c:
            return c.count(item)

    def index(self, item, *args) -> int:
        with self._ctx() as c:
            return c.index(item, *args)

    def as_list(self) -> list:
        with self._ctx() as c:
            return list(c)

    def __repr__(self) -> str:
        with self._ctx() as c:
            return f"ListConfig({self._path!r}, {c!r})"

    def set(self, index: int, value) -> None:
        with self._ctx(save=True) as c:
            c[index] = value

    def __setitem__(self, index, value) -> None:
        with self._ctx(save=True) as c:
            c[index] = value

    def __delitem__(self, index) -> None:
        with self._ctx(save=True) as c:
            del c[index]

    def append(self, item) -> None:
        with self._ctx(save=True) as c:
            c.append(item)

    def insert(self, index: int, item) -> None:
        with self._ctx(save=True) as c:
            c.insert(index, item)

    def extend(self, items) -> None:
        items = list(items)
        if not items:
            return
        with self._ctx(save=True) as c:
            c.extend(items)

    def remove(self, item) -> None:
        with self._ctx(save=True) as c:
            c.remove(item)

    def pop(self, index: int = -1):
        with self._ctx(save=True) as c:
            return c.pop(index)

    def sort(self, *, key=None, reverse: bool = False) -> None:
        with self._ctx(save=True) as c:
            c.sort(key=key, reverse=reverse)

    def reverse(self) -> None:
        with self._ctx(save=True) as c:
            c.reverse()

    def unique(self) -> None:
        """保序去重"""
        with self._ctx(save=True) as c:
            seen: set = set()
            result = []
            for item in c:
                try:
                    key = item
                    hash(key)
                except TypeError:
                    key = id(item)
                if key not in seen:
                    seen.add(key)
                    result.append(item)
            c[:] = result
