#!/usr/bin/python
# Copyright (C) 2014 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import argparse
import filecmp
import fnmatch
import os
import re
import shutil
import sys
import datetime
import json

parser = argparse.ArgumentParser()
parser.add_argument('input_file', nargs='*', help='Input JS files which builtins generated from')
parser.add_argument('--input-directory', help='All JS files will be used as input from this directory.')
parser.add_argument('--output', help='path to output cpp or h file')
parser.add_argument('--prefix', default='JSC', help='prefix used for public macros')
parser.add_argument('--namespace', default='JSC', help='C++ namespace')
args = parser.parse_args()

copyrightText = """ *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */\n
"""

generatorString = "/* Generated by %s do not hand edit. */\n" % os.path.basename(__file__)

functionHeadRegExp = re.compile(r"(?:@[\w|=\[\] \"\.]+\s*\n)*(?:async\s+)?function\s+\w+\s*\(.*?\)", re.MULTILINE | re.DOTALL)
functionGlobalPrivateRegExp = re.compile(r".*^@globalPrivate", re.MULTILINE | re.DOTALL)
functionIntrinsicRegExp = re.compile(r".*^@intrinsic=(\w+)", re.MULTILINE | re.DOTALL)
functionIsConstructorRegExp = re.compile(r".*^@constructor", re.MULTILINE | re.DOTALL)
functionIsGetterRegExp = re.compile(r".*^@getter", re.MULTILINE | re.DOTALL)
functionIsAsyncRegExp = re.compile(r"(async)?\s*function", re.MULTILINE | re.DOTALL)
functionNameRegExp = re.compile(r"function\s+(\w+)\s*\(", re.MULTILINE | re.DOTALL)
functionOverriddenNameRegExp = re.compile(r".*^@overriddenName=(\".+\")$", re.MULTILINE | re.DOTALL)
functionParameterFinder = re.compile(r"^(?:async\s+)?function\s+(?:\w+)\s*\(((?:\s*\w+)?\s*(?:\s*,\s*\w+)*)?\s*\)", re.MULTILINE | re.DOTALL)

multilineCommentRegExp = re.compile(r"\/\*.*?\*\/", re.MULTILINE | re.DOTALL)
singleLineCommentRegExp = re.compile(r"\/\/.*?\n", re.MULTILINE | re.DOTALL)
keyValueAnnotationCommentRegExp = re.compile(r"^\/\/ @(\w+)=([^=]+?)\n", re.MULTILINE | re.DOTALL)
flagAnnotationCommentRegExp = re.compile(r"^\/\/ @(\w+)[^=]*?\n", re.MULTILINE | re.DOTALL)
lineWithOnlySingleLineCommentRegExp = re.compile(r"^\s*\/\/\n", re.MULTILINE | re.DOTALL)
lineWithTrailingSingleLineCommentRegExp = re.compile(r"\s*\/\/\n", re.MULTILINE | re.DOTALL)
multipleEmptyLinesRegExp = re.compile(r"\n{2,}", re.MULTILINE | re.DOTALL)

# These match WK_lcfirst and WK_ucfirst defined in CodeGenerator.pm.
def WK_lcfirst(str):
    str = str[:1].lower() + str[1:]
    str = str.replace('hTML', 'html')
    str = str.replace('uRL', 'url')
    str = str.replace('jS', 'js')
    str = str.replace('xML', 'xml')
    str = str.replace('xSLT', 'xslt')
    str = str.replace('cSS', 'css')
    str = str.replace('rTC', 'rtc')
    return str

def WK_ucfirst(str):
    str = str[:1].upper() + str[1:]
    str = str.replace('Xml', 'XML')
    str = str.replace('Svg', 'SVG')
    return str

