#!/usr/bin/env python3
# -*- coding: utf-8 -*-
PROG_VERSION = u"Time-stamp: <2025-10-10 17:14:06 vk>"

import os
import sys
import re
import logging
from optparse import OptionParser
from datetime import datetime, timedelta
import shutil
import fnmatch  # for searching matching directories
import readline  # for raw_input() reading from stdin

# TODO:
# * fix parts marked with «FIXXME»
# * document "using default folder when no target folder given in interactive mode"
# * get_potential_target_directories(): list also folders with datestamps of 1-2 days before
# * think of documenting HIDDEN FEATURES marked below

# better performance if ReEx is pre-compiled:

# search for: «YYYY-MM-DD»
DATESTAMP_REGEX = re.compile(r"\d\d\d\d-[01]\d-[0123]\d")

# Directories to ignore when looking for non-ISO subdirectories
# These are never valid targets for filing
IGNORE_DIRECTORIES = [
    '.git',           # Git repository
    '.svn',           # Subversion repository
    '.hg',            # Mercurial repository
    '.stfolder',      # Syncthing folder
    '.stversions',    # Syncthing versions
    'tagtrees',       # Tag trees directory
    '__pycache__',    # Python cache
    '.DS_Store',      # macOS directory metadata
    'Thumbs.db',      # Windows thumbnail cache
    '.Trash',         # Trash folder
    '.recycle',       # Recycle bin
    'lost+found',     # Linux/Unix lost+found
]

## this setting is highly specific for the current user and most probably needs adaptation:
if os.path.isdir(os.path.join(os.path.expanduser("~"), "archive", "events_memories")):
    ## this is the author's personal choice according to https://karl-voit.at/folder-hierarchy/
    DEFAULT_ARCHIVE_PATH = os.path.join(os.path.expanduser("~"), "archive", "events_memories")
else:
    ## this is the more generic choice:
    DEFAULT_ARCHIVE_PATH = os.path.join(os.path.expanduser("~"), "archive")

# Configurable number of days to look before/after for date suggestions
DAYS_RANGE_FOR_SUGGESTIONS = 2  # Will look for folders ±2 days from the file's date

PAUSEONEXITTEXT = "    press <Enter> to quit"
PROG_VERSION_DATE = PROG_VERSION[13:23]

USAGE = """
    {0} <options> <file(s)>

This script moves items (files or directories) containing ISO datestamps
like "YYYY-MM-DD" into a directory stucture for the corresponding year.

You define the base directory either in this script (or using the
command line argument "--archivedir"). The convention is e.g.:

        <archivepath>/2009
        <archivepath>/2010
        <archivepath>/2011

By default, this script extracts the year from the datestamp of
each file and moves it into the corresponding directory for its year:

     {0} 2010-01-01_Jan2010.txt 2011-02-02_Feb2011.txt
... moves "2010-01-01_Jan2010.txt" to "<archivepath>/2010/"
... moves "2011-02-02_Feb2011.txt" to "<archivepath>/2011/"

OPTIONALLY you can define a sub-directory name with option "-d DIR". If it
contains no datestamp by itself, a datestamp from the first file of the
argument list will be used. This datestamp will be put in front of the name:

     {0}  -d "2009-02-15 bar"  one two three
... moves all items to: "<archivepath>/2009/2009-02-15 bar/"

     {0}  -d bar  2011-10-10_one 2008-01-02_two 2011-10-12_three
... moves all items to: "<archivepath>/2011/2011-10-10 bar/"

If you feel uncomfortable you can simulate the behavior using the "--dryrun"
option. You see what would happen without changing anything at all.


:copyright: (c) 2011 and later by Karl Voit <tools@Karl-Voit.at>
:license: GPL v2 or any later version
:bugreports: <tools@Karl-Voit.at>
:version: {1} \n""".format(sys.argv[0], PROG_VERSION_DATE)

