#!/usr/bin/env python3
#
# This file is part of Audio Tuner.
#
# Copyright 2025, 2026 Jessie Blue Cassell <bluesloth600@gmail.com>
#
# 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 3 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 <https://www.gnu.org/licenses/>.


"""A module for saving and restoring file specific options.
"""

__author__ = 'Jessie Blue Cassell'


__all__ = [
            'SavedOptions',
          ]


import sys
import os
import configparser
import logging
import argparse
from collections.abc import Iterable

import audio_tuner.common as com
import audio_tuner.argument_parser as ap
import audio_tuner.error_handling as eh


_ARGLIST = (
            'tuning',
            'ref_freq',
            'ref_note',
            'start',
            'end',
            'low_cut',
            'high_cut',
            'dB_range',
            'max_peaks',
            'pitch',
            'tempo',
           )


class SavedOptions():
    """A class that handles saving and loading audio file specific
    options from an ini file.

    Parameters
    ----------
    path : str
        The path of the file to use for storing saved options.
    print_msg : callable, optional
        A function to be called if a message needs to be printed.  The
        call signature should be:
            f(msg: str, level: int) -> None
        where `msg` is the message and `level` is one of the severity
        levels defined in audio_tuner.error_handling (ERROR, WARNING,
        NORMAL or DEBUG).

    Attributes
    ----------
    path : str
        The path of the file that stores saved options.
    path_good : bool
        True if the file pointed to by `path` either doesn't exist yet,
        exists and is empty, or has been successfully read.  False if
        the file exists and is unreadable or contains invalid data.
    """

    def __init__(self,
                 path: str,
                 print_msg: callable = None,):
        self._path = os.path.expanduser(path)
        self.print_msg = print_msg

        self._path_good = False

        self._read()

    @property
    def path(self):
        return self._path

    @property
    def path_good(self) -> bool:
        return self._path_good

    def _read(self):
        file_parser = configparser.ConfigParser(allow_no_value=True,
                                                empty_lines_in_values=False)
        file_parser.optionxform = str
        try:
            parsed = file_parser.read((self.path), encoding='utf-8')
            if self.path in parsed:
                self._path_good = True
            elif not os.path.exists(self.path):
                # A savefile not existing yet is legit.
                self._path_good = True
            elif os.path.getsize(self.path) == 0:
                # An empty savefile is also legit.
                # NOTE:  This may be unreachable code because
                # ConfigParser treats empty files as successfully
                # parsed, but since that's not documented behavior I
                # don't want to rely on it.
                self._path_good = True
        # pylint: disable=broad-exception-caught
        except Exception as err:
            s = str(err)
            if self.print_msg:
                self.print_msg(s, eh.ERROR)
            else:
                logging.warning(s)
        self.file_parser = file_parser

    def has_options(self, audio_path: str) -> bool:
        """Determine whether there are any saved options associated with
        an audio file.

        Parameters
        ----------
        audio_path : str
            The audio file.

        Returns
        -------
        bool
            True if there are saved options associated with the audio
            file, False otherwise.
        """

        abs_path = com.normalize_path(audio_path)
        return self.file_parser.has_section(abs_path)

    def get_args(self, audio_path: str) -> argparse.Namespace:
        """Get options associated with an audio file and return them as
        an argparse.Namespace object.

        Parameters
        ----------
        audio_path : str
            The audio file.

        Returns
        -------
        argparse.Namespace | None
            The options, or None if something went wrong.
        """

        err = False
        abs_path = com.normalize_path(audio_path)
        try:
            saved_args = ap.configparser_to_argparse(self.file_parser,
                                                     abs_path,
                                                     print_msg=self.print_msg)
        except SystemExit:
            err = True
        if not err and not ap.validate(saved_args,
                                       prog=os.path.basename(sys.argv[0]),
                                       config_mode=True,
                                       print_msg=self.print_msg):
            err = True

        if err:
            s = f'ignoring [{abs_path}] section in {self.path}'
            if self.print_msg:
                self.print_msg(s, eh.WARNING)
            else:
                logging.warning(s)
            return None

        return saved_args

    def merge_args(self,
                   audio_path: str,
                   orig_args: argparse.Namespace,
                   skiplist: Iterable[str] = ()) -> argparse.Namespace:
        """Take an argparse.Namespace object and return another one
        containing the original options merged with saved options, with
        the saved options taking priority.

        Parameters
        ----------
        audio_path : str
            The audio file which the saved options are for.
        orig_args : argparse.Namespace
            The original options.
        skiplist : Iterable[str], optional
            A list of options to leave unchanged.

        Returns
        -------
        argparse.Namespace
            The merged options.
        """

        saved_args = self.get_args(audio_path)

        merged_args = argparse.Namespace()

        for key, value in vars(orig_args).items():
            setattr(merged_args, key, value)
        if saved_args is not None:
            for key, value in vars(saved_args).items():
                if key not in skiplist and value is not None:
                    setattr(merged_args, key, value)

        return merged_args

    def _write(self):
        if not self.path_good:
            raise RuntimeError('Attempting to write to invalid save file')

        directory = os.path.dirname(self.path)
        tempfile = '.' + os.path.basename(self.path) + '.tmp'
        temppath = os.path.join(directory, tempfile)

        with open(temppath, 'x', encoding='utf-8') as savefile:
            self.file_parser.write(savefile, space_around_delimiters=True)
        os.replace(temppath, self.path)

    def delete(self, audio_path: str) -> bool:
        """Remove the options associated with an audio file from the
        saved options file.

        Parameters
        ----------
        audio_path : str
            The audio file.

        Returns
        -------
        bool
            True if the options were successfully removed, False
            otherwise.
        """

        if not self.path_good:
            s = f'Invalid savefile {self.path}.  Not attempting to delete.'
            if self.print_msg:
                self.print_msg(s, eh.ERROR)
            else:
                eh.error(s)
            return False

        abs_path = com.normalize_path(audio_path)

        s = f'Deleting options for {abs_path} from {self.path}'
        if self.print_msg:
            self.print_msg(s, eh.NORMAL)
        else:
            eh.info(s)

        if self.file_parser.remove_section(abs_path):
            try:
                self._write()
            except OSError as err:
                s = 'Not deleted from file!'
                if self.print_msg:
                    self.print_msg(str(err), eh.ERROR)
                    self.print_msg(s, eh.WARNING)
                else:
                    eh.error(str(err))
                    eh.warning(s)
                return False

        return True

    def save(self,
             args: argparse.Namespace,
             audio_path: str,
             arg_list: Iterable[str] = _ARGLIST) -> bool:
        """Add the options associated with an audio file to the saved
        options file.

        Parameters
        ----------
        args : argparse.Namespace
            The options.
        audio_path : str
            The audio file.
        arg_list : Iterable[str], optional
            A list of options to add.  Default is ('tuning', 'ref_freq',
            'ref_note', 'start', 'end', 'low_cut', 'high_cut',
            'dB_range', 'max_peaks', 'pitch', 'tempo')

        Returns
        -------
        bool
            True if the options were successfully saved, False
            otherwise.
        """

        if not self.path_good:
            s = f'Invalid savefile {self.path}.  Not attempting to save.'
            if self.print_msg:
                self.print_msg(s, eh.ERROR)
            else:
                eh.error(s)
            return False

        abs_path = com.normalize_path(audio_path)

        s = f'Saving options for {abs_path} to {self.path}'
        if self.print_msg:
            self.print_msg(s, eh.NORMAL)
        else:
            eh.info(s)

        self.file_parser[abs_path] = {}
        for arg in arg_list:
            if value := getattr(args, arg, None):
                self.file_parser[abs_path][arg] = str(value)

        try:
            self._write()
        except OSError as err:
            s = 'Not saved to file!'
            if self.print_msg:
                self.print_msg(str(err), eh.ERROR)
                self.print_msg(s, eh.WARNING)
            else:
                eh.error(str(err))
                eh.warning(s)
            return False

        return True
