#! /usr/bin/python3
"""
Description:

reorganize dir containing timestamp formatted filenames
to dir/year, e.g.:

mv dir/20090102-120123-00.ext -> dir/2009/20090102-120123-00.ext

Usage: %(appname)s [-hvVstdX][-k #][-p pattern][-y year,...] directory...
       -h, --help         this message
       -V, --version      print version and exit
       -v, --verbose      verbose mode (cumulative)
       -s, --simulate     simulate action
       -t, --timestamp    use files mtime
       -d, --dirs         process directories as well
       -k, --keep         keep this number of items
       -p, --pattern      source file pattern
       -y, --years        comma separated list of years to limit
                          this operation on
       -X, --excurryear   exclude current year

Note, that, unless option --timestamp is given, it only checks for
names starting with 4 digits and interprets them as year.

Unless --dirs is given, it ignores directories. Be careful to not select
any (former) year directories. Better use patterns in that case.

pattern can be given more than once, and is a unix filename pattern:
*       matches everything
?       matches any single character
[seq]   matches any character in seq
[!seq]  matches any character not in seq

Version: %(version)s

Copyright:
(C)reated by %(author)s

License:
%(license)s

"""
#
# Changelog:
# 2010-06-02    hp  0.1  initial version
# 2012-02-24    hp  0.2  rework, timestamp and pattern option
# 2017-07-28    hp  0.3  exclude dirs (process files only)
# 2017-12-14    hp  0.4  keep and dirs option, multiple pattern
#
# vim:set et ts=8 sw=4:
#

__version__ = '0.4'
__author__ = 'Hans-Peter Jansen, LISA GmbH, Bingen, Germany <hpj@urpla.net>'
__license__ = 'GNU GPL 2 - see http://www.gnu.org/licenses/gpl.txt for details'


import sys
import os
import time
import getopt
import fnmatch


class gpar:
    """ global parameter class """
    appdir, appname = os.path.split(sys.argv[0])
    if appdir == '.':
        appdir = os.getcwd()
    version = __version__
    author = __author__
    license = __license__
    verbose = 0
    simulate = False
    timestamp = False
    dirs = False
    keep = None
    pattern = []
    excurryear = None
    years = []
    # internal
    simmkdirs = []


def out(arg, ch = sys.stdout):
    err(arg, ch)


def vout(arg, lvl = 1):
    if lvl <= gpar.verbose:
        err(arg)


def err(arg, ch = sys.stderr):
    if arg:
        ch.write(arg)
        if arg[-1] != "\n":
            ch.write("\n")
    else:
        ch.write("\n")
    ch.flush()


def exit(ret = 0, msg = None, usage = False):
    if msg:
        err("%s: %s" % (gpar.appname, msg))
    if usage:
        err(__doc__ % gpar.__dict__)
    sys.exit(ret)


def do_rename(base, name, year):
    dest = os.path.join(base, year)
    if not os.path.isdir(dest):
        msg = "mkdir: %s" % dest
        if gpar.simulate:
            if dest not in gpar.simmkdirs:
                vout("would %s" % msg)
                gpar.simmkdirs.append(dest)
        else:
            vout(msg, 2)
            os.makedirs(dest, 0o755)

    msg = "rename: %s -> %s" % (os.path.join(base, name), os.path.join(dest, name))
    if gpar.simulate:
        vout("would %s" % msg)
    else:
        vout(msg)
        os.rename(os.path.join(base, name), os.path.join(dest, name))


def reorgdir(base):
    if base.endswith(os.sep):
        base = base[:-1]

    todo = []
    for fname in os.listdir(base):
        fpath = os.path.join(base, fname)
        # check, if dir
        if not gpar.dirs and os.path.isdir(fpath):
            continue
        # check conditions
        match = False
        for pattern in gpar.pattern:
            if fnmatch.fnmatch(fname, pattern):
                match = True
                break

        if pattern and not match:
            vout("excluded: %s (pattern mismatch)" % fpath, 2)
            continue

        if gpar.timestamp:
            try:
                mtime = os.path.getmtime(fpath)
            except OSError as e:
                err("stat failed: %s" % e)
                continue
            year = str(time.localtime(mtime).tm_year)
        else:
            year = fname[:4]

        if year.isdigit():
            if gpar.years and year not in gpar.years:
                vout("excluded: %s" % fpath, 2)
                continue
            if gpar.excurryear and year == gpar.excurryear:
                vout("excluded current year: %s" % fpath, 2)
                continue
        else:
            vout("excluded invalid filename: %s" % fpath, 2)
            continue

        todo.append((year, fname))

    # sort todo list
    todo.sort()

    # remove items to be kept
    if gpar.keep:
        todo = todo[:-gpar.keep]

    # process items
    for year, fname in todo:
        do_rename(base, fname, year)


def reorg(dirs):
    for d in dirs:
        if not os.path.isdir(d):
            vout("%s: not a directory: ignored" % d)
            continue
        reorgdir(d)


if __name__ == '__main__':
    try:
        optlist, args = getopt.getopt(sys.argv[1:], "hvVstdk:p:y:X",
            ("help", "verbose", "version", "simulate", "timestamp",
             "dirs", "keep=", "pattern=", "excurryear", "years="))
    except getopt.error as msg:
        exit(1, msg, True)

    for opt, par in optlist:
        if opt in ('-h', '--help'):
            exit(usage = True)
        elif opt in ('-v', '--verbose'):
            gpar.verbose += 1
        elif opt in ('-V', '--version'):
            exit(msg = 'version: %s' % gpar.version)
        elif opt in ('-s', '--simulate'):
            gpar.simulate = True
        elif opt in ('-t', '--timestamp'):
            gpar.timestamp = True
        elif opt in ('-d', '--dirs'):
            gpar.dirs = True
        elif opt in ('-k', '--keep'):
            try:
                gpar.keep = int(par)
            except ValueError:
                exit(2, "invalid parameter for keep: '%s'" % par)
        elif opt in ('-p', '--pattern'):
            gpar.pattern.append(par)
        elif opt in ('-y', '--years'):
            gpar.years.extend(par.split(','))
        elif opt in ('-X', '--excurryear'):
            gpar.excurryear = time.strftime("%Y")
    if gpar.simulate and not gpar.verbose:
        gpar.verbose = 1

    if not args:
        exit(1, "no directory given", True)

    reorg(args)
