# Lazygal, a lazy static web gallery generator.
# Copyright (C) 2007-2012 Alexandre Rossi <alexandre.rossi@gmail.com>
#
# 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; either version 2 of the License, or
# (at your option) any later version.
#
# 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; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import os
import locale
import logging
import gc
import genshi
import sys
import re
import fnmatch
import shutil

from .config import LazygalConfig
from .config import USER_CONFIG_PATH, LazygalConfigDeprecated
from .sourcetree import SOURCEDIR_CONFIGFILE
from .metadata import GExiv2

from . import make
from . import pathutils
from . import sourcetree
from . import pindex
from . import tpl
from . import newsize
from . import metadata
from . import genpage
from . import genmedia
from . import genfile
from . import mediautils
from . import theme


from lazygal import INSTALL_MODE, INSTALL_PREFIX
if INSTALL_MODE == 'source':
    DATAPATH = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))
elif INSTALL_MODE == 'installed':
    DATAPATH = os.path.join(INSTALL_PREFIX, 'share', 'lazygal')
    if not os.path.exists(os.path.join(DATAPATH, 'themes')):
        print(_('Could not find themes dir, check your installation!'))
        sys.exit(1)


DEST_SHARED_DIRECTORY_NAME = 'shared'


class SubgalSort(make.MakeTask):
    """
    This task sorts the medias within a gallery according to the chosen rule.
    """

    def __init__(self, webgal_dir):
        super().__init__()
        self.set_dep_only()
        self.webgal_dir = webgal_dir

    def build(self):
        logging.info(_("  SORTING pics and subdirs"))

        order = self.webgal_dir.subgal_sort_by['order']
        if order == 'exif':
            subgal_sortkey = lambda x: x.source_dir.latest_media_stamp()
        elif order == 'mtime':
            subgal_sortkey = lambda x: x.source_dir.get_mtime()
        elif order == 'numeric':
            subgal_sortkey = lambda x: x.source_dir.name_numeric()
        elif order == 'dirname' or order == 'filename':  # Backward compat
            subgal_sortkey = lambda x: x.source_dir.filename
        else:
            raise ValueError(_("Unknown sorting criterion '%s'")
                             % self.webgal_dir.subgal_sort_by[0])
        self.webgal_dir.subgals.sort(key=subgal_sortkey,
            reverse=self.webgal_dir.subgal_sort_by['reverse'])

        order = self.webgal_dir.pic_sort_by['order']
        if order == 'exif':
            sortkey = lambda x: x.media.sortkey()
        elif order == 'mtime':
            sortkey = lambda x: x.media.get_mtime()
        elif order == 'numeric':
            sortkey = lambda x: x.media.name_numeric()
        elif order == 'filename':
            sortkey = lambda x: x.media.filename
        else:
            raise ValueError(_("Unknown sorting criterion '%s'")
                             % self.webgal_dir.pic_sort_by[0])
        self.webgal_dir.medias.sort(key=sortkey,
            reverse=self.webgal_dir.pic_sort_by['reverse'])

        # chain medias
        previous = None
        for media in self.webgal_dir.medias:
            if previous:
                previous.set_next(media)
                media.set_previous(previous)
            previous = media