FILENAME_COMPONENT_REGEX = re.compile("[a-zA-Z]+")
FILENAME_COMPONENT_LOWERCASE_BLACKLIST = ['img', 'jpg', 'jpeg', 'png', 'bmp']

parser = OptionParser(usage=USAGE)

parser.add_option("-d", "--directory", dest="targetdir",
                  help="name of a target directory that should be created (optionally add datestamp)", metavar="DIR")

parser.add_option("--batchmode", dest="batchmode", action="store_true",
                  help='suppress interactive asking for anything. If no --directory is given, ' +
                       'the DEFAULT + year of the date-stamp within the file name is used:' +
                       '"%s" (which can be modified in "%s")' % (DEFAULT_ARCHIVE_PATH, sys.argv[0]))

parser.add_option("-a", "--append", dest="append", action="store_true",
                  help="if target directory already exists, append to it " +
                       "instead of aborting.")

parser.add_option("--archivepath", dest="archivepath",
                  help='overwrite the default archive base directory which contains one ' +
                       'subdirectory per year. DEFAULT is currently "%s" (which can be modified in "%s")' % (DEFAULT_ARCHIVE_PATH, sys.argv[0]), metavar="DIR")

parser.add_option("--days-range", dest="days_range", type="int", default=DAYS_RANGE_FOR_SUGGESTIONS,
                  help="Number of days before/after file datestamp to look for matching folders (default: %d)" % DAYS_RANGE_FOR_SUGGESTIONS)

# parser.add_option("-b", "--batch", dest="batchmode", action="store_true",
#                   help="Do not ask for user interaction (at the end of the process)")

parser.add_option("--dryrun", dest="dryrun", action="store_true",
                  help="Does not make any changes to the file system. Useful for testing behavior.")

parser.add_option("--pauseonexit", dest="pauseonexit", action="store_true",
                  help="Asks for pressing the Enter key on any exit.")

parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
                  help="enable verbose mode")

parser.add_option("--version", dest="version", action="store_true",
                  help="display version and exit")

(options, args) = parser.parse_args()

global user_selected_suggested_directory
user_selected_suggested_directory = False


def handle_logging():
    """Log handling and configuration"""

    if options.verbose:
        FORMAT = "%(levelname)-8s %(asctime)-15s %(message)s"
        logging.basicConfig(level=logging.DEBUG, format=FORMAT)
    else:
        FORMAT = "%(levelname)-8s %(message)s"
        logging.basicConfig(level=logging.INFO, format=FORMAT)


def error_exit(errorcode, text):
    """exits with return value of errorcode and prints to stderr"""

    sys.stdout.flush()
    logging.error(text)

    if options.dryrun or options.pauseonexit:
        input(PAUSEONEXITTEXT)

    sys.exit(errorcode)


class SimpleCompleter(object):
    # happily stolen from http://pymotw.com/2/readline/

    def __init__(self, options):
        self.options = sorted(options)
        return

    def complete(self, text, state):
        response = None
        if state == 0:
            # This is the first time for this text, so build a match list.
            if text:
                self.matches = [s
                                for s in self.options
                                if s and s.startswith(text)]
                logging.debug('%s matches: %s', repr(text), self.matches)
            else:
                self.matches = self.options[:]
                logging.debug('(empty input) matches: %s', self.matches)

        # Return the state'th item from the match list,
        # if we have that many.
        try:
            response = self.matches[state]
        except IndexError:
            response = None
        logging.debug('complete(%s, %s) => %s',
                      repr(text), state, repr(response))
        return response


