#!/usr/bin/python3
# Soong parser and Makefile generator
#
# Based on an implementation of a simple JSON parser shipped with pyparsing
#
# Copyright 2006, 2007, 2016 Paul McGuire
# Copyright 2020 Andrej Shadura
#
# SPDX-License-Identifier: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import pyparsing as pp
from pyparsing import pyparsing_common as ppc
from typing import Mapping, Sequence
from functools import lru_cache
from dataclasses import dataclass
import os
import sh

dpkg_architecture = sh.Command('dpkg-architecture')

def make_keyword(kwd_str, kwd_value):
    return pp.Keyword(kwd_str).setParseAction(pp.replaceWith(kwd_value))

TRUE = make_keyword("true", True)
FALSE = make_keyword("false", False)
NULL = make_keyword("null", None)

LBRACK, RBRACK, LBRACE, RBRACE, COLON, EQUALS, COMMA, PLUS = map(pp.Suppress, "[]{}:=,+")

@dataclass
class Assignment:
    variable: str
    value: object

    def __init__(self, tokens):
        self.variable = tokens[0][0]
        self.value = tokens[0][1]

    def run(self):
        global variables
        variables[self.variable] = self.value

@dataclass
class Module:
    name: str
    arguments: dict

    def __init__(self, tokens):
        self.name = tokens[0][0]
        self.arguments = tokens[0][1].asDict()

    def run(self):
        globals()[self.name](**self.arguments)

quotedString = pp.quotedString().setParseAction(pp.removeQuotes)
number = ppc.integer()

def add_things(tokens):
    return [tokens[0] + tokens[1]]

numberExp = (number + PLUS + number).setParseAction(add_things)

bracedMap = pp.Forward()
singleValue = pp.Forward()
listElements = pp.delimitedList(quotedString) + pp.Optional(COMMA)
stringList = pp.Group(LBRACK + pp.Optional(listElements, []) + RBRACK)

stringListExp = (stringList + PLUS + stringList).setParseAction(add_things)

singleValue << (
    quotedString | numberExp | number | pp.Group(bracedMap) | TRUE | FALSE | NULL | stringListExp | stringList
)
singleValue.setName("value")

def add_dicts(a, b):
    # currently unused
    new = {}
    for k, v in b.items():
        if k in a:
            # the docco isn’t clear on this but it’s better to overwrite strings
            if isinstance(v, str):
                new[k] = b[k]
            elif isinstance(v, Mapping):
                new[k] = add_dicts(a[k], b[k])
            elif isinstance(v, Sequence):
                new[k] = a[k] + b[k]
        else:
            new[k] = v
    return new


memberDef = pp.Group((quotedString | ppc.identifier) + COLON + singleValue)
mapMembers = pp.delimitedList(memberDef) + pp.Optional(COMMA)
mapMembers.setName("dictionary member")
bracedMap << pp.Dict(LBRACE + pp.Optional(mapMembers) + RBRACE)

comment = pp.cppStyleComment

soongAssignment = pp.Group(ppc.identifier + EQUALS + singleValue).setParseAction(Assignment)
soongModule = pp.Group(ppc.identifier + pp.Group(bracedMap)).setParseAction(Module)
soongStatement = soongAssignment | soongModule
soong = soongStatement[...]
soong.ignore(comment)

defaults = dict()
variables = dict()

all_targets = list()
targets_binary = list()
targets_shlib = list()

flag_blacklist = ['-Werror', '-U_FORTIFY_SOURCE', '-m32', '-m64', '-Wno-#pragma-messages']

@lru_cache
def detect_arch():
    if 'DEB_HOST_ARCH' in os.environ:
        return os.environ
    else:
        return dpkg_architecture('-q', 'DEB_HOST_ARCH').rstrip()

def map_arch():
    arch = detect_arch()
    if arch == 'amd64':
        return 'x86_64'
    elif arch == 'i386':
        return 'x86'
    elif arch == 'arm64':
        return 'arm64'
    elif arch.startswith('arm'):
        return 'arm'
    elif arch.startswith('mips64'):
        return 'mips64'
    elif arch.startswith('mips'):
        return 'mips'
    return arch

def multilib():
    if map_arch().endswith('64'):
        return 'lib64'
    else:
        return 'lib32'