class SubgalBreak(make.MakeTask):
    """
    This task breaks galleries into multiple pages.
    """

    def __init__(self, webgal_dir):
        super().__init__()
        self.webgal_dir = webgal_dir

        self.__last_page_number = -1

    def next_page_number(self):
        self.__last_page_number += 1
        return self.__last_page_number

    def how_many_pages(self):
        return self.__last_page_number + 1

    def build(self):
        logging.info(_("  BREAKING web gallery into multiple pages"))

        if self.webgal_dir.thumbs_per_page == 0:
            self.__fill_no_pagination()
        else:
            if self.webgal_dir.flatten_below():
                self.__fill_loose_pagination()
            else:
                self.__fill_real_pagination()

    def __fill_no_pagination(self):
        galleries = []
        galleries.append((self.webgal_dir, self.webgal_dir.medias))
        if self.webgal_dir.flatten_below():
            subgals = []
            for dir in self.webgal_dir.get_all_subgals():
                dir.call_populate_deps()
                galleries.append((dir, dir.medias))
        else:
            subgals = self.webgal_dir.subgals
        self.webgal_dir.add_index_page(subgals, galleries)

    def __fill_loose_pagination(self):
        """
        Loose pagination not breaking subgals (chosen if subgals are flattened).
        """
        subgals = []  # No subgal links as they are flattened

        galleries = []
        how_many_medias = 0
        subgals_it = iter([self.webgal_dir] + self.webgal_dir.get_all_subgals())
        for subgal in subgals_it:
            how_many_medias += subgal.pindex.get_media_count()
            galleries.append((subgal, subgal.medias))
            if how_many_medias > self.webgal_dir.thumbs_per_page:
                self.webgal_dir.add_index_page(subgals, galleries)
                galleries = []
                how_many_medias = 0

        if galleries:
            self.webgal_dir.add_index_page(subgals, galleries)

    def __fill_real_pagination(self):
        medias_amount = len(self.webgal_dir.medias)
        how_many_pages = medias_amount // self.webgal_dir.thumbs_per_page
        if medias_amount == 0\
                or medias_amount % self.webgal_dir.thumbs_per_page > 0:
            how_many_pages = how_many_pages + 1

        for page_number in range(0, how_many_pages):
            step = page_number * self.webgal_dir.thumbs_per_page
            end_index = step + self.webgal_dir.thumbs_per_page
            shown_medias = self.webgal_dir.medias[step:end_index]
            galleries = [(self.webgal_dir, shown_medias)]

            # subgal links only for first page
            if page_number == 0:
                subgals = self.webgal_dir.subgals
            else:
                subgals = []

            self.webgal_dir.add_index_page(subgals, galleries)


class WebalbumMediaTask(make.GroupTask):

    def __init__(self, webgal, media):
        super().__init__()

        self.webgal = webgal
        self.media = media

        self.previous = None
        self.next = None

        self.original = None
        self.resized = {}
        self.browse_pages = {}

        if self.webgal.original and not self.webgal.orig_base:
            self.add_dependency(self.get_original())

        for size_name in self.webgal.browse_sizes:
            self.resized[size_name] = self.get_resized(size_name)
            if self.resized[size_name] not in self.deps:
                self.add_dependency(self.resized[size_name])

            if self.webgal.album.theme.kind == 'static':
                self.browse_pages[size_name] = self.get_browse_page(size_name)
                if self.webgal.album.force_gen_pages:
                    self.browse_pages[size_name].stamp_delete()

    def set_next(self, media):
        self.next = media
        if media:
            for bpage in self.browse_pages.values():
                if media.thumb: bpage.add_dependency(media.thumb)

    def set_previous(self, media):
        self.previous = media
        if media:
            for bpage in self.browse_pages.values():
                if media.thumb: bpage.add_dependency(media.thumb)

    def get_original_or_symlink(self):
        if not self.webgal.orig_symlink:
            return genfile.CopyMediaOriginal(self.webgal, self.media)
        else:
            return genfile.SymlinkMediaOriginal(self.webgal, self.media)

    def get_original(self):
        if not self.original:
            self.original = self.get_original_or_symlink()
        return self.original

    def get_browse_page(self, size_name):
        return genpage.WebalbumBrowsePage(self.webgal, size_name, self)

    def make(self):
        super().make()
        self.webgal.media_done()


class WebalbumImageTask(WebalbumMediaTask):
    """
    This task builds all items related to one picture.
    """

    def __init__(self, webgal, image):
        super().__init__(webgal, image)

        self.thumb = genmedia.ImageOtherSize(self.webgal, self.media,
                                             genmedia.THUMB_SIZE_NAME)
        self.add_dependency(self.thumb)

    def get_resized(self, size_name):
        if self.webgal.newsizers[size_name] == 'original':
            return self.get_original_or_symlink()
        else:
            sized = genmedia.ImageOtherSize(self.webgal, self.media, size_name)
            if sized.get_size() == sized.source_media.get_size():
                # Do not process if size is the same
                return self.get_original()
            else:
                return sized


class WebalbumVideoTask(WebalbumMediaTask):
    """
    This task builds all items related to one video.
    """

    def __init__(self, webgal, video):
        self.webvideo = None

        super().__init__(webgal, video)

        self.thumb = genmedia.VideoThumb(self.webgal, self.media,
                                         genmedia.THUMB_SIZE_NAME)
        self.add_dependency(self.thumb)

    def get_resized(self, size_name):
        if not self.webvideo:
            self.webvideo = genmedia.WebVideo(self.webgal, self.media,
                                              genmedia.VIDEO_SIZE_NAME,
                                              self.webgal.progress)

        return self.webvideo


