#!/usr/bin/env python3
#
# Copyright 2020 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import sys
import lark
import argparse
from contextlib import suppress
from collections import namedtuple
from datetime import datetime

# GN grammar from https://gn.googlesource.com/gn/+/master/src/gn/parser.cc.
GN_GRAMMAR = """
    ?file : statement_list

    ?statement     : assignment | call | condition
    ?lvalue        : IDENTIFIER | array_access | scope_access
    assignment     : lvalue assign_op expr
    call           : IDENTIFIER "(" [ expr_list ] ")" [ block ]
    condition      : "if" "(" expr ")" block [ "else" ( condition | block ) ]
    ?block         : "{" statement_list "}"
    statement_list : statement*

    array_access   : IDENTIFIER "[" expr "]"
    scope_access   : IDENTIFIER "." IDENTIFIER
    ?primary_expr  : IDENTIFIER | INTEGER | STRING | call
                   | array_access | scope_access | block
                   | "(" expr ")" -> par_expr
                   | array
    array          : "[" [ expr ( "," expr? )* ] "]"
    expr_list      : expr ( "," expr )*

    ?assign_op : "="  -> asgn_op
               | "+=" -> asgn_add_op
               | "-=" -> asgn_sub_op

    ?expr      : expr1
    ?expr1     : expr1 "||" expr2 -> or
               | expr2
    ?expr2     : expr2 "&&" expr3 -> and
               | expr3
    ?expr3     : expr3 "==" expr4 -> eq
               | expr3 "!=" expr4 -> ne
               | expr4
    ?expr4     : expr4 "<=" expr5 -> le
               | expr4 "<" expr5  -> lt
               | expr4 ">=" expr5 -> ge
               | expr4 ">" expr5  -> gt
               | expr5
    ?expr5     : expr5 "+" expr6  -> add
               | expr5 "-" expr6  -> sub
               | expr6
    ?expr6     : "!" primary_expr -> neg
               | primary_expr

    COMMENT : /#.*/

    %import common.ESCAPED_STRING -> STRING
    %import common.SIGNED_INT -> INTEGER
    %import common.CNAME -> IDENTIFIER
    %import common.WS
    %ignore WS
    %ignore COMMENT
"""

V8_TARGET_TYPES = (
    'v8_component',
    'v8_source_set',
    'v8_executable',
)

OPS = (
    'neg',
    'eq',
    'ne',
    'le',
    'lt',
    'ge',
    'gt',
    'and',
    'or',
)


class UnsupportedOperation(Exception):
    pass


