# config.py - deals with loading configuration information.

# Loads config data from a .cfg file.  Also caches the compiled
# data back into a .cfc file.

# If you are wondering how to avoid needing .cfg files (eg,
# if you are freezing Pythonwin etc) I suggest you create a
# .py file, and put the config info in a docstring.  Then
# pass a CStringIO file (rather than a filename) to the
# config manager.
import glob
import importlib.util
import marshal
import os
import stat
import sys
import traceback
import types

import pywin
import win32api

from . import keycodes

debugging = 0
if debugging:
    import win32traceutil  # Some trace statements fire before the interactive window is open.

    def trace(*args):
        sys.stderr.write(" ".join(map(str, args)) + "\n")

else:
    trace = lambda *args: None

compiled_config_version = 3


def split_line(line, lineno):
    comment_pos = line.find("#")
    if comment_pos >= 0:
        line = line[:comment_pos]
    sep_pos = line.rfind("=")
    if sep_pos == -1:
        if line.strip():
            print(f"Warning: Line {lineno}: {line!r} is an invalid entry")
            return None, None
        return "", ""
    return line[:sep_pos].strip(), line[sep_pos + 1 :].strip()


def get_section_header(line):
    # Returns the section if the line is a section header, else None
    if line[0] == "[":
        end = line.find("]")
        if end == -1:
            end = len(line)
        rc = line[1:end].lower()
        try:
            i = rc.index(":")
            return rc[:i], rc[i + 1 :]
        except ValueError:
            return rc, ""
    return None, None


def find_config_file(f):
    return os.path.join(next(iter(pywin.__path__)), f + ".cfg")


def find_config_files():
    return [
        os.path.split(x)[1]
        for x in [
            os.path.splitext(x)[0]
            for x in glob.glob(os.path.join(next(iter(pywin.__path__)), "*.cfg"))
        ]
    ]


