#!/usr/bin/env python3

# Copyright (C) 2007-2018 Damon Lynch <damonlynch@gmail.com>

# This file is part of Rapid Photo Downloader.
#
# Rapid Photo Downloader 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.
#
# Rapid Photo Downloader 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 Rapid Photo Downloader.  If not,
# see <http://www.gnu.org/licenses/>.

# Special key in each dictionary which specifies the order of elements.
# It is very important to have a consistent and rational order when displaying 
# these prefs to the user, and dictionaries are unsorted.

__author__ = 'Damon Lynch'
__copyright__ = "Copyright 2007-2018, Damon Lynch"

import os
from collections import OrderedDict
from typing import List, Optional, Tuple

from gettext import gettext as _

# PLEASE NOTE: these values are duplicated in a dummy class whose function
# is to have them put into the translation template. If you change the values below
# then you MUST change the value in class i18TranslateMeThanks as well!! 

# *** Level 0, i.e. first column of values presented to user
DATE_TIME = 'Date time'
TEXT = 'Text'
FILENAME = 'Filename'
METADATA = 'Metadata'
SEQUENCES = 'Sequences'
JOB_CODE = 'Job code'

SEPARATOR = os.sep

# *** Level 1, i.e. second column of values presented to user

# Date time
IMAGE_DATE = 'Image date'
TODAY = 'Today'
YESTERDAY = 'Yesterday'
VIDEO_DATE = 'Video date'
DOWNLOAD_TIME = 'Download time'

# File name 
NAME = 'Name'
IMAGE_NUMBER = 'Image number'
VIDEO_NUMBER = 'Video number'

# pre 0.9.0a4 File name values: NAME_EXTENSION, EXTENSION
NAME_EXTENSION = 'Name + extension'

# however extension is used for subfolder generation in all versions
EXTENSION = 'Extension'


# Metadata
APERTURE = 'Aperture'
ISO = 'ISO'
EXPOSURE_TIME = 'Exposure time'
FOCAL_LENGTH = 'Focal length'
CAMERA_MAKE = 'Camera make'
CAMERA_MODEL = 'Camera model'
SHORT_CAMERA_MODEL = 'Short camera model'
SHORT_CAMERA_MODEL_HYPHEN = 'Hyphenated short camera model'
SERIAL_NUMBER = 'Serial number'
SHUTTER_COUNT = 'Shutter count'
# Currently the only file number is Exif.CanonFi.FileNumber,
# which is in the format xxx-yyyy, where xxx is the folder and yyyy the image
FILE_NUMBER = 'File number'
OWNER_NAME = 'Owner name'
COPYRIGHT = 'Copyright'
ARTIST = 'Artist'

# Video metadata
CODEC = 'Codec'
WIDTH = 'Width'
HEIGHT = 'Height'
FPS = 'Frames Per Second'
LENGTH = 'Length'

# Image sequences
DOWNLOAD_SEQ_NUMBER = 'Downloads today'
SESSION_SEQ_NUMBER = 'Session number'
SUBFOLDER_SEQ_NUMBER = 'Subfolder number'
STORED_SEQ_NUMBER = 'Stored number'
SEQUENCE_LETTER = 'Sequence letter'

# *** Level 2, i.e. third and final column of values presented to user

# Image number
IMAGE_NUMBER_ALL = 'All digits'
IMAGE_NUMBER_1 = 'Last digit'
IMAGE_NUMBER_2 = 'Last 2 digits'
IMAGE_NUMBER_3 = 'Last 3 digits'
IMAGE_NUMBER_4 = 'Last 4 digits'

# Case
ORIGINAL_CASE = "Original Case"
UPPERCASE = "UPPERCASE"
LOWERCASE = "lowercase"

# Sequence number
SEQUENCE_NUMBER_1 = "One digit"
SEQUENCE_NUMBER_2 = "Two digits"
SEQUENCE_NUMBER_3 = "Three digits"
SEQUENCE_NUMBER_4 = "Four digits"
SEQUENCE_NUMBER_5 = "Five digits"
SEQUENCE_NUMBER_6 = "Six digits"
SEQUENCE_NUMBER_7 = "Seven digits"

# File number
FILE_NUMBER_FOLDER = "Folder only"
FILE_NUMBER_ALL = "Folder and file"

# Now, define dictionaries and lists of valid combinations of preferences.

# Level 2

# Date 