class V8GNTransformer(object):
    """
    Traverse GN parse-tree and build resulting object.
    """
    def __init__(self, builder, filtered_targets):
        self.builder = builder
        self.filtered_targets = filtered_targets
        self.current_target = None

    def Traverse(self, tree):
        self.builder.BuildPrologue()
        self.TraverseTargets(tree)
        self.builder.BuildEpilogue()

    def TraverseTargets(self, tree):
        'Traverse top level GN targets and call the builder functions'
        for stmt in tree.children:
            if stmt.data != 'call':
                continue
            target_type = stmt.children[0]
            if target_type not in V8_TARGET_TYPES:
                continue
            target = stmt.children[1].children[0].strip('\"')
            if target not in self.filtered_targets:
                continue
            self.current_target = target
            self._Target(target_type, target, stmt.children[2].children)

    def _Target(self, target_type, target, stmts):
        stmts = self._StatementList(stmts)
        return self.builder.BuildTarget(target_type, target, stmts)

    def _StatementList(self, stmts):
        built_stmts = []
        for stmt in stmts:
            built_stmts.append(self._Statement(stmt))
        return [stmt for stmt in built_stmts if stmt]

    def _Statement(self, stmt):
        # Handle only interesting gn statements.
        with suppress(KeyError):
            return self.STATEMENTS[stmt.data](self, *stmt.children)

    def _Assignment(self, left, op, right):
        return self.ASSIGN_TYPES[op.data](self, left, right)

    def _AssignEq(self, left, right):
        if left == 'sources':
            return self.builder.BuildSourcesList(
                self.current_target, [str(token) for token in right.children])

    def _AssignAdd(self, left, right):
        if left == 'sources':
            return self.builder.BuildAppendSources(
                self.current_target, [str(token) for token in right.children])

    def _AssignSub(self, left, right):
        if left == 'sources':
            return self.builder.BuildRemoveSources(
                self.current_target, [str(token) for token in right.children])

    def _Condition(self, cond_expr, then_stmts, else_stmts=None):
        'Visit GN condition: if (cond) {then_stmts} else {else_stmts}'
        cond_expr = self._Expr(cond_expr)
        then_stmts = self._StatementList(then_stmts.children)
        if not then_stmts:
            # Ignore conditions with empty then stmts.
            return
        if else_stmts is None:
            return self.builder.BuildCondition(cond_expr, then_stmts)
        elif else_stmts.data == 'condition':
            else_cond = self._Condition(*else_stmts.children)
            return self.builder.BuildConditionWithElseCond(
                cond_expr, then_stmts, else_cond)
        else:
            assert 'statement_list' == else_stmts.data
            else_stmts = self._StatementList(else_stmts.children)
            return self.builder.BuildConditionWithElseStmts(
                cond_expr, then_stmts, else_stmts)

    def _Expr(self, expr):
        'Post-order traverse expression trees'
        if isinstance(expr, lark.Token):
            if expr.type == 'IDENTIFIER':
                return self.builder.BuildIdentifier(str(expr))
            elif expr.type == 'INTEGER':
                return self.builder.BuildInteger(str(expr))
            else:
                return self.builder.BuildString(str(expr))
        if expr.data == 'par_expr':
            return self.builder.BuildParenthesizedOperation(
                self._Expr(*expr.children))
        if expr.data not in OPS:
            raise UnsupportedOperation(
                f'The operator "{expr.data}" is not supported')
        if len(expr.children) == 1:
            return self._UnaryExpr(expr.data, *expr.children)
        if len(expr.children) == 2:
            return self._BinaryExpr(expr.data, *expr.children)
        raise UnsupportedOperation(f'Unsupported arity {len(expr.children)}')

    def _UnaryExpr(self, op, right):
        right = self._Expr(right)
        return self.builder.BuildUnaryOperation(op, right)

    def _BinaryExpr(self, op, left, right):
        left = self._Expr(left)
        right = self._Expr(right)
        return self.builder.BuildBinaryOperation(left, op, right)

    STATEMENTS = {
        'assignment': _Assignment,
        'condition': _Condition,
    }

    ASSIGN_TYPES = {
        'asgn_op': _AssignEq,
        'asgn_add_op': _AssignAdd,
        'asgn_sub_op': _AssignSub,
    }


TARGETS = {
    'v8_libbase': 'lib',
    'v8_cppgc_shared': 'lib',
    'cppgc_base': 'lib',
    'cppgc_standalone': 'sample',
    'cppgc_unittests_sources': 'tests',
    'cppgc_unittests': 'tests',
}