def locate_and_parse_controlled_vocabulary():
    """This method is looking for filenames in the current directory
    and parses them. This results in a list of words which are used for tab completion.

    @param return: either False or a list of found words (strings)

    """

    cv = []
    files = [f for f in os.listdir('.') if os.path.isfile(f)]
    for f in files:
        # extract all words from the file name that don't contain numbers
        new_items = FILENAME_COMPONENT_REGEX.findall(os.path.splitext(os.path.basename(f))[0])
        # remove words that are too small
        new_items = [item for item in new_items if len(item) > 1]
        # remove words that are listed in the blacklist
        new_items = [item for item in new_items if item.lower() not in FILENAME_COMPONENT_LOWERCASE_BLACKLIST]
        # remove words that are already in the controlled vocabulary
        new_items = [item for item in new_items if item not in cv]
        # append newly found words to the controlled vocabulary
        cv.extend(new_items)

    if len(cv) > 0:
        return cv
    else:
        return False


def extract_date(text):
    """extracts the date from a text. Returns a datetime-object if a valid date was found, otherwise returns None."""

    components = re.search(DATESTAMP_REGEX, os.path.basename(text.strip()))
    try:
        return datetime.strptime(components.group(), "%Y-%m-%d")
    except (ValueError, AttributeError):
        return None


def extract_targetdirbasename_with_datestamp(targetdirbasename, args):
    """extracts the full targetdirname including ISO datestamp"""

    current_datestamp = extract_date(targetdirbasename)
    if current_datestamp:
        logging.debug('targetdir "%s" contains datestamp. Extracting nothing.' % targetdirbasename)
        return targetdirbasename
    else:
        first_datestamp = None
        logging.debug('targetdir "' + targetdirbasename + '" contains no datestamp. '
                      'Trying to extract one from the arguments ...')
        for item in args:
            itembasename = os.path.basename(item.strip())
            current_datestamp = extract_date(itembasename)
            if current_datestamp:
                logging.debug('found datestamp "%s" in item "%s"' % (current_datestamp.isoformat()[:10], item.strip()))
                if first_datestamp:
                    logging.debug('comparing current datestamp "%s" with first datestamp' % current_datestamp.isoformat()[:10])
                    if current_datestamp != first_datestamp:
                        logging.warning('Datestamp of item "%s" differs from previously found datestamp "%s". '
                                        'Taking previously found.' % (item.strip(), first_datestamp.isoformat()[:10]))
                    else:
                        logging.debug("current datestamp is the same as the first one")
                else:
                    logging.debug('setting first datestamp to "%s"' % current_datestamp.isoformat()[:10])
                    first_datestamp = current_datestamp
            else:
                logging.warning('item "%s" has got no datestamp!' % item.strip())

        if first_datestamp:
            final_targetdir = first_datestamp.isoformat()[:10] + " " + targetdirbasename
            logging.debug('proposed targetdir "%s"' % final_targetdir)
            return final_targetdir
        else:
            error_exit(2, "could not generate any targetdir containing datestamp. Exiting.")

    error_exit(9, "should not reach this line! internal error.")


def assert_each_item_has_datestamp(items):
    """make sure that each item has a valid datestamp"""

    logging.debug("checking each item for valid datestamp")
    for item in items:
        components = re.search(DATESTAMP_REGEX, os.path.basename(item.strip()))
        try:
            item_date = datetime.strptime(components.group(), "%Y-%m-%d")
            return item_date.year
        except (ValueError, AttributeError):
            error_exit(3, 'item "%s" has got no valid datestamp! Can not process this item.' % item)


def make_sure_targetdir_exists(archivepath, targetdir):
    """create directory if necessary; abort if existing and no append options given"""

    logging.debug("make_sure_target_exists: archivepath [%s] targetdir [%s]" % (archivepath, targetdir))
    year = get_year_from_itemname(targetdir)
    complete_target_path = os.path.join(str(archivepath), str(year), str(targetdir))
    global user_selected_suggested_directory

    if os.path.isdir(complete_target_path):
        if options.append or user_selected_suggested_directory:
            logging.debug("target directory already exists. Appending files...")
        else:
            error_exit(4, "target directory already exists. Aborting.")
    else:
        if not options.dryrun:
            logging.info('creating target directory: "%s"' % complete_target_path)
            os.mkdir(complete_target_path)
        else:
            logging.info('creating target directory: "%s"' % complete_target_path)

    return complete_target_path