class WebalbumDir(make.GroupTask):
    """
    This is a built web gallery with its files, thumbs and reduced pics.
    """

    def __init__(self, dir, subgals, album, album_dest_dir, progress=None):
        self.source_dir = dir
        self.path = os.path.join(album_dest_dir, self.source_dir.strip_root())
        if self.path.endswith(os.sep): self.path = os.path.dirname(self.path)

        super().__init__()

        self.progress = progress

        self.subgals = subgals
        self.album = album
        self.feed = None

        self.flattening_dir = None

        self.config = LazygalConfig()
        self.config.load(self.album.config)
        self.__configure()

        self.pindex = pindex.PersistentIndex(self)
        self.add_dependency(self.pindex)
        self.webassets = pindex.WebAssets(self)
        self.webassets.add_dependency(self.pindex)

    def populate_deps(self):
        super().populate_deps()

        for s in self.subgals:
            if not s.has_media_below():
                self.subgals.remove(s)

        self.medias = []
        self.sort_task = SubgalSort(self)
        self.sort_task.add_dependency(self.source_dir)
        for media in self.source_dir.medias:
            media.load_metadata(self.pindex)
            if self.tagfilters:
                # tag-filtering is requested
                res = True
                for tagf in self.tagfilters:
                    # concatenate the list of tags as a string of words,
                    # space-separated.  to ensure that we match the full
                    # keyword and not only a subpart of it, we also surround
                    # the matching pattern with spaces

                    # we look for tag words, partial matches are not wanted
                    regex = re.compile(r"\b" + tagf + r"\b")

                    kwlist = ' '.join(media.md['metadata']['keywords'])
                    if re.search(regex, kwlist) is None:
                        res = False
                        break
                if not res:
                    self.pindex.unpublish_media(media)
                    self.media_done()
                    continue

            self.sort_task.add_dependency(media)

            media_task = None
            if media.type == 'image':
                media_task = WebalbumImageTask(self, media)
            elif media.type == 'video':
                if not self.config.get('webgal', 'novideo'):
                    media_task = WebalbumVideoTask(self, media)
                else:
                    logging.warning(_('Video support disabled: ignoring %s.')
                                    % media.filename)
            else:
                raise NotImplementedError("Unknown media type '%s'"
                                          % media.type)

            if media_task:
                self.medias.append(media_task)
                self.add_dependency(media_task)

        self.index_pages = []

        if self.has_media_below() and not self.should_be_flattened():
            self.break_task = SubgalBreak(self)
            self.break_task.add_dependency(self.pindex)

            if self.thumbs_per_page > 0:
                self.break_task.add_dependency(self.sort_task)

            # This task is special because it populates dependencies. This is
            # why it needs to be built before a build check.
            self.break_task.make()

            self.webgal_pic = genmedia.WebalbumPicture(self)
            self.add_dependency(self.webgal_pic)
        else:
            self.break_task = None

        self.dirzip = None
        if self.config.get('webgal', 'dirzip') and len(self.medias) > 1:
            self.dirzip = genfile.WebalbumArchive(self)
            self.add_dependency(self.dirzip)
            self.webassets.add_dependency(self.dirzip)

    def __parse_browse_sizes(self, sizes_defs):
        fixed_sizes_defs = sizes_defs
        if isinstance(sizes_defs, dict):
            # simpler defs
            fixed_sizes_defs = []
            first = True
            for name, defs in sizes_defs.items():
                fixed_sizes_defs.append({
                    "name" : name,
                    "defs" : defs,
                    "default" : first,
                })
                first = False

        for size_defs in fixed_sizes_defs:
            name = size_defs['name']
            if name == genmedia.THUMB_SIZE_NAME:
                raise ValueError(_("Size name '%s' is reserved for internal processing.") % genmedia.THUMB_SIZE_NAME)
            self.__parse_size(name, size_defs['defs'])
            self.browse_sizes.append(name)
            if 'default' in size_defs and size_defs['default']:
                self.default_size_name = name

    def __parse_size(self, size_name, size_string):
        if size_string == '0x0':
            self.newsizers[size_name] = 'original'
        elif size_string in self.newsizers:
            pass # size reference, do nothing
        else:
            try:
                self.newsizers[size_name] = newsize.get_newsizer(size_string)
            except newsize.NewsizeStringParseError:
                raise ValueError(_("'%s' for size '%s' does not describe a known size syntax.") % (size_string, size_name, ))

    def __load_tpl_vars(self):
        # Load tpl vars from config
        tpl_vars = {}
        if self.config.has_section('template-vars'):
            tpl_vars = {}
            for option in self.config.options('template-vars'):
                value = self.config.get('template-vars', option)
                tpl_vars[option] = genshi.core.Markup(value)

        return tpl_vars

    def __configure(self):
        config_dirs = self.source_dir.parent_paths()[:-1]  # strip root dir
        config_dirs.reverse()  # from root to deepest
        config_files = list(map(lambda d: os.path.join(d, SOURCEDIR_CONFIGFILE),
                                config_dirs))
        logging.debug(_("  Trying loading gallery configs: %s"),
                      ', '.join(map(self.source_dir.strip_root,
                                    config_files)))
        for c in config_files:
            self.config.load_any(c)

        self.browse_sizes = []
        self.newsizers = {}
        self.__parse_browse_sizes(self.config.get('webgal', 'image-size'))
        self.__parse_size(genmedia.THUMB_SIZE_NAME,
                          self.config.get('webgal', 'thumbnail-size'))
        self.__parse_size(genmedia.VIDEO_SIZE_NAME,
                          self.config.get('webgal', 'video-size'))

        self.tpl_vars = self.__load_tpl_vars()
        styles = self.album.theme.get_avail_styles(
            self.config.get('webgal', 'default-style'))
        self.tpl_vars.update({'styles': styles})

        self.set_original(self.config.get('webgal', 'original'),
                          self.config.get('webgal', 'original-baseurl'),
                          self.config.get('webgal', 'original-symlink'))

        self.thumbs_per_page = self.config.get('webgal', 'thumbs-per-page')

        self.quality = self.config.get('webgal', 'jpeg-quality')
        self.save_options = {}
        if self.config.get('webgal', 'jpeg-optimize'):
            self.save_options['optimize'] = True
        if self.config.get('webgal', 'jpeg-progressive'):
            self.save_options['progressive'] = True

        self.pic_sort_by = self.config.get('webgal', 'sort-medias')
        self.subgal_sort_by = self.config.get('webgal', 'sort-subgals')
        self.tagfilters = self.config.get('webgal', 'filter-by-tag')

        self.webalbumpic_bg = self.config.get('webgal', 'webalbumpic-bg')
        self.webalbumpic_type = self.config.get('webgal', 'webalbumpic-type')
        try:
            self.webalbumpic_size = list(map(int, self.config.get('webgal', 'webalbumpic-size').split('x')))
            if len(self.webalbumpic_size) != 2:
                raise ValueError
        except ValueError:
            logging.error(_('Bad syntax for webalbumpic-size.'))
            sys.exit(1)
        self.keep_gps = self.config.get('webgal', 'keep-gps')

    def set_original(self, original=False, orig_base=None, orig_symlink=False):
        self.original = original or orig_symlink
        self.orig_symlink = orig_symlink
        if self.original and orig_base and not orig_symlink:
            self.orig_base = orig_base
        else:
            self.orig_base = None

    def get_webalbumpic_filename(self):
        if self.webalbumpic_bg == 'transparent':
            ext = '.png'  # JPEG does not have an alpha channel
        else:
            ext = '.jpg'
        return genmedia.WebalbumPicture.BASEFILENAME + ext

    def _add_size_qualifier(self, path, size_name, force_extension=None):
        filename, extension = os.path.splitext(path)
        if force_extension is not None:
            extension = force_extension

        if size_name == self.default_size_name and extension == '.html':
            # Do not append default size name to HTML page filename
            return path
        elif size_name in self.browse_sizes\
                and self.newsizers[size_name] == 'original'\
                and extension != '.html':
            # Do not append size_name to unresized images.
            return path
        else:
            return "%s_%s%s" % (filename, size_name, extension)

    def add_index_page(self, subgals, galleries):
        page_number = self.break_task.next_page_number()
        pages = []
        for size_name in self.browse_sizes:
            page = genpage.WebalbumIndexPage(self, size_name, page_number,
                                             subgals, galleries)
            if self.album.force_gen_pages:
                page.stamp_delete()
            self.add_dependency(page)
            pages.append(page)
        self.index_pages.append(pages)

    def register_output(self, output):
        # We only care about output in the current directory
        if os.path.dirname(output) == self.path:
            super().register_output(output)

    def register_feed(self, feed):
        self.feed = feed

    def get_subgal_count(self):
        if self.flatten_below():
            return 0
        else:
            return len(self.source_dir.subdirs)

    def get_all_subgals(self):
        all_subgals = list(self.subgals)  # We want a copy here.
        for subgal in self.subgals:
            all_subgals.extend(subgal.get_all_subgals())
        return all_subgals

    def has_media(self):
        if self.tagfilters:
            return True if self.medias else False
        else:
            return True if self.source_dir.medias else False

    def has_media_below(self):
        if self.has_media():
            return True
        for subgal in self.subgals:
            if subgal.has_media_below():
                return True
        return False

    def should_be_flattened(self, path=None):
        if path is None: path = self.source_dir.path
        return self.album.dir_flattening_depth is not False\
            and self.source_dir.get_album_level(path) > self.album.dir_flattening_depth

    def flatten_below(self):
        if self.album.dir_flattening_depth is False:
            return False
        elif self.source_dir.subdirs:
            # As all subdirs are at the same level, if one should be flattened,
            # all should.
            return self.subgals[0].should_be_flattened()
        else:
            return False

    def rel_path_to_src(self, target_srcdir_path):
        """
        Returns the relative path to go from this directory to
        target_srcdir_path.
        """
        return self.source_dir.rel_path(self.source_dir.path,
                                        target_srcdir_path)

    def rel_path(self, path):
        """
        Returns the relative path to go from this directory to the path
        supplied as argument.
        """
        return os.path.relpath(path, self.path)

    def flattening_srcpath(self, srcdir_path):
        """
        Returns the source path in which srcdir_path should flattened, that is
        the path of the gallery index that will point to srcdir_path's
        pictures.
        """
        if self.should_be_flattened(srcdir_path):
            cur_path = srcdir_path
            while self.should_be_flattened(cur_path):
                cur_path, dummy = os.path.split(cur_path)
            return cur_path
        else:
            return ''

    def list_foreign_files(self):
        self.call_populate_deps()

        if not os.path.isdir(self.path):
            return []

        foreign_files = []

        # Check dest for junk files
        extra_files = []
        if self.source_dir.is_album_root():
            extra_files.append(os.path.join(self.path,
                                            DEST_SHARED_DIRECTORY_NAME))

        dirnames = [d.source_dir.name for d in self.subgals]
        expected_dirs = list(map(lambda dn: os.path.join(self.path, dn),
                             dirnames))
        for dest_file in os.listdir(self.path):
            dest_file = os.path.join(self.path, dest_file)
            if dest_file not in self.output_items and\
                dest_file not in expected_dirs and\
                    dest_file not in extra_files:
                foreign_files.append(dest_file)

        return foreign_files

    def skip_media(self, src_media):
        return False

    def needs_build_quick(self):
        """
        Faster check if needs to be built.
        """
        if self._deps_populated or self.tagfilters:
            return self.needs_build()

        return self.pindex.needs_build()

    def build(self):
        for dest_file in self.list_foreign_files():
            self.album.cleanup(dest_file, self.path)

    def make(self, force=False):
        super().make(force)
        self.update_build_status()

    def media_done(self):
        if self.progress is not None:
            self.progress.media_done()


