# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4

###############################################################################
# OpenLP - Open Source Lyrics Projection                                      #
# --------------------------------------------------------------------------- #
# Copyright (c) 2004-2016 OpenLP Developers                                   #
# --------------------------------------------------------------------------- #
# 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 of the License.                              #
#                                                                             #
# 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., 59  #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
###############################################################################

"""
Windows Build Script
--------------------

This script is used to build the Windows binary and the accompanying installer.
For this script to work out of the box, it depends on a number of things:

Python 3.4

PyQt5
    You should already have this installed, OpenLP doesn't work without it. The
    version the script expects is the packaged one available from River Bank
    Computing.

PyEnchant
    This script expects the precompiled, installable version of PyEnchant to be
    installed. You can find this on the PyEnchant site.

Inno Setup 5
    Inno Setup should be installed into "C:\%PROGRAMFILES%\Inno Setup 5"

Sphinx
    This is used to build the documentation.  The documentation trunk must be at
    the same directory level as OpenLP trunk and named "documentation".

HTML Help Workshop
    This is used to create the help file.

PyInstaller
    PyInstaller should be a git clone of
    https://github.com/matysek/pyinstaller branch develop

Bazaar
    You need the command line "bzr" client installed.

OpenLP
    A checkout of the latest code, in a branch directory, which is in a Bazaar
    shared repository directory. This means your code should be in a directory
    structure like this: "openlp\branch-name".

Visual C++ 2008 Express Edition
    This is to build pptviewlib.dll, the library for controlling the
    PowerPointViewer.

windows-builder.py
    This script, of course. It should be in the "windows-installer" directory
    at the same level as OpenLP trunk.

psvince.dll
    This dll is used during the actual install of OpenLP to check if OpenLP is
    running on the users machine prior to the setup.  If OpenLP is running,
    the install will fail.  The dll can be obtained from here:

        http://www.vincenzo.net/isxkb/index.php?title=PSVince)

    The dll is presently included with this script.

Mako
    Mako Templates for Python.  This package is required for building the
    remote plugin.  It can be installed by going to your
    python_directory\scripts\.. and running "easy_install Mako".  If you do not
    have easy_install, the Mako package can be obtained here:

        http://www.makotemplates.org/download.html

MuPDF
    Required for PDF support in OpenLP. Download the windows build from
    mupdf.com, extract it, and set the mutoolbin option in the config file to
    point to mutool.exe.

MediaInfo
    Required for the media plugin. Download the 32-bit CLI windows build from
    https://mediaarea.net/nn/MediaInfo/Download/Windows and set the
    mediainfobin option in the config file to point to MediaInfo.exe.

Portable App Builds
    The following are required if you are planning to make a portable build of
    OpenLP.  The portable build conforms to the standards published by
    PortableApps.com:

        http://portableapps.com/development/portableapps.com_format

    PortableApps.com Installer:

        http://portableapps.com/apps/development/portableapps.com_installer

    PortableApps.com Launcher:

        http://portableapps.com/apps/development/portableapps.com_launcher

    NSIS Portable (Unicode version):

        http://portableapps.com/apps/development/nsis_portable
"""

import os
import sys
from shutil import copy, rmtree, move
from distutils import dir_util
from subprocess import Popen, PIPE
from configparser import ConfigParser
from argparse import ArgumentParser

from builder import Builder