def make_sure_subdir_exists(currentdir, subdir):
    """create directory if necessary; abort if existing and no append options given"""

    logging.debug("make_sure_subdir_exists: currentdir [%s] subdir [%s]" % (currentdir, subdir))
    complete_target_path = os.path.join(currentdir, subdir)

    if os.path.isdir(complete_target_path):
        logging.debug("target directory already exists. Appending files...")
    else:
        if not options.dryrun:
            logging.info('creating directory: "%s"' % complete_target_path)
            os.mkdir(complete_target_path)
        else:
            logging.info('creating directory: "%s"' % complete_target_path)

    return complete_target_path


def get_year_from_itemname(itemname):
    """extract year from item string"""

    # assert: main() makes sure that each item has datestamp!
    components = re.search(DATESTAMP_REGEX, os.path.basename(itemname))
    try:
        return datetime.strptime(components.group(), "%Y-%m-%d").year
    except (ValueError, AttributeError):
        error_exit(7, 'item "%s" should have a valid datestamp in it. '
                      'Should have been checked before, internal error :-(' % str(itemname))


def pretty_print_move_item_information(item, destination):
    """prints a nice screen output of item and target destination"""

    assert(type(item) == str)
    assert(type(destination) == str)

    if len(item)+len(destination) < 80:
        in_between_linebreak = '\n'
    else:
        in_between_linebreak = ''

    try:
        print('• %s%s  →  %s\n' % (item, in_between_linebreak, destination))
    except:
        print('- %s%s  ->  %s\n' % (item.encode('latin-1', 'ignore'), in_between_linebreak, destination.encode('latin-1', 'ignore')))


def move_item(item, destination):
    """move an item to the destination directory"""

    pretty_print_move_item_information(item, destination)

    if not options.dryrun:
        if os.path.isdir(destination):
            basename = os.path.basename(item)
            destinationfilename = os.path.join(destination, basename)
            if os.path.isfile(destinationfilename):
                logging.warning('Cannot move "%s" to "%s" because it already exists. Skipping.' % (item, destinationfilename))
            else:
                try:
                    shutil.move(item, destination)
                except IOError as detail:
                    error_exit(5, 'Cannot move "%s" to "%s". Aborting.\n%s' % (item, destination, detail))
        else:
            error_exit(6, 'Destination directory "%s" does not exist! Aborting.' % destination)


def handle_item(itemname, archivepath, targetdir):
    """handles one item and moves it to targetdir"""

    logging.debug("--------------------------------------------")
    logging.debug('processing item "%s"' % itemname)
    logging.debug("with archivepath[%s]  and  targetdir[%s]" % (archivepath, targetdir))

    if not os.path.exists(itemname):
        logging.error('item "%s" does not exist! Ignoring.' % itemname)
    elif targetdir:
        # targetdir option is given and this directory is created before
        # so just move items here:
        move_item(itemname, targetdir)
    else:
        # find the correct <YYYY> subdirectory for each item:
        year = get_year_from_itemname(itemname)
        logging.debug('extracted year "%d" from item "%s"' % (year, itemname))
        destination = os.path.join(archivepath, str(year))
        move_item(itemname, destination)


def generate_absolute_target_dir(targetdir, args, archivepath):
    """returns existing target directory containing a datestamp"""

    logging.debug("trying to find a target dir with datestamp")
    targetdirname = extract_targetdirbasename_with_datestamp(targetdir, args)
    logging.debug('extract_targetdirbasename... returned "%s"' % targetdirname)
    return make_sure_targetdir_exists(archivepath, targetdirname)