SUBSECONDS = 'Subseconds'

# NOTE 1: if changing LIST_DATE_TIME_L2, you MUST update the default
# subfolder preference immediately below
# NOTE 2: if changing LIST_DATE_TIME_L2, you MUST also update
# DATE_TIME_CONVERT below
# NOTE 3: if changing LIST_DATE_TIME_L2, you MUST also update
# PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV 
LIST_DATE_TIME_L2 = [
    'YYYYMMDD',  # 0
    'YYYY-MM-DD',
    'YYYY_MM_DD',  # 2
    'YYMMDD',
    'YY-MM-DD',  # 4
    'YY_MM_DD',
    'MMDDYYYY',  # 6
    'MMDDYY',
    'MMDD',  # 8
    'DDMMYYYY',
    'DDMMYY',  # 10
    'YYYY',
    'YY',  # 12
    'MM',
    'DD',  # 14
    'Month (full)',
    'Month (abbreviated)',  # 16
    'Weekday (full)',
    'Weekday (abbreviated)', # 18
    'HHMMSS',
    'HHMM',  # 20
    'HH-MM-SS',
    'HH-MM',  # 22
    'HH',
    'MM (minutes)',  # 24
    'SS'
]

LIST_IMAGE_DATE_TIME_L2 = LIST_DATE_TIME_L2 + [SUBSECONDS]

DEFAULT_SUBFOLDER_PREFS = [
    DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11], '/', '', '', DATE_TIME, IMAGE_DATE,
    LIST_DATE_TIME_L2[0]
]
DEFAULT_VIDEO_SUBFOLDER_PREFS = [
    DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11], '/', '', '', DATE_TIME,
    VIDEO_DATE, LIST_DATE_TIME_L2[0]
]

DEFAULT_PHOTO_RENAME_PREFS = [FILENAME, NAME, ORIGINAL_CASE]
DEFAULT_VIDEO_RENAME_PREFS = [FILENAME, NAME, ORIGINAL_CASE]


class i18TranslateMeThanks:
    """ this class is never used in actual running code
    Its purpose is to have these values inserted into the program's i18n template file
    
    """

    def __init__(self):
        _('Date time')
        _('Text')
        _('Filename')
        _('Metadata')
        _('Sequences')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#jobcode
        _('Job code')
        _('Image date')
        _('Video date')
        _('Today')
        _('Yesterday')
        # Translators: Download time is the time and date that the download started (when the
        # user clicked the Download button)
        _('Download time')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
        _('Name')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
        _('Extension')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
        _('Image number')
        _('Video number')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
        _('Aperture')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
        _('ISO')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
        _('Exposure time')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
        _('Focal length')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
        _('Camera make')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
        _('Camera model')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
        _('Short camera model')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
        _('Hyphenated short camera model')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
        _('Serial number')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
        _('Shutter count')
        # File number currently refers to the Exif value Exif.Canon.FileNumber
        _('File number')
        # Only the folder component of the Exif.Canon.FileNumber value
        _('Folder only')
        # The folder and file component of the Exif.Canon.FileNumber value
        _('Folder and file')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
        _('Owner name')
        _('Codec')
        _('Width')
        _('Height')
        _('Length')
        _('Frames Per Second')
        _('Artist')
        _('Copyright')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers
        _('Downloads today')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers
        _('Session number')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers
        _('Subfolder number')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers
        _('Stored number')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#sequenceletters
        _('Sequence letter')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
        _('All digits')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
        _('Last digit')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
        _('Last 2 digits')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
        _('Last 3 digits')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
        _('Last 4 digits')
        # Translators: please not the capitalization of this text, and keep it the same if your
        # language features capitalization
        _("Original Case")
        # Translators: please not the capitalization of this text, and keep it the same if your
        # language features capitalization
        _("UPPERCASE")
        # Translators: please not the capitalization of this text, and keep it the same if your
        # language features capitalization
        _("lowercase")
        _("One digit")
        _("Two digits")
        _("Three digits")
        _("Four digits")
        _("Five digits")
        _("Six digits")
        _("Seven digits")
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('Subseconds')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('YYYYMMDD')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('YYYY-MM-DD')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('YYYY_MM_DD')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('YYMMDD')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('YY-MM-DD')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('YY_MM_DD')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('MMDDYYYY')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('MMDDYY')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('MMDD')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('DDMMYYYY')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('DDMMYY')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('YYYY')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('YY')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('MM')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('DD')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('Month (full)'),
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('Month (abbreviated)'),
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('Weekday (full)')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('Weekday (abbreviated)')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('HHMMSS')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('HHMM')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('HH-MM-SS')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('HH-MM')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('HH')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('MM (minutes)')
        # Translators: for an explanation of what this means,
        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
        _('SS')

    # Convenience values for python datetime conversion using values in


