import configparser
import gettext
import os
import logging
import json
import re
import copy

from io import StringIO

from os import path

SCREEN_BLANKING_OPTIONS = [
    "300",    # 5 Minutes
    "900",    # 15 Minutes
    "1800",   # 30 Minutes
    "3600",   # 1 Hour
    "7200",   # 2 Hours
    "14400",  # 4 Hours
]

class ConfigError(Exception):
    pass

class KlipperScreenConfig:
    config = None
    configfile_name = "KlipperScreen.conf"
    do_not_edit_line = "#~# --- Do not edit below this line. This section is auto generated --- #~#"
    do_not_edit_prefix = "#~#"

    def __init__(self, configfile, screen=None):
        self.default_config_path = "%s/ks_includes/%s" % (os.getcwd(), "defaults.conf")
        self.config = configparser.ConfigParser()
        self.config_path = self.get_config_file_location(configfile)
        logging.debug("Config path location: %s" % self.config_path)
        self.defined_config = None

        try:
            self.config.read(self.default_config_path)
            if self.config_path != self.default_config_path:
                user_def, saved_def = self.separate_saved_config(self.config_path)
                self.defined_config = configparser.ConfigParser()
                self.defined_config.read_string(user_def)

                includes = [i[8:] for i in self.defined_config.sections() if i.startswith("include ")]
                for include in includes:
                    self._include_config("/".join(self.config_path.split("/")[:-1]), include)

                for i in ['menu __main', 'menu __print', 'menu __splashscreen', 'preheat']:
                    for j in self.defined_config.sections():
                        if j.startswith(i):
                            for k in list(self.config.sections()):
                                if k.startswith(i):
                                    del self.config[k]
                            break

                self.log_config(self.defined_config)
                self.config.read_string(user_def)
                if saved_def is not None:
                    self.config.read_string(saved_def)
                    logging.info("====== Saved Def ======\n%s\n=======================" % saved_def)
        except KeyError:
            raise ConfigError(f"Error reading config: {self.config_path}")
        except Exception:
            logging.exception("Unknown error with config")

        printers = sorted([i for i in self.config.sections() if i.startswith("printer ")])
        self.printers = []
        for printer in printers:
            self.printers.append({
                printer[8:]: {
                    "moonraker_host": self.config.get(printer, "moonraker_host", fallback="127.0.0.1"),
                    "moonraker_port": self.config.get(printer, "moonraker_port", fallback="7125"),
                    "moonraker_api_key": self.config.get(printer, "moonraker_api_key", fallback=False)
                }
            })
        if len(printers) <= 0:
            self.printers.append({
                "Printer": {
                    "moonraker_host": self.config.get("main", "moonraker_host", fallback="127.0.0.1"),
                    "moonraker_port": self.config.get("main", "moonraker_port", fallback="7125"),
                    "moonraker_api_key": self.config.get("main", "moonraker_api_key", fallback="")
                }
            })

        conf_printers_debug = copy.deepcopy(self.printers)
        for printer in conf_printers_debug:
            name = list(printer)[0]
            item = conf_printers_debug[conf_printers_debug.index(printer)]
            if item[list(printer)[0]]['moonraker_api_key'] != "":
                item[list(printer)[0]]['moonraker_api_key'] = "redacted"
        logging.debug("Configured printers: %s" % json.dumps(conf_printers_debug, indent=2))

        lang = self.get_main_config_option("language", None)
        lang = [lang] if lang is not None and lang != "default" else None
        logging.info("Detected language: %s" % lang)
        self.lang = gettext.translation('KlipperScreen', localedir='ks_includes/locales', languages=lang,
                                        fallback=True)

        self._create_configurable_options(screen)

    def _create_configurable_options(self, screen):
        _ = self.lang.gettext
        _n = self.lang.ngettext

        self.configurable_options = [
            {"invert_x": {"section": "main", "name": _("Invert X"), "type": "binary", "value": "False"}},
            {"invert_y": {"section": "main", "name": _("Invert Y"), "type": "binary", "value": "False"}},
            {"invert_z": {"section": "main", "name": _("Invert Z"), "type": "binary", "value": "False"}},
            {"language": {"section": "main", "name": _("Language"), "type": "dropdown", "value": "system_lang",
                          "callback": screen.restart_warning, "options": [
                              {"name": _("System") + " " + _("(default)"), "value": "system_lang"}
            ]}},
            {"move_speed": {
                "section": "main", "name": _("Move Speed (mm/s)"), "type": "scale", "value": "20",
                "range": [5, 100], "step": 1}},
            {"print_sort_dir": {"section": "main", "type": None, "value": "name_asc"}},
            {"print_estimate_method": {
                "section": "main", "name": _("Estimated Time Method"), "type": "dropdown",
                "value": "file", "options": [
                    {"name": _("File") + " " + _("(default)"), "value": "file"},
                    {"name": _("Duration Only"), "value": "duration"},
                    {"name": _("Filament Used"), "value": "filament"},
                    {"name": _("Slicer"), "value": "slicer"}]}},
            {"screen_blanking": {
                "section": "main", "name": _("Screen Power Off Time"), "type": "dropdown",
                "value": "3600", "callback": screen.set_screenblanking_timeout, "options": [
                    {"name": _("Off"), "value": "off"}]
            }},
            {"theme": {
                "section": "main", "name": _("Icon Theme"), "type": "dropdown",
                "value": "z-bolt", "callback": screen.restart_warning, "options": [
                    {"name": "Z-bolt" + " " + _("(default)"), "value": "z-bolt"}]}},
            {"24htime": {"section": "main", "name": _("24 Hour Time"), "type": "binary", "value": "True"}},
            {"side_macro_shortcut": {
                "section": "main", "name": _("Macro shortcut on sidebar"), "type": "binary",
                "value": "True", "callback": screen.toggle_macro_shortcut}},
            {"font_size": {
                "section": "main", "name": _("Font Size"), "type": "dropdown",
                "value": "medium", "callback": screen.restart_warning, "options": [
                    {"name": _("Small"), "value": "small"},
                    {"name": _("Medium") + " " + _("(default)"), "value": "medium"},
                    {"name": _("Large"), "value": "large"}]}},
            # {"": {"section": "main", "name": _(""), "type": ""}}
        ]

        lang_path = os.path.join(os.getcwd(), 'ks_includes/locales')
        langs = [d for d in os.listdir(lang_path) if not os.path.isfile(os.path.join(lang_path, d))]
        langs.sort()
        lang_opt = self.configurable_options[3]['language']['options']

        for lang in langs:
            lang_opt.append({"name": lang, "value": lang})

        t_path = os.path.join(os.getcwd(), 'styles')
        themes = [d for d in os.listdir(t_path) if (not os.path.isfile(os.path.join(t_path, d)) and d != "z-bolt")]
        themes.sort()
        theme_opt = self.configurable_options[8]['theme']['options']

        for theme in themes:
            theme_opt.append({"name": theme, "value": theme})

        index = self.configurable_options.index(
            [i for i in self.configurable_options if list(i)[0] == "screen_blanking"][0])
        for num in SCREEN_BLANKING_OPTIONS:
            hour = int(int(num)/3600)
            if hour > 0:
                name = str(hour) + " " + _n("hour", "hours", hour)
            else:
                name = str(int(int(num)/60)) + " " + _("minutes")
            self.configurable_options[index]['screen_blanking']['options'].append({
                "name": name,
                "value": num
            })

        for item in self.configurable_options:
            name = list(item)[0]
            vals = item[name]
            if vals['section'] not in self.config.sections():
                self.config.add_section(vals['section'])
            if name not in list(self.config[vals['section']]):
                self.config.set(vals['section'], name, vals['value'])

    def _include_config(self, dir, path):
        full_path = path if path[0] == "/" else "%s/%s" % (dir, path)
        parse_files = []

        if "*" in full_path:
            parent_dir = "/".join(full_path.split("/")[:-1])
            file = full_path.split("/")[-1]
            if not os.path.exists(parent_dir):
                logging.info("Config Error: Directory %s does not exist" % parent_dir)
                return
            files = os.listdir(parent_dir)
            regex = "^%s$" % file.replace('*', '.*')
            for file in files:
                if re.match(regex, file):
                    parse_files.append(os.path.join(parent_dir, file))
        else:
            if not os.path.exists(os.path.join(full_path)):
                logging.info("Config Error: %s does not exist" % full_path)
                return
            parse_files.append(full_path)

        logging.info("Parsing files: %s" % parse_files)
        for file in parse_files:
            config = configparser.ConfigParser()
            config.read(file)
            includes = [i[8:] for i in config.sections() if i.startswith("include ")]
            for include in includes:
                self._include_config("/".join(full_path.split("/")[:-1]), include)
            self.config.read(file)
            self.defined_config.read(file)

    def separate_saved_config(self, config_path):
        user_def = []
        saved_def = None
        found_saved = False
        if not path.exists(config_path):
            return [None, None]
        with open(config_path) as file:
            for line in file:
                line = line.replace('\n', '')
                if line == self.do_not_edit_line:
                    found_saved = True
                    saved_def = []
                    continue
                if found_saved is False:
                    user_def.append(line.replace('\n', ''))
                else:
                    if line.startswith(self.do_not_edit_prefix):
                        saved_def.append(line[(len(self.do_not_edit_prefix)+1):])
        return ["\n".join(user_def), None if saved_def is None else "\n".join(saved_def)]

    def get_config_file_location(self, file):
        logging.info("Passed config file: %s" % file)
        if not path.exists(file):
            file = "%s/%s" % (os.getcwd(), self.configfile_name)
            if not path.exists(file):
                file = os.path.expanduser("~/") + "klipper_config/%s" % (self.configfile_name)
                if not path.exists(file):
                    file = self.default_config_path

        logging.info("Found configuration file at: %s" % file)
        return file

    def get_config(self):
        return self.config

    def get_configurable_options(self):
        return self.configurable_options

    def get_lang(self):
        return self.lang

    def get_main_config(self):
        return self.config['main']

    def get_main_config_option(self, option, default=None):
        return self.config['main'].get(option, default)

    def get_menu_items(self, menu="__main", subsection=""):
        if subsection != "":
            subsection = subsection + " "
        index = "menu %s %s" % (menu, subsection)
        items = [i[len(index):] for i in self.config.sections() if i.startswith(index)]
        menu_items = []
        for item in items:
            split = item.split()
            if len(split) == 1:
                menu_items.append(self._build_menu_item(menu, index + item))

        return menu_items

    def get_menu_name(self, menu="__main", subsection=""):
        name = ("menu %s %s" % (menu, subsection)) if subsection != "" else ("menu %s" % menu)
        if name not in self.config:
            return False
        return self.config[name].get('name')


    def get_preheat_options(self):
        index = "preheat "
        items = [i[len(index):] for i in self.config.sections() if i.startswith(index)]

        preheat_options = {}
        for item in items:
            preheat_options[item] = self._build_preheat_item(index + item)

        return preheat_options

    def get_printer_config(self, name):
        if not name.startswith("printer "):
            name = "printer %s" % name

        if name not in self.config:
            return None
        return self.config[name]

    def get_printer_power_name(self):
        return self.config['settings'].get("printer_power_name", "printer")

    def get_printers(self):
        return self.printers

    def get_user_saved_config(self):
        if self.config_path != self.default_config_path:
            print("Get")

    def save_user_config_options(self):
        save_config = configparser.ConfigParser()
        for item in self.configurable_options:
            name = list(item)[0]
            opt = item[name]
            curval = self.config[opt['section']].get(name)
            if curval != opt["value"] or (
                    self.defined_config is not None and opt['section'] in self.defined_config.sections() and
                    self.defined_config[opt['section']].get(name, None) not in (None, curval)):
                if opt['section'] not in save_config.sections():
                    save_config.add_section(opt['section'])
                save_config.set(opt['section'], name, str(curval))

        macro_sections = [i for i in self.config.sections() if i.startswith("displayed_macros")]
        for macro_sec in macro_sections:
            for item in self.config.options(macro_sec):
                value = self.config[macro_sec].getboolean(item, fallback=True)
                if value is False or (self.defined_config is not None and
                                      macro_sec in self.defined_config.sections() and
                                      self.defined_config[macro_sec].getboolean(item, fallback=True) is False and
                                      self.defined_config[macro_sec].getboolean(item, fallback=True) != value):
                    if macro_sec not in save_config.sections():
                        save_config.add_section(macro_sec)
                    save_config.set(macro_sec, item, str(value))

        save_output = self._build_config_string(save_config).split("\n")
        for i in range(len(save_output)):
            save_output[i] = "%s %s" % (self.do_not_edit_prefix, save_output[i])

        if self.config_path == self.default_config_path:
            user_def = ""
            saved_def = None
        else:
            user_def, saved_def = self.separate_saved_config(self.config_path)

        extra_lb = "\n" if saved_def is not None else ""
        contents = "%s\n%s%s\n%s\n%s\n%s\n" % (
            user_def, self.do_not_edit_line, extra_lb, self.do_not_edit_prefix, "\n".join(save_output),
            self.do_not_edit_prefix)

        if self.config_path != self.default_config_path:
            path = self.config_path
        else:
            path = os.path.expanduser("~/")
            if os.path.exists(path+"klipper_config/"):
                path = path + "klipper_config/KlipperScreen.conf"
            else:
                path = path + "KlipperScreen.conf"

        try:
            file = open(path, 'w')
            file.write(contents)
            file.close()
        except Exception:
            logging.error("Error writing configuration file in %s" % path)

    def set(self, section, name, value):
        self.config.set(section, name, value)

    def log_config(self, config):
        lines = [
            " "
            "===== Config File =====",
            re.sub(
                r'(moonraker_api_key\s*=\s*\S+)',
                'moonraker_api_key = [redacted]',
                self._build_config_string(config)
            ),
            "======================="
        ]
        logging.info("\n".join(lines))

    def _build_config_string(self, config):
        sfile = StringIO()
        config.write(sfile)
        sfile.seek(0)
        return sfile.read().strip()

    def _build_menu_item(self, menu, name):
        if name not in self.config:
            return False
        cfg = self.config[name]
        item = {
            "name": cfg.get("name"),
            "icon": cfg.get("icon"),
            "panel": cfg.get("panel", False),
            "method": cfg.get("method", False),
            "confirm": cfg.get("confirm", False),
            "enable": cfg.get("enable", True)
        }

        try:
            item["params"] = json.loads(cfg.get("params", "{}"))
        except Exception:
            logging.debug("Unable to parse parameters for [%s]" % name)
            item["params"] = {}

        return {name[(len(menu) + 6):]: item}

    def _build_preheat_item(self, name):
        if name not in self.config:
            return False
        cfg = self.config[name]
        item = {
            "extruder": cfg.getint("extruder", 0),
            "bed": cfg.getint("bed", 0),
            "heater_generic": cfg.getint("heater_generic", 0),
            "gcode": cfg.get("gcode", None)
        }
        return item
