#! /usr/bin/env python3
import argparse
import os
import sys
import shutil


def stdout(msg):
    sys.stdout.write(msg + '\n')


class DirHelper:
    def __init__(self, is_dir, list_dir, walk, rmtree):
        self.is_dir = is_dir
        self.list_dir = list_dir
        self.walk = walk
        self.rmtree = rmtree


class FileSystemHelper:
    def __init__(self, open_, path_join, move, exists):
        self.open_ = open_
        self.path_join = path_join
        self.move = move
        self.exists = exists


class Replacer:
    """Encapsulates a simple text replace"""

    def __init__(self, from_, to):
        self.from_ = from_
        self.to = to

    def process(self, text):
        return text.replace(self.from_, self.to)


class FileHandler:
    """Applies a series of replacements the contents of a file inplace"""

    def __init__(self, name, replacers, opener):
        self.name = name
        self.replacers = replacers
        self.opener = opener

    def process(self):
        text = self.opener(self.name, 'r').read()

        for replacer in self.replacers:
            text = replacer.process(text)

        self.opener(self.name, 'w').write(text)


class Remover:
    def __init__(self, exists, remove):
        self.exists = exists
        self.remove = remove

    def __call__(self, name):
        if self.exists(name):
            self.remove(name)


class ForceRename:
    def __init__(self, renamer, remove):
        self.renamer = renamer
        self.remove = remove

    def __call__(self, from_, to):
        self.remove(to)
        self.renamer(from_, to)


class VerboseRename:
    def __init__(self, renamer, stream):
        self.renamer = renamer
        self.stream = stream

    def __call__(self, from_, to):
        self.stream.write(
            "Renaming directory '%s' -> '%s'\n"
            % (os.path.basename(from_), os.path.basename(to))
        )

        self.renamer(from_, to)


class DirectoryHandler:
    """Encapsulates renaming a directory by removing its first character"""

    def __init__(self, name, root, renamer):
        self.name = name
        self.new_name = name[1:]
        self.root = str(root) + os.sep
        self.renamer = renamer

    def path(self):
        return os.path.join(self.root, self.name)

    def relative_path(self, directory, filename):
        path = directory.replace(self.root, '', 1)
        return os.path.join(path, filename)

    def new_relative_path(self, directory, filename):
        path = self.relative_path(directory, filename)
        return path.replace(self.name, self.new_name, 1)

    def process(self):
        from_ = os.path.join(self.root, self.name)
        to = os.path.join(self.root, self.new_name)
        self.renamer(from_, to)


class HandlerFactory:
    def create_file_handler(self, name, replacers, opener):
        return FileHandler(name, replacers, opener)

    def create_dir_handler(self, name, root, renamer):
        return DirectoryHandler(name, root, renamer)


class OperationsFactory:
    def create_force_rename(self, renamer, remover):
        return ForceRename(renamer, remover)

    def create_verbose_rename(self, renamer, stream):
        return VerboseRename(renamer, stream)

    def create_replacer(self, from_, to):
        return Replacer(from_, to)

    def create_remover(self, exists, remove):
        return Remover(exists, remove)


class Layout:
    """
    Applies a set of operations which result in the layout
    of a directory changing
    """

    def __init__(self, directory_handlers, file_handlers):
        self.directory_handlers = directory_handlers
        self.file_handlers = file_handlers

    def process(self):
        for handler in self.file_handlers:
            handler.process()

        for handler in self.directory_handlers:
            handler.process()


class NullLayout:
    """
    Layout class that does nothing when asked to process
    """

    def process(self):
        pass


