#!/usr/bin/env python3
#
# Copyright (c) 2014-2018 Apple Inc. All rights reserved.
# Copyright (c) 2014 University of Washington. 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. AND ITS CONTRIBUTORS ``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 ITS 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 logging
import string
from string import Template

try:
    from .cpp_generator import CppGenerator
    from .cpp_generator_templates import CppGeneratorTemplates as CppTemplates
    from .generator import Generator, ucfirst
    from .models import ObjectType, ArrayType, AliasedType, EnumType
except ImportError:
    from cpp_generator import CppGenerator
    from cpp_generator_templates import CppGeneratorTemplates as CppTemplates
    from generator import Generator, ucfirst
    from models import ObjectType, ArrayType, AliasedType, EnumType

log = logging.getLogger('global')


class CppBackendDispatcherImplementationGenerator(CppGenerator):
    def __init__(self, *args, **kwargs):
        CppGenerator.__init__(self, *args, **kwargs)

    def output_filename(self):
        return "%sBackendDispatchers.cpp" % self.protocol_name()

    def domains_to_generate(self):
        return [domain for domain in Generator.domains_to_generate(self) if len(self.commands_for_domain(domain)) > 0]

    def generate_output(self):
        secondary_includes = self._generate_secondary_header_includes()

        if self.model().framework.setting('alternate_dispatchers', False):
            secondary_includes.append('')
            secondary_includes.append('#if ENABLE(INSPECTOR_ALTERNATE_DISPATCHERS)')
            secondary_includes.append('#include "%sAlternateBackendDispatchers.h"' % self.protocol_name())
            secondary_includes.append('#endif // ENABLE(INSPECTOR_ALTERNATE_DISPATCHERS)')

        header_args = {
            'primaryInclude': '"%sBackendDispatchers.h"' % self.protocol_name(),
            'secondaryIncludes': '\n'.join(secondary_includes),
        }

        sections = []
        sections.append(self.generate_license())
        sections.append(Template(CppTemplates.ImplementationPrelude).substitute(None, **header_args))
        sections.append("\n".join(map(self._generate_handler_class_destructor_for_domain, self.domains_to_generate())))
        sections.extend(list(map(self._generate_dispatcher_implementations_for_domain, self.domains_to_generate())))
        sections.append(Template(CppTemplates.ImplementationPostlude).substitute(None, **header_args))
        return "\n\n".join(sections)

    # Private methods.

    def _generate_secondary_header_includes(self):
        header_includes = [
            (["JavaScriptCore", "WebKit", "WebDriverBidi"], ("JavaScriptCore", "inspector/InspectorFrontendRouter.h")),
            (["JavaScriptCore", "WebKit", "WebDriverBidi"], ("WTF", "wtf/NeverDestroyed.h")),
            (["JavaScriptCore", "WebKit", "WebDriverBidi"], ("WTF", "wtf/text/MakeString.h")),
        ]
        return self.generate_includes_from_entries(header_includes)


    def _generate_handler_class_destructor_for_domain(self, domain):
        destructor_args = {
            'domainName': domain.domain_name
        }
        destructor = '%(domainName)sBackendDispatcherHandler::~%(domainName)sBackendDispatcherHandler() { }' % destructor_args
        return self.wrap_with_guard_for_condition(domain.condition, destructor)

    def _generate_dispatcher_implementations_for_domain(self, domain):
        implementations = []

        constructor_args = {
            'domainName': domain.domain_name,
            'domainExposedAs': domain.domain_exposed_as
        }
        implementations.append(Template(CppTemplates.BackendDispatcherImplementationDomainConstructor).substitute(None, **constructor_args))

        commands = self.commands_for_domain(domain)

        if len(commands) <= 5:
            implementations.append(self._generate_small_dispatcher_switch_implementation_for_domain(domain))
        else:
            implementations.append(self._generate_large_dispatcher_switch_implementation_for_domain(domain))

        use_legacy_async_callbacks = self.model().framework.setting('legacy_async_callbacks', False)
        for command in commands:
            if command.is_async and use_legacy_async_callbacks:
                implementations.append(self._generate_legacy_async_command_response_thunk_class_for_domain(command, domain))

            implementations.append(self._generate_dispatcher_implementation_for_command(command, domain))

        return self.wrap_with_guard_for_condition(domain.condition, '\n\n'.join(implementations))

    def _generate_small_dispatcher_switch_implementation_for_domain(self, domain):
        commands = self.commands_for_domain(domain)

        cases = []

        first_command_string = "\n".join([
            '    if (protocol_method == "%s"_s) {' % commands[0].command_name,
            '        %s(protocol_requestId, WTF::move(protocol_parameters));' % commands[0].command_name,
            '        return;',
            '    }',
        ])
        cases.append(self.wrap_with_guard_for_condition(commands[0].condition, first_command_string))

        for command in commands[1:]:
            additional_command_string = "\n".join([
                '    if (protocol_method == "%s"_s) {' % command.command_name,
                '        %s(protocol_requestId, WTF::move(protocol_parameters));' % command.command_name,
                '        return;',
                '    }',
            ])
            cases.append(self.wrap_with_guard_for_condition(command.condition, additional_command_string))

        switch_args = {
            'domainName': domain.domain_name,
            'domainExposedAs': domain.domain_exposed_as,
            'dispatchCases': "\n".join(cases)
        }

        return Template(CppTemplates.BackendDispatcherImplementationSmallSwitch).substitute(None, **switch_args)

    def _generate_large_dispatcher_switch_implementation_for_domain(self, domain):
        commands = self.commands_for_domain(domain)

        cases = []
        for command in commands:
            args = {
                'domainName': domain.domain_name,
                'commandName': command.command_name
            }
            cases.append(self.wrap_with_guard_for_condition(command.condition, '        { "%(commandName)s"_s, &%(domainName)sBackendDispatcher::%(commandName)s },' % args))

        switch_args = {
            'domainName': domain.domain_name,
            'domainExposedAs': domain.domain_exposed_as,
            'dispatchCases': "\n".join(cases)
        }

        return Template(CppTemplates.BackendDispatcherImplementationLargeSwitch).substitute(None, **switch_args)

    def _generate_legacy_async_command_response_thunk_class_for_domain(self, command, domain):
        return_assignments = []
        callback_parameters = []

        for parameter in command.return_parameters:
            parameter_name = parameter.parameter_name
            if parameter.is_optional:
                parameter_name = 'opt_' + parameter_name

            parameter_value = parameter_name

            _type = parameter.type
            if isinstance(_type, AliasedType):
                _type = _type.aliased_type
            if isinstance(_type, EnumType) and _type.is_anonymous:
                _type = _type.primitive_type

            if _type.is_enum():
                if parameter.is_optional:
                    parameter_value = '*' + parameter_value
                parameter_value = 'Protocol::%s::getEnumConstantValue(%s)' % (self.helpers_namespace(), parameter_value)
            elif CppGenerator.should_release_argument(_type, parameter.is_optional):
                parameter_value = parameter_value + '.releaseNonNull()'
            elif CppGenerator.should_dereference_argument(_type, parameter.is_optional):
                parameter_value = '*' + parameter_value
            elif CppGenerator.should_move_argument(_type, parameter.is_optional):
                parameter_value = 'WTF::move(%s)' % parameter_value

            param_args = {
                'keyedSetMethod': CppGenerator.cpp_setter_method_for_type(_type),
                'parameterKey': parameter.parameter_name,
                'parameterName': parameter_name,
                'parameterValue': parameter_value,
            }

            callback_parameters.append('%s %s' % (CppGenerator.cpp_type_for_command_return_argument(_type, parameter.is_optional), parameter_name))

            if parameter.is_optional:
                return_assignments.append('    if (!!%(parameterName)s)' % param_args)
                return_assignments.append('        protocol_jsonMessage->%(keyedSetMethod)s("%(parameterKey)s"_s, %(parameterValue)s);' % param_args)
            else:
                return_assignments.append('    protocol_jsonMessage->%(keyedSetMethod)s("%(parameterKey)s"_s, %(parameterValue)s);' % param_args)

        async_args = {
            'domainName': domain.domain_name,
            'callbackName': ucfirst(command.command_name) + 'Callback',
            'callbackParameters': ", ".join(callback_parameters),
            'returnAssignments': "\n".join(return_assignments)
        }
        return self.wrap_with_guard_for_condition(command.condition, Template(CppTemplates.BackendDispatcherImplementationAsyncCommandReplyThunk).substitute(None, **async_args))

    def _generate_dispatcher_implementation_for_command(self, command, domain):
        parameter_declarations = []
        parameter_enum_resolutions = []
        alternate_dispatcher_method_parameters = ['protocol_requestId']
        method_parameters = []
        enum_error_code = self.model().framework.setting('enum_invalid_value_error_code', 'ServerError')

        # Step 1: build up arguments for the call.
        for parameter in command.call_parameters:
            parameter_name = parameter.parameter_name
            if parameter.is_optional:
                parameter_name = 'opt_' + parameter_name
            parameter_name = 'in_' + parameter_name

            variable_name = parameter_name

            _type = parameter.type
            if isinstance(_type, AliasedType):
                _type = _type.aliased_type
            if isinstance(_type, EnumType) and _type.is_anonymous:
                _type = _type.primitive_type

            if _type.is_enum():
                parameter_name = parameter_name + '_json'

                alternate_dispatcher_method_parameters.append(parameter_name)

                enum_args = {
                    'helpersNamespace': self.helpers_namespace(),
                    'parameterKey': parameter.parameter_name,
                    'enumType': CppGenerator.cpp_protocol_type_for_type(_type),
                    'enumVariableName': variable_name,
                    'stringVariableName': parameter_name,
                }

                if len(parameter_enum_resolutions):
                    parameter_enum_resolutions.append('')
                parameter_enum_resolutions.append('    auto %(enumVariableName)s = Protocol::%(helpersNamespace)s::parseEnumValueFromString<%(enumType)s>(%(stringVariableName)s);' % enum_args)
                enum_args['enumErrorCode'] = enum_error_code
                if parameter.is_optional:
                    parameter_enum_resolutions.append('    if (!%(stringVariableName)s.isEmpty() && !%(enumVariableName)s) {' % enum_args)
                    parameter_enum_resolutions.append('        m_backendDispatcher->reportProtocolError(BackendDispatcher::%(enumErrorCode)s, makeString("Unknown %(parameterKey)s: "_s, %(stringVariableName)s));' % enum_args)
                    parameter_enum_resolutions.append('        return;')
                    parameter_enum_resolutions.append('    }')
                    parameter_expression = 'WTF::move(%s)' % variable_name
                else:
                    parameter_enum_resolutions.append('    if (!%(enumVariableName)s) {' % enum_args)
                    parameter_enum_resolutions.append('        m_backendDispatcher->reportProtocolError(BackendDispatcher::%(enumErrorCode)s, makeString("Unknown %(parameterKey)s: "_s, %(stringVariableName)s));' % enum_args)
                    parameter_enum_resolutions.append('        return;')
                    parameter_enum_resolutions.append('    }')
                    parameter_expression = '*' + variable_name
            else:
                if _type.raw_name() == 'string':
                    parameter_expression = variable_name
                elif parameter.is_optional:
                    parameter_expression = 'WTF::move(%s)' % variable_name
                elif _type.raw_name() in ['boolean', 'integer', 'number']:
                    parameter_expression = '*' + variable_name
                else:
                    parameter_expression = variable_name + '.releaseNonNull()'
                alternate_dispatcher_method_parameters.append(parameter_expression)
            method_parameters.append(parameter_expression)

            param_args = {
                'parameterKey': parameter.parameter_name,
                'parameterName': parameter_name,
                'keyedGetMethod': CppGenerator.cpp_getter_method_for_type(_type),
                'required': 'false' if parameter.is_optional else 'true',
            }
            parameter_declarations.append('    auto %(parameterName)s = m_backendDispatcher->%(keyedGetMethod)s(protocol_parameters.get(), "%(parameterKey)s"_s, %(required)s);' % param_args)

        result_destructured_names = []
        result_conversion_lines = []

        # Step 2: build up return value expressions if needed.
        for parameter in command.return_parameters:
            parameter_name = parameter.parameter_name
            if parameter.is_optional:
                parameter_name = 'opt_' + parameter_name
            parameter_name = 'out_' + parameter_name

            result_destructured_names.append(parameter_name)

            parameter_value = parameter_name

            _type = parameter.type
            if isinstance(_type, AliasedType):
                _type = _type.aliased_type
            if isinstance(_type, EnumType) and _type.is_anonymous:
                _type = _type.primitive_type

            if _type.is_enum():
                if parameter.is_optional:
                    parameter_value = '*' + parameter_value
                parameter_value = 'Protocol::%s::getEnumConstantValue(%s)' % (self.helpers_namespace(), parameter_value)
            elif CppGenerator.should_release_argument(_type, parameter.is_optional):
                parameter_value = parameter_value + '.releaseNonNull()'
            elif CppGenerator.should_dereference_argument(_type, parameter.is_optional):
                parameter_value = '*' + parameter_value
            elif CppGenerator.should_move_argument(_type, parameter.is_optional):
                parameter_value = 'WTF::move(%s)' % parameter_value

            param_args = {
                'keyedSetMethod': CppGenerator.cpp_setter_method_for_type(_type),
                'parameterKey': parameter.parameter_name,
                'parameterName': parameter_name,
                'parameterValue': parameter_value,
            }

            if parameter.is_optional:
                result_conversion_lines.append('    if (!!%(parameterName)s)' % param_args)
                result_conversion_lines.append('        protocol_jsonMessage->%(keyedSetMethod)s("%(parameterKey)s"_s, %(parameterValue)s);' % param_args)
            else:
                result_conversion_lines.append('    protocol_jsonMessage->%(keyedSetMethod)s("%(parameterKey)s"_s, %(parameterValue)s);' % param_args)

        if command.is_async:
            # FIXME: Remove legacy_async_callbacks when IndexedDBAgent moves off of it. <https://bugs.webkit.org/show_bug.cgi?id=288203>
            use_legacy_async_callbacks = self.model().framework.setting('legacy_async_callbacks', False)
            if use_legacy_async_callbacks:
                callback_name = '%sBackendDispatcherHandler::%sCallback' % (domain.domain_name, ucfirst(command.command_name))
                method_parameters.append('adoptRef(*new %s(m_backendDispatcher.copyRef(), protocol_requestId))' % callback_name)
            else:
                if len(command.return_parameters) > 1:
                    result_type_alias = 'CommandResultOf'
                    type_arguments = [CppGenerator.cpp_type_for_command_return_declaration(parameter.type, parameter.is_optional) for parameter in command.return_parameters]
                elif len(command.return_parameters):
                    result_type_alias = 'CommandResult'
                    type_arguments = [CppGenerator.cpp_type_for_command_return_declaration(parameter.type, parameter.is_optional) for parameter in command.return_parameters]
                else:
                    result_type_alias = 'CommandResult'
                    type_arguments = ['void']

                # Immediately send an error message since this is an async response with a single error.
                thunkLines = [
                    '[backendDispatcher = m_backendDispatcher.copyRef(), protocol_requestId](%s<%s> result) {' % (result_type_alias, ", ".join(type_arguments)),
                    '        if (!result) {',
                    '           ASSERT(!result.error().isEmpty());',
                    '           backendDispatcher->reportProtocolError(protocol_requestId, BackendDispatcher::ServerError, result.error());',
                    '           backendDispatcher->sendPendingErrors();',
                    '           return;',
                    '        }',
                ]

                if len(result_destructured_names) > 1:
                    thunkLines.append('        auto [%s] = WTF::move(result.value());' % ", ".join(result_destructured_names))
                elif len(result_destructured_names):
                    thunkLines.append('        auto %s = WTF::move(result.value());' % ", ".join(result_destructured_names))

                thunkLines.append('')
                thunkLines.append('        auto protocol_jsonMessage = JSON::Object::create();')
                thunkLines.extend(['    ' + line for line in result_conversion_lines])
                thunkLines.append('        backendDispatcher->sendResponse(protocol_requestId, WTF::move(protocol_jsonMessage), false);')
                thunkLines.append('    }')
                method_parameters.append("\n".join(thunkLines))

        command_args = {
            'domainName': domain.domain_name,
            'domainExposedAs': domain.domain_exposed_as,
            'commandName': command.command_name,
            'parameterDeclarations': '\n'.join(parameter_declarations),
            'invocationParameters': ', '.join(method_parameters),
            'alternateInvocationParameters': ', '.join(alternate_dispatcher_method_parameters),
        }

        # Step 3: spit out the generated code one line at a time.
        lines = []
        if len(command.call_parameters) == 0:
            lines.append('void %(domainName)sBackendDispatcher::%(commandName)s(long protocol_requestId, RefPtr<JSON::Object>&&)' % command_args)
        else:
            lines.append('void %(domainName)sBackendDispatcher::%(commandName)s(long protocol_requestId, RefPtr<JSON::Object>&& protocol_parameters)' % command_args)
        lines.append('{')

        if len(command.call_parameters) > 0:
            lines.append(Template(CppTemplates.BackendDispatcherImplementationPrepareCommandArguments).substitute(None, **command_args))

        if self.model().framework.setting('alternate_dispatchers', False):
            lines.append('#if ENABLE(INSPECTOR_ALTERNATE_DISPATCHERS)')
            lines.append('    if (m_alternateDispatcher) {')
            lines.append('        m_alternateDispatcher->%(commandName)s(%(alternateInvocationParameters)s);' % command_args)
            lines.append('        return;')
            lines.append('    }')
            lines.append('#endif // ENABLE(INSPECTOR_ALTERNATE_DISPATCHERS)')
            lines.append('')

        if len(parameter_enum_resolutions):
            lines.extend(parameter_enum_resolutions)
            lines.append('')

        if command.is_async:
            lines.append('    m_agent->%(commandName)s(%(invocationParameters)s);' % command_args)
        else:
            lines.append('    auto result = m_agent->%(commandName)s(%(invocationParameters)s);' % command_args)
            lines.append('    if (!result) {')
            lines.append('        ASSERT(!result.error().isEmpty());')
            lines.append('        m_backendDispatcher->reportProtocolError(BackendDispatcher::ServerError, result.error());')
            lines.append('        return;')
            lines.append('    }')
            lines.append('')
            if len(result_destructured_names) == 1:
                lines.append('    auto %s = WTF::move(result.value());' % result_destructured_names[0])
                lines.append('')
            elif len(result_destructured_names) > 1:
                lines.append('    auto [%s] = WTF::move(result.value());' % ", ".join(result_destructured_names))
                lines.append('')
            lines.append('    auto protocol_jsonMessage = JSON::Object::create();')
            lines.extend(result_conversion_lines)
            lines.append('    m_backendDispatcher->sendResponse(protocol_requestId, WTF::move(protocol_jsonMessage), false);')

        lines.append('}')
        return self.wrap_with_guard_for_condition(command.condition, "\n".join(lines))
