import argparse
import hashlib
import os
import shutil
import subprocess

from pandac.PandaModules import *
import pytz


parser = argparse.ArgumentParser()
parser.add_argument('--distribution', default='en',
                    help='The distribution token.')
parser.add_argument('--build-dir', default='build',
                    help='The directory in which to store the build files.')
parser.add_argument('--src-dir', default='..',
                    help='The directory of the Toontown Infinite source code.')
parser.add_argument('--server-ver', default='tti-REVISION',
                    help='The server version of this build.\n'
                         'REVISION tokens will be replaced with the current Git revision string.')
parser.add_argument('--build-mfs', action='store_true',
                    help='When present, multifiles will be built.')
parser.add_argument('--resources-dir', default='../resources',
                    help='The directory of the Toontown Infinite resources.')
parser.add_argument('modules', nargs='*', default=['shared', 'infinite'],
                    help='The Toontown Infinite modules to be included in the build.')
args = parser.parse_args()

print 'Preparing the client...'

# Create a clean build directory for us to store our build material:
if not os.path.exists(args.build_dir):
    os.mkdir(args.build_dir)
print 'Build directory = {0}'.format(args.build_dir)

# This next part is only required if the invoker wants to include the Git
# revision string in their server version:
revision = ''
if 'REVISION' in args.server_ver:
    # If we don't have Git on our path, let's attempt to add it:
    paths = (
        '{0}\\Git\\bin'.format(os.environ['ProgramFiles']),
        '{0}\\Git\\cmd'.format(os.environ['ProgramFiles'])
    )
    for path in paths:
        if path not in os.environ['PATH']:
            os.environ['PATH'] += ';' + path

    # Now, let's get that revision string:
    revision = subprocess.Popen(
        ['git', 'rev-parse', 'HEAD'],
        stdout=subprocess.PIPE,
        cwd=args.src_dir).stdout.read().strip()[:7]

# Replace any REVISION tokens in the server version:
serverVersion = args.server_ver.replace('REVISION', revision)
print 'serverVersion = {0}'.format(serverVersion)

# Copy the provided Toontown Infinite modules:

# NonRepeatableRandomSourceUD.py, and NonRepeatableRandomSourceAI.py are
# required to be included. This is because they are explicitly imported by the
# DC file:
includes = ('NonRepeatableRandomSourceUD.py', 'NonRepeatableRandomSourceAI.py')

# This is a list of explicitly excluded files:
excludes = ('ServiceStart.py')

def minify(f):
    """
    Returns the "minified" file data with removed __debug__ code blocks.
    """

    data = ''

    debugBlock = False  # Marks when we're in a __debug__ code block.
    elseBlock = False  # Marks when we're in an else code block.

    # The number of spaces in which the __debug__ condition is indented:
    indentLevel = 0

    for line in f:
        thisIndentLevel = len(line) - len(line.lstrip())
        if ('if __debug__:' not in line) and (not debugBlock):
            data += line
            continue
        elif 'if __debug__:' in line:
            debugBlock = True
            indentLevel = thisIndentLevel
            continue
        if thisIndentLevel <= indentLevel:
            if 'else' in line:
                elseBlock = True
                continue
            if 'elif' in line:
                line = line[:thisIndentLevel] + line[thisIndentLevel+2:]
            data += line
            debugBlock = False
            elseBlock = False
            indentLevel = 0
            continue
        if elseBlock:
            data += line[4:]

    return data

for module in args.modules:
    print 'Writing module...', module
    for root, folders, files in os.walk(os.path.join(args.src_dir, module)):
        outputDir = root.replace(args.src_dir, args.build_dir)
        if not os.path.exists(outputDir):
            os.mkdir(outputDir)
        for filename in files:
            if filename not in includes:
                if not filename.endswith('.py'):
                    continue
                if filename.endswith('UD.py'):
                    continue
                if filename.endswith('AI.py'):
                    continue
                if filename in excludes:
                    continue
            with open(os.path.join(root, filename), 'r') as f:
                data = minify(f)
            with open(os.path.join(outputDir, filename), 'w') as f:
                f.write(data)

# Let's write game_data.py now. game_data.py is a compile-time generated
# collection of data that will be used by the game at runtime. It contains the
# PRC file data, (stripped) DC file, and time zone info.

# First, we need the PRC file data:
configFileName = 'config_{0}.prc'.format(args.distribution)
configData = []
with open(os.path.join(args.src_dir, 'config', configFileName)) as f:
    data = f.read()
    configData.append(data.replace('SERVER_VERSION', serverVersion))
print 'Using config file: {0}'.format(configFileName)

# Next, we need the (stripped) DC file:
dcFile = DCFile()
filepath = os.path.join(args.src_dir, 'astron')
for filename in os.listdir(filepath):
    if filename.endswith('.dc'):
        dcFile.read(Filename.fromOsSpecific(os.path.join(filepath, filename)))
dcStream = StringStream()
dcFile.write(dcStream, True)
dcData = dcStream.getData()

# Now, collect our timezone info:
zoneInfo = {}
for timezone in pytz.all_timezones:
    zoneInfo['zoneinfo/' + timezone] = pytz.open_resource(timezone).read()

# Finally, write our data to game_data.py:
print 'Writing game_data.py...'
gameData = '''\
CONFIG = %r
DC = %r
ZONEINFO = %r'''
with open(os.path.join(args.build_dir, 'game_data.py'), 'w') as f:
    f.write(gameData % (configData, dcData, zoneInfo))


def getDirectoryMD5Hash(directory):
    def _updateChecksum(checksum, dirname, filenames):
        for filename in sorted(filenames):
            path = os.path.join(dirname, filename)
            if os.path.isfile(path):
                fh = open(path, 'rb')
                while True:
                    buf = fh.read(4096)
                    if not buf:
                        break
                    checksum.update(buf)
                fh.close()
    checksum = hashlib.md5()
    directory = os.path.normpath(directory)
    if os.path.exists(directory):
        if os.path.isdir(directory):
            os.path.walk(directory, _updateChecksum, checksum)
        elif os.path.isfile(directory):
            _updateChecksum(
                checksum, os.path.dirname(directory),
                os.path.basename(directory))
    return checksum.hexdigest()


# We have all of the code gathered together. Let's create the multifiles now:
if args.build_mfs:
    print 'Building multifiles...'
    dest = os.path.join(args.build_dir, 'resources')
    if not os.path.exists(dest):
        os.mkdir(dest)
    dest = os.path.realpath(dest)
    os.chdir(args.resources_dir)
    if not os.path.exists('local-patcher.ver'):
        with open('local-patcher.ver', 'w') as f:
            f.write('RESOURCES = {}')
    with open('local-patcher.ver', 'r') as f:
        exec(f.read())
    for phase in os.listdir('.'):
        if not phase.startswith('phase_'):
            continue
        if not os.path.isdir(phase):
            continue
        phaseMd5 = getDirectoryMD5Hash(phase)
        if phase in RESOURCES:
            if RESOURCES[phase] == phaseMd5:
                continue
        filename = phase + '.mf'
        print 'Writing...', filename
        filepath = os.path.join(dest, filename)
        os.system('multify -c -f {0} {1}'.format(filepath, phase))
        RESOURCES[phase] = phaseMd5
    with open('local-patcher.ver', 'w') as f:
        f.write('RESOURCES = %r' % RESOURCES)

print 'Done preparing the client.'