# Default subfolder options that appear in drop-down menu in Destination views
# Any change to PHOTO_SUBFOLDER_MENU_DEFAULTS must also be reflected in 
# PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV

# The following values will be displayed in the menu after an os.sep.join() operation

PHOTO_SUBFOLDER_MENU_DEFAULTS = (
    (_('Date'), _('YYYY'), _('YYYYMMDD')),
    (_('Date (hyphens)'), _('YYYY'), _('YYYY-MM-DD')),
    (_('Date (underscores)'), _('YYYY'), _('YYYY_MM_DD')),
    (_('Date and Job Code'), _('YYYY'), _('YYYYMM_Job Code')),
    (_('Date and Job Code Subfolder'), _('YYYY'), _('YYYYMM'), _('Job Code'))
)

# Any change to PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV must also be reflected in 
# PHOTO_SUBFOLDER_MENU_DEFAULTS

PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV = (
    # 0
    [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11],
     '/', '', '', 
     DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0]
    ],
    # 1
    [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11],
     '/', '', '',
     DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[1]
    ],
    # 2
    [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11],
     '/', '', '',
     DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[2]
    ],
    # 3
    [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11],
     '/', '', '',
     DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11],
     DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[13],
     TEXT, '_', '',
     JOB_CODE, '', ''],
    # 4
    [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11],
     '/', '', '',
     DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11],
     DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[13],
     '/', '', '',
     JOB_CODE, '', '',
     ],
)

PHOTO_RENAME_MENU_DEFAULTS = (
    (_('Original Filename'), 'IMG_1234'),
    (_('Date-Time and Downloads today'), _('YYYYMMDD-HHMM-1')),
    (_('Date and Downloads today'), _('YYYYMMDD-1')),
    (_('Date-Time and Image number'), _('YYYYMMDD-1234')),
    (_('Date-Time and Job Code'), _('YYYYMMDD-HHMM-Job Code-1')),
    (_('Date and Job Code'), _('YYYYMMDD-Job Code-1'))
)

PHOTO_RENAME_MENU_DEFAULTS_CONV = (
    # 0 Original Filename
    [FILENAME, NAME, ORIGINAL_CASE],
    # 1 Date-Time and Downloads today
    [
        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0],
        TEXT, '-', '',
        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[20],
        TEXT, '-', '',
        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
    ],
    # 2 Date and Downloads today
    [
        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0],
        TEXT, '-', '',
        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
    ],
    # 3 Date-Time and Image number
    [
        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0],
        TEXT, '-', '',
        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[20],
        TEXT, '-', '',
        FILENAME, IMAGE_NUMBER, IMAGE_NUMBER_ALL
    ],
    # 4 Date-Time and Job Code
    [
        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0],
        TEXT, '-', '',
        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[20],
        TEXT, '-', '',
        JOB_CODE, '', '',
        TEXT, '-', '',
        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
    ],
    # 5 Date and Job Code
    [
        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0],
        TEXT, '-', '',
        JOB_CODE, '', '',
        TEXT, '-', '',
        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
    ]
)

# See notes above regarding keeping values in sync
VIDEO_SUBFOLDER_MENU_DEFAULTS = PHOTO_SUBFOLDER_MENU_DEFAULTS
VIDEO_SUBFOLDER_MENU_DEFAULTS_CONV = (
    # 0
    [
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11],
        SEPARATOR, '', '',
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0]
     ],
    # 1
    [
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11],
        SEPARATOR, '', '',
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[1]
     ],
    # 2
    [
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11],
        SEPARATOR, '', '',
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[2]
     ],
    # 3
    [
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11],
        SEPARATOR, '', '',
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11],
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[13],
        TEXT, '_', '',
        JOB_CODE, '', ''
    ],
    # 4
    [
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11],
        SEPARATOR, '', '',
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11],
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[13],
        SEPARATOR, '', '',
        JOB_CODE, '', '',
     ],
)

