"""Handling of existing files by the user - overwrite, skip, etc."""

import abc
import os
from typing import Dict, Optional, Tuple

from src.path import uniquify


class OverwriteChooser(metaclass=abc.ABCMeta):
  """Interface to indicate how to handle existing files.

  For example, if the user attempts to save a file to a path that already
  exists, subclasses of this class can be used to provide options to the user on
  how to handle the existing file (e.g. skip, overwrite or rename the existing
  file).
  """

  def __init__(self, overwrite_mode: str):
    """Initializes the instance with a default overwrite mode."""
    super().__init__()

    self._overwrite_mode = overwrite_mode
  
  @property
  def overwrite_mode(self) -> str:
    """The overwrite mode chosen by the user.

    By default, this is set to the value provided during object instantiation.
    """
    return self._overwrite_mode

  @overwrite_mode.setter
  def overwrite_mode(self, value: str):
    self._overwrite_mode = value
  
  @abc.abstractmethod
  def choose(
        self,
        filepath: Optional[str] = None,
        message: Optional[str] = None,
  ) -> str:
    """Returns a value indicating how to handle the conflicting file.

    The user is assumed to choose one of the possible overwrite modes. The
    overwrite modes and the implementation of handling conflicting files are
    left to the developer using the return value provided by this method.
    
    ``filepath`` is a file path that points to an existing file. This class uses
    the file path to simply display it to the user.

    ``message`` is an optional custom message displayed to the user. If
    ``None``, a default message will be used instead.

    If the existing file is a directory and the overwrite mode is
    `OverwriteModes.REPLACE`, then `OverwriteModes.RENAME_NEW` must be returned
    instead. The rationale is that a directory can only be replaced with a file
    by deleting the entire directory first, which is unsafe.
    """
    pass


class NoninteractiveOverwriteChooser(OverwriteChooser):
  """Class that simply stores an overwrite mode specified upon instantiation.

  The overwrite mode cannot be modified after instantiation.

  This class is suitable to be used in a non-interactive environment, i.e. with
  no user interaction.
  """
  
  def choose(
        self,
        filepath: Optional[str] = None,
        message: Optional[str] = None,
  ) -> str:
    if os.path.isdir(filepath) and self._overwrite_mode == OverwriteModes.REPLACE:
      return OverwriteModes.RENAME_NEW
    else:
      return self._overwrite_mode


class InteractiveOverwriteChooser(OverwriteChooser, metaclass=abc.ABCMeta):
  """Abstract class for choosing an overwrite mode interactively."""
  
  def __init__(
        self,
        values_and_display_names: Dict[str, str],
        default_value: str,
        default_response: str,
  ):
    super().__init__(default_value)
    
    self.values_and_display_names = values_and_display_names
    """Dictionary of (value, display name) pairs which define overwrite modes
    and their human-readable names.
    """

    self._values = list(self.values_and_display_names)
    
    if default_value not in self._values:
      raise ValueError(
        f'invalid default mode "{default_value}"; must be one of the following: {self._values}')

    self.default_response = default_response
    """Default overwrite mode to return if the user made a choice that
    returns a value not in `values_and_display_names`. This attribute
    does not have to be any of the values in `values_and_display_names`.
    """

    self._apply_to_all = False

  @property
  def apply_to_all(self) -> bool:
    """If ``True``, the user's choice applies to the current and all subsequent
    files. If ``False``, the user's choice applies to current file only.
    """
    return self._apply_to_all

  def choose(
        self,
        filepath: Optional[str] = None,
        message: Optional[str] = None,
  ) -> str:
    if self._overwrite_mode is None or not self._apply_to_all:
      return self._choose(filepath, message)
    else:
      if os.path.isdir(filepath) and self._overwrite_mode == OverwriteModes.REPLACE:
        return OverwriteModes.RENAME_NEW
      else:
        return self._overwrite_mode
  
  @abc.abstractmethod
  def _choose(
        self,
        filepath: Optional[str] = None,
        message: Optional[str] = None,
  ) -> str:
    """Allows the user to choose the overwrite mode and return it.
    
    If the choice results in a value that is not in
    `values_and_display_names`, `default_response` must be returned.
    """
    pass