class ConfigManager:
    def __init__(self, f):
        self.filename = "unknown"
        self.last_error = None
        self.key_to_events = {}
        b_close = False
        if hasattr(f, "readline"):
            fp = f
            self.filename = "<config string>"
            compiled_name = None
        else:
            try:
                f = find_config_file(f)
                src_stat = os.stat(f)
            except OSError:
                self.report_error("Config file '%s' not found" % f)
                return
            self.filename = f
            self.basename = os.path.basename(f)
            trace("Loading configuration", self.basename)
            compiled_name = os.path.splitext(f)[0] + ".cfc"
            try:
                cf = open(compiled_name, "rb")
                try:
                    ver = marshal.load(cf)
                    ok = compiled_config_version == ver
                    if ok:
                        kblayoutname = marshal.load(cf)
                        magic = marshal.load(cf)
                        size = marshal.load(cf)
                        mtime = marshal.load(cf)
                        if (
                            magic == importlib.util.MAGIC_NUMBER
                            and win32api.GetKeyboardLayoutName() == kblayoutname
                            and src_stat[stat.ST_MTIME] == mtime
                            and src_stat[stat.ST_SIZE] == size
                        ):
                            self.cache = marshal.load(cf)
                            trace("Configuration loaded cached", compiled_name)
                            return  # We are ready to roll!
                finally:
                    cf.close()
            except (OSError, EOFError):
                pass
            fp = open(f)
            b_close = True
        self.cache = {}
        lineno = 1
        line = fp.readline()
        while line:
            # Skip to the next section (maybe already there!)
            section, subsection = get_section_header(line)
            while line and section is None:
                line = fp.readline()
                if not line:
                    break
                lineno += 1
                section, subsection = get_section_header(line)
            if not line:
                break

            if section == "keys":
                line, lineno = self._load_keys(subsection, fp, lineno)
            elif section == "extensions":
                line, lineno = self._load_extensions(subsection, fp, lineno)
            elif section == "idle extensions":
                line, lineno = self._load_idle_extensions(subsection, fp, lineno)
            elif section == "general":
                line, lineno = self._load_general(subsection, fp, lineno)
            else:
                self.report_error(
                    f"Unrecognised section header '{section}:{subsection}'"
                )
                line = fp.readline()
                lineno += 1
        if b_close:
            fp.close()
        # Check critical data.
        if not self.cache.get("keys"):
            self.report_error("No keyboard definitions were loaded")
        if not self.last_error and compiled_name:
            try:
                cf = open(compiled_name, "wb")
                marshal.dump(compiled_config_version, cf)
                marshal.dump(win32api.GetKeyboardLayoutName(), cf)
                marshal.dump(importlib.util.MAGIC_NUMBER, cf)
                marshal.dump(src_stat[stat.ST_SIZE], cf)
                marshal.dump(src_stat[stat.ST_MTIME], cf)
                marshal.dump(self.cache, cf)
                cf.close()
            except (OSError, EOFError):
                pass  # Ignore errors - may be read only.

    def configure(self, editor, subsections=None):
        # Execute the extension code, and find any events.
        # First, we "recursively" connect any we are based on.
        if subsections is None:
            subsections = []
        subsections = [""] + subsections
        general = self.get_data("general")
        if general:
            parents = general.get("based on", [])
            for parent in parents:
                trace("Configuration based on", parent, "- loading.")
                parent = self.__class__(parent)
                parent.configure(editor, subsections)
                if parent.last_error:
                    self.report_error(parent.last_error)

        bindings = editor.bindings
        codeob = self.get_data("extension code")
        if codeob is not None:
            ns = {}
            try:
                exec(codeob, ns)
            except:
                traceback.print_exc()
                self.report_error("Executing extension code failed")
                ns = None
            if ns:
                num = 0
                for name, func in ns.items():
                    if isinstance(func, types.FunctionType) and name[:1] != "_":
                        bindings.bind(name, func)
                        num += 1
                trace("Configuration Extension code loaded", num, "events")
        # Load the idle extensions
        for subsection in subsections:
            for ext in self.get_data("idle extensions", {}).get(subsection, []):
                try:
                    editor.idle.IDLEExtension(ext)
                    trace("Loaded IDLE extension", ext)
                except:
                    self.report_error("Can not load the IDLE extension '%s'" % ext)

        # Now bind up the key-map (remembering a reverse map
        subsection_keymap = self.get_data("keys")
        num_bound = 0
        for subsection in subsections:
            keymap = subsection_keymap.get(subsection, {})
            bindings.update_keymap(keymap)
            num_bound += len(keymap)
        trace("Configuration bound", num_bound, "keys")

    def get_key_binding(self, event, subsections=None):
        if subsections is None:
            subsections = []
        subsections = [""] + subsections

        subsection_keymap = self.get_data("keys")
        for subsection in subsections:
            map = self.key_to_events.get(subsection)
            if map is None:  # Build it
                keymap = subsection_keymap.get(subsection, {})
                map = {map_event: key_info for key_info, map_event in keymap.items()}
                self.key_to_events[subsection] = map

            info = map.get(event)
            if info is not None:
                return keycodes.make_key_name(info[0], info[1])
        return None

    def report_error(self, msg):
        self.last_error = msg
        print(f"Error in {self.filename}: {msg}")

    def report_warning(self, msg):
        print(f"Warning in {self.filename}: {msg}")

    def _readline(self, fp, lineno, bStripComments=1):
        line = fp.readline()
        lineno += 1
        if line:
            bBreak = (
                get_section_header(line)[0] is not None
            )  # A new section is starting
            if bStripComments and not bBreak:
                pos = line.find("#")
                if pos >= 0:
                    line = line[:pos] + "\n"
        else:
            bBreak = 1
        return line, lineno, bBreak

    def get_data(self, name, default=None):
        return self.cache.get(name, default)

    def _save_data(self, name, data):
        self.cache[name] = data
        return data

    def _load_general(self, sub_section, fp, lineno):
        map = {}
        while 1:
            line, lineno, bBreak = self._readline(fp, lineno)
            if bBreak:
                break

            key, val = split_line(line, lineno)
            if not key:
                continue
            key = key.lower()
            l = map.get(key, [])
            l.append(val)
            map[key] = l
        self._save_data("general", map)
        return line, lineno

    def _load_keys(self, sub_section, fp, lineno):
        # Builds a nested dictionary of
        # (scancode, flags) = event_name
        main_map = self.get_data("keys", {})
        map = main_map.get(sub_section, {})
        while 1:
            line, lineno, bBreak = self._readline(fp, lineno)
            if bBreak:
                break

            key, event = split_line(line, lineno)
            if not event:
                continue
            sc, flag = keycodes.parse_key_name(key)
            if sc is None:
                self.report_warning("Line %d: Invalid key name '%s'" % (lineno, key))
            else:
                map[sc, flag] = event
        main_map[sub_section] = map
        self._save_data("keys", main_map)
        return line, lineno

    def _load_extensions(self, sub_section, fp, lineno):
        start_lineno = lineno
        lines = []
        while 1:
            line, lineno, bBreak = self._readline(fp, lineno, 0)
            if bBreak:
                break
            lines.append(line)
        try:
            c = compile(
                "\n" * start_lineno + "".join(lines),  # produces correct tracebacks
                self.filename,
                "exec",
            )
            self._save_data("extension code", c)
        except SyntaxError as details:
            errlineno = details.lineno + start_lineno
            # Should handle syntax errors better here, and offset the lineno.
            self.report_error(
                "Compiling extension code failed:\r\nFile: %s\r\nLine %d\r\n%s"
                % (details.filename, errlineno, details.msg)
            )
        return line, lineno

    def _load_idle_extensions(self, sub_section, fp, lineno):
        extension_map = self.get_data("idle extensions")
        if extension_map is None:
            extension_map = {}
        extensions = []
        while 1:
            line, lineno, bBreak = self._readline(fp, lineno)
            if bBreak:
                break
            line = line.strip()
            if line:
                extensions.append(line)
        extension_map[sub_section] = extensions
        self._save_data("idle extensions", extension_map)
        return line, lineno


def test():
    import time

    start = time.clock()
    f = "default"
    cm = ConfigManager(f)
    map = cm.get_data("keys")
    took = time.clock() - start
    print(f"Loaded {len(map)} items in {took:.4f} secs")


if __name__ == "__main__":
    test()
