#
# simpleconfig.py - representation of a simple configuration file (sh-like)
#
# Copyright (C) 1999-2015 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties 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 this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
import os
import shlex
import string
import tempfile
from pyanaconda.core.string import upper_ascii

_SAFECHARS = frozenset(string.ascii_letters + string.digits + '@%_-+=:,./')


def unquote(s):
    return ' '.join(shlex.split(s))


def quote(s, always=False):
    """ If always is set it returns a quoted value
    """
    if not always:
        for c in s:
            if c not in _SAFECHARS:
                break
        else:
            return s
    return '"' + s.replace('"', '\\"') + '"'


def find_comment(s):
    """ Look for a # comment outside of a quoted string.
        If there are no quotes, find the last # in the string.

        :param str s: string to check for comment and quotes
        :returns: index of comment or None
        :rtype: int or None

        Handles comments inside quotes and quotes inside quotes.
    """
    q = None
    for i in range(len(s)):
        if not q and s[i] == '#':
            return i

        # Ignore quotes inside other quotes
        if s[i] in "'\"":
            if s[i] == q:
                q = None
            elif q is None:
                q = s[i]
    return None


def write_tmpfile(filename, data):
    # Create a temporary in the same directory as the target file to ensure
    # the new file is on the same filesystem
    tmpf = tempfile.NamedTemporaryFile(mode="w", delete=False,
                                       dir=os.path.dirname(filename) or '.',
                                       prefix="." + os.path.basename(filename))
    tmpf.write(data)
    tmpf.close()

    # Change the permissions (currently 0600) to match the original file
    if os.path.exists(filename):
        m = os.stat(filename).st_mode
    else:
        m = 0o0644
    os.chmod(tmpf.name, m)

    # Move the temporary file over the top of the original
    os.rename(tmpf.name, filename)


class SimpleConfigFile(object):
    """ Edit values in a configuration file without changing comments.
        Supports KEY=VALUE lines and ignores everything else.
        Supports adding new keys.
        Supports deleting keys.
        Preserves comment, blank lines and comments on KEY lines
        Does not support duplicate key entries.
    """
    def __init__(self, filename=None, read_unquote=True, write_quote=True,
                 always_quote=False):
        self.filename = filename
        self.read_unquote = read_unquote
        self.write_quote = write_quote
        self.always_quote = always_quote
        self.reset()

    def reset(self):
        self._lines = []
        self.info = {}

    def read(self, filename=None):
        """ passing filename will override the filename passed to init.

            save the lines into self._lines and the key/value pairs into
            self.info
        """
        filename = filename or self.filename
        with open(filename) as f:
            for line in f:
                self._lines.append(line)
                key, value, _comment = self._parseline(line)
                if key:
                    self.info[key] = value

    def write(self, filename=None, use_tmp=True):
        """ passing filename will override the filename passed to init.
        """
        filename = filename or self.filename
        if not filename:
            return None

        if use_tmp:
            write_tmpfile(filename, str(self))
        else:
            # write directly to the file
            with open(filename, "w") as fobj:
                fobj.write(str(self))

    def set(self, *args):
        for key, value in args:
            self.info[upper_ascii(key)] = value

    def unset(self, *keys):
        for key in (upper_ascii(k) for k in keys):
            if key in self.info:
                del self.info[key]

    def get(self, key):
        return self.info.get(upper_ascii(key), "")

    def _parseline(self, line):
        """ parse a line into a key, value and comment

            :param str line: Line to be parsed
            :returns: Tuple of key, value, comment
            :rtype: tuple

            Handle comments and optionally unquote quoted strings
            Returns (key, value, comment) or (None, None, comment)
            key is always UPPERCASE and comment may by "" if none was found.
        """
        s = line.strip()
        # Look for a # outside any quotes
        comment = ""
        comment_index = find_comment(s)
        if comment_index is not None:
            comment = s[comment_index:]
            s = s[:comment_index]   # remove from comment to EOL

        key, eq, val = s.partition('=')
        key = key.strip()
        val = val.strip()
        if self.read_unquote:
            val = unquote(val)
        if key != '' and eq == '=':
            return (upper_ascii(key), val, comment)
        else:
            return (None, None, comment)

    def _kvpair(self, key, comment=""):
        value = self.info[key]
        if self.write_quote or self.always_quote:
            value = quote(value, self.always_quote)
        if comment:
            comment = " " + comment
        return key + '=' + value + comment + "\n"

    def __str__(self):
        """ Return the file that was read, replacing existing keys with new values
            removing keys that have been deleted and adding new keys.
        """
        oldkeys = []
        s = ""
        for line in self._lines:
            key, _value, comment = self._parseline(line)
            if key is None:
                s += line
            else:
                if key not in self.info:
                    continue
                oldkeys.append(key)
                s += self._kvpair(key, comment)

        # Add new keys
        for key in self.info:
            if key not in oldkeys:
                s += self._kvpair(key)

        return s
