# Copyright 2017 IBM
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

# Implementation of ini add / remove for devstack. We don't use the
# python ConfigFile parser because that ends up rewriting the entire
# file and doesn't ensure comments remain.

import os.path
import re
import shutil
import tempfile


class IniFile(object):
    """Class for manipulating ini files in place."""

    def __init__(self, fname):
        self.fname = fname

    def has(self, section, name):
        """Returns True if section has a key that is name"""

        current_section = ""
        if not os.path.exists(self.fname):
            return False

        with open(self.fname, "r+") as reader:
            for line in reader.readlines():
                m = re.match("\[([^\[\]]+)\]", line)
                if m:
                    current_section = m.group(1)
                if current_section == section:
                    if re.match("%s\s*\=" % name, line):
                        return True
        return False

    def add(self, section, name, value):
        """add a key / value to an ini file in a section.

        The new key value will be added at the beginning of the
        section, if no section is found a new section and key value
        will be added to the end of the file.
        """
        temp = tempfile.NamedTemporaryFile(mode='r')
        if os.path.exists(self.fname):
            shutil.copyfile(self.fname, temp.name)
        else:
            with open(temp.name, "w+"):
                pass

        found = False
        with open(self.fname, "w+") as writer:
            with open(temp.name) as reader:
                for line in reader.readlines():
                    writer.write(line)
                    m = re.match("\[([^\[\]]+)\]", line)
                    if m and m.group(1) == section:
                        found = True
                        writer.write("%s = %s\n" % (name, value))
            if not found:
                writer.write("[%s]\n" % section)
                writer.write("%s = %s\n" % (name, value))

    def _at_existing_key(self, section, name, func, match="%s\s*\="):
        """Run a function at a found key.

        NOTE(sdague): if the file isn't found, we end up
        exploding. This seems like the right behavior in nearly all
        circumstances.

        """
        temp = tempfile.NamedTemporaryFile(mode='r')
        shutil.copyfile(self.fname, temp.name)
        current_section = ""
        with open(temp.name) as reader:
            with open(self.fname, "w+") as writer:
                for line in reader.readlines():
                    m = re.match("\[([^\[\]]+)\]", line)
                    if m:
                        current_section = m.group(1)
                    if current_section == section:
                        if re.match(match % name, line):
                            # run function with writer and found line
                            func(writer, line)
                        else:
                            writer.write(line)
                    else:
                        writer.write(line)

    def remove(self, section, name):
        """remove a key / value from an ini file in a section."""
        def _do_remove(writer, line):
            pass

        self._at_existing_key(section, name, _do_remove)

    def comment(self, section, name):
        def _do_comment(writer, line):
            writer.write("# %s" % line)

        self._at_existing_key(section, name, _do_comment)

    def uncomment(self, section, name):
        def _do_uncomment(writer, line):
            writer.write(re.sub("^#\s*", "", line))

        self._at_existing_key(section, name, _do_uncomment,
                              match="#\s*%s\s*\=")

    def set(self, section, name, value):
        def _do_set(writer, line):
            writer.write("%s = %s\n" % (name, value))
        if self.has(section, name):
            self._at_existing_key(section, name, _do_set)
        else:
            self.add(section, name, value)