def get_potential_target_directories_extended(args, archivepath, days_range):
    """Enhanced version that returns three lists of directory suggestions:
    1. Exact datestamp matches (original functionality)
    2. Folders within ±days_range of the file's datestamp
    3. All non-datestamp folders from the year archive (excluding ignored directories)
    
    Returns a tuple: (exact_matches, near_date_matches, non_datestamp_dirs)
    """
    
    firstfile = args[0]
    
    if not os.path.exists(firstfile):
        error_exit(11, 'File/Folder "%s" does not exist! Aborting.' % firstfile)
    
    firstfile = os.path.basename(firstfile)
    assert_each_item_has_datestamp([firstfile])
    
    item_date = extract_date(firstfile)
    yearfolder = os.path.join(archivepath, str(item_date.year))
    if not os.path.exists(yearfolder):
        new_year = str(os.path.join(archivepath, yearfolder))
        try:
            os.mkdir(new_year)
        except IOError:
            print('The creation of new folder "%s" failed.' % new_year)
            sys.exit()
    
    # Initialize result lists
    exact_matches = []
    near_date_matches = []
    non_datestamp_dirs = []
    
    # Get all directories in the year folder
    all_dirs = []
    for root, dirs, files in os.walk(yearfolder):
        if root == yearfolder:  # Only look at direct subdirectories
            all_dirs = dirs
            break
    
    logging.debug("Found %d total directories in year folder %s" % (len(all_dirs), yearfolder))
    
    # Process each directory
    for directory in all_dirs:
        # Skip ignored directories
        if directory in IGNORE_DIRECTORIES:
            logging.debug("Ignoring directory: %s (in ignore list)" % directory)
            continue
            
        dir_date = extract_date(directory)
        
        if dir_date:
            # Directory has a datestamp
            date_diff = abs((dir_date - item_date).days)
            
            if date_diff == 0:
                # Exact match
                logging.debug("Found exact match: %s" % directory)
                exact_matches.append(directory)
            elif date_diff <= days_range:
                # Within range
                logging.debug("Found near match (%d days diff): %s" % (date_diff, directory))
                near_date_matches.append(directory)
        else:
            # No datestamp in directory name (and not in ignore list)
            logging.debug("Found non-datestamp directory: %s" % directory)
            non_datestamp_dirs.append(directory)
    
    # Sort the near_date_matches by date proximity to the file's date
    if near_date_matches:
        near_date_matches.sort(key=lambda d: abs((extract_date(d) - item_date).days))
    
    # Sort non-datestamp directories alphabetically
    non_datestamp_dirs.sort()
    
    logging.debug("Summary: %d exact, %d near (±%d days), %d non-datestamp" % 
                  (len(exact_matches), len(near_date_matches), days_range, len(non_datestamp_dirs)))
    
    return (exact_matches, near_date_matches, non_datestamp_dirs)


def get_potential_target_directories(args, archivepath):
    """Original function maintained for backward compatibility.
    Now calls the extended version and returns only exact matches."""
    
    exact_matches, _, _ = get_potential_target_directories_extended(args, archivepath, 0)
    return exact_matches


def longestSubstringFinder(string1, string2):
    """
    Returns the longest common substring starting from the first character
    of both input strings.
    
    Args:
        string1 (str): First input string.
        string2 (str): Second input string.
        
    Returns:
        str: The longest common prefix of the two strings.
    """
    # Find the minimum length to avoid index errors
    min_len = min(len(string1), len(string2))
    
    # Iterate through each character up to the minimum length
    for i in range(min_len):
        if string1[i] != string2[i]:
            # Return the substring up to the point of mismatch
            return string1[:i]
    
    # If we've gone through all characters without a mismatch,
    # the common substring is the shorter of the two strings
    return string1[:min_len]