def mergedefaults(a, b):
    for k, v in b.items():
        if k in a:
            if isinstance(v, Mapping):
                new = v.copy()
                new.update(a[k])
                a[k] = new
            elif isinstance(v, Sequence):
                a[k] = v + a[k]
        else:
            a[k] = v
    return a

def print_vars(target, kv, names):
    for name in names:
        if name in kv:
            print(f"{target}_{name.upper():<8} = {' '.join(kv[name])}")

def collect_defaults(args):
    global defaults
    def_cxxflags = []
    def_cflags = []
    def_ldflags = []
    def_ldlibs = []
    def_srcs = []
    for default in args.get('defaults', []):
        if default in defaults:
            def_cxxflags += [f"$({default}_CXXFLAGS)"]
            def_cflags += [f"$({default}_CFLAGS)"]
            def_ldflags += [f"$({default}_LDFLAGS)"]
            def_ldlibs += [f"$({default}_LDLIBS)"]
            def_srcs += [f"$({default}_SRCS)"]
    arch_specific = args.get('arch', {}).get(map_arch(), {})
    mergedefaults(args, arch_specific)
    target_specific = args.get('target', {}).get('linux_glibc', {})
    mergedefaults(args, target_specific)
    multilib_specific = args.get('multilib', {}).get(multilib(), {})
    mergedefaults(args, multilib_specific)
    return def_cxxflags, def_cflags, def_ldflags, def_ldlibs, def_srcs

def filter_flags(flags):
    return [flag for flag in flags if flag not in flag_blacklist]

def cc_defaults(**args):
    def_cxxflags, def_cflags, def_ldflags, def_ldlibs, def_srcs = collect_defaults(args)
    defaults[args['name']] = {k: v for k, v in args.items() if k != 'name'}
    local_include_dirs = args.get('local_include_dirs', [])
    cxxflags = filter_flags(def_cxxflags + args.get('cppflags', []) + [f"-I{inc}" for inc in local_include_dirs])
    cflags = filter_flags(def_cflags + args.get('cflags', []) + [f"-I{inc}" for inc in local_include_dirs])
    ldflags = filter_flags(def_ldflags + args.get('ldflags', []))
    ldlibs = def_ldlibs + [f"-l{lib[3:]}" for lib in args.get('shared_libs', [])]
    srcs = def_srcs + args.get('srcs', [])
    print_vars(args['name'], locals(), ['cxxflags', 'cflags', 'ldflags', 'ldlibs', 'srcs'])
    print()

def cc_compile_link(binary: bool, shared: bool, args):
    print(f"# link {args['name']}")

    def_cxxflags, def_cflags, def_ldflags, def_ldlibs, def_srcs = collect_defaults(args)
    local_include_dirs = args.get('local_include_dirs', [])
    cxxflags = filter_flags(def_cxxflags + args.get('cppflags', []) + [f"-I{inc}" for inc in local_include_dirs])
    cflags = filter_flags(def_cflags + args.get('cflags', []) + [f"-I{inc}" for inc in local_include_dirs])
    ldflags = filter_flags(def_ldflags + args.get('ldflags', []))
    ldlibs = def_ldlibs + [f"-l{lib[3:]}" for lib in args.get('shared_libs', [])]
    static_libs = [f"{lib}.a" for lib in args.get('static_libs', [])]
    if 'whole_static_libs' in args:
        static_libs += ["-Wl,--whole-archive"]
        static_libs += [f"{lib}.a" for lib in args.get('whole_static_libs', [])]
        static_libs += ["-Wl,--no-whole-archive"]
    srcs = def_srcs + args.get('srcs', [])
    print_vars(args['name'], locals(), ['cxxflags', 'cflags', 'ldflags', 'ldlibs', 'srcs'])
    print()
    if not binary:
        suffix = ".so.0" if shared else ".a"
        soname = f"{args['name']}{suffix}"
        target = soname
        shared_flag = f"-shared -Wl,-soname,{soname}" if shared else ""
    else:
        target = args['name']
        shared_flag = ""
    print(f"{target}: $({args['name']}_SRCS)")
    print("\t" + ' '.join([
        "$(CC) $^ -o $@" if shared or binary else "$(CC) $^ -c",
        ' '.join(static_libs),
        f"$(CPPFLAGS)",
        f"$(CFLAGS) $({args['name']}_CFLAGS)",
        f"$(CXXFLAGS) $({args['name']}_CXXFLAGS)" if have_cxx(srcs) else "",
        f"$(LDFLAGS) $({args['name']}_LDFLAGS) {shared_flag}" if shared or binary else "",
        "-lstdc++" if have_cxx(srcs) else "",
        f"$(LDLIBS) $({args['name']}_LDLIBS)" if shared or binary else "",
    ]))
    if not shared and not binary:
        print(f"\tar rcs {soname} $(patsubst %,%.o,$(notdir $(basename $({args['name']}_SRCS))))")
        print(f"\trm $(patsubst %,%.o,$(notdir $(basename $({args['name']}_SRCS))))")
    all_targets.append(target)
    if shared:
        print()
        print(f"install-{target}: {target}")
        print(f"\tinstall -m644 -D -t $(DESTDIR)$(libdir) $<")
        print(f"\tln -s $< $(DESTDIR)$(libdir)/$(^:.0=)")
        targets_shlib.append(target)
    elif binary:
        print()
        print(f"install-{target}: {target}")
        print(f"\tinstall -m755 -D -t $(DESTDIR)$(prefix)/bin $<")
        targets_binary.append(target)
    print()