class LocalConf(object):
    """Class for manipulating local.conf files in place."""

    def __init__(self, fname):
        self.fname = fname

    def _conf(self, group, conf):
        current_section = ""
        for line in self._section(group, conf):
            m = re.match("\[([^\[\]]+)\]", line)
            if m:
                current_section = m.group(1)
                continue
            else:
                m2 = re.match(r"(\w+)\s*\=\s*(.+)", line)
                if m2:
                    yield current_section, m2.group(1), m2.group(2)

    def groups(self):
        """Return a list of all groups in the local.conf"""
        groups = []
        with open(self.fname) as reader:
            for line in reader.readlines():
                m = re.match(r"\[\[([^\[\]]+)\|([^\[\]]+)\]\]", line)
                if m:
                    group = (m.group(1), m.group(2))
                    groups.append(group)
        return groups

    def _section(self, group, conf):
        """Yield all the lines out of a meta section."""
        in_section = False
        with open(self.fname) as reader:
            for line in reader.readlines():
                if re.match(r"\[\[%s\|%s\]\]" % (
                        re.escape(group),
                        re.escape(conf)),
                        line):
                    in_section = True
                    continue
                # any other meta section means we aren't in the
                # section we want to be.
                elif re.match("\[\[.*\|.*\]\]", line):
                    in_section = False
                    continue
                if in_section:
                    yield line

    def _has_local_section(self):
        for group in self.groups():
            if group == ("local", "localrc"):
                return True
        return False

    def extract(self, group, conf, target):
        ini_file = IniFile(target)
        for section, name, value in self._conf(group, conf):
            ini_file.set(section, name, value)

    def extract_localrc(self, target):
        with open(target, "a+") as f:
            for line in self._section("local", "localrc"):
                f.write(line)

    def _at_insert_point_local(self, name, func):
        """Run function when we are at the right insertion point in file.

        This lets us process an arbitrary file and insert content at
        the correct point. It has a few different state flags that we
        are looking for.

        Does this file have a local section at all? If not, we need to
        write one early in the file (this means we work with an empty
        file, as well as a file that has only post-config sections.

        Are we currently in a local section, if so, we need to write
        out content to the end, because items added to local always
        have to be added at the end.

        Did we write out the work that we expected? If so, just blast
        all lines to the end of the file.

        """
        temp = tempfile.NamedTemporaryFile(mode='r')
        shutil.copyfile(self.fname, temp.name)
        in_local = False
        has_local = self._has_local_section()
        done = False
        with open(self.fname, "w+") as writer:
            with open(temp.name) as reader:
                for line in reader.readlines():
                    if done:
                        writer.write(line)
                        continue

                    if re.match(re.escape("[[local|localrc]]"), line):
                        in_local = True
                    elif in_local and re.match(re.escape("[["), line):
                        func(writer, None)
                        done = True
                        in_local = False
                    elif not has_local and re.match(re.escape("[["), line):
                        writer.write("[[local|localrc]]\n")
                        func(writer, None)
                        done = True
                        in_local = False
                        has_local = True

                    # otherwise, just write what we found
                    writer.write(line)
            if not done:
                func(writer, None)

    def set_local(self, line):
        if not os.path.exists(self.fname):
            with open(self.fname, "w+") as writer:
                writer.write("[[local|localrc]]\n")
                writer.write("%s\n" % line.rstrip())
                return

        def _do_set(writer, no_line):
            writer.write("%s\n" % line.rstrip())
        self._at_insert_point_local(line, _do_set)

    def _at_insert_point(self, group, conf, section, name, func):
        temp = tempfile.NamedTemporaryFile(mode='r')
        shutil.copyfile(self.fname, temp.name)
        in_meta = False
        in_section = False
        done = False
        with open(self.fname, "w+") as writer:
            with open(temp.name) as reader:
                for line in reader.readlines():
                    if done:
                        writer.write(line)
                        continue

                    if re.match(re.escape("[[%s|%s]]" % (group, conf)), line):
                        in_meta = True
                        writer.write(line)
                    elif re.match("\[\[.*\|.*\]\]", line):
                        # if we're not done yet, we
                        if in_meta:
                            if not in_section:
                                # if we've not found the section yet,
                                # write out section as well.
                                writer.write("[%s]\n" % section)
                            func(writer, None)
                            done = True
                        writer.write(line)
                        in_meta = False
                        in_section = False
                    elif re.match(re.escape("[%s]" % section), line):
                        # we found a relevant section
                        writer.write(line)
                        in_section = True
                    elif re.match("\[[^\[\]]+\]", line):
                        if in_meta and in_section:
                            # We've ended our section, in our meta,
                            # never found the key. Time to add it.
                            func(writer, None)
                            done = True
                        in_section = False
                        writer.write(line)
                    elif (in_meta and in_section and
                          re.match("\s*%s\s*\=" % re.escape(name), line)):
                        # we found our match point
                        func(writer, line)
                        done = True
                    else:
                        # write out whatever we find
                        writer.write(line)
            if not done:
                if not in_meta:
                    writer.write("[[%s|%s]]\n" % (group, conf))
                    in_section = False
                if not in_section:
                    writer.write("[%s]\n" % (section))
                func(writer, None)

    def set(self, group, conf, section, name, value):
        if not os.path.exists(self.fname):
            with open(self.fname, "w+") as writer:
                writer.write("[[%s|%s]]\n" % (group, conf))
                writer.write("[%s]\n" % section)
                writer.write("%s = %s\n" % (name, value))
                return

        def _do_set(writer, line):
            writer.write("%s = %s\n" % (name, value))
        self._at_insert_point(group, conf, section, name, _do_set)

    def merge_lc(self, lcfile):
        lc = LocalConf(lcfile)
        groups = lc.groups()
        for group, conf in groups:
            if group == "local":
                for line in lc._section(group, conf):
                    self.set_local(line)
            else:
                for section, name, value in lc._conf(group, conf):
                    self.set(group, conf, section, name, value)