def BUGGY_longestSubstringFinder(string1, string2):
    ## this is from: https://stackoverflow.com/a/18717762
    ## print(longestSubstringFinder("apple pie available", "apple pies")) ## apple pie
    ## print(longestSubstringFinder("apples", "appleses")) ## apples
    ## print(longestSubstringFinder("bapples", "cappleses")) ## apples

    ## 2025-10-10: this function has an issue with:
    ## FIXXME: debug issue and think of re-using this instead of the other function.
    ## longestSubstringFinder(string1='Das ist ein Event - foo bar baz', string2='Das ist ein Event - foo bar')
    ## where it returns no common string.
    
    '''Its called Longest Common Substring problem. Here I present a
    simple, easy to understand but inefficient solution. It will take
    a long time to produce correct output for large strings, as the
    complexity of this algorithm is O(N^2).'''

    logging.debug(f"longestSubstringFinder({string1=}, {string2=})")
    answer = ""
    len1, len2 = len(string1), len(string2)
    for i in range(len1):
        match = ""
        for j in range(len2):
            if (i + j < len1 and string1[i + j] == string2[j]):
                match += string2[j]
                #logging.debug(f"longestSubstringFinder 1 {match=}")
            else:
                if (len(match) > len(answer)): answer = match
                match = ""
                #logging.debug(f"longestSubstringFinder 2 {match=}")
    #logging.debug(f"longestSubstringFinder {answer=} {match=}")
    return answer


def startswith_datestamp(filename):
    "returns true if a string starts with an ISO datestamp, false if not."
    
    components = re.search(DATESTAMP_REGEX, filename)
    if components:
        return True
    else:
        return False


def guess_new_directory_basename(filename1, filename2):
    """analyzes two filenames and tries to extract a potential archive directory name.
    E.g., "2023-06-11T12.26.18 Wedding of Paula and John - Guests arriving.jpg"
    with  "2023-06-11T13.05.48 Wedding of Paula and John - Ceremony starts.jpg"
    results in: "Wedding of Paula and John"
    """

    logging.debug(f"guess_new_directory_basename({filename1=}, {filename2=})")
    
    # omit path and filename extensions:
    file1 = os.path.basename(os.path.splitext(filename1)[0])
    file2 = os.path.basename(os.path.splitext(filename2)[0])
    
    if startswith_datestamp(file1) and startswith_datestamp(file2):
        # cut most probably identical datestamp to avoid false positive matchstring:
        logging.debug('guess_new_directory_basename: finding longest common prefix after omitting time- or datestamps (remove anything before first space or underliner)')
        substring = longestSubstringFinder(re.sub(r'.*?[ _](.*)', r'\1', file1), re.sub(r'.*?[ _](.*)', r'\1', file2))
    else:
        logging.debug('guess_new_directory_basename: finding longest common prefix')
        substring = longestSubstringFinder(file1, file2)

    if substring:
        # delete any pre- or postfixes with a dash and space:
        strippedsubstring = substring.replace(' -', '').replace('- ', '').strip()
        if len(strippedsubstring) > 3:
            # FIXXME assumption: a potential substring candidate for a directory name needs at least 4 characters.
            return(strippedsubstring)
    else:
        return None    

    
def print_potential_target_directories_extended(exact_matches, near_date_matches, 
                                                non_datestamp_dirs, new_dir_basename_guess,
                                                days_range):
    """Enhanced version that prints all three categories of directory suggestions."""
    
    logging.debug(f"print_potential_target_directories_extended({exact_matches=}, {near_date_matches=}, {non_datestamp_dirs=}, {new_dir_basename_guess=}, {days_range=})")
    
    total_suggestions = len(exact_matches) + len(near_date_matches) + len(non_datestamp_dirs)
    if new_dir_basename_guess:
        total_suggestions += 1
    
    if total_suggestions > 0:
        print('\n Directory suggestions (enter number to select):')
        
    index = 1  # caution: for usability purposes, we do not start with 0 here!
    
    # Print exact matches
    if exact_matches:
        print('\n  === Exact date matches ===')
        for directory in exact_matches:
            print('  [%d]  %s' % (index, directory))
            index += 1
    
    # Print near date matches
    if near_date_matches:
        print('\n  === Within ±%d days ===' % days_range)
        for directory in near_date_matches:
            print('  [%d]  %s' % (index, directory))
            index += 1
    
    # Print non-datestamp directories
    if non_datestamp_dirs:
        print('\n  === Other folders (no datestamp) ===')
        for directory in non_datestamp_dirs:
            print('  [%d]  %s' % (index, directory))
            index += 1
    
    # Print new directory suggestion
    if new_dir_basename_guess:
        print('\n  === New directory ===')
        print('  [%d]  CREATE: %s' % (index, new_dir_basename_guess))
    
    print('\n')
    
    return total_suggestions