class WindowsBuilder(Builder):
    """
    The :class:`WindowsBuilder` class encapsulates everything that is needed
    to build a Windows installer.
    """
    def get_platform(self):
        """
        Return the platform we're building for
        """
        return 'Windows'

    def get_config_defaults(self):
        """
        Build some default values for the config file
        """
        config_defaults = super().get_config_defaults()
        config_defaults.update({
            'pyroot': self.python_root,
            'progfiles': self.program_files,
            'sitepackages': self.site_packages,
            'projects': os.path.abspath(os.path.join(self.script_path, '..', '..'))
        })
        return config_defaults

    def get_sphinx_build(self):
        """
        Tell Sphinx we want to build HTML help
        """
        return "htmlhelp"

    def add_extra_args(self, parser):
        """
        Add extra arguments to the command line argument parser
        """
        parser.add_argument('--portable', metavar='PATH', default=None,
                            help='Specify the path to build the portable installation.')

    def setup_system_paths(self):
        """
        Set up some system paths.
        """
        super().setup_system_paths()
        self.python_root = os.path.dirname(self.python)
        self.site_packages = os.path.join(self.python_root, 'Lib', 'site-packages')
        self.program_files = os.getenv('PROGRAMFILES')

    def setup_paths(self):
        """
        Set up a variety of paths that we use throughout the build process.
        """
        self.portable_path = None
        super().setup_paths()
        self.dist_path = os.path.join(self.work_path, 'dist', 'OpenLP')
        self.helpfile_path = os.path.join(self.manual_build_path, 'htmlhelp')
        self.winres_path = os.path.join(self.branch_path, 'resources', 'windows')
        self.pptviewlib_path = os.path.join(self.source_path, 'plugins', 'presentations', 'lib', 'pptviewlib')
        if self.args.portable:
            self.portable_path = os.path.abspath(self.args.portable)

    def copy_extra_files(self):
        """
        Copy all the Windows-specific files.
        """
        self._print('Copying extra files for Windows...')
        self._print_verbose('... OpenLP.ico')
        copy(os.path.join(self.script_path, 'OpenLP.ico'), os.path.join(self.dist_path, 'OpenLP.ico'))
        self._print_verbose('... LICENSE.txt')
        copy(os.path.join(self.script_path, 'LICENSE.txt'), os.path.join(self.dist_path, 'LICENSE.txt'))
        self._print_verbose('... psvince.dll')
        copy(self.psvince, os.path.join(self.dist_path, 'psvince.dll'))
        if os.path.isfile(os.path.join(self.helpfile_path, 'OpenLP.chm')):
            self._print_verbose('... OpenLP.chm')
            copy(os.path.join(self.helpfile_path, 'OpenLP.chm'), os.path.join(self.dist_path, 'OpenLP.chm'))
        else:
            self._print('... WARNING: Windows help file not found')
        self._print_verbose('... mutool.exe')
        if self.mutoolbin and os.path.isfile(self.mutoolbin):
            copy(os.path.join(self.mutoolbin), os.path.join(self.dist_path, 'mutool.exe'))
        else:
            self._print('... WARNING: mutool.exe not found')
        self._print_verbose('... MediaInfo.exe')
        if self.mediainfo_bin and os.path.isfile(self.mediainfo_bin):
            copy(os.path.join(self.mediainfo_bin), os.path.join(self.dist_path, 'MediaInfo.exe'))
        else:
            self._print('... WARNING: MediaInfo.exe not found')

    def after_run_sphinx(self):
        """
        Run HTML Help Workshop to convert the Sphinx output into a manual.
        """
        self._print('Running HTML Help Workshop...')
        os.chdir(os.path.join(self.manual_build_path, 'htmlhelp'))
        self._run_command([self.htmlhelp, 'OpenLP.chm'], 'Error running HTML Help Workshop')

    def build_package(self):
        """
        Build the installer
        """
        self.build_pptviewlib()
        self.create_innosetup_file()
        self.run_innosetup()
        if self.portable_path and os.path.exists(self.portable_path):
            self.run_portableapp_builder()

    def build_pptviewlib(self):
        """
        Build the PowerPoint Viewer DLL using Visual Studio.
        """
        self._print('Building PPTVIEWLIB.DLL...')
        if not os.path.exists(self.vcbuild_exe):
            self._print('... WARNING: vcbuild.exe was not found, skipping building pptviewlib.dll')
            return
        self._run_command([self.vcbuild, '/rebuild', os.path.join(self.pptviewlib_path, 'pptviewlib.vcproj'),
                         'Release|Win32'], 'Error building pptviewlib.dll')
        copy(os.path.join(self.pptviewlib_path, 'Release', 'pptviewlib.dll'), self.pptviewlib_path)

    def create_innosetup_file(self):
        """
        Create an InnoSetup file pointing to the branch being built.
        """
        self._print('Creating Inno Setup file...')
        with open(os.path.join(self.script_path, 'OpenLP.iss.default'), 'r') as input_file, \
                open(os.path.join(self.script_path, 'OpenLP.iss'), 'w') as output_file:
            content = input_file.read()
            content = content.replace('%(branch)s', self.branch_path)
            content = content.replace('%(display_version)s', self.version)
            output_file.write(content)

    def run_innosetup(self):
        """
        Run InnoSetup to create an installer.
        """
        self._print('Running Inno Setup...')
        os.chdir(self.script_path)
        self._run_command([self.innosetup_exe, os.path.join(self.script_path, 'OpenLP.iss'), '/q'],
                          'Error running InnoSetup')

    def create_portableapp_directory(self):
        """
        Checks the PortableApp directory structure amd creates
        missing subdirs
        """
        self._print('  Checking PortableApps directory structure...')
        launcher_path = os.path.join(self.portable_path, 'App', 'Appinfo', 'Launcher')
        if not os.path.exists(launcher_path):
            os.makedirs(launcher_path)
        settings_path = os.path.join(self.portable_path, 'Data', 'Settings')
        if not os.path.exists(settings_path):
            os.makedirs(settings_path)

    def create_portableapps_appinfo_file(self):
        """
        Create a Portabbleapps appinfo.ini file.
        """
        self._print('  Creating PortableApps appinfo file ...')
        portable_version = self.version.replace('-', '.') + '.0' * (3 - self.version.count('.'))
        with open(os.path.join(self.script_path, 'appinfo.ini.default'), 'r') as input_file, \
                open(os.path.join(self.portable_path, 'App', 'Appinfo', 'appinfo.ini'), 'w') as output_file:
            content = input_file.read()
            content = content.replace('%(display_version)s', self.version)
            content = content.replace('%(package_version)s', portable_version)
            output_file.write(content)

    def run_portableapp_builder(self):
        """
        Creates a portable installer.
        1  Copies the distribution to the portable apps directory
        2  Builds the PortableApps Launcher
        3  Builds the PortableApps Install
        """
        self._print('Running PortableApps Builder...')
        self._print('  Clearing old files')
        # Remove previous contents of portableapp build directory.
        if os.path.exists(self.portable_path):
            rmtree(self.portable_path)
        self._print('  Creating PortableApps build directory')
        # Copy the contents of the OpenLPPortable directory to the portable
        # build directory.
        dir_util.copy_tree(os.path.join(self.script_path, 'OpenLPPortable'), self.portable_path)
        self.check_portableapp_directory()
        self.create_portableapps_appinfo_file()
        # Copy distribution files to portableapp build directory.
        self._print('  Copying distribution files')
        portable_app_path = os.path.join(self.portable_path, 'App', 'OpenLP')
        dir_util.copy_tree(self.dist_path, portable_app_path)
        # Copy help files to portableapp build directory.
        if os.path.isfile(os.path.join(self.helpfile_path, 'OpenLP.chm')):
            self._print('  Copying help files')
            dir_util.copy_tree(self.helpfile_path, os.path.join(portable_app_path, 'help'))
        else:
            self._print('... WARNING: Windows help file not found')
        # Build the launcher.
        self._print('  Building PortableApps Launcher')
        portableapps = Popen((self.portablelauncher, self.portable_path), stdout=PIPE)
        code = portableapps.wait()
        if code != 0:
            raise Exception('Error creating PortableAppa Launcher')
        # Build the portable installer.
        self._print('  Building PortableApps Installer')
        self._run_command([self.portableinstaller, self.portable_path], 'Error running PortableApps Installer')
        portable_app = os.path.abspath(os.path.join(self.portable_path, '..',
                                                    'OpenLPPortable_%s.paf.exe' % self.version.replace('-', '.')))
        if os.path.exists(portable_app):
            move(portable_app, os.path.abspath(os.path.join(self.dist_path, '..')))
            self._print('  PortableApp build complete')
        else:
            raise Exception('PortableApp failed to build')

    def main(self):
        """
        The main function to run the Windows builder.
        """
        self._print_verbose('OpenLP main script: ......%s', self.openlp_script)
        self._print_verbose('Script path: .............%s', os.path.dirname(os.path.abspath(__file__)))
        self._print_verbose('Branch path: .............%s', self.branch_path)
        self._print_verbose('Source path: .............%s', self.source_path)
        self._print_verbose('Dist path: ...............%s', self.dist_path)
        self._print_verbose('Portable path: ...........%s', self.portable_path)
        self._print_verbose('PyInstaller: .............%s', self.pyinstaller)
        self._print_verbose('Documentation branch path:%s', self.documentation_path)
        self._print_verbose('Help file build path: ....%s', self.helpfile_path)
        self._print_verbose('Inno Setup path: .........%s', self.innosetup)
        self._print_verbose('PortableApp Launcher......%s', self.portablelauncher)
        self._print_verbose('PortableApp Installer.....%s', self.portableinstaller)
        self._print_verbose('Windows resources: .......%s', self.winres_path)
        self._print_verbose('VCBuild path: ............%s', self.vcbuild)
        self._print_verbose('PPTVIEWLIB path: .........%s', self.pptviewlib_path)
        self._print_verbose('Mutool binary ............%s', self.mutoolbin)
        self._print_verbose('')
        if not self.args.skip_update:
            self.update_code()
        self.build_pptviewlib()
        self.run_pyinstaller()
        self.write_version_file()
        self.copy_default_theme()
        self.copy_plugins()
        self.copy_media_player()
        if os.path.exists(self.manual_path):
            self.run_sphinx()
            self.run_htmlhelp()
        else:
            self._print('')
            self._print('WARNING: Documentation trunk not found. Windows')
            self._print('         Help file will not be included in build')
            self._print('')
        self.copy_windows_files()
        if not self.args.skip_translations:
            self.update_translations()
        self.compile_translations()
        self.create_innosetup_file()
        self.run_innosetup()
        if self.args.portable:
            self.run_portableapp_builder()
        self._print('Done.')


if __name__ == '__main__':
    WindowsBuilder().main()
