#!/usr/bin/python3
#########################################################################################
#
# Description:  This script helps to build drivers in a Windows environment for PHP 7+ (32-bit/64-bit)
#
# Requirement:
#               python 3.x
#               PHP SDK and PHP Source 
#               Driver source code folder / GitHub repository
#               Visual Studio 2015 (PHP 7.0* and 7.1*) and Visual Studio 2017 (PHP 7.2*)
#
# Execution: Run with command line with required options.
# Examples: 
#           py builddrivers.py (for interactive mode)
#           py builddrivers.py --PHPVER=7.0.22 --ARCH=x64 --THREAD=nts --DRIVER=all --DEBUG
#
# Output: Build the drivers using PHP SDK. When running for local development, if build is unsuccessful, 
#         the log file will be launched for examination. Otherwise, the drivers will be renamed 
#         and copied to the designated location (if defined).
#
#############################################################################################

import sys
import shutil
import os.path
import argparse
import subprocess
from buildtools import BuildUtil

class BuildDriver(object):
    """Build sqlsrv and/or pdo_sqlsrv drivers with PHP source with the following properties:
    
    Attributes:
        util            # BuildUtil object whose constructor takes phpver, driver, arch, thread, debug 
        repo            # GitHub repository
        branch          # GitHub repository branch
        dest_path       # alternative destination for the drivers (None for development builds)
        rebuild         # a boolean flag - whether the user is rebuilding
        make_clean      # a boolean flag - whether make clean is necessary
        source_path     # path to a local source folder
        testing         # whether the user has turned on testing mode
    """
    
    def __init__(self, phpver, driver, arch, thread, debug, repo, branch, source, path, testing, no_rename):
        self.util = BuildUtil(phpver, driver, arch, thread, no_rename, debug)
        self.repo = repo
        self.branch = branch
        self.source_path = source
        self.dest_path = path
        self.testing = testing
        self.rebuild = False
        self.make_clean = False
    
    def show_config(self):
        print()
        print('PHP Version: ', self.util.phpver)
        print('Arch: ', self.util.arch)
        print('Thread: ', self.util.thread)
        print('Driver: ', self.util.driver) 
        print('Source: ', self.source_path)
        print('Debug enabled: ', self.util.debug_enabled) 
        print()

    def clean_or_remove(self, root_dir, work_dir):
        """Only check this for local development and not rebuilding. If the php source directory 
        already exists, this will prompt user whether to rebuild, clean, or superclean, the last option
        will remove the entire php source directory.
        
        :param  root_dir: the C:\ drive
        :param  work_dir: the directory of this script
        :outcome: the old binaries, if exist, will be removed
        """
        phpsrc = self.util.phpsrc_root(root_dir)
        if os.path.exists( phpsrc ):
            print(phpsrc + " exists.")
            build_choice = validate_input("(r)ebuild for the same configuration, (c)lean otherwise, (s)uperclean if unsure ", "r/c/s")
            self.make_clean = False
            if build_choice == 'r':
                print('Will rebuild the binaries')
                # only the old binaries based on the current configuration will be removed
                self.util.remove_prev_build(root_dir)
            elif build_choice == 'c':
                print('Will make clean')
                self.make_clean = True
                # all old builds are removed, and this step is necessary because 
                # the user might have changed the configuration
                self.util.remove_old_builds(root_dir)
            else:
                print('Will remove ' + phpsrc)
                os.system('RMDIR /s /q ' + phpsrc)
                
            os.chdir(work_dir)  # change back to the working directory

    def get_local_source(self, source_path):
        """This assumes interactive mode (not testing) and takes care of getting 
        the user's input to the path of the local source files for the drivers
        """
        while True:
            if source_path is None:
                source = input('Enter the full path to the source folder: ')
            else:
                source = input("Hit ENTER to use '" + source_path + "' or provide another path to the source folder: ")
                if len(source) == 0:
                    source = source_path

            valid = True
            if os.path.exists(source) and os.path.exists(os.path.join(source, 'shared')):
                # Checking the existence of 'shared' folder only, assuming
                # sqlsrv and/or pdo_sqlsrv are also present if it exists
                self.source_path = source
                break
                
            print("The path provided is invalid. Please re-enter.")
        return source
    
    def build_extensions(self, root_dir, logfile):
        """This takes care of getting the drivers' source files, building the drivers. 
        If dest_path is defined, the binaries will be copied to the designated destinations.
        
        :param  root_dir: the root directory
        :param  logfile: the name of the logfile
        :outcome: the drivers and symbols will renamed and placed in the appropriate location(s)

        """
        work_dir = os.path.dirname(os.path.realpath(__file__))
        
        get_source = False if self.source_path is None else True
        if self.repo is None or self.branch is None:
            # If GitHub repo or branch is None, get the source locally 
            get_source = True

        if not get_source:
            # This will download from the specified branch on GitHub repo and copy the source
            self.util.download_msphpsql_source(repo, branch)
        else:
            source = self.source_path 
            # Do not prompt user for input if it's in a testing mode 
            if not self.testing:
                source = self.get_local_source(self.source_path)
            
            print('Copying source files from', source)
                
            os.system('ROBOCOPY ' + source + '\shared ' + work_dir + '\Source\shared /xx /xo ')
            os.system('ROBOCOPY ' + source + '\sqlsrv ' + work_dir + '\Source\sqlsrv /xx /xo ')
            os.system('ROBOCOPY ' + source + '\pdo_sqlsrv ' + work_dir + '\Source\pdo_sqlsrv /xx /xo ')
                    
        print('Start building PHP with the extension...')

        # If not testing, dest should be the root drive. Otherwise, dest should be None. 
        dest = None if self.testing else root_dir

        # ext_dir is the directory where we can find the built extension(s)
        ext_dir = self.util.build_drivers(self.make_clean, dest, logfile)

        # Copy the binaries if a destination path is defined
        if self.dest_path is not None:
            dest_drivers = os.path.join(self.dest_path, self.util.major_version(), self.util.arch)
            dest_symbols = os.path.join(dest_drivers, 'Symbols', self.util.thread)
            
            # All intermediate directories will be created in order to create the leaf directory
            if os.path.exists(dest_symbols) == False:
                os.makedirs(dest_symbols)
                
            # Now copy all the binaries
            if self.util.driver == 'all':
                self.util.copy_binary(ext_dir, dest_drivers, 'sqlsrv', '.dll')
                self.util.copy_binary(ext_dir, dest_symbols, 'sqlsrv', '.pdb')
                self.util.copy_binary(ext_dir, dest_drivers, 'pdo_sqlsrv', '.dll')
                self.util.copy_binary(ext_dir, dest_symbols, 'pdo_sqlsrv', '.pdb')
            else:
                self.util.copy_binary(ext_dir, dest_drivers, self.util.driver, '.dll')
                self.util.copy_binary(ext_dir, dest_symbols, self.util.driver, '.pdb')

        return ext_dir

    def build(self):
        """This is the main entry point of building drivers for PHP. 
        For development, this will loop till the user decides to quit.
       
        """
        self.show_config()
    
        work_dir = os.path.dirname(os.path.realpath(__file__))
        root_dir = 'C:' + os.sep
        
        quit = False
        while not quit:
            if self.testing:
                self.make_clean = True
                self.util.remove_old_builds(work_dir)
            elif not self.rebuild: 
                self.clean_or_remove(root_dir, work_dir)
                
            logfile = self.util.get_logfile_name()

            try:
                ext_dir = self.build_extensions(root_dir, logfile)
                print('Build Completed')
            except:
                print('Something went wrong, launching log file', logfile)
                # display log file only when not testing
                if not self.testing:
                    os.startfile(os.path.join(root_dir, 'php-sdk', logfile))
                os.chdir(work_dir)
                exit(1)

            if not self.testing:
                choice = input("Rebuild using the same configuration(yes) or quit (no) [yes/no]: ")
                choice = choice.lower()
                if choice == 'yes' or choice == 'y' or choice == '':
                    print('Rebuilding drivers...')
                    self.make_clean = False
                    self.rebuild = True
                    self.util.remove_prev_build(root_dir)
                else:
                    quit = True
            else:
                quit = True
            
            os.chdir(work_dir)    

