# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
#
# This file is part of Timeline.
#
# Timeline 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.
#
# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.


"""
Handle application configuration.

This module is global and can be used by all modules. Before accessing
configurations, the read function should be called. To save the current
configuration back to file, call the write method.

:doc:`Tests are found here <unit_config_dotfile>`.
"""


from configparser import ConfigParser, MissingSectionHeaderError
from configparser import DEFAULTSECT
import os.path
import sys

from timelinelib.calendar.gregorian.dateformatter import GregorianDateFormatter
from timelinelib.config.dateformatparser import DateFormatParser
from timelinelib.general.observer import Observable
from timelinelib.wxgui.utils import display_information_message
from timelinelib.utils import ex_msg
from timelinelib.wxgui.utils import display_error_message
from timelinelib.config.arguments import ApplicationArguments


# Name used in ConfigParser
SELECTED_EVENT_BOX_DRAWER = "selected_event_box_drawer"
WINDOW_WIDTH = "window_width"
WINDOW_HEIGHT = "window_height"
WINDOW_XPOS = "window xpos"
WINDOW_YPOS = "window ypos"
RECENT_FILES = "recent_files"
WEEK_START = "week_start"
DATE_FORMAT = "date_format"
DEFAULTS = {
    SELECTED_EVENT_BOX_DRAWER: "Default Event box drawer",
    WINDOW_WIDTH: "900",
    WINDOW_HEIGHT: "500",
    WINDOW_XPOS: "-1",
    WINDOW_YPOS: "-1",
    RECENT_FILES: "",
    WEEK_START: "monday",
    DATE_FORMAT: "yyyy-mm-dd",
}
# Some settings
MAX_NBR_OF_RECENT_FILES_SAVED = 5


class TimelineConfigParser(ConfigParser):

    def _write_section(self, fp, section_name, section_items, delimiter):
        """Write a single section to the specified `fp'."""
        fp.write("[{}]\n".format(section_name))
        for key, value in section_items:
            value = self._interpolation.before_write(self, section_name, key,
                                                     value)
            if value is not None or not self._allow_no_value:
                value = delimiter + str(value).replace('\n', '\n\t')
            else:
                value = ""
            try:
                fp.write("{}{}\n".format(key, value))
            except UnicodeEncodeError:
                print(f"UnicodeEncodeError detected. key:{key}  value:{value}")
                raise
        fp.write("\n")


def read_config():
    application_arguments = ApplicationArguments(sys.argv[1:])
    config = Config(application_arguments)
    config.read()
    return config


