#!/usr/bin/env python
# File: /home/stefano/python/FoBiS/src/main/python/fobis/Fobos.py
# Author: Stefano Zaghi <stefano.zaghi@gmail.com>
# Date: 28.08.2017
# Last Modified Date: 28.08.2017
# Last Modified By: Stefano Zaghi <stefano.zaghi@gmail.com>
"""
fobos.py, module definition of fobos class.
This is a class aimed at fobos file handling.
"""

# Copyright (C) 2015  Stefano Zaghi
#
# This file is part of FoBiS.py.
#
# FoBiS.py is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# FoBiS.py is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with FoBiS.py. If not, see <http://www.gnu.org/licenses/>.
import configparser
import os
import re
import sys
from collections.abc import Callable
from copy import deepcopy
from typing import Any, Optional

from .utils import check_results, print_fake, syswork


class Fobos:
    """
    Fobos is an object that handles fobos file, its attributes and methods.
    """

    def __init__(
        self,
        cliargs: Any,
        print_n: Optional[Callable[..., None]] = None,
        print_w: Optional[Callable[..., None]] = None,
    ) -> None:
        """
        Parameters
        ----------
        cliargs : argparse object
        print_n : {None}
          function for printing normal message
        print_w : {None}
          function for printing emphized warning message
        """
        if print_n is None:
            self.print_n = print_fake
        else:
            self.print_n = print_n

        if print_w is None:
            self.print_w = print_fake
        else:
            self.print_w = print_w

        self.fobos = None
        self.mode = None
        self.local_variables = {}
        if cliargs.fobos:
            filename = cliargs.fobos
        else:
            filename = "fobos"
        if os.path.exists(filename):
            self.fobos = configparser.RawConfigParser()
            if not cliargs.fobos_case_insensitive:
                self.fobos.optionxform = str  # case sensitive
            self.fobos.read(filename)
            self._set_cliargs(cliargs=cliargs)
        return

    def _check_mode(self, mode):
        """
        Function for checking the presence of the selected mode into the set defined inside the fobos.

        Parameters
        ----------
        mode : str
          name of the selcted mode
        """
        if self.fobos:
            if self.fobos.has_option("modes", "modes"):
                if mode in self.fobos.get("modes", "modes"):
                    self.mode = mode
                else:
                    self.print_w('Error: the mode "' + mode + '" is not defined into the fobos file.')
                    self.modes_list()
                    sys.exit(1)
            else:
                self.print_w('Error: fobos file has not "modes" section.')
                sys.exit(1)
        return

    def _set_mode(self, mode=None):
        """
        Function for setting the selected mode.

        Parameters
        ----------
        mode : {None}
          selected mode
        """
        if self.fobos:
            if mode:
                self._check_mode(mode=mode)
            else:
                if self.fobos.has_option("modes", "modes"):
                    self.mode = self.fobos.get("modes", "modes").split()[0]  # first mode selected
                else:
                    if self.fobos.has_section("default"):
                        self.mode = "default"
                    else:
                        self.print_w('Warning: fobos file has not "modes" section neither "default" one')
        return

    def _check_template(self):
        """
        Check and apply template sections.

        Each mode may specify one or more template names as a space-separated
        list.  Templates are applied left-to-right: the mode itself always
        wins, and earlier templates take precedence over later ones.
        Template-of-template inheritance is expanded depth-first.
        Circular references are detected and cause an error.
        """
        if not self.fobos:
            return

        def _template_names(section):
            if self.fobos.has_option(section, "template"):
                return self.fobos.get(section, "template").split()
            return []

        def _resolve(section, visiting):
            """Return ordered list of template names to apply for section."""
            if section in visiting:
                self.print_w('Error: circular template reference detected involving "' + section + '"')
                sys.exit(1)
            visiting = visiting | {section}
            resolved = []
            for name in _template_names(section):
                if not self.fobos.has_section(name):
                    self.print_w('Error: mode "' + section + '" uses template "' + name + '" that is NOT defined')
                    sys.exit(1)
                if name not in resolved:
                    resolved.append(name)
                for sub in _resolve(name, visiting):
                    if sub not in resolved:
                        resolved.append(sub)
            return resolved

        for section in self.fobos.sections():
            if not self.fobos.has_option(section, "template"):
                continue
            for template in _resolve(section, set()):
                for item in self.fobos.items(template):
                    if item[0] == "template":
                        continue
                    if not self.fobos.has_option(section, item[0]):
                        self.fobos.set(section, item[0], item[1])
        return

    def _get_local_variables(self):
        """
        Get the definition of local variables defined into any sections (modes).
        """
        if self.fobos:
            for section in self.fobos.sections():
                for item in self.fobos.items(section):
                    if item[0].startswith("$"):
                        self.local_variables[item[0]] = item[1].replace("\n", " ")
        return

    def _substitute_local_variables_mode(self):
        """
        Substitute the definition of local variables defined into the mode (section) selected.
        """
        if self.fobos and self.mode:
            self._substitute_local_variables_section(section=self.mode)
        return

    def _substitute_local_variables_section(self, section):
        """
        Substitute the definition of local variables defined into a section.
        """
        if self.fobos:
            if self.fobos.has_section(section):
                for item in self.fobos.items(section):
                    item_val = item[1]
                    for key, value in list(self.local_variables.items()):
                        item_val = re.sub(re.escape(key), value, item_val)
                        # item_val = re.sub(r"(?!" + re.escape(key) + r"[aZ_-])\s*" + re.escape(key) + r"\s*", value, item_val)
                    self.fobos.set(section, item[0], item_val)
        return

    def _check_local_variables(self):
        """
        Get and substitute the definition of local variables defined into any sections (modes).
        """
        if self.fobos:
            self._get_local_variables()
            if len(self.local_variables) > 0:
                self._substitute_local_variables_mode()
        return

    def _set_cliargs_attributes(self, cliargs, cliargs_dict):
        """
        Set attributes of cliargs from fobos options.

        Parameters
        ----------
        cliargs : argparse object
        cliargs_dict : argparse object attributes dictionary
        """
        if self.mode:
            for item in self.fobos.items(self.mode):
                if item[0] in cliargs_dict:
                    if isinstance(cliargs_dict[item[0]], bool):
                        setattr(cliargs, item[0], self.fobos.getboolean(self.mode, item[0]))
                    elif isinstance(cliargs_dict[item[0]], int):
                        setattr(cliargs, item[0], int(item[1]))
                    elif isinstance(cliargs_dict[item[0]], list):
                        setattr(cliargs, item[0], item[1].split())
                    else:
                        setattr(cliargs, item[0], item[1])
        return

    @staticmethod
    def _check_cliargs_cflags(cliargs, cliargs_dict):
        """
        Method for setting attribute of cliargs.

        Parameters
        ----------
        cliargs : argparse object
        cliargs_dict : argparse object attributes dictionary
        """
        for item in cliargs_dict:
            if item in ["cflags", "lflags", "preproc"]:
                val_cli = cliargs_dict[item]
                val_fobos = getattr(cliargs, item)
                if item == "cflags":
                    if val_cli == "-c":
                        match = re.search(r"(-c\s+|-c$)", val_fobos)
                        if match:
                            val_cli = ""  # avoid multiple -c flags
                if val_fobos and val_cli:
                    setattr(cliargs, item, val_fobos + " " + val_cli)
        return

    def _set_cliargs(self, cliargs):
        """
        Set cliargs from fobos options.

        Parameters
        ----------
        cliargs : argparse object
        """
        if self.fobos:
            cliargs_dict = deepcopy(cliargs.__dict__)
            self._set_mode(mode=cliargs.mode)
            self._check_template()
            self._check_local_variables()
            self._set_cliargs_attributes(cliargs=cliargs, cliargs_dict=cliargs_dict)
            self._check_cliargs_cflags(cliargs=cliargs, cliargs_dict=cliargs_dict)
        return

    def get(self, option: str, mode: Optional[str] = None, toprint: bool = True) -> Optional[str]:
        """
        Get options defined into the fobos file.

        Parameters
        ----------
        option : str
          option name
        mode : {None}
          eventual mode name
        toprint : {True}
          return of the value: if toprint==False the value is return otherwise is printed to stdout
        """
        value = ""
        if self.fobos:
            self._set_mode(mode=mode)
            if self.fobos.has_option(self.mode, option):
                value = self.fobos.get(self.mode, option)
        if toprint:
            # self.print_w(value)
            print(value)
            return
        else:
            return value

    def get_output_name(self, mode: Optional[str] = None, toprint: bool = True) -> Optional[str]:
        """
        Method for building the output name accordingly to the fobos options.

        Parameters
        ----------
        mode : {None}
          eventual mode name
        toprint : {True}
          return of the value: if toprint==False the value is return otherwise is printed to stdout
        """
        output = ""
        build_dir = self.get(option="build_dir", mode=mode, toprint=False)
        mklib = self.get(option="mklib", mode=mode, toprint=False)
        if self.fobos:
            self._set_mode(mode=mode)
            if self.fobos.has_option(self.mode, "output"):
                output = self.fobos.get(self.mode, "output")
                output = os.path.normpath(os.path.join(build_dir, output))
            elif self.fobos.has_option(self.mode, "target"):
                output = self.fobos.get(self.mode, "target")
                output = os.path.splitext(os.path.basename(output))[0]
                if mklib.lower() == "shared":
                    output = output + ".so"
                elif mklib.lower() == "static":
                    output = output + ".a"
                output = os.path.normpath(os.path.join(build_dir, output))
        if toprint:
            # self.print_w(output)
            print(output)
            return
        else:
            return output

    def modes_list(self) -> None:
        """List defined modes."""
        if self.fobos:
            self.print_n("The fobos file defines the following modes:")
            if self.fobos.has_option("modes", "modes"):
                modes = self.fobos.get("modes", "modes").split()
                for mode in modes:
                    if self.fobos.has_section(mode):
                        if self.fobos.has_option(mode, "help"):
                            helpmsg = self.fobos.get(mode, "help")
                        else:
                            helpmsg = ""
                        self.print_n('  - "' + mode + '" ' + helpmsg)
            else:
                self.print_w("Error: no modes are defined into the fobos file!")
                sys.exit(1)
        sys.exit(0)
        return

    @staticmethod
    def print_template(cliargs: Any) -> None:
        """
        Print fobos template.

        Parameters
        ----------
        cliargs : argparse object
        """
        print("[default]")
        for argument in vars(cliargs):
            attribute = getattr(cliargs, argument)
            if isinstance(attribute, list):
                attribute = " ".join(attribute)
            print(str(argument) + " = " + str(attribute))

    def get_project_info(self) -> dict[str, Any]:
        """
        Parse [project] section and return project metadata.

        Returns
        -------
        dict
          dict with keys 'name' (str), 'authors' (list of str),
          'version' (str, raw value as written in fobos — not resolved),
          'summary' (str), 'repository' (str), 'website' (str), and 'email' (str).
          All values are empty/empty-list if the section or option is absent.
        """
        info = {"name": "", "authors": [], "version": "", "summary": "", "repository": "", "website": "", "email": ""}
        if self.fobos and self.fobos.has_section("project"):
            if self.fobos.has_option("project", "name"):
                info["name"] = self.fobos.get("project", "name").strip()
            if self.fobos.has_option("project", "authors"):
                raw = self.fobos.get("project", "authors")
                info["authors"] = [a.strip() for a in raw.splitlines() if a.strip()]
            if self.fobos.has_option("project", "version"):
                info["version"] = self.fobos.get("project", "version").strip()
            if self.fobos.has_option("project", "summary"):
                info["summary"] = self.fobos.get("project", "summary").strip()
            if self.fobos.has_option("project", "repository"):
                info["repository"] = self.fobos.get("project", "repository").strip()
            if self.fobos.has_option("project", "website"):
                info["website"] = self.fobos.get("project", "website").strip()
            if self.fobos.has_option("project", "email"):
                info["email"] = self.fobos.get("project", "email").strip()
        return info

    def get_version(self) -> str:
        """
        Resolve the project version from [project] and/or git tags.

        Resolution steps
        ----------------
        1. Read 'version' from [project] in fobos.  If the value is a
           file path (relative to the git repository root), the version
           string is read from that file.
        2. Query the most recent git tag via ``git describe --tags --abbrev=0``.
        3. If both sources provide a version and they disagree, emit a
           warning with a suggested fix.
        4. Return the fobos version when available; fall back to the git
           tag; return '' when neither source is determinable.

        Returns
        -------
        str
          Resolved version string, or '' if not determinable.
        """
        fobos_version = ""
        if self.fobos and self.fobos.has_section("project"):
            if self.fobos.has_option("project", "version"):
                raw = self.fobos.get("project", "version").strip()
                # try to resolve as a file path relative to the git repo root
                git_root_result = syswork("git rev-parse --show-toplevel")
                if git_root_result[0] == 0:
                    candidate = os.path.join(git_root_result[1].strip(), raw)
                    if os.path.isfile(candidate):
                        with open(candidate) as ver_file:
                            fobos_version = ver_file.read().strip()
                    else:
                        fobos_version = raw
                else:
                    fobos_version = raw  # not inside a git repo; treat as literal
                if fobos_version and not fobos_version.startswith("v"):
                    fobos_version = "v" + fobos_version

        # query the most recent git tag
        git_version = ""
        git_result = syswork("git describe --tags --abbrev=0")
        if git_result[0] == 0:
            git_version = git_result[1].strip()

        # warn on mismatch
        if fobos_version and git_version and fobos_version != git_version:
            git_version_v = git_version if git_version.startswith("v") else "v" + git_version
            self.print_w("Warning: project version mismatch!")
            self.print_w("  fobos [project] version : " + fobos_version)
            self.print_w("  git tag version         : " + git_version)
            self.print_w("  To fix, either:")
            self.print_w("    - update fobos: set  version = " + git_version_v + "  under [project]")
            self.print_w("    - create a matching tag: git tag " + fobos_version + " && git push --tags")

        return fobos_version or git_version

    def get_dependencies(self) -> dict[str, str]:
        """
        Parse [dependencies] section and return dict of {name: spec_string}.

        Returns
        -------
        dict
          mapping of dependency name to its spec string, or empty dict if no section
        """
        deps = {}
        if self.fobos and self.fobos.has_section("dependencies"):
            for name, spec in self.fobos.items("dependencies"):
                if name == "deps_dir":
                    continue
                deps[name] = spec
        return deps

    def get_deps_dir(self, default: str = ".fobis_deps") -> str:
        """
        Read deps_dir from [dependencies] section of fobos.

        Parameters
        ----------
        default : str
          value returned when the option is absent [default: '.fobis_deps']

        Returns
        -------
        str
          deps_dir value from fobos, or default if not set
        """
        if self.fobos and self.fobos.has_section("dependencies"):
            if self.fobos.has_option("dependencies", "deps_dir"):
                return self.fobos.get("dependencies", "deps_dir").strip()
        return default

    def rules_list(self, quiet: bool = False) -> None:
        """
        Function for listing defined rules.

        Parameters
        ----------
        quiet : {False}
          less verbose outputs than default
        """
        if self.fobos:
            self.print_n("The fobos file defines the following rules:")
            for rule in self.fobos.sections():
                if rule.startswith("rule-"):
                    if self.fobos.has_option(rule, "help"):
                        helpmsg = self.fobos.get(rule, "help")
                    else:
                        helpmsg = ""
                    self.print_n('  - "' + rule.split("rule-")[1] + '" ' + helpmsg)
                    if self.fobos.has_option(rule, "quiet"):
                        quiet = self.fobos.getboolean(rule, "quiet")
                    for rul in self.fobos.options(rule):
                        if rul.startswith("rule"):
                            if not quiet:
                                self.print_n("       Command => " + self.fobos.get(rule, rul))
        sys.exit(0)
        return

    def rule_execute(self, rule: str, quiet: bool = False, log: bool = False) -> None:
        """
        Function for executing selected rule.

        Parameters
        ----------
        rule : str
          rule name
        quiet : {False}
          less verbose outputs than default
        log : {False}
          bool for activate errors log saving
        """
        if self.fobos:
            self.print_n('Executing rule "' + rule + '"')
            rule_name = "rule-" + rule
            if self.fobos.has_section(rule_name):
                self._get_local_variables()
                self._substitute_local_variables_section(section=rule_name)
                results = []
                quiet = False
                log = False
                if self.fobos.has_option(rule_name, "quiet"):
                    quiet = self.fobos.getboolean(rule_name, "quiet")
                if self.fobos.has_option(rule_name, "log"):
                    log = self.fobos.getboolean(rule_name, "log")
                for rul in self.fobos.options(rule_name):
                    if rul.startswith("rule"):
                        if not quiet:
                            self.print_n("   Command => " + self.fobos.get(rule_name, rul))
                        result = syswork(self.fobos.get(rule_name, rul))
                        results.append(result)
                if log:
                    check_results(results=results, log="rules_errors.log", print_w=self.print_w)
                else:
                    check_results(results=results, print_w=self.print_w)
            else:
                self.print_w('Error: the rule "' + rule + '" is not defined into the fobos file. Defined rules are:')
                self.rules_list(quiet=quiet)
                sys.exit(1)
        return