VIDEO_RENAME_MENU_DEFAULTS = (
    (_('Original Filename'), 'MVI_1234'),
    (_('Date-Time and Downloads today'), _('YYYYMMDD-HHMM-1')),
    (_('Date and Downloads today'), _('YYYYMMDD-1')),
    (_('Date-Time and Video number'), _('YYYYMMDD_1234')),
    (_('Date-Time and Job Code'), _('YYYYMMDD-HHMM-Job Code-1')),
    (_('Date and Job Code'), _('YYYYMMDD-Job Code-1')),
    (_('Resolution'), _('YYYYMMDD-HHMM-1-1920x1080'))
)

VIDEO_RENAME_MENU_DEFAULTS_CONV = (
    # 0 Original Filename
    [FILENAME, NAME, ORIGINAL_CASE],
    # 1 Date-Time and Downloads today
    [
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0],
        TEXT, '-', '',
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[20],
        TEXT, '-', '',
        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
    ],
    # 2 Date and Downloads today
    [
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0],
        TEXT, '-', '',
        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
    ],
    # 3 Date-Time and Image number
    [
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0],
        TEXT, '-', '',
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[20],
        TEXT, '-', '',
        FILENAME, VIDEO_NUMBER, IMAGE_NUMBER_ALL
    ],
    # 4 Date-Time and Job Code
    [
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0],
        TEXT, '-', '',
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[20],
        TEXT, '-', '',
        JOB_CODE, '', '',
        TEXT, '-', '',
        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
    ],
    # 5 Date and Job Code
    [
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0],
        TEXT, '-', '',
        JOB_CODE, '', '',
        TEXT, '-', '',
        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
    ],
    # 6 Resolution
    [
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0],
        TEXT, '-', '',
        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[20],
        TEXT, '-', '',
        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1,
        TEXT, '-', '',
        METADATA, WIDTH, '',
        TEXT, 'x', '',
        METADATA, HEIGHT, ''
    ]
)

# See notes above regarding keeping values in sync
DATE_TIME_CONVERT = [
    '%Y%m%d',  # 0
    '%Y-%m-%d',
    '%Y_%m_%d',  # 2
    '%y%m%d',
    '%y-%m-%d',   # 4
    '%y_%m_%d',
    '%m%d%Y',  # 6
    '%m%d%y',
    '%m%d',  # 8
    '%d%m%Y',
    '%d%m%y', # 10
    '%Y',
    '%y',  # 12
    '%m',
    '%d',  # 14
    '%B',
    '%b',  # 16
    '%A',
    '%a',     # 18
    '%H%M%S',
    '%H%M',  # 20
    '%H-%M-%S',
    '%H-%M',  # 22
    '%H',
    '%M',  # 24
    '%S'
]

LIST_IMAGE_NUMBER_L2 = [
    IMAGE_NUMBER_ALL, IMAGE_NUMBER_1, IMAGE_NUMBER_2, IMAGE_NUMBER_3, IMAGE_NUMBER_4
]

LIST_CASE_L2 = [ORIGINAL_CASE, UPPERCASE, LOWERCASE]

LIST_SEQUENCE_LETTER_L2 = [
    UPPERCASE,
    LOWERCASE
]

LIST_SEQUENCE_NUMBERS_L2 = [
    SEQUENCE_NUMBER_1,
    SEQUENCE_NUMBER_2,
    SEQUENCE_NUMBER_3,
    SEQUENCE_NUMBER_4,
    SEQUENCE_NUMBER_5,
    SEQUENCE_NUMBER_6,
    SEQUENCE_NUMBER_7,
]

LIST_SHUTTER_COUNT_L2 = [
    SEQUENCE_NUMBER_3,
    SEQUENCE_NUMBER_4,
    SEQUENCE_NUMBER_5,
    SEQUENCE_NUMBER_6,
]
FILE_NUMBER_L2 = [
    FILE_NUMBER_FOLDER,
    FILE_NUMBER_ALL
]

# Level 1

DICT_DATE_TIME_L1 = OrderedDict(
    [
        (IMAGE_DATE, LIST_IMAGE_DATE_TIME_L2),
        (TODAY, LIST_DATE_TIME_L2),
        (YESTERDAY, LIST_DATE_TIME_L2),
        (DOWNLOAD_TIME, LIST_DATE_TIME_L2),
    ]
)

