#!/usr/bin/python3
"""
* Copyright 2023 Centreon
*
* Licensed under the Apache License, Version 2.0(the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* For more information : contact@centreon.com
"""

from os import listdir
from os.path import isfile, join
import re
import argparse
import sys


file_begin_content = """syntax = "proto3";

import "opentelemetry/proto/collector/metrics/v1/metrics_service.proto";
"""

file_message_centreon_event = """
message CentreonEvent {
    oneof content {
        bytes buffer = 1;
"""

cc_file_begin_content = """/**
 * Copyright 2023 Centreon (https://www.centreon.com/)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * For more information : contact@centreon.com
 *
 *
 *  File generated by generate_proto.py
 *
 */

#include "grpc_stream.pb.h"
#include "com/centreon/broker/io/protobuf.hh"

#include "com/centreon/broker/grpc/stream.hh"
#include "common/log_v2/log_v2.hh"

using namespace com::centreon::broker;
using com::centreon::common::log_v2::log_v2;

namespace com::centreon::broker::grpc {
namespace detail {

/**
 * @brief the goal of this class is to not copy protobuf object
 * event created by grpc receive layer is embedded in this object subclass of
 * io::protobuf
 *
 * @tparam T A Protobuf message
 * @tparam Typ The type to associate to this class as a BBDO type.
 */
template <typename T, uint32_t Typ>
class received_protobuf : public io::protobuf<T, Typ> {
 public:
  typedef const T& (grpc_event_type::*const_accessor)() const;
  typedef T* (grpc_event_type::*mutable_accessor)();

 private:
  event_ptr _received;
  const_accessor _const_access;
  mutable_accessor _mutable_access;

 public:
  /**
   * @brief Construct a new received protobuf object
   *
   * @param received object created by grpc layer
   * @param const_access accessor to const T such as grpc_event_type::welcome_()
   * @param mutable_access accessor to T such as
   * grpc_event_type::mutable_welcome_()
   */
  received_protobuf(const event_ptr received,
                    const_accessor const_access,
                    mutable_accessor mutable_access)
      : _received(received),
        _const_access(const_access),
        _mutable_access(mutable_access) {
    io::data::source_id = received->source_id();
    io::data::destination_id = received->destination_id();
    io::protobuf_base::set_message(((*_received).*_mutable_access)());
  }

  const T& obj() const override{ return ((*_received).*_const_access)(); }

  T& mut_obj() override { return *((*_received).*_mutable_access)(); }

  void set_obj([[maybe_unused]] T&& obj) override {
    throw com::centreon::exceptions::msg_fmt("unauthorized usage {}",
                                             static_cast<const char*>(typeid(*this).name()));
  }
};
  
}  // namespace detail

"""


cc_file_protobuf_to_event_function = """
/**
 * @brief this function creates a io::protobuf_object from grpc received message
 *
 * @param stream_content message received
 * @return std::shared_ptr<io::data> shared_ptr<io::protobuf<xxx>>, null if
 * unknown content received
 */
std::shared_ptr<io::data> protobuf_to_event(const event_ptr & stream_content) {
    switch(stream_content->content_case()) {
"""

cc_file_create_event_with_data_function = """

/**
 * @brief this function create a event_with_data structure that will be send on grpc.
 * stream_content don't have a copy of event, so event mustn't be
 * deleted before stream_content
 *
 * @param event to send
 * @return object used for send on the wire
 */
std::shared_ptr<event_with_data> create_event_with_data(const std::shared_ptr<io::data> & event) {
    std::shared_ptr<event_with_data> ret;
    switch(event->type()) {
"""
cc_file_create_event_with_data_function_end = """
    default:
      {
        auto logger = log_v2::instance().get(log_v2::GRPC);
        SPDLOG_LOGGER_ERROR(logger, "unable to send unknown event type: {}", *event);
      }  
    }
    if (ret) {
        ret->grpc_event.set_destination_id(event->destination_id);
        ret->grpc_event.set_source_id(event->source_id);
    }
    return ret;
}
"""


parser = argparse.ArgumentParser(
    prog='generate_proto.py', description='generate grpc_stream.proto file by referencing message found in protobuf files')
parser.add_argument('-f', '--proto_file',
                    help='outputs files use by grpc module', required=True)
parser.add_argument('-c', '--cc_file',
                    help='output source file that must be compiled and linked int target library', required=True)
parser.add_argument('-d', '--proto_directory',
                    help='directory where to find protobuf files', required=True, action='append')

args = parser.parse_args()

message_parser = r'^message\s+(\w+)\s+\{'
io_protobuf_parser = r'\/\*\s*(\w+::\w+\s*,\s*\w+::\w+)\s*,\s*(\d+)\s*\*\/'
ignore_message = "/* Ignore */"

message_save = []