def getCopyright(source):
    copyrightBlock = multilineCommentRegExp.findall(source)[0]
    copyrightBlock = copyrightBlock[:copyrightBlock.index("Redistribution")]
    copyRightLines = []

    for line in copyrightBlock.split("\n"):
        line = line.replace("/*", "")
        line = line.replace("*/", "")
        line = line.replace("*", "")
        line = line.replace("Copyright", "")
        line = line.replace("copyright", "")
        line = line.replace("(C)", "")
        line = line.replace("(c)", "")
        line = line.strip()
        if len(line) == 0:
            continue

        copyRightLines.append(line)
    
    return list(set(copyRightLines))

class Function(object):
    def __init__(self, name, source, is_async, isConstructor, parameters, intrinsic, overridden_name):
        self.name = name
        self.source = source
        self.is_async = is_async
        self.isConstructor = isConstructor
        self.parameters = parameters
        self.intrinsic = intrinsic
        self.overridden_name = overridden_name

    def mangleName(self, object):
        qName = object + "." + self.name
        mangledName = ""
        i = 0
        while i < len(qName):
            if qName[i] == '.':
                mangledName = mangledName + qName[i + 1].upper()
                i = i + 1
            else:
                mangledName = mangledName + qName[i]
            i = i + 1
        if self.isConstructor:
            mangledName = mangledName + "Constructor"
        return mangledName

    def __str__(self):
        interface = "%s(%s)" % (self.function_name, ', '.join(self.parameters))
        if self.is_constructor:
            interface = interface + " [Constructor]"

        if self.is_async:
            interface = "async " + interface

        return interface

    def __lt__(self, other):
        return self.function_name < other.function_name

def getFunctions(source):

    source = multilineCommentRegExp.sub("/**/", singleLineCommentRegExp.sub("//\n", source))

    matches = [ f for f in functionHeadRegExp.finditer(source)]
    functionBounds = []
    start = 0
    end = 0
    for match in matches:
        start = match.start()
        if start < end:
            continue
        end = match.end()
        while source[end] != '{':
            end = end + 1
        depth = 1
        end = end + 1
        while depth > 0:
            if source[end] == '{':
                depth = depth + 1
            elif source[end] == '}':
                depth = depth - 1
            end = end + 1
        functionBounds.append((start, end))

    functions = [source[start:end].strip() for (start, end) in functionBounds]
    result = []
    for function in functions:
        function_source = multilineCommentRegExp.sub("", function)
        
        intrinsic = "JSC::NoIntrinsic"
        intrinsicMatch = functionIntrinsicRegExp.search(function_source)
        if intrinsicMatch:
            intrinsic = intrinsicMatch.group(1)
            function_source = functionIntrinsicRegExp.sub("", function_source)
        
        overridden_name = None
        overriddenNameMatch = functionOverriddenNameRegExp.search(function_source)
        if overriddenNameMatch:
            overridden_name = overriddenNameMatch.group(1)
            function_source = functionOverriddenNameRegExp.sub("", function_source)

        function_name = functionNameRegExp.findall(function_source)[0]
        async_match = functionIsAsyncRegExp.match(function_source)
        is_async = async_match != None and async_match.group(1) == "async"
        functionIsConstructor = functionIsConstructorRegExp.match(function_source) != None
        is_getter = functionIsGetterRegExp.match(function_source) != None
        functionParameters = functionParameterFinder.findall(function_source)[0].split(',')
        if len(functionParameters[0]) == 0:
            functionParameters = []
           
        if is_getter and not overridden_name:
            overridden_name = "\"get %s\"" % (function_name)

        if not overridden_name:
            overridden_name = "static_cast<const char*>(0)"
           
        result.append(Function(function_name, function_source, is_async, functionIsConstructor, functionParameters, intrinsic, overridden_name))
    return result
        
def writeIncludeDirectives(writer, headerNames):
    for headerName in headerNames:
        writer.write("#include " + headerName + "\n")

