import base64
import json
import os
import re
import sys
import shutil
import tempfile
import textwrap
import zipfile


class _Formatting(object):
    @staticmethod
    def is_str(strval):
        if sys.version_info >= (3, 0, 0):
            return isinstance(strval, (bytes, str))
        else:
            return isinstance(strval, basestring)

    @staticmethod
    def encoding_needed(strval):
        if sys.version_info >= (3, 0, 0):
            return isinstance(strval, str)
        else:
            return isinstance(strval, unicode)

    @staticmethod
    def escape_special_symbols(strval):
        encoding_needed = _Formatting.encoding_needed(strval)
        c_str = strval.encode('utf-8') if encoding_needed else strval
        retval = b""
        for c in c_str:
            if sys.version_info >= (3, 0, 0):
                c = bytes([c])
            if c in ("\\", "\""):
                retval += "\\" + c
            elif ord(c) < ord(' '):
                retval += c.decode('latin-1').encode('unicode_escape')
            else:
                retval += c
        return retval.decode('utf-8') if encoding_needed else retval

    @staticmethod
    def escape_line_feed(strval, indent='    '):
        return strval.replace(r'\n', '\\n"\\\n' + indent + '"')

    @staticmethod
    def escape_trigraphs(strval):
        return strval.replace(r'?', '\\?')

    @staticmethod
    def escaped_define(strkey, val):
        name = "#define " + strkey + " "
        if _Formatting.is_str(val):
            define = "\"" + _Formatting.escape_line_feed(
                _Formatting.escape_trigraphs(_Formatting.escape_special_symbols(val))) + "\""
        else:
            define = str(val)
        return name + define

    @staticmethod
    def escaped_go_map_key(strkey, strval):
        if _Formatting.is_str(strval):
            return '    ' + '"' + strkey + '": "' + _Formatting.escape_special_symbols(strval) + '",'
        else:
            return '    ' + '"' + strkey + '": "' + str(strval) + '",'


def get_default_json():
    return json.loads('''{
    "ARCADIA_SOURCE_HG_HASH": "0000000000000000000000000000000000000000",
    "ARCADIA_SOURCE_LAST_AUTHOR": "<UNKNOWN>",
    "ARCADIA_SOURCE_LAST_CHANGE": -1,
    "ARCADIA_SOURCE_PATH": "/",
    "ARCADIA_SOURCE_REVISION": -1,
    "ARCADIA_SOURCE_URL": "",
    "BRANCH": "unknown-vcs-branch",
    "BUILD_DATE": "",
    "BUILD_TIMESTAMP": 0,
    "BUILD_HOST": "localhost",
    "BUILD_USER": "nobody",
    "PROGRAM_VERSION": "Arc info:\\n    Branch: unknown-vcs-branch\\n    Commit: 0000000000000000000000000000000000000000\\n    Author: <UNKNOWN>\\n    Summary: No VCS\\n\\n",
    "SCM_DATA": "Arc info:\\n    Branch: unknown-vcs-branch\\n    Commit: 0000000000000000000000000000000000000000\\n    Author: <UNKNOWN>\\n    Summary: No VCS\\n",
    "VCS": "arc",
    "ARCADIA_PATCH_NUMBER": 0,
    "ARCADIA_TAG": ""
}''')


def get_json(file_name):
    try:
        with open(file_name, 'r') as f:
            out = json.load(f)

        # TODO: check 'tar+svn' parsing
        for i in ['ARCADIA_SOURCE_REVISION', 'ARCADIA_SOURCE_LAST_CHANGE', 'SVN_REVISION']:
            if i in out and _Formatting.is_str(out[i]):
                try:
                    out[i] = int(out[i])
                except:
                    out[i] = -1
        return out
    except:
        return get_default_json()


def print_c(json_file, output_file, argv):
    """ params:
            json file
            output file
            $(SOURCE_ROOT)/build/scripts/c_templates/svn_interface.c"""
    def gen_header(info):
        lines = []
        for k, v in info.items():
            lines.append(_Formatting.escaped_define(k, v))
        return lines

    interface = argv[0]

    with open(interface) as c:
        c_file = c.read()
    with open(output_file, 'w') as f:
        header = '\n'.join(gen_header(json_file))
        if sys.version_info < (3, 0, 0):
            header = header.encode('utf-8')
        f.write(header + '\n' + c_file)


def merge_java_content(old_content, json_file):
    new_content, names = print_java_mf(json_file)

    def split_to_sections(content):
        sections = []
        cur_section = []
        for l in content:
            if l.rstrip():
                cur_section.append(l)
            else:
                sections.append(cur_section)
                cur_section = []

        if cur_section:  # should not be needed according to format specification
            sections.append(cur_section)

        return sections

    def drop_duplicate_entries(main_section, names):
        header = re.compile('^([A-Za-z0-9][A-Za-z0-9_-]*): .*$')
        new_main_section = []
        for l in main_section:
            match = header.match(l)
            # duplicate entry
            if match:
                skip = match.group(1) in names

            if not skip:
                new_main_section.append(l)
        return new_main_section

    if old_content:
        sections = split_to_sections(old_content)
        sections[0] = drop_duplicate_entries(sections[0], names)
    else:
        sections = [['Manifest-Version: 1.0\n']]

    sections[0].extend(map(lambda x: x + '\n', new_content))

    return ''.join(map(lambda x: ''.join(x), sections)) + '\n'