VIDEO_DICT_DATE_TIME_L1 = OrderedDict(
    [
        (VIDEO_DATE, LIST_IMAGE_DATE_TIME_L2),
        (TODAY, LIST_DATE_TIME_L2),
        (YESTERDAY, LIST_DATE_TIME_L2),
        (DOWNLOAD_TIME, LIST_DATE_TIME_L2),
    ]
)

DICT_FILENAME_L1 = OrderedDict(
    [
        (NAME, LIST_CASE_L2),
        (IMAGE_NUMBER, LIST_IMAGE_NUMBER_L2),
    ]
)

# pre 0.9.0a4 values for DICT_FILENAME_L1:
#(NAME_EXTENSION, LIST_CASE_L2),
# (EXTENSION, LIST_CASE_L2),

DICT_VIDEO_FILENAME_L1 = OrderedDict(
    [
        (NAME, LIST_CASE_L2),
        (VIDEO_NUMBER, LIST_IMAGE_NUMBER_L2),
    ]
)

# pre 0.9.0a4 values for DICT_VIDEO_FILENAME_L1:
# (NAME_EXTENSION, LIST_CASE_L2),
# (EXTENSION, LIST_CASE_L2),

DICT_SUBFOLDER_FILENAME_L1 = {
    EXTENSION: LIST_CASE_L2,
}

DICT_METADATA_L1 = OrderedDict(
    [
        (APERTURE, None),
        (ISO, None),
        (EXPOSURE_TIME, None),
        (FOCAL_LENGTH, None),
        (CAMERA_MAKE, LIST_CASE_L2),
        (CAMERA_MODEL, LIST_CASE_L2),
        (SHORT_CAMERA_MODEL, LIST_CASE_L2),
        (SHORT_CAMERA_MODEL_HYPHEN, LIST_CASE_L2),
        (SERIAL_NUMBER, None),
        (SHUTTER_COUNT, LIST_SHUTTER_COUNT_L2),
        (FILE_NUMBER, FILE_NUMBER_L2),
        (OWNER_NAME, LIST_CASE_L2),
        (ARTIST, LIST_CASE_L2),
        (COPYRIGHT, LIST_CASE_L2),
    ]
)

DICT_VIDEO_METADATA_L1 = OrderedDict(
    [
        (CODEC, LIST_CASE_L2),
        (WIDTH, None),
        (HEIGHT, None),
        (LENGTH, None),
        (FPS, None),
    ]
)

DICT_SEQUENCE_L1 = OrderedDict(
    [
        (DOWNLOAD_SEQ_NUMBER, LIST_SEQUENCE_NUMBERS_L2),
        (STORED_SEQ_NUMBER, LIST_SEQUENCE_NUMBERS_L2),
        (SESSION_SEQ_NUMBER, LIST_SEQUENCE_NUMBERS_L2),
        (SEQUENCE_LETTER, LIST_SEQUENCE_LETTER_L2),
    ]
)

LIST_SEQUENCE_L1 = list(DICT_SEQUENCE_L1.keys())

# Level 0

DICT_IMAGE_RENAME_L0 = OrderedDict(
    [
        (DATE_TIME, DICT_DATE_TIME_L1),
        (TEXT, None),
        (FILENAME, DICT_FILENAME_L1),
        (METADATA, DICT_METADATA_L1),
        (SEQUENCES, DICT_SEQUENCE_L1),
        (JOB_CODE, None),
    ]
)

DICT_VIDEO_RENAME_L0 = OrderedDict(
    [
        (DATE_TIME, VIDEO_DICT_DATE_TIME_L1),
        (TEXT, None),
        (FILENAME, DICT_VIDEO_FILENAME_L1),
        (METADATA, DICT_VIDEO_METADATA_L1),
        (SEQUENCES, DICT_SEQUENCE_L1),
        (JOB_CODE, None),
    ]
)

DICT_SUBFOLDER_L0 = OrderedDict(
    [
        (DATE_TIME, DICT_DATE_TIME_L1),
        (TEXT, None),
        (FILENAME, DICT_SUBFOLDER_FILENAME_L1),
        (METADATA, DICT_METADATA_L1),
        (SEPARATOR, None),
        (JOB_CODE, None),
    ]
)

DICT_VIDEO_SUBFOLDER_L0 = OrderedDict(
    [
        (DATE_TIME, VIDEO_DICT_DATE_TIME_L1),
        (TEXT, None),
        (FILENAME, DICT_SUBFOLDER_FILENAME_L1),
        (METADATA, DICT_VIDEO_METADATA_L1),
        (SEPARATOR, None),
        (JOB_CODE, None),
    ]
)

