#! /usr/bin/env python3

# flo-renumber-files --- Renumber files such as foobar0012.jpg to get rid of
#                        holes in numbering
# Copyright (c) 2008, 2015  Florent Rougon
#
# 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; version 2 dated June, 1991.
#
# 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; see the file COPYING. If not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA  02110-1301 USA.


import sys, os, re, getopt, random

progname = os.path.basename(sys.argv[0])
progversion = "0.3"
version_blurb = """%s version %s
Copyright (c) 2008, 2015  Florent Rougon""" % \
(progname, progversion)

usage = """Usage: %s [OPTION]... DIRECTORY [PREFIX SUFFIX NUMDIGITS]
Renumber files such as foobar0012.jpg to get rid of holes in numbering.

The files to rename are looked up in DIRECTORY, starting with PREFIX,
followed by NUMDIGITS digits and SUFFIX.
They are renamed, in proper order, to, e.g., <PREFIX>0001<SUFFIX>,
<PREFIX>0002<SUFFIX>, etc. (according to NUMDIGITS and --start if specified).

If PREFIX, SUFFIX and NUMDIGITS are not provided, an attempt is made to guess
them based on the contents of DIRECTORY (the user is asked for confirmation
before proceeding).

In order to avoid name clashes during the renaming operation, the files are
first renamed to some temporary names in a first phase, then all temporary
files are renamed to the final names.

Only regular files are considered, not directories.

Options:
  -s, --start=NUMBER        start numbering at NUMBER (default: 1)
      --help                display this message and exit
      --version             output version information and exit""" \
  % progname


class ProgramError(Exception):
    """Exception raised when the program finds that it has a bug."""
    pass

class error(Exception):
    """Base class for exceptions in this module (except ProgramError)."""
    def __init__(self, message=None):
        self.message = message
    def __str__(self):
        return "<%s: %s>" % (self.__class__.__name__, self.message)
    def complete_message(self):
        if self.message:
            return "%s: %s" % (self.ExceptionShortDescription, self.message)
        else:
            return "%s" % self.ExceptionShortDescription
    ExceptionShortDescription = "Generic exception for " \
                                "%s" % progname


def get_key_for_value_in_dict(d, val):
    for key, value in d.items():
        if value == val:
            res = key
            break
    else:
        res = None

    return res


def guess_parameters():
    prefixes = {}
    suffixes = {}
    ndigits = {}

    pattern = re.compile(r"^(?P<prefix>.*?)(?P<num>[0-9]+)(?P<suffix>.*?)$")

    for f in os.listdir("."):
        if os.path.isfile(f):
            mo = pattern.match(f)
            if mo is not None:
                try:
                    prefixes[mo.group("prefix")] += 1
                except KeyError:
                    prefixes[mo.group("prefix")] = 1

                try:
                    suffixes[mo.group("suffix")] += 1
                except KeyError:
                    suffixes[mo.group("suffix")] = 1

                try:
                    ndigits[len(mo.group("num"))] += 1
                except KeyError:
                    ndigits[len(mo.group("num"))] = 1

    prefix_max_count = max(prefixes.values())
    suffix_max_count = max(suffixes.values())
    ndigits_max_count = max(ndigits.values())

    return (get_key_for_value_in_dict(prefixes, prefix_max_count),
            get_key_for_value_in_dict(suffixes, suffix_max_count),
            get_key_for_value_in_dict(ndigits, ndigits_max_count))


def main():
    try:
        opts, args = getopt.getopt(sys.argv[1:], "s:",
                                   ["start=",
                                    "help",
                                    "version"])
    except getopt.GetoptError as message:
        sys.stderr.write(usage + "\n")
        sys.exit(1)

    # Default values for options
    first_number = 1
#    lenient_mode = False

    for option, value in opts:
        if option in ("-s", "--start"):
            try:
                first_number = int(value)
                if first_number < 0:
                    raise ValueError("--start argument must be non-negative")
            except ValueError:
                sys.exit("Invalid value for --start argument: '%s'" % value)
#         elif option in ("-l", "--lenient"):
#             lenient_mode = True
        elif option == "--help":
            print(usage)
            sys.exit(0)
        elif option == "--version":
            print(version_blurb)
            sys.exit(0)
        else:
            sys.exit(
                "Unexpected option '%s' received from the getopt module; "
                "probably\na bug in %s" % (option, progname))

    if len(args) not in (1,4):
        sys.stderr.write(usage + "\n")
        sys.exit(1)

    input_directory = args[0]
    # Remove the trailing slashes from input_directory, if any
    if not os.path.split(input_directory)[1]:
        input_directory = os.path.split(input_directory)[0]

    os.chdir(input_directory)

    if len(args) == 4:
        prefix = args[1]
        suffix = args[2]
        numdigits = int(args[3])
    elif len(args) == 1:
        print("Trying to guess prefix, suffix and number of digits...")
        prefix, suffix, numdigits = guess_parameters()
        print("Proposed parameters:")
        print("  prefix: '%s'" % prefix)
        print("  suffix: '%s'" % suffix)
        print("  number of digits: '%u'" % numdigits)
        print()

        while True:
            print("Proceed with these parameters? [Y/n/q] ")
            reply = input("").lower()
            if (reply == 'y') or (reply == ''):
                break
            elif reply == 'n':
                prefix = input("Prefix? ")
                suffix = input("Suffix? ")

                while True:
                    try:
                        numdigits = int(input("Number of digits? "))
                        break
                    except ValueError:
                        continue

                continue
            elif reply == 'q':
                sys.exit(0)
            else:
                pass
    else:
        raise ProgramError()

#     if lenient_mode:
#         middle_string_re = r"[0-9]+"
#     else:
    middle_string_re = r"[0-9]{%u}" % numdigits

    filename_rec = re.compile(r"^%s(?P<num>%s)%s$" % (re.escape(prefix),
                                             middle_string_re,
                                             re.escape(suffix)))

    try:
        input_numbers = []

        for f in os.listdir("."):
            mo = filename_rec.match(f)
            if mo is not None and os.path.isfile(f):
                input_numbers.append(int(mo.group("num")))

        tmp_suffix = ".tmp.%s-%u-%u" % (progname, os.getpid(),
                                        random.randint(0, sys.maxsize))

        input_numbers.sort()
        current_num = first_number

        for num in input_numbers:
            os.rename("%s%0*u%s" % (prefix,
                                    numdigits, num,
                                    suffix),
                      "%s%0*u%s%s" % (prefix,
                                      numdigits, current_num,
                                      suffix,
                                      tmp_suffix))
            current_num += 1

        for i in range(len(input_numbers)):
            current_num = first_number + i
            os.rename("%s%0*u%s%s" % (prefix,
                                      numdigits, current_num,
                                      suffix,
                                      tmp_suffix),
                      "%s%0*u%s" % (prefix,
                                    numdigits, current_num,
                                    suffix))

        print("Processed %u file(s)." % len(input_numbers))

    except error as exc_instance:
        sys.stderr.write("Error: %s.\n" % exc_instance.complete_message())
        sys.exit(1)
    except KeyboardInterrupt:
        sys.stderr.write("\nProgram interrupted by signal SIGINT. "
                         "Aborting.\n")
        sys.exit(1)

    sys.exit(0)

if __name__ == "__main__": main()