class Config(Observable):

    """
    Provide read and write access to application configuration settings.

    Built as a wrapper around ConfigParser: Properties exist to read and write
    values but ConfigParser does the actual reading and writing of the
    configuration file.

    The :doc:`application_arguments <timelinelib_config_arguments>` argument handles
    the parsing of the command line arguments and options.
    """

    def __init__(self, application_arguments):
        Observable.__init__(self)
        self._application_arguments = application_arguments
        if self.debug_enabled:
            self.config_parser = TimelineConfigParser(DEFAULTS)
        else:
            self.config_parser = ConfigParser(DEFAULTS)

    @property
    def path(self):
        """The path to the configuration file."""
        return self._application_arguments.config_file_path

    @property
    def has_files(self):
        """Returns True if the command line contains a file specification."""
        return self._application_arguments.has_files

    @property
    def first_file(self):
        """Returns the first filename given on the command line, or None if no file is specified."""
        return self._application_arguments.first_file

    @property
    def debug_enabled(self):
        """Returns True if the comman line option ``--debug`` is given on the command line."""
        return self._application_arguments.debug_flag

    def read(self):
        """Read settings from file specified in the path property."""
        if self.path:
            try:
                self.config_parser.read(self.path, encoding='utf-8')
            except:
                try:
                    self.config_parser.read(self.path)
                except MissingSectionHeaderError:
                    # The cfg-file is probably corrupted, so we ignore it to make Timeline start
                    # instead of crashing.
                    pass

    def write(self):
        """Write settings to the file specified in path property and raise IOError if failed."""
        try:
            with open(self.path, "w", encoding='utf-8') as f:
                self.config_parser.write(f)
        except IOError as ex:
            friendly = _("Unable to write configuration file.") + f'\n{self.path}'
            msg = "%s\n\n%s" % (friendly, ex_msg(ex))
            display_error_message(msg)

    def get_selected_event_box_drawer(self):
        return self.config_parser.get(DEFAULTSECT, SELECTED_EVENT_BOX_DRAWER)

    def set_selected_event_box_drawer(self, selected):
        self.config_parser.set(DEFAULTSECT, SELECTED_EVENT_BOX_DRAWER, selected)

    def get_window_size(self):
        return (self.config_parser.getint(DEFAULTSECT, WINDOW_WIDTH),
                self.config_parser.getint(DEFAULTSECT, WINDOW_HEIGHT))

    def set_window_size(self, size):
        width, height = size
        self.config_parser.set(DEFAULTSECT, WINDOW_WIDTH, str(width))
        self.config_parser.set(DEFAULTSECT, WINDOW_HEIGHT, str(height))

    def get_window_pos(self):
        width, _ = self.get_window_size()
        # Make sure that some area of the window is visible on the screen
        # Some part of the titlebar must be visible
        xpos = max(-width + 100,
                   self.config_parser.getint(DEFAULTSECT, WINDOW_XPOS))
        # Titlebar must not be above the upper screen border
        ypos = max(0, self.config_parser.getint(DEFAULTSECT, WINDOW_YPOS))
        return xpos, ypos

    def set_window_pos(self, pos):
        xpos, ypos = pos
        self.config_parser.set(DEFAULTSECT, WINDOW_XPOS, str(xpos))
        self.config_parser.set(DEFAULTSECT, WINDOW_YPOS, str(ypos))

    def get_recently_opened(self):
        ro = self.config_parser.get(DEFAULTSECT, RECENT_FILES).split(",")
        # Filter out empty elements: "".split(",") will return [""] but we want
        # the empty list
        ro_filtered = [x for x in ro if x]
        return ro_filtered

    def has_recently_opened_files(self):
        if not self.open_recent_at_startup:
            return False
        else:
            return len(self.get_recently_opened()) > 0

    def get_latest_recently_opened_file(self):
        try:
            return self.get_recently_opened()[0]
        except IndexError:
            return None

    def append_recently_opened(self, path):
        if path in [":tutorial:", ":numtutorial:"]:
            # Special timelines should not be saved
            return
        if isinstance(path, bytes):
            # This path might have come from the command line so we need to convert
            # it to unicode
            path = path.decode(sys.getfilesystemencoding())
        abs_path = os.path.abspath(path)
        current = self.get_recently_opened()
        # Just keep one entry of the same path in the list
        if abs_path in current:
            current.remove(abs_path)
        current.insert(0, abs_path)
        self.config_parser.set(DEFAULTSECT, RECENT_FILES,
                               (",".join(current[:MAX_NBR_OF_RECENT_FILES_SAVED])))

    def get_week_start(self):
        return self.config_parser.get(DEFAULTSECT, WEEK_START)

    def week_starts_on_monday(self):
        return self.get_week_start() == "monday"

    def set_week_start(self, week_start):
        if week_start not in ["monday", "sunday"]:
            raise ValueError("Invalid week start.")
        self.config_parser.set(DEFAULTSECT, WEEK_START, week_start)
        self._notify()

    def get_shortcut_key(self, cfgid, default):
        try:
            return self.config_parser.get(DEFAULTSECT, cfgid)
        except:
            self.set_shortcut_key(cfgid, default)
            return default

    def set_shortcut_key(self, cfgid, value):
        self.config_parser.set(DEFAULTSECT, cfgid, value)

    def _string_to_tuple(self, tuple_string):
        return tuple([int(x.strip()) for x in tuple_string[1:-1].split(",")])

    def _tuple_to_string(self, tuple_data):
        return str(tuple_data)

    def get_date_formatter(self, formatter_class=GregorianDateFormatter):
        parser = DateFormatParser().parse(self.get_date_format())
        date_formatter = formatter_class()
        date_formatter.set_defaults(self.use_date_default_values, self.default_year, self.default_month, self.default_day)
        date_formatter.set_separators(*parser.get_separators())
        date_formatter.set_region_order(*parser.get_region_order())
        date_formatter.use_abbreviated_name_for_month(parser.use_abbreviated_month_names())
        return date_formatter

    def get_date_format(self):
        return self.config_parser.get(DEFAULTSECT, DATE_FORMAT)

    def set_date_format(self, date_format):
        self.config_parser.set(DEFAULTSECT, DATE_FORMAT, date_format)
        self._notify()
    date_format = property(get_date_format, set_date_format)

    def _toStr(self, value):
        try:
            return str(value)
        except UnicodeEncodeError:
            display_information_message(_("Warning"), _("The selected value contains invalid characters and can't be saved"))

    def get(self, key):
        if key in BOOLEANS:
            return self._get_boolean(key)
        elif key in INTS:
            return self._get_int(key)
        elif key in COLOURS:
            return self._get_colour(key)
        elif key in FONTS:
            return self._get_font(key)
        else:
            return self.config_parser.get(DEFAULTSECT, key)

    def _get_int(self, key):
        value = self.config_parser.get(DEFAULTSECT, key)
        return int(value)

    def _get_boolean(self, key):
        return self.config_parser.getboolean(DEFAULTSECT, key)

    def _get_colour(self, key):
        return self._string_to_tuple(self.config_parser.get(DEFAULTSECT, key))

    def _get_font(self, key):
        return self.config_parser.get(DEFAULTSECT, key)

    def set(self, key, value):
        if key in COLOURS:
            self._set_colour(key, value)
        elif key in FONTS:
            self._set_font(key, value)
        else:
            if self._toStr(value) is not None:
                self.config_parser.set(DEFAULTSECT, key, self._toStr(value))
                self._notify()
                self._notify(key)

    def _set_colour(self, key, value):
        self.config_parser.set(DEFAULTSECT, key, self._tuple_to_string(value))

    def _set_font(self, key, value):
        if self._toStr(value) is not None:
            self.config_parser.set(DEFAULTSECT, key, value)
            self._notify()