# preference elements that require metadata
# note there is no need to specify lower level elements if a higher level 
# element is necessary for them to be present to begin with
METADATA_ELEMENTS = [METADATA, IMAGE_DATE]

# preference elements that are sequence numbers or letters             
SEQUENCE_ELEMENTS = [
    DOWNLOAD_SEQ_NUMBER,
    SESSION_SEQ_NUMBER,
    SUBFOLDER_SEQ_NUMBER,
    STORED_SEQ_NUMBER,
    SEQUENCE_LETTER
]

# preference elements that do not require metadata and are not fixed
# as above, there is no need to specify lower level elements if a higher level 
# element is necessary for them to be present to begin with
DYNAMIC_NON_METADATA_ELEMENTS = [TODAY, YESTERDAY, FILENAME] + SEQUENCE_ELEMENTS

PHOTO_RENAME_COMPLEX = [
    'Date time', 'Image date', 'YYYYMMDD', 'Text', '-', '', 'Date time', 'Image date', 'HHMM',
    'Text', '-', '', 'Sequences', 'Downloads today', 'One digit', 'Text', '-iso', '', 'Metadata',
    'ISO', '', 'Text', '-f', '', 'Metadata', 'Aperture', '', 'Text', '-', '', 'Metadata',
    'Focal length', '', 'Text', 'mm-', '', 'Metadata', 'Exposure time', ''
]
PHOTO_RENAME_SIMPLE = [
    'Date time', 'Image date', 'YYYYMMDD', 'Text', '-', '', 'Date time', 'Image date', 'HHMM',
    'Text', '-', '', 'Sequences', 'Downloads today', 'One digit'
]

VIDEO_RENAME_SIMPLE = [x if x != 'Image date' else 'Video date' for x in PHOTO_RENAME_SIMPLE]

JOB_CODE_RENAME_TEST = ['Job code', '', '', 'Sequences', 'Downloads today', 'One digit']


def upgrade_pre090a4_rename_pref(pref_list: List[str]) -> Tuple[List[str], str]:
    r"""
    Upgrade photo and video rename preference list

    :param pref_list: pref list to upgrade
    :return: tuple of new pref list, and if found, the case to be used for the
     extension

    >>> upgrade_pre090a4_rename_pref([FILENAME, NAME_EXTENSION, ORIGINAL_CASE])
    (['Filename', 'Name', 'Original Case'], 'Original Case')
    >>> upgrade_pre090a4_rename_pref(PHOTO_RENAME_SIMPLE + [FILENAME, EXTENSION, LOWERCASE])
    ... # doctest: +NORMALIZE_WHITESPACE
    (['Date time', 'Image date', 'YYYYMMDD',
      'Text', '-', '',
      'Date time', 'Image date', 'HHMM',
      'Text', '-', '',
      'Sequences', 'Downloads today', 'One digit'], 'lowercase')
    >>> upgrade_pre090a4_rename_pref(PHOTO_RENAME_COMPLEX + [FILENAME, EXTENSION, UPPERCASE])
    ... # doctest: +NORMALIZE_WHITESPACE
    (['Date time', 'Image date', 'YYYYMMDD', 'Text', '-', '',
      'Date time', 'Image date', 'HHMM', 'Text', '-', '', 'Sequences',
      'Downloads today', 'One digit', 'Text', '-iso', '',
      'Metadata', 'ISO', '', 'Text', '-f', '', 'Metadata',
      'Aperture', '', 'Text', '-', '', 'Metadata', 'Focal length', '',
      'Text', 'mm-', '', 'Metadata', 'Exposure time', ''], 'UPPERCASE')
     >>> upgrade_pre090a4_rename_pref([FILENAME, NAME, LOWERCASE])
     (['Filename', 'Name', 'lowercase'], None)

    """
    if not pref_list:
        return (pref_list, None)

    # get extension case from last value
    if pref_list[-2] in (NAME_EXTENSION, EXTENSION):
        case = pref_list[-1]
    else:
        case = None

    new_pref_list = []
    for idx in range(0, len(pref_list), 3):
        l1 = pref_list[idx + 1]
        if  l1 != EXTENSION:
            if l1 == NAME_EXTENSION:
                l1 = NAME
            new_pref_list.extend([pref_list[idx], l1, pref_list[idx + 2]])
    return new_pref_list, case