def handle_overwrite(
      filepath: str,
      overwrite_chooser: OverwriteChooser,
      position: Optional[int] = None,
      message: Optional[str] = None,
) -> Tuple[str, str]:
  """Resolves how to handle an existing file path.

  ``overwrite_chooser`` is presented to the user to let them choose the
  overwrite mode. The ``overwrite_chooser`` instance must support overwrite
  modes specified in `OverwriteModes`. See `OverwriteModes` for information
  about the possible values and their meanings.

  If ``filepath`` does not exist, there is no need to perform any command, hence
  `OverwriteModes.DO_NOTHING` is returned.

  If the chosen overwrite mode is `OverwriteModes.RENAME_NEW` or
  `OverwriteModes.RENAME_EXISTING`, the new or existing file path is made
  unique, respectively. A unique substring is appended to the end of the file
  name. The position of the substring can be customized via ``position`` (for
  example, to place the substring before the file extension).

  ``message`` is an optional custom message displayed to the user. If
  ``None``, a default message will be used instead.

  Returns:
    A tuple of (chosen overwrite mode, file path).

    The overwrite mode is returned by ``overwrite_chooser``, which the caller
    of this function can further use (especially `OverwriteModes.SKIP` or
    `OverwriteModes.CANCEL`).

    The returned file path is identical to the one passed as the argument,
    unless the `OverwriteModes.RENAME_NEW` mode is chosen, in which case a
    modified file path is returned.
  """
  if os.path.exists(filepath):
    overwrite_mode = overwrite_chooser.choose(filepath=os.path.abspath(filepath), message=message)

    if overwrite_mode in [OverwriteModes.RENAME_NEW, OverwriteModes.RENAME_EXISTING]:
      processed_filepath = uniquify.uniquify_filepath(filepath, position)
      if overwrite_mode == OverwriteModes.RENAME_NEW:
        filepath = processed_filepath
      else:
        os.rename(filepath, processed_filepath)

    return overwrite_mode, filepath
  else:
    return OverwriteModes.DO_NOTHING, filepath


class OverwriteModes:
  """Overwrite modes used by `handle_overwrite()` and recommended to be handled
  by custom `OverwriteChooser` subclasses.
  """
  REPLACE = 'replace'
  """Indicates to overwrite an existing file with new contents."""

  SKIP = 'skip'
  """Indicates to avoid overwriting an existing file."""

  RENAME_NEW = 'rename_new'
  """Indicates to rename the file path whose contents are about to be written
  to the file system.
  """

  RENAME_EXISTING = 'rename_existing'
  """Indicates to rename the existing file path in the file system."""

  CANCEL = 'cancel'
  """Use this value if the user terminated the overwrite chooser, for example 
  by closing a dialog if an interactive overwrite chooser is used.
  """

  DO_NOTHING = 'do_nothing'
  """Use this value if there is no need to display an overwrite chooser, 
  i.e. if a file path does not exist and no action should be taken.
  """

  ASK = 'ask'
  """Indicates to display an interactive dialog, prompting the user to choose
  one of the other overwrite modes.
  
  This value should only be used when the application is run interactively.
  """

  OVERWRITE_MODES = REPLACE, SKIP, RENAME_NEW, RENAME_EXISTING, CANCEL, DO_NOTHING, ASK


def get_overwrite_strings(path: str) -> Tuple[str, str]:
  """Returns strings usable when a user is prompted to handle conflicting
  files.

  These strings are used by default as a message in e.g. `handle_overwrite()`.
  You can use this function as a basis to augment the message with
  additional text and pass that as the ``message`` parameter.
  """
  if path is not None:
    if not os.path.isdir(path):
      dirpath, filename = os.path.split(path)
      if dirpath:
        path_exists_message = (
          _('A file named "{}" already exists in the folder "{}".').format(
            filename, os.path.basename(dirpath)))
      else:
        path_exists_message = _('A file named "{}" already exists.').format(filename)
    else:
      dirpath, dirname = os.path.split(path)
      if dirpath:
        path_exists_message = (
          _('A folder named "{}" already exists in the folder "{}".').format(
            dirname, os.path.basename(dirpath)))
      else:
        path_exists_message = _('A folder named "{}" already exists.').format(dirname)
  else:
    path_exists_message = _('A file with the same name already exists.')

  choose_message = _('Choose how to handle this file.')

  return path_exists_message, choose_message