def cc_binary_host(**args):
    cc_compile_link(True, False, args)

def cc_library_shared(**args):
    cc_compile_link(False, True, args)

def cc_library_host_shared(**args):
    cc_compile_link(False, True, args)

def cc_library_static(**args):
    cc_compile_link(False, False, args)

def cc_library_host_static(**args):
    cc_compile_link(False, False, args)

def cc_test(**args):
    pass

def cc_test_host(**args):
    pass

def cc_benchmark_host(**args):
    pass

def have_cxx(files):
    return any(is_cxx(f) for f in files)

def is_cxx(filename):
    return (filename.endswith('.cc') or
            filename.endswith('.cxx') or
            filename.endswith('.cpp') or
            filename.endswith('.CPP') or
            filename.endswith('.c++') or
            filename.endswith('.cp') or
            filename.endswith('.C'))

def flag_defaults():
    print("DPKG_EXPORT_BUILDFLAGS = 1")
    print("-include /usr/share/dpkg/buildflags.mk\n")
    print(".PHONY: build install\n")
    print(".DEFAULT_GOAL := build\n")
    print("DESTDIR ?=")
    print("prefix ?= /usr")
    print("libdir ?= ${prefix}/lib\n")
    print("CXXFLAGS += " + ' '.join([
        "-D__STDC_FORMAT_MACROS",
        "-D__STDC_CONSTANT_MACROS",
        "-std=c++11",
    ]))
    print("CFLAGS += " + ' '.join([
        "-D_FILE_OFFSET_BITS=64",
        "-D_LARGEFILE_SOURCE=1",
        "-Wa,--noexecstack",
        "-fPIC",
        "-fcommon",
    ]))
    print("LDFLAGS += " + ' '.join([
        "-Wl,-z,noexecstack",
        "-Wl,--no-undefined-version",
        "-Wl,--as-needed",
    ]))
    print("LDLIBS += " + ' '.join([
        f'-l{lib}' for lib in [
            "c",
            "dl",
            "gcc",
            #"gcc_s",
            "m",
            #"ncurses",
            "pthread",
            #"resolv",
            "rt",
            "util",
        ]
    ]))
    print()

if __name__ == "__main__":
    import sys

    if len(sys.argv) > 1:
        bp = sys.argv[1]
    else:
        bp = "Android.bp"
    recipe = open(bp).read()

    if len(sys.argv) > 2:
        sys.stdout = open(sys.argv[2], 'w')

    flag_defaults()

    results = soong.parseString(recipe)
    for r in results:
        r.run()

    print(f"build: {' '.join(all_targets)}")
    print(f"clean:\n\trm -f {' '.join(all_targets)}\n")
    if targets_binary:
        print(f"install-binaries: install-{' install-'.join(targets_binary)}")
    else:
        print(f"install-binaries:")
    if targets_shlib:
        print(f"install-shlibs: install-{' install-'.join(targets_shlib)}")
    else:
        print("install-shlibs:")
    print("install: install-binaries install-shlibs")
