#!/usr/bin/env python
# -*- coding: UTF-8 -*-

"""Update by merging .strings file(s) of an Xcode project."""

import os
import sys
import shlex
import shutil
import tempfile
import re
from string import Template
from subprocess import Popen, PIPE

__author__ = "Ali Servet Donmez"
__email__ = "asd@pittle.org"
__version__ = "0.9.1"

# ==============================================================================
# SETTINGS
# ==============================================================================

GENSTRING_SEARCH_PATHS = [
]
"""Recursive search paths where Objective-C source file(s) will be searched."""

BASE_LANG = 'en'
"""Base localization language which will be overridden at all times."""

OTHER_LANGS = [
]
"""List of additional localization languages which will be updated."""

BASE_RESOURCES = ''
"""Base resources directory which will be overridden at all times."""

OTHER_RESOURCES = [
]
"""List of additional resources that will be updated."""

# ==============================================================================
# DO NOT TOUCH BELOW HERE
# ==============================================================================

class LocalizedString():

    def __init__(self, key, value, comment=None):
        self.key = key
        self.value = value
        self.comment = comment
        self.todoc = False

    def __str__(self):
        return Template('/* $todoc$comment */\n"$key" = "$value";').substitute(
            key=self.key,
            value=self.value,
            comment=self.comment or "No comment provided by engineer.",
            todoc='TODOC ' if self.todoc else '',
        )

    def __lt__(self, other):
        return self.key.lower() < other.key.lower()

def check_and_setup_settings():
    global OTHER_LANGS
    # Remove duplicate entries
    OTHER_LANGS = list(set(OTHER_LANGS))
    if BASE_LANG in OTHER_LANGS:
        sys.stderr.write("OTHER_LANGS must not include base language: %s.\n" % BASE_LANG)
        return False
    return True

def check_xcode_setup():
    for res in [BASE_RESOURCES] + OTHER_RESOURCES:
        for lang in [BASE_LANG] + OTHER_LANGS:
            lang_dirname = os.path.join(res, '%s.lproj' % lang)
            if not os.path.isdir(lang_dirname):
                sys.stderr.write('Missing directory: %s.\n' % lang_dirname)
                return False
    return True

def genstrings(output_path):
    """Recursively search current working directory for Objective-C source code
    file(s) and return output generated by internal genstrings utility for lines
    containing text of the form NSLocalizedString("key", comment) or
    CFCopyLocalizedString("key", comment).

    """
    find_cmd = r"find -E %s -iregex '.*\.(h|m|mm)' -print0" % ' '.join(GENSTRING_SEARCH_PATHS)
    genstrings_cmd = 'xargs -0 genstrings -o "%s"' % output_path

    try:
        p1 = Popen(shlex.split(find_cmd), stdout=PIPE)
        p2 = Popen(shlex.split(genstrings_cmd), stdin=p1.stdout, stdout=PIPE)
        p1.stdout.close()  # Allow p1 to receive a SIGPIPE if p2 exits.
        p2.communicate()
    except OSError:
        sys.stderr.write("Error (e.g., trying to execute a non-existent file).\n")
        sys.exit()
    except ValueError:
        sys.stderr.write("Invalid arguments.\n")
        sys.exit()

    with open(os.path.join(output_path, 'Localizable.strings'), 'r') as f:
        return f.read().decode('utf16').strip()

def parse_strings_file(data):
    """Parse .strings file and return a list of LocalizedString objects.

    Keyword arguments:
    data -- .strings file content

    """
    return [LocalizedString(*parse_localized_string(s)) for s in data.split('\n\n')]

re_comment  = re.compile(r'^/\* (.*) \*/$')
re_l10n     = re.compile(r'^"(.+)" = "(.+)";$')

def parse_localized_string(text):
    """Parse text and return key, value and comment tuple.

    Keyword arguments:
    text -- localized strings text, leading and trailing characters will be removed if necessary.

    """
    split = text.strip().split('\n')

    if len(split) == 2:
        return re_l10n.match(split[1]).groups() + (re_comment.match(split[0]).groups()[0],)

def save_strings(strings, base_path):
    """Write Localizable.strings to disk.

    Keyword arguments:
    strings -- list of LocalizedString objects.
    base_path -- where Localizable.strings file will be written.

    """
    write_data = unicode()
    for s in strings:
        write_data += "%s\n\n" % unicode(s)
    with open(os.path.join(base_path, 'Localizable.strings'), 'wb') as f:
        f.write(write_data.encode('utf16'))

def merge_strings(base_strings, other_strings):
    """Merge two list of localized strings.

    Keyword arguments:
    base_strings -- more up-to-date list of localized strings.
    other_strings -- previous list of localized strings.

    """
    # Local copy of base_strings since we don't want to change it outside too
    merged = base_strings[:]
    for s in merged:
        s.todoc = True
    for i, base in enumerate(merged):
        for other in other_strings:
            if base.key == other.key:
                merged[i] = other
    return merged

def main():
    filename = os.path.split(__file__)[1]

    # Check if script is configured okay
    if not check_and_setup_settings():
        sys.stderr.write("%s: configuration error.\n" % filename)
        return os.EX_CONFIG

    # Check if Xcode project is setup ready for localizations to take place
    if not check_xcode_setup():
        sys.stderr.write("%s: project is not setup correctly.\n" % filename)
        return os.EX_CONFIG

    # All checks went well, here comes the good part...

    # Generate latest string table from source code
    dtemp_path = tempfile.mkdtemp()
    latest_strings = parse_strings_file(genstrings(dtemp_path))
    shutil.rmtree(dtemp_path)

    # Save base .strings file as it is by overwriting the old one
    save_strings(latest_strings, os.path.join(BASE_RESOURCES, '%s.lproj' % BASE_LANG))

    # For any other languages do the merge-magic and write to disk
    for lang in OTHER_LANGS:
        merged = None
        try:
            read_data = None
            with open(os.path.join(BASE_RESOURCES, '%s.lproj' % lang, 'Localizable.strings'), 'r') as f:
                read_data = f.read().decode('utf16')
            merged = merge_strings(latest_strings, parse_strings_file(read_data.strip()))
        except IOError:
            merged = latest_strings

        save_strings(sorted(merged), os.path.join(BASE_RESOURCES, '%s.lproj' % lang))

    # For all other languages for all other resources do the merge-magic and write to disk
    # FIXME This code block is almost identical to one above, wrap these two
    for res in OTHER_RESOURCES:
        for lang in [BASE_LANG] + OTHER_LANGS:
            read_data = None
            with open(os.path.join(res, '%s.lproj' % lang, 'Localizable.strings'), 'r') as f:
                read_data = f.read().decode('utf16')
            merged = merge_strings(latest_strings, parse_strings_file(read_data.strip()))
            save_strings(sorted(merged), os.path.join(res, '%s.lproj' % lang))

    return os.EX_OK

if __name__ == '__main__':
    status = main()
    sys.exit(status)

# End of file