class PrefError(Exception):
    """ base class """

    def __init__(self):
        super().__init__()
        self.msg = ''

    def unpackList(self, l: List[str]):
        """
        Make the preferences presentable to the user
        """
        return ', '.join("'{}'".format(i) for i in l)

    def __str__(self):
        return self.msg


class PrefKeyError(PrefError):
    def __init__(self, error):
        super().__init__()
        value = error[0]
        expectedValues = self.unpackList(error[1])
        self.msg = "Preference key '%(key)s' is invalid.\nExpected one of %(value)s" % {
            'key': value, 'value': expectedValues}


class PrefValueInvalidError(PrefKeyError):
    def __init__(self, error):
        super().__init__(error)
        value = error[0]
        self.msg = "Preference value '%(value)s' is invalid" % {'value': value}


class PrefLengthError(PrefError):
    def __init__(self, error):
        super().__init__()
        self.msg = "These preferences are not well formed:" + "\n %s" % self.unpackList(error)


class PrefValueKeyComboError(PrefError):
    def __init__(self, error):
        super().__init__()
        self.msg = error


def check_pref_valid(pref_defn, prefs, modulo=3) -> bool:
    """
    Checks to see if user preferences are valid according to their
    definition. Raises appropriate exception if an error is found.

    :param prefs: list of preferences
    :param pref_defn: is a Dict specifying what is valid
    :param modulo: how many list elements are equivalent to one line
    of preferences.
    :return: True if prefs match with pref_defn
    """

    if (len(prefs) % modulo != 0) or not prefs:
        raise PrefLengthError(prefs)
    else:
        for i in range(0, len(prefs), modulo):
            _check_pref_valid(pref_defn, prefs[i:i + modulo])

    return True


def _check_pref_valid(pref_defn, prefs):
    key = prefs[0]
    value = prefs[1]

    if key in pref_defn:

        next_pref_defn = pref_defn[key]

        if value is None:
            # value should never be None, at any time
            raise PrefValueInvalidError((None, next_pref_defn))

        if next_pref_defn and not value:
            raise PrefValueInvalidError((value, next_pref_defn))

        if isinstance(next_pref_defn, dict):
            return _check_pref_valid(next_pref_defn, prefs[1:])
        else:
            if isinstance(next_pref_defn, list):
                result = value in next_pref_defn
                if not result:
                    raise PrefValueInvalidError((value, next_pref_defn))
                return True
            elif not next_pref_defn:
                return True
            else:
                result = next_pref_defn == value
                if not result:
                    raise PrefValueInvalidError((value, next_pref_defn))
                return True
    else:
        raise PrefKeyError((key, list(pref_defn.keys())))


def filter_subfolder_prefs(pref_list: List[str],
                           pref_colors: Optional[List[str]]=None) \
        -> Tuple[bool, List[str], Optional[List[str]]]:
    """
    Filters out extraneous preference choices.

    :param pref_list: the list of user specified preferences
    :param pref_colors: optional list of colors associated with displaying the
     generated sample name while editing the preferences
    :return: bool indicating whether list changed, the pref list, and optionally the
     list of colors
    """

    prefs_changed = False
    continue_check = True
    while continue_check and pref_list:
        continue_check = False
        if pref_list[0] == SEPARATOR:
            # subfolder preferences should not start with a /
            pref_list = pref_list[3:]
            if pref_colors is not None:
                pref_colors = pref_colors[1:]
            prefs_changed = True
            continue_check = True
        elif pref_list[-3] == SEPARATOR:
            # subfolder preferences should not end with a /
            pref_list = pref_list[:-3]
            if pref_colors is not None:
                pref_colors = pref_colors[:-1]
            continue_check = True
            prefs_changed = True
        else:
            for i in range(0, len(pref_list) - 3, 3):
                if pref_list[i] == SEPARATOR and pref_list[i + 3] == SEPARATOR:
                    # subfolder preferences should not contain two /s side by side
                    continue_check = True
                    prefs_changed = True
                    # note we are messing with the contents of the pref list,
                    # must exit loop and try again
                    pref_list = pref_list[:i] + pref_list[i + 3:]
                    if pref_colors is not None:
                        pref_colors = pref_colors[:i//3] + pref_colors[i//3 + 1:]
                    break

    return (prefs_changed, pref_list, pref_colors)