def print_potential_target_directories(directory_suggestions, new_dir_basename_guess):
    """Original function maintained for backward compatibility."""
    
    number_of_suggestions = len(directory_suggestions)

    if number_of_suggestions > 1:
        print('\n ' + str(number_of_suggestions) +
              ' matching target directories found. Enter its number if you want to use one of it:')
    elif number_of_suggestions == 1:
        print('\n One matching target directory found. Enter "1" if you want to use it:')

    index = 1  # caution: for usability purposes, we do not start with 0 here!

    if number_of_suggestions > 0:
        for directory in directory_suggestions:
            print('  [' + str(index) + ']  ' + directory)
            index += 1
        print('\n')

    if new_dir_basename_guess:
        print('  [' + str(index) + '] suggested new directory: ' + new_dir_basename_guess)

    return


def is_an_integer(data):
    """returns true if string is an integer"""
    try:
        int(data)
        return True
    except ValueError:
        return False


def main():
    """Main function"""

    if options.version:
        print("%s version %s" % (os.path.basename(sys.argv[0]), PROG_VERSION_DATE))
        sys.exit(0)

    handle_logging()
    logging.debug("options: " + str(options))
    logging.debug("args: " + str(args))

    if options.dryrun:
        logging.info('Option "--dryrun" found, running a simulation, not modifying anything on file system:')

    if options.append and not options.targetdir:
        logging.warning('The "--append" options is only necessary in combination '
                        'with the "--directory" option. Ignoring this time.')