class SharedFiles(make.FileMakeObject):

    def __init__(self, album, dest_dir, tpl_vars):
        self.path = os.path.join(dest_dir, DEST_SHARED_DIRECTORY_NAME)
        self.album = album

        # Create the shared files directory if it does not exist
        if not os.path.isdir(self.path):
            logging.info(_("MKDIR %SHAREDDIR%"))
            logging.debug("(%s)", self.path)
            os.makedirs(self.path)

        super().__init__(self.path)

        self.expected_shared_files = []
        for shared in self.album.theme.shared_files:
            shared_file_dest = os.path.join(self.path, shared['dest'])

            if self.album.theme.tpl_loader.is_known_template_type(shared['source']):
                sf = genpage.SharedFileTemplate(album, shared['source'],
                                                shared_file_dest,
                                                tpl_vars)
                if self.album.force_gen_pages:
                    sf.stamp_delete()
            else:
                sf = genfile.SharedFileCopy(shared['source'], shared_file_dest)
            self.expected_shared_files.append(sf.path)

            self.add_dependency(sf)

    def build(self):
        # Cleanup themes files which are not in themes anymore.
        for present_file in os.listdir(self.path):
            file_path = os.path.join(self.path, present_file)
            if file_path not in self.expected_shared_files:
                self.album.cleanup(file_path, self.path)