# To add a new boolean, integer, colour or string configuration item
# you only have to add that item to one of the dictionaries below.
BOOLEAN_CONFIGS = (
    {'name': 'show_toolbar', 'default': 'True'},
    {'name': 'show_sidebar', 'default': 'True'},
    {'name': 'show_legend', 'default': 'True'},
    {'name': 'show_label_filtering', 'default': 'False'},
    {'name': 'window_maximized', 'default': 'False'},
    {'name': 'open_recent_at_startup', 'default': 'True'},
    {'name': 'balloon_on_hover', 'default': 'True'},
    {'name': 'use_inertial_scrolling', 'default': 'False'},
    {'name': 'never_show_period_events_as_point_events', 'default': 'False'},
    {'name': 'draw_point_events_to_right', 'default': 'False'},
    {'name': 'event_editor_show_period', 'default': 'False'},
    {'name': 'event_editor_show_time', 'default': 'False'},
    {'name': 'center_event_texts', 'default': 'False'},
    {'name': 'uncheck_time_for_new_events', 'default': 'False'},
    {'name': 'text_below_icon', 'default': 'False'},
    {'name': 'filtered_listbox_export', 'default': 'False'},
    {'name': 'colorize_weekends', 'default': 'False'},
    {'name': 'use_bold_nowline', 'default': 'False'},
    {'name': 'skip_s_in_decade_text', 'default': 'False'},
    {'name': 'display_checkmark_on_events_done', 'default': 'False'},
    {'name': 'never_use_time', 'default': 'False'},
    {'name': 'use_second', 'default': 'False'},
    {'name': 'use_date_default_values', 'default': 'False'},
    {'name': 'hide_events_done', 'default': 'False'},
    {'name': 'use_sidebar_text_coloring', 'default': 'False'},
    {'name': 'use_sidebar_filter_hint', 'default': 'True'},
    {'name': 'real_time_redraw_interval_navigate', 'default': 'False'},
    {'name': 'milestone_as_circle', 'default': 'False'},
    {'name': 'text_to_the_left_of_period_events', 'default': 'False'},
)
INT_CONFIGS = (
    {'name': 'sidebar_width', 'default': '200'},
    {'name': 'divider_line_slider_pos', 'default': '50'},
    {'name': 'vertical_space_between_events', 'default': '5'},
    {'name': 'real_time_redraw_interval', 'default': '0'},
    {'name': 'legend_pos', 'default': '0'},
    {'name': 'time_scale_pos', 'default': '1'},
    {'name': 'workday_length', 'default': '8'},
)
STR_CONFIGS = (
    {'name': 'experimental_features', 'default': ''},
    {'name': 'event_editor_tab_order', 'default': '01234:'},
    {'name': 'fuzzy_icon', 'default': 'fuzzy.png'},
    {'name': 'locked_icon', 'default': 'locked.png'},
    {'name': 'hyperlink_icon', 'default': 'hyperlink.png'},
    {'name': 'default_year', 'default': '2020'},
    {'name': 'default_month', 'default': '02'},
    {'name': 'default_day', 'default': '03'},
)
COLOUR_CONFIGS = (
    {'name': 'now_line_colour', 'default': '(200, 0, 0)'},
    {'name': 'weekend_colour', 'default': '(255, 255, 255)'},
    {'name': 'bg_colour', 'default': '(255, 255, 255)'},
    {'name': 'minor_strip_divider_line_colour', 'default': '(200, 200, 200)'},
    {'name': 'major_strip_divider_line_colour', 'default': '(200, 200, 200)'},
)
FONT_CONFIGS = (
    {'name': 'minor_strip_font', 'default': '10:74:90:90:False:Tahoma:33:(0, 0, 0, 255)'},
    {'name': 'major_strip_font', 'default': '10:74:90:90:False:Tahoma:33:(0, 0, 0, 255)'},
    {'name': 'legend_font', 'default': '10:74:90:90:False:Tahoma:33:(0, 0, 0, 255)'},
    {'name': 'balloon_font', 'default': '10:74:90:90:False:Tahoma:33:(0, 0, 0, 255)'},
    {'name': 'event_font', 'default': 'nfi|1;8;-11;0;0;0;400;0;0;0;1;0;0;2;32;Arial|(0, 0, 0, 255)'},
    {'name': 'era_font', 'default': 'nfi|1;8;-11;0;0;0;400;0;0;0;1;0;0;2;32;Arial|(0, 0, 0, 255)'},
    {'name': 'event_editor_font', 'default': 'nfi|1;8;-11;0;0;0;400;0;0;0;1;0;0;2;32;Arial|(0, 0, 0, 255)'},
)
BOOLEANS = [d['name'] for d in BOOLEAN_CONFIGS]
INTS = [d['name'] for d in INT_CONFIGS]
COLOURS = [d['name'] for d in COLOUR_CONFIGS]
FONTS = [d['name'] for d in FONT_CONFIGS]


def setatt(name):
    setattr(Config, name, property(lambda self: self.get(name),
                                   lambda self, value: self.set(name, str(value))))


# Create properties dynamically
for data in BOOLEAN_CONFIGS + INT_CONFIGS + STR_CONFIGS + COLOUR_CONFIGS + FONT_CONFIGS:
    setatt(data['name'])
    DEFAULTS[data['name']] = data['default']
