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

# NOTE: The master copy of this file is in the psiphon-ios-client-common-library repo.

# Copyright (c) 2017, Psiphon Inc.
# All rights reserved.
#
# 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, either version 3 of the License, or
# (at your option) any later version.
#
# 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, see <http://www.gnu.org/licenses/>.

"""
This script extracts strings from source (`.m` and `.plist`) files to create `.strings` files.
"""

from __future__ import print_function
import json
import os
import shutil
import errno
import re
import subprocess
import shlex
import glob
import codecs
import plistlib
from collections import OrderedDict


def load_config():
    """
    Load config from file.
    """
    default_config_filename = 'i18n_conf.json'

    with open(default_config_filename) as config_fp:
        config = json.load(config_fp)

    if not config:
        raise Exception('Unable to load config contents from %s' %
                        default_config_filename)

    return config


def process_objc(config):
    """
    Generate strings file from Objective-C source files.
    """
    # Set up the temp dir
    temp_dir = config['enLprojDir'] + '.temp'
    shutil.rmtree(temp_dir, ignore_errors=True)
    _mkdir_p(temp_dir)

    # Gather our source files
    objc_files = [os.path.join(dirpath, f)
                  for dirpath, _, files in os.walk(config['objcRootDir'])
                  for f in files if re.match(r'.+\.(m|h|swift)$', f)]

    # Exclude ignored dirs
    if config['objcIgnoreDirs']:
        for ignore in config['objcIgnoreDirs']:
            ignore = os.path.join(config['objcRootDir'], ignore)
            objc_files = [f for f in objc_files
                          if os.path.commonprefix([f, ignore]) != ignore]

    # Create the UTF-16LE encoded .strings files with genstrings
    subprocess.check_call(shlex.split(
        'genstrings -o "%s" "%s"' % (temp_dir, '" "'.join(objc_files))))

    # Convert to UTF-8
    for strings_file in glob.glob(os.path.join(temp_dir, '*.strings')):
        with codecs.open(strings_file, 'r', 'utf-16-le') as infile:
            with codecs.open(os.path.join(config['enLprojDir'], os.path.basename(strings_file)),
                             'w', 'utf-8') as outfile:
                outfile.write('/* THIS FILE IS GENERATED. DO NOT EDIT. */\n\n')
                data = infile.read()
                if data[0] == u'\uFEFF':
                    # Strip the BOM
                    data = data[1:]
                outfile.write(data)

    shutil.rmtree(temp_dir, ignore_errors=True)

# From https://stackoverflow.com/a/600612/729729
def _mkdir_p(path):
    """
    Like `mkdir -p`
    """
    try:
        os.makedirs(path)
    except OSError as exc:  # Python >2.5
        if exc.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else:
            raise


PLIST_STRING_KEYS = ['Title', 'ShortTitle', 'FooterText', 'IASKSubtitle', 'IASKPlaceholder']
PLIST_MULTISTRING_KEYS = ['Titles', 'ShortTitles']


def process_plist(plist_fname, strings):
    """
    Copy strings, keys, and comments from `plist_fname` into the
    `strings` dict, which is `key => {'key':..., 'default':..., 'description':...}`
    """
    with open(plist_fname, 'rb') as fp:
        plist = plistlib.loads(fp.read())
        for item in plist['PreferenceSpecifiers']:
            _process_plist_dict(item, strings)


def _process_plist_dict(plist_dict, strings):
    """
    Helper for process_plist to process a single dict in a plist, which might
    contains strings.
    """
    if not isinstance(plist_dict, dict):
        return

    for multistring_key in PLIST_MULTISTRING_KEYS:
        if multistring_key not in plist_dict:
            continue

        for plist_subdict in plist_dict.get(multistring_key):
            _process_plist_dict(plist_subdict, strings)

    for string_key in PLIST_STRING_KEYS:
        if string_key not in plist_dict:
            continue

        key = plist_dict.get(string_key)
        default = plist_dict.get(string_key + 'Default')
        description = plist_dict.get(string_key + 'Description')

        if not key:
            raise Exception('ERROR: Empty key found: %s' % plist_dict)
        elif not default:
            # Skipping. This is probably an item covered by common-lib.
            print('SKIPPING: %s' % key.encode('utf-8'))
            continue
        elif not description:
            raise Exception(
                'ERROR: Missing string description (if this string belongs to common-lib, '
                'exclude the default; otherwise do not be lazy and add a description): %s'
                % plist_dict)

        default = default.replace('"', '\\"').replace('\n', '\\n')

        if key in strings:
            # The key is already present in strings, so we'll combine the
            # descriptions (if necessary).
            if default != strings[key]['default']:
                raise Exception(
                    'ERROR: key used multiple times with non-matching defaults '
                    '(same key must have same default): %s' % plist_dict)
            elif description != strings[key]['description']:
                strings[key]['description'] += '\n   ' + description
        else:
            strings[key] = {
                'key': key,
                'default': default,
                'description': description}


def process_all_plists(config):
    """
    Extract strings from configured plist files into Root.strings.
    """
    strings = OrderedDict()

    for plist_fname in config['plistFiles']:
        process_plist(plist_fname, strings)

    with codecs.open(os.path.join(config['enLprojDir'], 'Root.strings'),
                     'w', 'utf-8') as strings_file:
        strings_file.write('/* THIS FILE IS GENERATED. DO NOT EDIT. */\n\n')
        for key in strings:
            strings_file.write('/* %s */\n"%s" = "%s";\n\n' % (strings[key]['description'],
                                                               strings[key]['key'],
                                                               strings[key]['default']))


def main():
    """
    Do all of the string extraction work.
    """
    conf = load_config()  # pylint: disable=invalid-name
    process_objc(conf)
    process_all_plists(conf)


if __name__ == '__main__':
    main()