class CMakeBuilder(object):
    """
    Builder that produces the main CMakeLists.txt.
    """
    def __init__(self):
        self.result = []
        self.source_sets = {}

    def BuildPrologue(self):
        self.result.append(f"""
# Copyright {datetime.now().year} the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# This file is automatically generated by {__file__}. Do NOT edit it.

cmake_minimum_required(VERSION 3.11)
project(cppgc CXX)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

option(CPPGC_ENABLE_OBJECT_NAMES "Enable object names in cppgc for debug purposes" OFF)
option(CPPGC_ENABLE_CAGED_HEAP "Enable heap reservation of size 4GB, only possible for 64bit archs" OFF)
option(CPPGC_ENABLE_YOUNG_GENERATION "Enable young generation in cppgc" OFF)
set(CPPGC_TARGET_ARCH "x64" CACHE STRING "Target architecture, possible options: x64, x86, arm, arm64, ppc64, s390x, mipsel, mips64el")

set(IS_POSIX ${{UNIX}})
set(IS_MAC ${{APPLE}})
set(IS_WIN ${{WIN32}})
if("${{CMAKE_SYSTEM_NAME}}" STREQUAL "Linux")
  set(IS_LINUX 1)
elseif("${{CMAKE_SYSTEM_NAME}}" STREQUAL "Fuchsia")
  set(IS_FUCHSIA 1)
endif()

set(CURRENT_CPU ${{CPPGC_TARGET_ARCH}})

if("${{CPPGC_TARGET_ARCH}}" STREQUAL "x64" OR
   "${{CPPGC_TARGET_ARCH}}" STREQUAL "arm64" OR
   "${{CPPGC_TARGET_ARCH}}" STREQUAL "ppc64" OR
   "${{CPPGC_TARGET_ARCH}}" STREQUAL "mips64el")
  if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
    message(FATAL_ERROR "64-bit arch specified for 32-bit compiler")
  endif()
  set(CPPGC_64_BITS ON)
endif()

if(CPPGC_ENABLE_CAGED_HEAP AND NOT CPPGC_64_BITS)
  message(FATAL_ERROR "Caged heap is only supported for 64bit archs")
endif()

if(CPPGC_64_BITS)
  # Always enable caged heap for 64bits archs.
  set(CPPGC_ENABLE_CAGED_HEAP ON CACHE BOOL "Enable caged heap for 64bit" FORCE)
endif()

if(CPPGC_ENABLE_YOUNG_GENERATION AND NOT CPPGC_ENABLE_CAGED_HEAP)
  message(FATAL_ERROR "Young generation is only supported for caged heap configuration")
endif()

if(NOT CPPGC_64_BITS)
  if(NOT MSVC)
    set(CMAKE_CXX_FLAGS "${{CMAKE_CXX_FLAGS}} -m32")
    set(CMAKE_C_FLAGS "${{CMAKE_C_FLAGS}} -m32")
    set(CMAKE_EXE_LINKER_FLAGS "${{CMAKE_EXE_LINKER_FLAGS}} -m32")
    set(CMAKE_SHARED_LINKER_FLAGS "${{CMAKE_SHARED_LINKER_FLAGS}} -m32")
    set(CMAKE_MODULE_LINKER_FLAGS "${{CMAKE_MODULE_LINKER_FLAGS}} -m32")
  endif()
endif()

find_package(Threads REQUIRED)

include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY "https://chromium.googlesource.com/external/github.com/google/googletest.git"
  GIT_TAG        "4fe018038f87675c083d0cfb6a6b57c274fb1753"
  SOURCE_DIR     "${{CMAKE_BINARY_DIR}}/third_party/googletest/src"
)

FetchContent_GetProperties(googletest)
if(NOT googletest_POPULATED)
  FetchContent_Populate(googletest)
  message("Fetched googletest into ${{googletest_SOURCE_DIR}}")
  add_subdirectory(${{googletest_SOURCE_DIR}} ${{googletest_BINARY_DIR}} EXCLUDE_FROM_ALL)
  include_directories("${{CMAKE_BINARY_DIR}}")
endif()
""")

    def BuildEpilogue(self):
        self.result.extend(
            self._GenTargetString(target, sets)
            for target, sets in self.source_sets.items())
        self.result.append("\ninstall(TARGETS cppgc)")

    def BuildTarget(self, target_type, target, rules):
        # Don't generate CMake targets yet, defer it to build_epilogue.
        comment = f"""
#===============================================================================
# {self._CMakeTarget(target)} sources.
#==============================================================================="""
        self.result.append(comment)
        self.result.extend(rules)
        self.source_sets.setdefault(
            TARGETS[target], []).append('${' + self._SourceVar(target) + '}')

    def BuildSourcesList(self, target, sources):
        sources = self._ExpandSources(target, sources)
        return f'set({self._SourceVar(target)} {sources})'

    def BuildAppendSources(self, target, sources):
        sources = self._ExpandSources(target, sources)
        return f'list(APPEND {self._SourceVar(target)} {sources})'

    def BuildRemoveSources(self, target, sources):
        sources = self._ExpandSources(target, sources)
        return f'list(REMOVE_ITEM {self._SourceVar(target)} {sources})'

    def BuildCondition(self, cond, then_stmts):
        return f"""
if({cond})
  {' '.join(then_stmts)}
endif()
        """.strip()

    def BuildConditionWithElseStmts(self, cond, then_stmts, else_stmts):
        return f"""
if({cond})
  {' '.join(then_stmts)}
{'else() ' + ' '.join(else_stmts)}
endif()
        """.strip()

    def BuildConditionWithElseCond(self, cond, then_stmts, else_cond):
        return f"""
if({cond})
  {' '.join(then_stmts)}
else{else_cond}
        """.strip()

    def BuildParenthesizedOperation(self, operation):
        return ''.join(['(', operation, ')'])

    def BuildUnaryOperation(self, op, right):
        OPS = {
            'neg': 'NOT',
        }
        return ' '.join([OPS[op], right])

    def BuildBinaryOperation(self, left, op, right):
        if op == 'ne':
            neg_result = self.BuildBinaryOperation(left, 'eq', right)
            return self.BuildUnaryOperation('neg', neg_result)
        OPS = {
            'eq': 'STREQUAL',
            'le': 'LESS_EQUAL',
            'lt': 'LESS',
            'ge': 'GREATER_EQUAL',
            'gt': 'GREATER',
            'and': 'AND',
            'or': 'OR',
        }
        return ' '.join([left, OPS[op], right])

    def BuildIdentifier(self, token):
        return self._CMakeVarRef(token)

    def BuildInteger(self, integer):
        return integer

    def BuildString(self, string):
        return string

    def GetResult(self):
        return '\n'.join(self.result)

    @staticmethod
    def _GenTargetString(target_type, source_sets):
        Target = namedtuple('Target', 'name cmake deps desc')
        CMAKE_TARGETS = {
            'lib':
            Target(name='cppgc',
                   cmake='add_library',
                   deps=['Threads::Threads'],
                   desc='Main library'),
            'sample':
            Target(name='cppgc_sample',
                   cmake='add_executable',
                   deps=['cppgc'],
                   desc='Example'),
            'tests':
            Target(name='cppgc_unittests',
                   cmake='add_executable',
                   deps=['cppgc', 'gtest', 'gmock'],
                   desc='Unittests')
        }
        target = CMAKE_TARGETS[target_type]
        return f"""
# {target.desc} target.
{target.cmake}({target.name} {' '.join(source_sets)})

{'target_link_libraries(' + target.name + ' ' + ' '.join(target.deps) + ')' if target.deps else ''}

target_include_directories({target.name} PRIVATE "${{CMAKE_SOURCE_DIR}}"
                                         PRIVATE "${{CMAKE_SOURCE_DIR}}/include")

if(CPPGC_ENABLE_OBJECT_NAMES)
  target_compile_definitions({target.name} PRIVATE "-DCPPGC_SUPPORTS_OBJECT_NAMES")
endif()
if(CPPGC_ENABLE_CAGED_HEAP)
  target_compile_definitions({target.name} PRIVATE "-DCPPGC_CAGED_HEAP")
endif()
if(CPPGC_ENABLE_YOUNG_GENERATION)
  target_compile_definitions({target.name} PRIVATE "-DCPPGC_YOUNG_GENERATION")
endif()"""

    @staticmethod
    def _ExpandSources(target, sources):
        if TARGETS[target] == 'tests':
            sources = ['\"test/unittests/' + s[1:] for s in sources]
        return ' '.join(sources)

    @staticmethod
    def _SourceVar(target):
        return CMakeBuilder._CMakeVar(target) + '_SOURCES'

    @staticmethod
    def _CMakeVar(var):
        return var.replace('v8_', '').upper()

    @staticmethod
    def _CMakeTarget(var):
        return var.replace('v8_', '')

    @staticmethod
    def _CMakeVarRef(var):
        return '\"${' + CMakeBuilder._CMakeVar(var) + '}"'