def validate_input(question, values):
    """Return the user selected value, and it must be valid based on *values*."""
    while True:
        options = values.split('/') 
        prompt = '[' + values + ']'
        value = input(question + prompt + ': ')
        value = value.lower()
        if not value in options:
            print("An invalid choice is entered. Choose from", prompt)
        else:
            break
    return value

################################### Main Function ###################################
if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--PHPVER', help="PHP version, e.g. 7.4.* etc.")
    parser.add_argument('--ARCH', choices=['x64', 'x86'])
    parser.add_argument('--THREAD', choices=['nts', 'ts'])
    parser.add_argument('--DRIVER', default='all', choices=['all', 'sqlsrv', 'pdo_sqlsrv'], help="driver to build (default: all)")
    parser.add_argument('--DEBUG', action='store_true', help="enable debug mode (default: False)")
    parser.add_argument('--REPO', default='Microsoft', help="GitHub repository (default: Microsoft)")
    parser.add_argument('--BRANCH', default='dev', help="GitHub repository branch or tag (default: dev)")
    parser.add_argument('--SOURCE', default=None, help="a local path to source file (default: None)")
    parser.add_argument('--TESTING', action='store_true', help="turns on testing mode (default: False)")
    parser.add_argument('--DESTPATH', default=None, help="an alternative destination for the drivers (default: None)")
    parser.add_argument('--NO_RENAME', action='store_true', help="drivers will not be renamed(default: False)")

    args = parser.parse_args()

    phpver = args.PHPVER
    arch = args.ARCH
    thread = args.THREAD
    driver = args.DRIVER
    debug = args.DEBUG
    repo = args.REPO
    branch = args.BRANCH
    source = args.SOURCE
    path = args.DESTPATH
    testing = args.TESTING
    no_rename = args.NO_RENAME

    if phpver is None:
        # starts interactive mode, testing mode is False
        # will not prompt for drivers' destination path, which is None by default
        while True:
            # perform some minimal checks
            phpver = input("PHP Version (e.g. 7.1.* or 7.2.*): ")
            if phpver == '':
                print('Empty PHP version entered! Please try again.')
            elif phpver[0] < '7':
                print('Only PHP 7.0 or above is supported. Please try again.')
            else:
                break
                
        arch_version = input("64-bit? [y/n]: ")
        thread = validate_input("Thread safe? ", "nts/ts")
        driver = validate_input("Driver to build? ", "all/sqlsrv/pdo_sqlsrv")
        debug_mode = input("Debug enabled? [y/n]: ")
        
        answer = input("Download source from a GitHub repo? [y/n]: ")
        if answer == 'yes' or answer == 'y' or answer == '':
            repo = input("Name of the repo (hit enter for 'Microsoft'): ")
            branch = input("Name of the branch or tag (hit enter for 'dev'): ")
            if repo == '':
                repo = 'Microsoft'
            if branch == '':
                branch = 'dev'
        else:
            repo = branch = None

        arch_version = arch_version.lower()
        arch = 'x64' if arch_version == 'yes' or arch_version == 'y' or arch_version == '' else 'x86'
        
        debug_mode = debug_mode.lower()
        debug = debug_mode == 'yes' or debug_mode == 'y' or debug_mode == '' 
        
    builder = BuildDriver(phpver, 
                          driver, 
                          arch, 
                          thread, 
                          debug, 
                          repo, 
                          branch, 
                          source, 
                          path,
                          testing,
                          no_rename)

    builder.build()