def merge_java_mf_jar(json_file, out_manifest, jar_file):
    try:
        temp_dir = tempfile.mkdtemp()
        try:
            with zipfile.ZipFile(jar_file, 'r') as jar:
                jar.extract(os.path.join('META-INF', 'MANIFEST.MF'), path=temp_dir)
        except KeyError:
            pass

        merge_java_mf_dir(json_file, out_manifest, temp_dir)
    finally:
        shutil.rmtree(temp_dir)


def merge_java_mf_dir(json_file, out_manifest, input_dir):
    manifest = os.path.join(input_dir, 'META-INF', 'MANIFEST.MF')

    old_lines = []
    if os.path.isfile(manifest):
        with open(manifest, 'r') as f:
            old_lines = f.readlines()

    with open(out_manifest, 'w') as f:
        f.write(merge_java_content(old_lines, json_file))


def merge_java_mf(json_file, out_manifest, input):
    if zipfile.is_zipfile(input):
        merge_java_mf_jar(json_file, out_manifest, input)
    elif os.path.isdir(input):
        merge_java_mf_dir(json_file, out_manifest, input)


def print_java_mf(info):
    wrapper = textwrap.TextWrapper(subsequent_indent=' ',
                                   break_long_words=True,
                                   replace_whitespace=False,
                                   drop_whitespace=False)
    names = set()

    def wrap(key, val):
        names.add(key[:-2])
        if not val:
            return []
        return wrapper.wrap(key + val)

    lines = wrap('Program-Version-String: ', base64.b64encode(info['PROGRAM_VERSION'].encode('utf-8')))
    lines += wrap('SCM-String: ', base64.b64encode(info['SCM_DATA'].encode('utf-8')))
    lines += wrap('Arcadia-Source-Path: ', info['ARCADIA_SOURCE_PATH'])
    lines += wrap('Arcadia-Source-URL: ', info['ARCADIA_SOURCE_URL'])
    lines += wrap('Arcadia-Source-Revision: ', str(info['ARCADIA_SOURCE_REVISION']))
    lines += wrap('Arcadia-Source-Hash: ', info['ARCADIA_SOURCE_HG_HASH'].rstrip())
    lines += wrap('Arcadia-Source-Last-Change: ', str(info['ARCADIA_SOURCE_LAST_CHANGE']))
    lines += wrap('Arcadia-Source-Last-Author: ', info['ARCADIA_SOURCE_LAST_AUTHOR'])
    lines += wrap('Build-User: ', info['BUILD_USER'])
    lines += wrap('Build-Host: ', info['BUILD_HOST'])
    lines += wrap('Version-Control-System: ', info['VCS'])
    lines += wrap('Branch: ', info['BRANCH'])
    lines += wrap('Arcadia-Tag: ', info.get('ARCADIA_TAG', ''))
    lines += wrap('Arcadia-Patch-Number: ', str(info.get('ARCADIA_PATCH_NUMBER', 42)))
    if 'SVN_REVISION' in info:
        lines += wrap('SVN-Revision: ', str(info['SVN_REVISION']))
        lines += wrap('SVN-Arcroot: ', info['SVN_ARCROOT'])
        lines += wrap('SVN-Time: ', info['SVN_TIME'])
    lines += wrap('Build-Date: ', info['BUILD_DATE'])
    if 'BUILD_TIMESTAMP' in info:
        lines += wrap('Build-Timestamp: ', str(info['BUILD_TIMESTAMP']))
    return lines, names


def print_java(json_file, output_file, argv):
    """ params:
            json file
            output file
            file"""
    input = argv[0] if argv else os.curdir
    merge_java_mf(json_file, output_file, input)


def print_go(json_file, output_file):
    def gen_map(info):
        lines = []
        for k, v in info.items():
            lines.append(_Formatting.escaped_go_map_key(k, v))
        return lines

    with open(output_file, 'w') as f:
        f.write('\n'.join([
            'package main',
            'import ("a.yandex-team.ru/library/go/core/buildinfo")',
            'var buildinfomap = map[string]string {'] + gen_map(json_file) + ['}'] +
            ['func init() {',
             '   buildinfo.InitBuildInfo(buildinfomap)',
             '}']
        ).encode('utf-8') + '\n')


if __name__ == '__main__':
    if 'output-go' in sys.argv:
        lang = 'Go'
        sys.argv.remove('output-go')
    elif 'output-java' in sys.argv:
        lang = 'Java'
        sys.argv.remove('output-java')
    else:
        lang = 'C'

    if 'no-vcs' in sys.argv:
        sys.argv.remove('no-vcs')
        json_file = get_default_json()
    else:
        json_name = sys.argv[1]
        json_file = get_json(json_name)

    if lang == 'Go':
        print_go(json_file, sys.argv[2])
    elif lang == 'Java':
        print_java(json_file, sys.argv[2], sys.argv[3:])
    else:
        print_c(json_file, sys.argv[2], sys.argv[3:])