for directory in args.proto_directory:
    proto_files = [f for f in listdir(
        directory) if f[-6:] == ".proto" and isfile(join(directory, f))]
    for file in proto_files:
        line_counter = 0
        flag_ignore = False
        with open(join(directory, file)) as proto_file:
            messages = []
            io_protobuf_match = None
            for line in proto_file.readlines():
                line_counter += 1
                m = re.match(message_parser, line)
                if m and io_protobuf_match:
                    # Check that the message and the io_protobuf_match are coherent
                    # Let's take the message name and remove the de_pb_ prefix if it exists
                    message_name = io_protobuf_match.group(1).split(',')[
                        1].split('::')[1]
                    message_name = message_name[3:] if message_name.startswith(
                        'de_') else message_name
                    message_name = message_name[3:] if message_name.startswith(
                        'pb_') else message_name
                    # Let's change the name into SnakeCase
                    message_name = ''.join(word.title()
                                           for word in message_name.split('_'))
                    if m.group(1) != message_name:
                        print(
                            f"generate_proto.py : Error: Message {{ {m.group(1)} }} does not match the io_protobuf_match {{ {io_protobuf_match[1]} }} : file :{file}:{line_counter}", file=sys.stderr)
                        exit(2)
                    messages.append(
                        [m.group(1), io_protobuf_match.group(1), io_protobuf_match.group(2)])
                    io_protobuf_match = None
                    flag_ignore = True
                else:
                    io_protobuf_match = re.match(io_protobuf_parser, line)

                # check if no bbo message have the comment: Ignore
                if ignore_message in line:
                    flag_ignore = True
                # check if message has comment ignore or it's bbdo message
                if flag_ignore and m:
                    flag_ignore = False
                elif not flag_ignore and m:
                    print(
                        f"generate_proto.py : Error: Message {{ {m.group(1)} }} has no protobuf id or missing the comment /* Ignore */ : file :{file}:{line_counter}", file=sys.stderr)
                    print(
                        f"Error Add /* Ignore */ or a protobuf id as example: /*io::bam, bam::de_pb_services_book_state, XXX */", file=sys.stderr)
                    exit(1)

        if len(messages) > 0:
            file_begin_content += f"import \"{file}\";\n"
            message_save += messages
# sort the message with index (io_protobuf_match.group(2))
message_save.sort(key=lambda x: int(x[2]))
for mess, id, index in message_save:
    # proto file
    file_message_centreon_event += f"        {mess} {mess}_ = {index};\n"
    # count index : needed for opentelemetry
    lower_mess = mess.lower()
    # cc file
    cc_file_protobuf_to_event_function += f"""        case ::stream::CentreonEvent::k{mess}:
return std::make_shared<detail::received_protobuf<
    {mess}, make_type({id})>>(
    stream_content, &grpc_event_type::{lower_mess}_,
    &grpc_event_type::mutable_{lower_mess}_);
"""
    cc_file_create_event_with_data_function += f"""        case make_type({id}):
    ret = std::make_shared<event_with_data>(
        event, reinterpret_cast<event_with_data::releaser_type>(
        &grpc_event_type::release_{lower_mess}_));
    ret->grpc_event.set_allocated_{lower_mess}_(&std::static_pointer_cast<io::protobuf<{mess}, make_type({id})>>(event)->mut_obj());
    break;

"""

# The following message is not in bbdo protobuff files so we need to add manually.
# 128 is reserved for this protobuf message
file_message_centreon_event += "        opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest ExportMetricsServiceRequest_ = 128;\n"

cc_file_protobuf_to_event_function += """
        case ::stream::CentreonEvent::kExportMetricsServiceRequest:
        return std::make_shared<detail::received_protobuf<
            ::opentelemetry::proto::collector::metrics::v1::
                ExportMetricsServiceRequest,
            make_type(io::storage, storage::de_pb_otl_metrics)>>(
            stream_content, &grpc_event_type::exportmetricsservicerequest_,
            &grpc_event_type::mutable_exportmetricsservicerequest_);
"""

cc_file_create_event_with_data_function += """
        case make_type(io::storage, storage::de_pb_otl_metrics):
        ret = std::make_shared<event_with_data>(
            event, reinterpret_cast<event_with_data::releaser_type>(
                        &grpc_event_type::release_exportmetricsservicerequest_));
        ret->grpc_event.set_allocated_exportmetricsservicerequest_(
            &std::static_pointer_cast<io::protobuf<
                ::opentelemetry::proto::collector::metrics::v1::
                    ExportMetricsServiceRequest,
                make_type(io::storage, storage::de_pb_otl_metrics)>>(event)
                ->mut_obj());
        break;
"""

with open(args.proto_file, 'w', encoding="utf-8") as fp:
    fp.write(file_begin_content)
    fp.write("""

package com.centreon.broker.stream;

""")
    fp.write(file_message_centreon_event)
    fp.write("""
    }
    uint32 destination_id = 126;
    uint32 source_id = 127;
}

service centreon_bbdo {
    //emitter connect to receiver
    rpc exchange(stream CentreonEvent) returns (stream CentreonEvent) {}
}
                          
""")

with open(args.cc_file, 'w') as fp:
    fp.write(cc_file_begin_content)
    fp.write(cc_file_protobuf_to_event_function)
    fp.write("""        default:
      {
        auto logger = log_v2::instance().get(log_v2::GRPC);
        SPDLOG_LOGGER_ERROR(logger, "unknown content type: {} => ignored",
                            static_cast<uint32_t>(stream_content->content_case()));
        return std::shared_ptr<io::data>();
      }
    }
}


""")
    fp.write(cc_file_create_event_with_data_function)
    fp.write(cc_file_create_event_with_data_function_end)
    fp.write("""

} // namespace com::centreon::broker::grpc

""")