def FormatCMake(contents):
    from cmake_format import configuration, lexer, parse, formatter
    cfg = configuration.Configuration()
    tokens = lexer.tokenize(contents)
    parse_tree = parse.parse(tokens)
    box_tree = formatter.layout_tree(parse_tree, cfg)
    return formatter.write_tree(box_tree, cfg, contents)


def SaveContents(contents, outfile):
    if outfile == '-':
        return print(contents)
    with open(outfile, 'w+') as ofile:
        ofile.write(contents)


def ParseGN(contents):
    parser = lark.Lark(GN_GRAMMAR, parser='lalr', start='file')
    return parser.parse(contents)


def ParseGNFile(filename):
    with open(filename, 'r') as file:
        contents = file.read()
        return ParseGN(contents)


def GenCMake(main_gn, test_gn, outfile):
    tree = ParseGNFile(main_gn)
    tree.children.extend(ParseGNFile(test_gn).children)
    builder = CMakeBuilder()
    V8GNTransformer(builder, TARGETS.keys()).Traverse(tree)
    result = FormatCMake(builder.GetResult())
    SaveContents(result, outfile)


def Main():
    arg_parser = argparse.ArgumentParser(
        description=
        'Generate CMake from the main GN file for targets needed to build CppGC.'
    )
    arg_parser.add_argument('--out', help='output CMake filename', default='-')
    arg_parser.add_argument('--main-gn',
                            help='main BUILD.gn input file',
                            default='BUILD.gn')
    arg_parser.add_argument('--test-gn',
                            help='unittest BUILD.gn input file',
                            default='test/unittests/BUILD.gn')
    args = arg_parser.parse_args()

    GenCMake(args.main_gn, args.test_gn, args.out)
    return 0


if __name__ == '__main__':
    sys.exit(Main())
