"""
php_ini - file ``/etc/php.ini``
===============================

This module provides the ``PHPConfig`` class parser, for reading the
options in the ``/etc/php.ini`` file.

Typical content of ``/etc/php.ini`` file is::

    [PHP]
    engine = On
    short_open_tag = Off
    precision = 14
    output_buffering = 4096
    zlib.output_compression = Off
    implicit_flush = Off
    unserialize_callback_func =
    serialize_precision = -1
    disable_functions =
    disable_classes =
    zend.enable_gc = On
    zend.exception_ignore_args = On
    zend.exception_string_param_max_len = 0
    expose_php = On
    max_execution_time = 30
    max_input_time = 60
    memory_limit = 128M
    error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
    default_mimetype = "text/html"

The class has one attribute ``data`` which is a nested ``dict`` representing sections
of the input INI file. Each section is represented as ``dict`` where keys are name
of options and values are values of those options.

Example:
    >>> php_conf["PHP"]["default_mimetype"].value
    'text/html'
    >>> php_config.data['PHP']['default_mimetype']
    'text/html'
    >>> php_conf.data['Session']['session.cache_limiter']
    'nocache'
    >>> php_conf["PHP"]["max_execution_time"].value
     30
    >>> php_conf["PHP"]["engine"].value  # 'On' turns to 'True'
    True
    >>> php_conf["PHP"]["short_opeh_tag"].value  # 'Off' turns to 'False'
    False
    >>> php_c['PHP']['precision'].value
    14
    >>> php_conf.get("PHP").get("memory_limit")  # '128M' is converted into bytes
    134217728
"""
import string

from insights.core import ConfigParser
from insights.core.filters import add_filter
from insights.core.exceptions import ParseException, SkipComponent
from insights.core.plugins import parser
from insights.parsr import (Char, EOF, HangingString, InSet, LeftBracket,
                            Lift, LineEnd, Literal, Many, Number,
                            OneLineComment, Opt, PosMarker, QuotedString,
                            RightBracket, String, WithIndent, WS, WSChar,
                            skip_none)
from insights.parsr.query import Directive, Entry, Section, eq
from insights.specs import Specs


# Filter to ensure that the section headings will always be included.
add_filter(Specs.php_ini, "[")


@parser(Specs.php_ini, continue_on_error=False)
class PHPConf(ConfigParser):
    """
    Class for php configuration file.
    """
    def parse_doc(self, content):
        try:
            def to_directive(x):
                name, rest = x
                rest = [rest] if rest is not None else []
                return Directive(name=name.value.strip(), attrs=rest, lineno=name.lineno, src=self)

            def to_section(name, rest):
                return Section(name=name.value.strip(), children=rest, lineno=name.lineno, src=self)

            def apply_defaults(cfg):
                if "DEFAULT" not in cfg:
                    return cfg

                defaults = cfg["DEFAULT"]
                not_defaults = cfg[~eq("DEFAULT")]
                for c in not_defaults:
                    for d in defaults.grandchildren:
                        if d.name not in c:
                            c.children.append(d)

                cfg.children = list(not_defaults)
                return cfg

            def make_bytes(number, char_multiple):
                if char_multiple.lower() == 'k':
                    return number * 2**10
                if char_multiple.lower() == 'm':
                    return number * 2**20
                if char_multiple.lower() == 'g':
                    return number * 2**30

            content = "\n".join(content)

            header_chars = (set(string.printable) - set(string.whitespace) - set("[]")) | set(" ")
            sep_chars = set("=")
            key_chars = header_chars - sep_chars
            value_chars = set(string.printable) - set("\n\r")

            On = Literal("on", True, ignore_case=True)
            Off = Literal("off", False, ignore_case=True)
            Tru = Literal("true", True, ignore_case=True)
            Fals = Literal("false", False, ignore_case=True)
            Boolean = ((On | Off | Tru | Fals) & (WSChar | LineEnd)) % "Boolean"
            Num = Number & (WSChar | LineEnd)
            QuoStr = QuotedString & (WSChar | LineEnd)
            # Handle php.ini shorthand notation for memory limits: 1G, 8M, 50K
            # https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
            MemNum = (Lift(make_bytes) * Number * (Char('K') | Char('M') | Char('G'))) & (WSChar | LineEnd)

            LeftEnd = (WS + LeftBracket + WS)
            RightEnd = (WS + RightBracket + WS)
            Header = (LeftEnd >> PosMarker(String(header_chars)) << RightEnd) % "Header"
            Key = WS >> PosMarker(String(key_chars)) << WS
            Sep = InSet(sep_chars, "Sep")
            Value = WS >> (Boolean | MemNum | Num | QuoStr | HangingString(value_chars))
            KVPair = WithIndent(Key + Opt(Sep >> Value)) % "KVPair"
            Comment = (WS >> (OneLineComment(";")).map(lambda x: None))

            Line = Comment | KVPair.map(to_directive)
            Sect = Lift(to_section) * Header * Many(Line).map(skip_none)
            Doc = Many(Comment | Sect).map(skip_none)
            Top = Doc << WS << EOF

            res = Entry(children=Top(content), src=self)
            return apply_defaults(res)

        except SkipComponent:
            raise
        except:
            raise ParseException("Could not parse content: '{0}'".
                                                format(content))

    def parse_content(self, content):
        super(PHPConf, self).parse_content(content)
        dict_all = {}
        for section in self.doc:
            section_dict = {}
            option_names = set(o.name for o in section)
            for name in option_names:
                options = [str(o.value) for o in section[name]]
                section_dict[name] = options[0] if len(options) == 1 else options
            dict_all[section.name] = section_dict
        self.data = dict_all