class DummyProgress(object):

    def dir_done(self):                   pass
    def media_done(self, how_many=1):     pass
    def set_task_progress(self, percent): pass
    def set_task_done(self):              pass
    def updated(self):                    pass


class AlbumGenProgress(object):

    def __init__(self, dirs_total, medias_total):
        self._dirs_total = dirs_total
        self._dirs_done = 0

        self._medias_total = medias_total
        self._medias_done = 0

        self._task_percent = None

    def dir_done(self):
        self._dirs_done = self._dirs_done + 1
        self.updated()

    def media_done(self, how_many=1):
        self._medias_done = self._medias_done + how_many
        self.updated()

    def set_task_progress(self, percent):
        self._task_percent = percent
        self.updated()

    def set_task_done(self):
        self._task_percent = None
        self.updated()

    def __str__(self):
        msg = []

        if self._dirs_total > 0:
            msg.append("dir %d/%d (%d%%)" \
                  % (self._dirs_done, self._dirs_total,
                     100 * self._dirs_done // self._dirs_total,
                    ))

        if self._medias_total > 0:
            msg.append("media %d/%d (%d%%)" \
                       % (self._medias_done, self._medias_total,
                          100 * self._medias_done // self._medias_total,
                         ))

        if self._task_percent is not None:
            msg.append(_("current task %d%%") % self._task_percent)

        return _("Progress: %s") % ', '.join(msg)

    def updated(self):
        pass


