#!/usr/bin/env python
#
# 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
except ValueError:
    from cpp_generator import CppGenerator
    from cpp_generator_templates import CppGeneratorTemplates as CppTemplates
    from generator import Generator, ucfirst
    from models import ObjectType, ArrayType

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"], ("JavaScriptCore", "inspector/InspectorFrontendRouter.h")),
            (["JavaScriptCore", "WebKit"], ("WTF", "wtf/JSONValues.h")),
            (["JavaScriptCore", "WebKit"], ("WTF", "wtf/NeverDestroyed.h")),
            (["JavaScriptCore", "WebKit"], ("WTF", "wtf/text/CString.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,
        }
        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))

        for command in commands:
            if command.is_async:
                implementations.append(self._generate_async_dispatcher_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 (method == "%s") {' % commands[0].command_name,
            '        %s(requestId, WTFMove(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 (method == "%s") {' % command.command_name,
                '        %s(requestId, WTFMove(parameters));' % command.command_name,
                '        return;',
                '    }',
            ])
            cases.append(self.wrap_with_guard_for_condition(command.condition, additional_command_string))

        switch_args = {
            'domainName': domain.domain_name,
            '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", &%(domainName)sBackendDispatcher::%(commandName)s },' % args))

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

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

    def _generate_async_dispatcher_class_for_domain(self, command, domain):
        out_parameter_assignments = []
        formal_parameters = []

        for parameter in command.return_parameters:
            param_args = {
                'keyedSetMethod': CppGenerator.cpp_setter_method_for_type(parameter.type),
                'parameterKey': parameter.parameter_name,
                'parameterName': parameter.parameter_name,
                'parameterType': CppGenerator.cpp_type_for_stack_in_parameter(parameter),
                'helpersNamespace': self.helpers_namespace(),
            }

            formal_parameters.append('%s %s' % (CppGenerator.cpp_type_for_formal_async_parameter(parameter), parameter.parameter_name))

            if parameter.is_optional:
                if CppGenerator.should_use_wrapper_for_return_type(parameter.type):
                    out_parameter_assignments.append('    if (%(parameterName)s.hasValue())' % param_args)
                    out_parameter_assignments.append('        jsonMessage->%(keyedSetMethod)s("%(parameterKey)s"_s, *%(parameterName)s);' % param_args)
                else:
                    out_parameter_assignments.append('    if (%(parameterName)s)' % param_args)
                    out_parameter_assignments.append('        jsonMessage->%(keyedSetMethod)s("%(parameterKey)s"_s, %(parameterName)s);' % param_args)
            elif parameter.type.is_enum():
                out_parameter_assignments.append('    jsonMessage->%(keyedSetMethod)s("%(parameterKey)s"_s, Inspector::Protocol::%(helpersNamespace)s::getEnumConstantValue(%(parameterName)s));' % param_args)
            else:
                out_parameter_assignments.append('    jsonMessage->%(keyedSetMethod)s("%(parameterKey)s"_s, %(parameterName)s);' % param_args)

        async_args = {
            'domainName': domain.domain_name,
            'callbackName': ucfirst(command.command_name) + 'Callback',
            'formalParameters': ", ".join(formal_parameters),
            'outParameterAssignments': "\n".join(out_parameter_assignments)
        }
        return self.wrap_with_guard_for_condition(command.condition, Template(CppTemplates.BackendDispatcherImplementationAsyncCommand).substitute(None, **async_args))

    def _generate_dispatcher_implementation_for_command(self, command, domain):
        in_parameter_declarations = []
        out_parameter_declarations = []
        out_parameter_assignments = []
        alternate_dispatcher_method_parameters = ['requestId']
        method_parameters = []
        if not command.is_async:
            method_parameters.append('error')

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

            out_success_argument = 'nullptr'
            if parameter.is_optional:
                out_success_argument = '&%s_valueFound' % parameter_name
                in_parameter_declarations.append('    bool %s_valueFound = false;' % parameter_name)

            # Now add appropriate operators.
            parameter_expression = parameter_name

            if CppGenerator.should_use_references_for_type(parameter.type):
                if parameter.is_optional:
                    parameter_expression = '%s.get()' % parameter_expression
                else:
                    # This assumes that we have already proved the object is non-null.
                    # If a required property is missing, InspectorBackend::getObject will
                    # append a protocol error, and the method dispatcher will return without
                    # invoking the backend method (and dereferencing the object).
                    parameter_expression = '*%s' % parameter_expression
            elif parameter.is_optional:
                parameter_expression = '&%s' % parameter_expression

            param_args = {
                'parameterType': CppGenerator.cpp_type_for_stack_in_parameter(parameter),
                'parameterKey': parameter.parameter_name,
                'parameterName': parameter_name,
                'parameterExpression': parameter_expression,
                'keyedGetMethod': CppGenerator.cpp_getter_method_for_type(parameter.type),
                'successOutParam': out_success_argument
            }

            in_parameter_declarations.append('    %(parameterType)s %(parameterName)s = m_backendDispatcher->%(keyedGetMethod)s(parameters.get(), "%(parameterKey)s"_s, %(successOutParam)s);' % param_args)

            if parameter.is_optional:
                optional_in_parameter_string = '%(parameterName)s_valueFound ? %(parameterExpression)s : nullptr' % param_args
                alternate_dispatcher_method_parameters.append(optional_in_parameter_string)
                method_parameters.append(optional_in_parameter_string)
            else:
                alternate_dispatcher_method_parameters.append(parameter_expression)
                method_parameters.append(parameter_expression)

        if command.is_async:
            async_args = {
                'domainName': domain.domain_name,
                'callbackName': ucfirst(command.command_name) + 'Callback'
            }

            out_parameter_assignments.append('        callback->disable();')
            out_parameter_assignments.append('        m_backendDispatcher->reportProtocolError(BackendDispatcher::ServerError, error);')
            out_parameter_assignments.append('        return;')
            method_parameters.append('callback.copyRef()')

        else:
            for parameter in command.return_parameters:
                param_args = {
                    'parameterType': CppGenerator.cpp_type_for_stack_out_parameter(parameter),
                    'parameterKey': parameter.parameter_name,
                    'parameterName': parameter.parameter_name,
                    'keyedSetMethod': CppGenerator.cpp_setter_method_for_type(parameter.type),
                    'helpersNamespace': self.helpers_namespace(),
                }

                out_parameter_declarations.append('    %(parameterType)s out_%(parameterName)s;' % param_args)
                if parameter.is_optional:
                    if CppGenerator.should_use_wrapper_for_return_type(parameter.type):
                        out_parameter_assignments.append('        if (out_%(parameterName)s.hasValue())' % param_args)
                        out_parameter_assignments.append('            result->%(keyedSetMethod)s("%(parameterKey)s"_s, *out_%(parameterName)s);' % param_args)
                    else:
                        out_parameter_assignments.append('        if (out_%(parameterName)s)' % param_args)
                        out_parameter_assignments.append('            result->%(keyedSetMethod)s("%(parameterKey)s"_s, out_%(parameterName)s);' % param_args)
                elif parameter.type.is_enum():
                    out_parameter_assignments.append('        result->%(keyedSetMethod)s("%(parameterKey)s"_s, Inspector::Protocol::%(helpersNamespace)s::getEnumConstantValue(out_%(parameterName)s));' % param_args)
                else:
                    out_parameter_assignments.append('        result->%(keyedSetMethod)s("%(parameterKey)s"_s, out_%(parameterName)s);' % param_args)

                if CppGenerator.should_pass_by_copy_for_return_type(parameter.type) or parameter.is_optional and CppGenerator.should_use_wrapper_for_return_type(parameter.type):
                    method_parameters.append('out_' + parameter.parameter_name)
                else:
                    method_parameters.append('&out_' + parameter.parameter_name)

        command_args = {
            'domainName': domain.domain_name,
            'callbackName': '%sCallback' % ucfirst(command.command_name),
            'commandName': command.command_name,
            'inParameterDeclarations': '\n'.join(in_parameter_declarations),
            'invocationParameters': ', '.join(method_parameters),
            'alternateInvocationParameters': ', '.join(alternate_dispatcher_method_parameters),
        }

        lines = []
        if len(command.call_parameters) == 0:
            lines.append('void %(domainName)sBackendDispatcher::%(commandName)s(long requestId, RefPtr<JSON::Object>&&)' % command_args)
        else:
            lines.append('void %(domainName)sBackendDispatcher::%(commandName)s(long requestId, RefPtr<JSON::Object>&& 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 command.is_async:
            lines.append('    Ref<%(domainName)sBackendDispatcherHandler::%(callbackName)s> callback = adoptRef(*new %(domainName)sBackendDispatcherHandler::%(callbackName)s(m_backendDispatcher.copyRef(), requestId));' % command_args)
        else:
            lines.append('    ErrorString error;')
            lines.append('    Ref<JSON::Object> result = JSON::Object::create();')

        if len(command.return_parameters) > 0:
            lines.extend(out_parameter_declarations)
        lines.append('    m_agent->%(commandName)s(%(invocationParameters)s);' % command_args)
        lines.append('')

        if not command.is_async:
            if len(command.return_parameters) > 1:
                lines.append('    if (!error.length()) {')
                lines.extend(out_parameter_assignments)
                lines.append('    }')
            elif len(command.return_parameters) == 1:
                lines.append('    if (!error.length())')
                lines.extend(out_parameter_assignments)
                lines.append('')

            lines.append('    if (!error.length())')
            lines.append('        m_backendDispatcher->sendResponse(requestId, WTFMove(result), false);')
            lines.append('    else')
            lines.append('        m_backendDispatcher->reportProtocolError(BackendDispatcher::ServerError, WTFMove(error));')

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