"""
calcdb: Utility to load and manipulate the database in Python
"""

import yaml
import os
import os.path
import re

class CalcDB:
    """The calculator database."""
    def __init__(self, base):
        self.calcs = {}
        self.lang = {}

        for (path, name) in self._list_folder(f"{base}/calculators"):
            self.calcs.update(self._load_yaml(path))

        for (path, name) in self._list_folder(f"{base}/lang"):
            self.lang[name] = Lang(self._load_yaml(path))
            self.lang[name]["name"] = name

        self.model = self._load_yaml(f"{base}/model.yaml")

    @staticmethod
    def _load_yaml(path):
        """Safely load a YAML file."""
        with open(path, "r") as fp:
            return yaml.safe_load(fp.read())

    @staticmethod
    def _list_folder(folder):
        """List elements of folder, with prefix."""
        for file in os.listdir(folder):
            yield (f"{folder}/{file}", os.path.splitext(file)[0])

    def all_calcs(self):
        return ((name, self.calcs[name]) for name in self.model["all_calcs"])
    def all_categories(self):
        return self.model["all_categories"]
    def all_fields(self, category):
        return self.model["all_fields"].get(category)

class Lang:
    """A language-data dictionary extended with methods for easy access."""
    def __init__(self, data):
        self.data = data
    def __getitem__(self, key):
        return self.data[key]
    def __setitem__(self, key, value):
        self.data[key] = value
    def get(self, key, default=None):
        return self.data.get(key, default)

    def category(self, name):
        return self.data["categories"].get(name, None)

    def field(self, category, field):
        return self.data["fields"].get(category, {}).get(field, None)

    def unit(self, name):
        return self.data.get("units", {}).get(name, None)

    def filter(self, text):
        regex = re.compile(r'@([a-z]+){([^}]+)}')
        return regex.sub(
            lambda match: match[2] if match[1] == self.data["name"] else "",
            text)

def render(db, lang, category, field, value):
    """
    Render a CalcDB field into text (Markdown when applicable) in the specified
    language. Returns a pair (text, style) where "text" is the string to
    display, and "style" is a list of named styles (for CSS mainly).
    """

    is_numeric  = isinstance(value, (int,float)) and value not in [True, False]
    is_base     = isinstance(value, (int,float,bool,str))
    is_str      = isinstance(value, str)
    is_list     = isinstance(value, list)
    is_dict     = isinstance(value, dict)

    f = f"{category}.{field}"
    styles = []

    # Unknown values
    if value is None:
        return ("?", ["bad"])

    # Styles for some fixed field/value pairs
    if f == "teaching.exam_mode":
        styles = { False: ["bad"], "exam_mode.bad": ["warn"], True: ["good"] }
        styles = styles.get(value,[])
    if (category == "links" or category == "manuals") and value == False:
        styles = ["warn"]

    # Apply units for numeric values with units
    if is_numeric:
        unit = db.model["units"].get(category,{}).get(field)
        suffixes = [(1e0, ""), (1e3, "k"), (1e6, "M"), (1e9, "G")]

        if unit:
            i = 0
            while i+1 < len(suffixes) and value >= suffixes[i+1][0]:
                i += 1
            value /= suffixes[i][0]

            # Reform an integer if possible
            if int(value) == value:
                value = int(value)
            value = f"{value} {suffixes[i][1]}{unit}"

    # Use enumerated values
    if is_base:
        if is_str and value.startswith(field + "."):
            v2 = value[len(field)+1:]
        else:
            v2 = value

        enum = lang["enums"].get(category,{}).get(field,{}).get(v2)
        if enum is not None:
            return (enum, styles)

    # Use constants
    if is_base and lang["constants"].get(value):
        return (lang["constants"].get(value), styles)

    # Query the full calculator name of fields holding a model name
    if f in ["general.predecessor", "general.successor"] and is_str:
        if value not in db.calcs:
            return (str(value), styles)
        else:
            return (db.calcs[value]["general"]["full_name"], styles)

    # Fixed formats for some fields
    if f == "packaging.size":
        return ("{} x {} x {}".format(*value), styles)
    if f == "packaging.power_runtime":
        return (f"~{value} @fr{{heures}}@en{{hours}}", styles)
    if f == "hardware.sd_card":
        return (f"@fr{{Carte SD}}@en{{SD card}} ≤ {value}", styles)
    if f == "devices.display_size_pixels" and len(value) == 3:
        return ("{}x{} ({})".format(*value), styles)
    if f == "devices.display_size_pixels":
        return ("{}x{}".format(*value), styles)
    if f == "devices.display_size_text":
        return ("{}x{}".format(*value), styles)
    if f == "misc.price_range":
        return ("{}-{} €".format(*value), styles)
    if category == "manuals":
        links = [ f"[{name}]({url})" for (name, url) in value.items() ]
        return (", ".join(links), styles)

    # For lists, use the empty constant or process each item individually
    if value == []:
        return (lang["constants"]["empty_list"], styles)
    elif is_list:
        value, styles = zip(*(render(db, lang, category, field, element)
            for element in value))
        return (", ".join(value), set().union(*styles))

    return (str(value), styles)