class Album(object):

    def __init__(self, source_dir, config=None):
        self.source_dir = os.path.abspath(source_dir)

        self.config = LazygalConfig(load_defaults=True)

        logging.debug(_("Trying loading user config %s"), USER_CONFIG_PATH)
        self.config.load_any(USER_CONFIG_PATH)

        sourcedir_configfile = os.path.join(source_dir, SOURCEDIR_CONFIGFILE)
        if os.path.isfile(sourcedir_configfile):
            logging.debug(_("Loading root config %s"), sourcedir_configfile)
            try:
                self.config.load_any(sourcedir_configfile)
            except LazygalConfigDeprecated:
                logging.error(_("'%s' uses a deprecated syntax: please refer to lazygal.conf(5) manual page."), sourcedir_configfile)
                sys.exit(1)
        if config is not None:  # Supplied config
            self.config.load(config)

        if self.config.get('runtime', 'quiet'):
            logging.getLogger().setLevel(logging.ERROR)
        if self.config.get('runtime', 'debug'):
            logging.getLogger().setLevel(logging.DEBUG)
            GExiv2.log_set_level(GExiv2.LogLevel.INFO)

        self.clean_dest = self.config.get('global', 'clean-destination')
        self.preserves = (self.config.get('global', 'preserve') +
			 self.config.get('global', 'preserve_args'))
        self.force_gen_pages = self.config.get('global', 'force-gen-pages')

        self.set_theme(self.config.get('global', 'theme'))
        self.excludes = (self.config.get('global', 'exclude') +
                        self.config.get('global', 'exclude_args'))

        self.dir_flattening_depth = self.config.get('global', 'dir-flattening-depth')

        self.__statistics = None

    def set_theme(self, theme_name=theme.DEFAULT_THEME):
        self.theme = theme.Theme(os.path.join(DATAPATH, 'themes'), theme_name)
        self.theme.prepare_tpl_loader(tpl.TplFactory)
        self.theme.check_shared_files()

    def _str_humanize(self, text):
        dash_replaced = text.replace('_', ' ')
        return dash_replaced

    def is_in_sourcetree(self, path):
        return pathutils.is_subdir_of(self.source_dir, path)

    def cleanup(self, file_path, context_path):
        if self.clean_dest:
            # Do not delete something out of dest-dir.
            assert pathutils.is_subdir_of(context_path, file_path)
            tail = os.path.basename(file_path)
            for pattern in self.preserves:
                # Do not delete something the user wants to keep.
                if fnmatch.fnmatch(tail, pattern):
                    logging.info('  PRESERVE %s', tail)
                    return
            if os.path.isdir(file_path):
                shutil.rmtree(file_path)
            else:
                os.unlink(file_path)
            logging.info('  RM %s', file_path)

    def generate_default_metadata(self):
        """
        Generate default metadata files if no exists.
        """
        logging.debug(_("Generating metadata in %s"), self.source_dir)

        for root, dirnames, filenames in pathutils.walk(self.source_dir):
            filenames.sort()  # This is required for the ignored files
                              # checks to be reliable.
            source_dir = sourcetree.Directory(root, [], filenames, self)
            logging.info(_("[Entering %%ALBUMROOT%%/%s]"), source_dir.strip_root())
            logging.debug("(%s)", source_dir.path)

            metadata.DefaultMetadata(source_dir, self).make()

    def stats(self):
        if self.__statistics is None:
            self.__statistics = {
                'total' : 0,
                'bydir' : {}
            }
            for root, dirnames, filenames in pathutils.walk(self.source_dir):
                dir_medias = len([f for f in filenames\
                                  if sourcetree.MediaHandler.is_known_media(f, self)])
                self.__statistics['total'] = self.__statistics['total']\
                                             + dir_medias
                self.__statistics['bydir'][root] = dir_medias
        return self.__statistics

    def generate(self, dest_dir=None, progress=None):
        if dest_dir is None:
            dest_dir = self.config.get('global', 'output-directory')
        sane_dest_dir = os.path.abspath(os.path.expanduser(dest_dir))

        if not progress:
            progress = DummyProgress()

        pub_url = self.config.get('global', 'puburl')
        check_all_dirs = self.config.get('runtime', 'check-all-dirs')

        if self.is_in_sourcetree(sane_dest_dir):
            raise ValueError(_("Fatal error, web gallery directory is within source tree."))

        logging.debug(_("Generating to %s"), sane_dest_dir)

        if pub_url:
            feed = genpage.WebalbumFeed(self, sane_dest_dir, pub_url)
        else:
            feed = None

        dir_heap = {}
        for root, dirnames, filenames in pathutils.walk(self.source_dir):

            if root in dir_heap:
                subdirs, subgals = dir_heap[root]
                del dir_heap[root]  # No need to keep it there
            else:
                subdirs = []
                subgals = []

            checked_dir = sourcetree.File(root, self)

            if checked_dir.should_be_skipped():
                logging.debug(_("(%s) has been skipped"), checked_dir.path)
                continue
            if checked_dir.path == os.path.join(sane_dest_dir,
                                                DEST_SHARED_DIRECTORY_NAME):
                logging.error(_("(%s) has been skipped because its name collides with the shared material directory name"), checked_dir.path)
                continue

            logging.info(_("[Entering %%ALBUMROOT%%/%s]"), checked_dir.strip_root())
            logging.debug("(%s)", checked_dir.path)

            source_dir = sourcetree.Directory(root, subdirs, filenames, self)

            destgal = WebalbumDir(source_dir, subgals, self, sane_dest_dir,
                                  progress)

            if source_dir.is_album_root():
                # Use root config tpl vars for shared files
                tpl_vars = destgal.tpl_vars

            if not source_dir.is_album_root():
                container_dirname = os.path.dirname(root)
                if container_dirname not in dir_heap:
                    dir_heap[container_dirname] = ([], [])
                container_subdirs, container_subgals = dir_heap[container_dirname]
                container_subdirs.append(source_dir)
                container_subgals.append(destgal)

            if feed and source_dir.is_album_root():
                feed.set_title(source_dir.human_name)
                md = destgal.source_dir.metadata.get()
                if 'album_description' in md.keys():
                    feed.set_description(md['album_description'])
                destgal.register_output(feed.path)

            if feed:
                feed.push_dir(destgal)
                destgal.register_feed(feed)

            if check_all_dirs or destgal.needs_build_quick():
                destgal.make()
            else:
                progress.media_done(self.stats()['bydir'][destgal.source_dir.path])
                logging.info(_("  SKIPPED because of mtime, touch source or use --check-all-dirs to override."))

            # Force some memory cleanups, this is usefull for big albums.
            del destgal
            gc.collect()

            progress.dir_done()

            logging.info(_("[Leaving  %%ALBUMROOT%%/%s]"), source_dir.strip_root())

        if feed:
            feed.make()

        # Force to check for unexpected files
        SharedFiles(self, sane_dest_dir, tpl_vars).make(True)


# vim: ts=4 sw=4 expandtab