#    if options.batchmode and not options.targetdir:
#        error_exit(10, 'Option "--batchmode" requires "--directory": ' +
#                   'you need to tell me what to do in batchmode.')

    archivepath = None
    if options.archivepath:
        logging.debug('overwriting default archive dir with: "%s"' % options.archivepath)
        archivepath = options.archivepath
    else:
        archivepath = DEFAULT_ARCHIVE_PATH

    if not os.path.isdir(archivepath):
        error_exit(1, '\n\nThe archive directory "%s" is not a directory!\n'
                      'modify default setting in "%s" or provide a valid '
                      'directory with command line option "--archivepath".\n' % (archivepath, sys.argv[0]))

    if len(args) < 1:
        parser.error("Please add at least one file name as argument")

    targetdirname = None
    if options.targetdir:
        targetdirname = generate_absolute_target_dir(options.targetdir, args, archivepath)
    elif not options.batchmode:

        # Get extended directory suggestions
        exact_matches, near_date_matches, non_datestamp_dirs = \
            get_potential_target_directories_extended(args, archivepath, options.days_range)
        
        # Combine all directory suggestions for easier selection
        all_directory_suggestions = exact_matches + near_date_matches + non_datestamp_dirs
        
        # Analyze filenames to suggest a new directory name if multiple files provided
        new_dir_basename_guess = False
        if len(args) > 1:
            new_dir_basename_guess = guess_new_directory_basename(args[0], args[1])
            if new_dir_basename_guess:
                # Extract the date from the first file to prepend to the suggestion
                first_file_date = extract_date(args[0])
                if first_file_date and not extract_date(new_dir_basename_guess):
                    # If the guess doesn't already have a date, add it
                    new_dir_basename_guess = first_file_date.isoformat()[:10] + " " + new_dir_basename_guess
                logging.debug("Suggested new directory from filename analysis: %s" % new_dir_basename_guess)
        
        # Calculate total number of suggestions
        number_of_suggestions = len(all_directory_suggestions)
        if new_dir_basename_guess:
            number_of_suggestions += 1
        
        if number_of_suggestions > 0:
            print_potential_target_directories_extended(exact_matches, near_date_matches,
                                                        non_datestamp_dirs, new_dir_basename_guess,
                                                        options.days_range)

        # parse file names for completion:
        vocabulary = locate_and_parse_controlled_vocabulary()

        if vocabulary:

            assert(vocabulary.__class__ == list)

            # Register our completer function
            readline.set_completer(SimpleCompleter(vocabulary).complete)

            # Use the tab key for completion
            readline.parse_and_bind('tab: complete')

            tabcompletiondescription = '; complete ' + str(len(vocabulary)) + ' words with TAB'

            print('         (abort with Ctrl-C' + tabcompletiondescription + ')\n')
        else:
            print('         (abort with Ctrl-C)\n')

        targetdirname = str(input('Please enter directory basename: ')).strip()

        if (not targetdirname):
            # if no folder is given by the user, act like askfordir is not the case:
            logging.debug("targetdirname was empty: using default target folder")
            assert_each_item_has_datestamp(args)
        else:

            if targetdirname == 'lp':
                # HIDDEN FEATURE: overriding targetdir with lp-shortcut:
                logging.debug("targetdir-shortcut 'lp' (low prio) found")
                targetdirname = make_sure_subdir_exists(os.getcwd(), 'lp')

            elif targetdirname == 'rp':
                # HIDDEN FEATURE: overriding targetdir with rp-shortcut:
                logging.debug("targetdir-shortcut 'rp' (Rohpanorama) found")
                targetdirname = make_sure_subdir_exists(os.getcwd(), 'Rohpanoramas')

            elif number_of_suggestions > 0 and is_an_integer(targetdirname):
                # special shortcut: numbers within number_of_suggestions are for suggested directories
                targetdirint = int(targetdirname)
                if targetdirint <= number_of_suggestions and targetdirint > 0:
                    global user_selected_suggested_directory
                    user_selected_suggested_directory = True
                    if targetdirint == number_of_suggestions and new_dir_basename_guess:
                        # User selected the "create new directory" option
                        targetdirname = generate_absolute_target_dir(new_dir_basename_guess, args, archivepath)
                    else:
                        # User selected an existing directory from the combined list
                        selected_dir = all_directory_suggestions[targetdirint - 1]  # -1 fixes that we start from 1 instead of 0
                        # For existing directories, use them as-is without modification
                        year = get_year_from_itemname(args[0])  # Get year from first file
                        targetdirname = os.path.join(archivepath, str(year), selected_dir)
                        logging.debug("user selected existing directory \"%s\"" % (targetdirname))
                else:
                    # if number is not in range of suggestions, use it as folder name like below:
                    targetdirname = generate_absolute_target_dir(targetdirname, args, archivepath)

            else:
                targetdirname = generate_absolute_target_dir(targetdirname, args, archivepath)
    else:
        assert_each_item_has_datestamp(args)

    if targetdirname:
        logging.debug('using targetdirname "%s"' % targetdirname)
    else:
        logging.debug("using no targetdir, sorting each item into %s/<YYYY>" % archivepath)

    print('\n')  # make it more sexy
    for itemname in args:
        handle_item(itemname.strip(), archivepath, targetdirname)

    logging.debug("successfully processed all items.")

    if options.dryrun or options.pauseonexit:
        input(PAUSEONEXITTEXT)


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:

        logging.info("Received KeyboardInterrupt")

# END OF FILE #################################################################
# end