def generateCode(source):
    inputFile = open(source, "r")
    baseName = os.path.basename(source).replace(".js", "")
    
    source = ""
    for line in inputFile:
        source = source + line
    
    if sys.platform == "cygwin":
        source = source.replace("\r\n", "\n")
    return (baseName, getFunctions(source), getCopyright(source))

builtins = []
copyrights = []
(output_base, _) = os.path.splitext(args.output)

if args.input_directory:
    for file in os.listdir(args.input_directory):
        args.input_file.append(os.path.join(args.input_directory, file))

for file in args.input_file:
    if fnmatch.fnmatch(file, '*.js'):
        (baseName, functions, objectCopyrights) = generateCode(file)
        copyrights.extend(objectCopyrights)
        builtins.append((baseName, functions))
namespace = args.namespace
macroPrefix = args.prefix
scopeName = os.path.splitext(os.path.basename(args.output))[0]
includeGuard = scopeName + "_H"
headerName = scopeName + ".h"

headerIncludes = ["\"BuiltinUtils.h\""] if namespace == "JSC" else ["<builtins/BuiltinUtils.h>"]
contentIncludes = ["\"BuiltinExecutables.h\"", "\"Executable.h\"", "\"JSCellInlines.h\"", "\"VM.h\""] if namespace == "JSC" else ["<runtime/Executable.h>", "<runtime/StructureInlines.h>", "<runtime/JSCJSValueInlines.h>", "<runtime/JSCellInlines.h>", "<runtime/VM.h>", "\"WebCoreJSClientData.h\""]

copyrights = list(set(copyrights))

copyrightBody = ""
for copyright in copyrights:
    copyrightBody = copyrightBody +" * Copyright (C) " + copyright + "\n"

builtinsHeader = open(output_base + ".h.tmp", "w")
builtinsImplementation = open(output_base + ".cpp.tmp", "w")
copyrightText = "/*\n" + copyrightBody + copyrightText
builtinsHeader.write("""%s
%s

#ifndef %s
#define %s

""" % (generatorString, copyrightText, includeGuard, includeGuard))

writeIncludeDirectives(builtinsHeader, headerIncludes)

builtinsHeader.write("""
namespace JSC {
    class FunctionExecutable;
}

namespace %s {

""" % namespace)

codeReferences = []

for (objectName, functions) in builtins:
    print("Generating bindings for the %s builtin." % objectName)
    builtinsHeader.write("/* %s functions */\n" % objectName)
    for function in functions:
        name = function.name
        mangledName = function.mangleName(objectName)
        mangledName = WK_lcfirst(mangledName) + "Code"
        codeReferences.append((mangledName, function))
        builtinsHeader.write("extern const char* s_%s;\n" % mangledName)
        builtinsHeader.write("extern const int s_%sLength;\n" % mangledName)
        builtinsHeader.write("extern const JSC::Intrinsic s_%sIntrinsic;\n" % mangledName)
        builtinsHeader.write("extern const JSC::ConstructAbility s_%sConstructAbility;\n" % mangledName)
    builtinsHeader.write("\n")
    builtinsHeader.write("#define %s_FOREACH_%s_BUILTIN(macro) \\\n" % (macroPrefix, objectName.replace(".", "_").upper()))
    for function in functions:
        mangledName = function.mangleName(objectName)
        builtinsHeader.write("    macro(%s, %s, %s, %d) \\\n" % (function.name, mangledName, function.overridden_name, len(function.parameters)))
    builtinsHeader.write("\n")
    for function in functions:
        builtinsHeader.write("#define %s_BUILTIN_%s 1\n" % (macroPrefix, function.mangleName(objectName).upper()))
    builtinsHeader.write("\n\n")
names = []
builtinsHeader.write("#define %s_FOREACH_BUILTIN(macro)\\\n" % macroPrefix)
for (codeReference, function) in codeReferences:
    builtinsHeader.write("    macro(%s, %s, %s, s_%sLength) \\\n" % (codeReference, function.name, function.overridden_name,codeReference))
    names.append(function.name)