class LayoutFactory:
    """Creates a layout object"""

    def __init__(
        self,
        operations_factory,
        handler_factory,
        file_helper,
        dir_helper,
        verbose,
        stream,
        force,
    ):
        self.operations_factory = operations_factory
        self.handler_factory = handler_factory

        self.file_helper = file_helper
        self.dir_helper = dir_helper

        self.verbose = verbose
        self.output_stream = stream
        self.force = force

    def create_layout(self, path):
        contents = self.dir_helper.list_dir(path)

        renamer = self.file_helper.move

        if self.force:
            remove = self.operations_factory.create_remover(
                self.file_helper.exists, self.dir_helper.rmtree
            )
            renamer = self.operations_factory.create_force_rename(renamer, remove)

        if self.verbose:
            renamer = self.operations_factory.create_verbose_rename(
                renamer, self.output_stream
            )

        # Build list of directories to process
        directories = [d for d in contents if self.is_underscore_dir(path, d)]
        underscore_directories = [
            self.handler_factory.create_dir_handler(d, path, renamer)
            for d in directories
        ]

        if not underscore_directories:
            if self.verbose:
                self.output_stream.write(
                    'No top level directories starting with an underscore '
                    "were found in '%s'\n" % path
                )
            return NullLayout()

        # Build list of files that are in those directories
        replacers = []
        for handler in underscore_directories:
            for directory, _, files in self.dir_helper.walk(handler.path()):
                for f in files:
                    replacers.append(
                        self.operations_factory.create_replacer(
                            handler.relative_path(directory, f),
                            handler.new_relative_path(directory, f),
                        )
                    )

        # Build list of handlers to process all files
        filelist = []
        for root, _, files in self.dir_helper.walk(path):
            for f in files:
                if f.endswith('.html'):
                    filelist.append(
                        self.handler_factory.create_file_handler(
                            self.file_helper.path_join(root, f),
                            replacers,
                            self.file_helper.open_,
                        )
                    )
                if f.endswith('.js'):
                    filelist.append(
                        self.handler_factory.create_file_handler(
                            self.file_helper.path_join(root, f),
                            [
                                self.operations_factory.create_replacer(
                                    "'_sources/'", "'sources/'"
                                )
                            ],
                            self.file_helper.open_,
                        )
                    )

        return Layout(underscore_directories, filelist)

    def is_underscore_dir(self, path, directory):
        return self.dir_helper.is_dir(
            self.file_helper.path_join(path, directory)
        ) and directory.startswith('_')


def sphinx_extension(app, exception):
    """Wrapped up as a Sphinx Extension"""

    if app.builder.name not in ('html', 'dirhtml'):
        return

    if not app.config.sphinx_to_github:
        if app.config.sphinx_to_github_verbose:
            stdout('Sphinx-to-github: Disabled, doing nothing.')
        return

    if exception:
        if app.config.sphinx_to_github_verbose:
            msg = 'Sphinx-to-github: ' 'Exception raised in main build, doing nothing.'
            stdout(msg)
        return

    dir_helper = DirHelper(os.path.isdir, os.listdir, os.walk, shutil.rmtree)

    file_helper = FileSystemHelper(
        lambda f, mode: open(f, mode, encoding=app.config.sphinx_to_github_encoding),
        os.path.join,
        shutil.move,
        os.path.exists,
    )

    operations_factory = OperationsFactory()
    handler_factory = HandlerFactory()

    layout_factory = LayoutFactory(
        operations_factory,
        handler_factory,
        file_helper,
        dir_helper,
        app.config.sphinx_to_github_verbose,
        sys.stdout,
        force=True,
    )

    layout = layout_factory.create_layout(app.outdir)
    layout.process()


def setup(app):
    """Setup function for Sphinx Extension"""

    app.add_config_value('sphinx_to_github', True, '')
    app.add_config_value('sphinx_to_github_verbose', True, '')
    app.add_config_value('sphinx_to_github_encoding', 'utf-8', '')

    app.connect('build-finished', sphinx_extension)


def main(args):
    parser = argparse.ArgumentParser(
        description='Process Sphinx HTML directory for GitHub Pages compatibility.'
    )
    parser.add_argument(
        '-v',
        '--verbose',
        action='store_true',
        default=False,
        help='Provides verbose output',
    )
    parser.add_argument(
        '-e',
        '--encoding',
        action='store',
        default='utf-8',
        help='Encoding for reading and writing files',
    )
    parser.add_argument('path', help='Path to the HTML directory')
    args = parser.parse_args()
    path = args.path

    dir_helper = DirHelper(os.path.isdir, os.listdir, os.walk, shutil.rmtree)

    file_helper = FileSystemHelper(
        lambda f, mode: open(f, mode, encoding=args.encoding),
        os.path.join,
        shutil.move,
        os.path.exists,
    )

    operations_factory = OperationsFactory()
    handler_factory = HandlerFactory()

    layout_factory = LayoutFactory(
        operations_factory,
        handler_factory,
        file_helper,
        dir_helper,
        args.verbose,
        sys.stdout,
        force=False,
    )

    layout = layout_factory.create_layout(path)
    layout.process()


if __name__ == '__main__':
    main(sys.argv[1:])
