#
# screen_access.py:  screen access management for the Anaconda UI
#
# Copyright (C) 2017
# Red Hat, Inc.  All rights reserved.
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Author(s):  Martin Kolman <mkolman@redhat.com>
#
import os
CONFIG_FILE_NAME = "anaconda"
CONFIG_FILE_PATH = os.path.join("/etc/sysconfig", CONFIG_FILE_NAME)
CONFIG_TRUE = "1"
CONFIG_FALSE = "0"
CONFIG_BOOLEAN_TABLE = {
    CONFIG_TRUE: True,
    CONFIG_FALSE: False
}
CONFIG_VISITED_KEY = "visited"
CONFIG_GENERAL_SECTION = "General"
CONFIG_DISABLE_POSTINST_TOOLS_KEY = "post_install_tools_disabled"
CONFIG_OPTION_PREFIX = "changed_"

import logging
log = logging.getLogger("anaconda")

from ConfigParser import ConfigParser, NoOptionError
from threading import RLock

from pyanaconda import startup_utils
from pyanaconda import iutil
from pyanaconda.flags import can_touch_runtime_system


class ScreenAccessManager(object):
    """A singleton that takes care about spoke visits and option changes.

    The visits and option changes are reflected in the user interaction config file.
    For specification of the user interaction config file see the
    user-interaction-config-file-spec.rst file in the docs directory of the
    Anaconda source code checkout.
    """

    def __init__(self):
        self._lock = RLock()
        self._config = ConfigParser()

    def open_config_file(self, config_path=None):
        """Try to open an existing config file."""
        with self._lock:
            # Don't load the user interaction config from
            # default path if no path is specified in image or
            # directory installation modes.
            # The config would be taken from the host system,
            # which is certainly not what we would want to happen.
            # But load the config if a path is specified,
            # so that it is possible to hide spokes in
            # image and directory installation modes.
            if config_path is None and can_touch_runtime_system(msg="write user interaction config file",
                                                                touch_live=True):
                config_path = CONFIG_FILE_PATH
            if config_path and os.path.exists(config_path):
                log.info("parsing existing user interaction config file in %s", config_path)
                with open(config_path, "r") as f:
                    self._config.readfp(f)

    def write_out_config_file(self, config_path=None):
        """Write the user interaction config file to persistent storage.
        - we always read the config file from the top level filesystem, as:
        -> on live media the file will be there
        -> on non-live media the config file (if any) will be injected to the top level
        -> filesystem by image generation tools or by an updates/product image

        - on the other hand the "output" config file needs to always end on the installed
          system, so that post installation setup tools (such as Initial Setup or
          Gnome Initial Setup) can use it
        -> therefore we always write the config file to the sysroot path
        """

        if config_path is None:
            config_path = iutil.getSysroot() + CONFIG_FILE_PATH

        with self._lock:
            new_config_file = not os.path.exists(config_path)
            try:
                with open(config_path, "w") as f:
                    if new_config_file:
                        # we are creating a new file, so add a header that it was created by Anaconda,
                        # including its version number
                        f.write(self._get_new_config_header())
                    self._config.write(f)
            except OSError:
                log.exception("Can't write user interaction config file.")

    def _get_new_config_header(self):
        """Generate a header for use when Anaconda generates the user interaction config file.

        :returns: an appropriate user config file header
        :rtype: str
        """

        return "# This file has been generated by the Anaconda Installer %s\n\n" % \
               startup_utils.get_anaconda_version_string()

    def _parse_bool_option(self, section_name, option_name):
        """Read a boolean option from the user config file.

        Make sure to convert the value used for boolean values (1 & 0) to Python booleans (True & False)
        and throw SyntaxError if an invalid value is specified and return None if the option has not been found.

        :param str section_name: section where to look for the option
        :param str option_name: name of the option to check
        :returns: True/False for 1/0 and None id the option has not been found in the section
        :rtype: bool or None
        """

        try:
            option_value = self._config.get(section_name, option_name)
            if option_value is None:
                return option_value
        except NoOptionError:
            # the section & key combination has not been found in the file
            return None

        # the option has some content
        option_boolean = CONFIG_BOOLEAN_TABLE.get(option_value)
        if option_boolean is None:
            # parsing failed for the option
            raise SyntaxError
        else:
            return option_boolean

    @property
    def post_install_tools_disabled(self):
        """Report if post install tools should be marked as disabled in the user interaction config file."""

        with self._lock:
            if self._config.has_section(CONFIG_GENERAL_SECTION):
                try:
                    # convert to bool in case the option is not set (the call returns None)
                    return bool(self._parse_bool_option(CONFIG_GENERAL_SECTION, CONFIG_DISABLE_POSTINST_TOOLS_KEY))
                except SyntaxError:
                    log.error("Can't check if post install tools have been disabled due to config file syntax error.")
            else:
                return False

    @post_install_tools_disabled.setter
    def post_install_tools_disabled(self, tools_disabled):
        """Controls if post install tools should be marked as disabled in the user interaction config file."""

        with self._lock:
            if not self._config.has_section(CONFIG_GENERAL_SECTION):
                self._config.add_section(CONFIG_GENERAL_SECTION)
            if tools_disabled:
                self._config.set(CONFIG_GENERAL_SECTION, CONFIG_DISABLE_POSTINST_TOOLS_KEY, CONFIG_TRUE)
            else:
                self._config.set(CONFIG_GENERAL_SECTION, CONFIG_DISABLE_POSTINST_TOOLS_KEY, CONFIG_FALSE)

    def mark_screen_visited(self, screen_name):
        """Mark the given screen as visited

        :param str screen_name: screen to mark as visited
        """

        with self._lock:
            if not self._config.has_section(screen_name):
                self._config.add_section(screen_name)
            self._config.set(screen_name, CONFIG_VISITED_KEY, CONFIG_TRUE)

    def get_screen_visited(self, screen_name):
        """Report if the given screen has been marked as visited

        Note that this takes into account both this installation run
        and any pre-installation tools run recorded in the user interaction config file.

        :param str screen_name: name of a screen to check
        :returns: True if the screen has already been visited, False otherwise
        :rtype: bool
        """

        with self._lock:
            if self._config.has_section(screen_name):
                try:
                    # convert to bool in case the option is not set (the call returns None)
                    return bool(self._parse_bool_option(screen_name, CONFIG_VISITED_KEY))
                except SyntaxError:
                    log.error("Can't check if %s screen has been visited due to config file syntax error.",
                              screen_name)
            return False

    def mark_screen_option_changed(self, screen_name, option_name):
        """Mark option on a screen as changed by the user.

        Note that to mark an option as changed the screen needs to be marked
        as visited (section needs to exist and have the visited key set to 1).

        :param str screen_name: name of the screen
        :param str option_name: name of the option to mark as changed
        """

        with self._lock:
            if self._config.has_section(screen_name):
                if self.get_screen_visited(screen_name):
                    self._config.set(screen_name, CONFIG_OPTION_PREFIX + option_name, CONFIG_TRUE)
                log.warning("Attempt to set option %s as changed for screen %s that has not been visited.",
                            CONFIG_OPTION_PREFIX + option_name, screen_name)

            else:
                log.warning("Attempt to set option %s as changed for an unknown screen %s.",
                            CONFIG_OPTION_PREFIX + option_name, screen_name)

    def get_screen_option_changed(self, screen_name, option_name):
        """Report if the given option on the specified screen has been changed by the user.

        If the screen or option is unknown or if its syntax is incorrect then the option
        is reported as not changed.

        :param str screen_name: name of the screen
        :param str option_name: name of the option to check
        :returns: True if the option was changed, False otherwise
        :rtype: bool
        """

        with self._lock:
            try:
                if self._config.has_section(screen_name):
                    return bool(self._parse_bool_option(screen_name, CONFIG_OPTION_PREFIX + option_name))
            except SyntaxError:
                log.error("Can't check if options %s in section %s has been changed due to config file syntax error.",
                          option_name, screen_name)
            return False

sam = None

def initSAM():
    global sam
    sam = ScreenAccessManager()