builtinsHeader.write("\n\n")
builtinsHeader.write("#define %s_FOREACH_BUILTIN_FUNCTION_NAME(macro) \\\n" % macroPrefix)
for name in sorted(set(names)):
    builtinsHeader.write("    macro(%s) \\\n" % name)
builtinsHeader.write("""

#define DECLARE_BUILTIN_GENERATOR(codeName, function_name, overriddenName, argumentCount) \\
    JSC::FunctionExecutable* codeName##Generator(JSC::JSGlobalObject*);

%s_FOREACH_BUILTIN(DECLARE_BUILTIN_GENERATOR)
#undef DECLARE_BUILTIN_GENERATOR

#define %s_BUILTIN_EXISTS(name) defined %s_BUILTIN_ ## name

}

#endif // %s

""" % (macroPrefix, macroPrefix, macroPrefix, includeGuard))

builtinsImplementation.write("""%s
%s

#include "config.h"

#include "%s"

"""  % (generatorString, copyrightText, headerName))

writeIncludeDirectives(builtinsImplementation, contentIncludes)

builtinsImplementation.write("""
namespace %s {

"""  % (namespace))



for (codeReference, function) in codeReferences:
    source = function.source
    # Wrap it in parens to avoid adding to global scope.
    function_type_string = "function "
    if function.is_async:
        function_type_string = "async " + function_type_string
    
    source = "(" + function_type_string + source[source.index("("):] + ")"
    lines = json.dumps(source)[1:-1].split("\\n")
    sourceLength = len(source)
    source = ""
    for line in lines:
        source = source + ("    \"%s\\n\" \\\n" % line)
    builtinsImplementation.write("const char* s_%s =\n%s;\n\n" % (codeReference, source))
    builtinsImplementation.write("const int s_%sLength = %d;\n\n" % (codeReference, sourceLength + 1)) # + 1 for \n
    builtinsImplementation.write("const JSC::Intrinsic s_%sIntrinsic = %s;\n\n" % (codeReference, function.intrinsic)) # + 1 for \n
    constructAbility = "JSC::ConstructAbility::CannotConstruct"
    if function.isConstructor:
        constructAbility = "JSC::ConstructAbility::CanConstruct"
    builtinsImplementation.write("const JSC::ConstructAbility s_%sConstructAbility = %s;\n\n" % (codeReference, constructAbility)) # + 1 for \n

builtinsImplementation.write("""

#define DEFINE_BUILTIN_GENERATOR(codeName, function_name, overriddenName, argumentCount) \\
JSC::FunctionExecutable* codeName##Generator(JSC::JSGlobalObject* globalObject) \\
""");

if (namespace == "JSC"):
    builtinsImplementation.write("""{\\
    return globalObject->builtinExecutables()->codeName##Executable(); \\
""")
else:
    builtinName = WK_lcfirst(scopeName)
    builtinsImplementation.write("""{\\
    return static_cast<JSDOMGlobalObject*>(globalObject)->builtinFunctions().%s().codeName##Executable(); \\
"""% (builtinName))

builtinsImplementation.write("""}
%s_FOREACH_BUILTIN(DEFINE_BUILTIN_GENERATOR)
#undef DEFINE_BUILTIN_GENERATOR
}

"""% (macroPrefix))

builtinsHeader.close()
builtinsImplementation.close()

if (not os.path.exists(output_base + ".h")) or (not filecmp.cmp(output_base + ".h.tmp", output_base + ".h", shallow=False)):
    os.rename(output_base + ".h.tmp", output_base + ".h")
else:
    os.remove(output_base + ".h.tmp")

if (not os.path.exists(output_base + ".cpp")) or (not filecmp.cmp(output_base + ".cpp.tmp", output_base + ".cpp", shallow=False)):
    os.rename(output_base + ".cpp.tmp", output_base + ".cpp")
else:
    os.remove(output_base + ".cpp.tmp")
