/*
 *
 * Copyright 2015, Google 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:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation AN/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT
 * OWNER 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.
 *
 */

#include <map>
#include <cctype>
#include <sstream>

#include "src/compiler/go_generator.h"

template <class T>
grpc::string as_string(T x) {
	std::ostringstream out;
	out << x;
	return out.str();
}

inline bool ClientOnlyStreaming(const grpc_generator::Method *method) {
  return method->ClientStreaming() && !method->ServerStreaming();
}

inline bool ServerOnlyStreaming(const grpc_generator::Method *method) {
  return !method->ClientStreaming() && method->ServerStreaming();
}

namespace grpc_go_generator {

// Returns string with first letter to lowerCase
grpc::string unexportName(grpc::string s) {
	if (s.empty())
		return s;
	s[0] = static_cast<char>(std::tolower(s[0]));
	return s;
}

// Returns string with first letter to uppercase
grpc::string exportName(grpc::string s) {
	if (s.empty())
		return s;
	s[0] = static_cast<char>(std::toupper(s[0]));
	return s;
}

void GenerateError(grpc_generator::Printer *printer,
                   std::map<grpc::string, grpc::string> vars,
                   const bool multiple_return = true) {
  printer->Print(vars, "if $Error_Check$ {\n");
  printer->Indent();
  vars["Return"] = multiple_return ? "nil, err" : "err";
  printer->Print(vars, "return $Return$\n");
  printer->Outdent();
  printer->Print("}\n");
}

// Generates imports for the service
void GenerateImports(grpc_generator::File *file, grpc_generator::Printer *printer,
                     std::map<grpc::string, grpc::string> vars) {
	vars["filename"] = file->filename();
	printer->Print("//Generated by gRPC Go plugin\n");
	printer->Print("//If you make any local changes, they will be lost\n");
	printer->Print(vars, "//source: $filename$\n\n");
	printer->Print(vars, "package $Package$\n\n");
	printer->Print("import (\n");
	printer->Indent();
	printer->Print(vars, "$context$ \"context\"\n");
  printer->Print("flatbuffers \"github.com/google/flatbuffers/go\"\n");
	printer->Print(vars, "$grpc$ \"google.golang.org/grpc\"\n");
  printer->Print("\"google.golang.org/grpc/codes\"\n");
  printer->Print("\"google.golang.org/grpc/status\"\n");
	printer->Outdent();
	printer->Print(")\n\n");
}

// Generates Server method signature source
void GenerateServerMethodSignature(const grpc_generator::Method *method, grpc_generator::Printer *printer,
                                   std::map<grpc::string, grpc::string> vars) {
  vars["Method"] = exportName(method->name());
	vars["Request"] = method->get_input_type_name();
	vars["Response"] = (vars["CustomMethodIO"] == "") ? method->get_output_type_name() : vars["CustomMethodIO"];
	if (method->NoStreaming()) {
		printer->Print(vars, "$Method$($context$.Context, *$Request$) (*$Response$, error)$Ending$");
	} else if (ServerOnlyStreaming(method)) {
		printer->Print(vars, "$Method$(*$Request$, $Service$_$Method$Server) error$Ending$");
	} else {
		printer->Print(vars, "$Method$($Service$_$Method$Server) error$Ending$");
	}
}

void GenerateServerMethod(const grpc_generator::Method *method, grpc_generator::Printer *printer,
                          std::map<grpc::string, grpc::string> vars) {
	vars["Method"] = exportName(method->name());
	vars["Request"] = method->get_input_type_name();
	vars["Response"] = (vars["CustomMethodIO"] == "") ? method->get_output_type_name() : vars["CustomMethodIO"];
	vars["FullMethodName"] = "/" + vars["ServicePrefix"] + vars["Service"] + "/" + vars["Method"];
	vars["Handler"] = "_" + vars["Service"] + "_" + vars["Method"] + "_Handler";
	if (method->NoStreaming()) {
		printer->Print(vars, "func $Handler$(srv interface{}, ctx $context$.Context,\n\tdec func(interface{}) error, interceptor $grpc$.UnaryServerInterceptor) (interface{}, error) {\n");
		printer->Indent();
		printer->Print(vars, "in := new($Request$)\n");
    vars["Error_Check"] = "err := dec(in); err != nil";
    GenerateError(printer, vars);
    printer->Print("if interceptor == nil {\n");
    printer->Indent();
    printer->Print(vars, "return srv.($Service$Server).$Method$(ctx, in)\n");
    printer->Outdent();
    printer->Print("}\n");
		printer->Print(vars, "info := &$grpc$.UnaryServerInfo{\n");
		printer->Indent();
		printer->Print("Server:     srv,\n");
		printer->Print(vars, "FullMethod: \"$FullMethodName$\",\n");
		printer->Outdent();
		printer->Print("}\n");
    printer->Outdent();
    printer->Print("\n");
    printer->Indent();
		printer->Print(vars, "handler := func(ctx $context$.Context, req interface{}) (interface{}, error) {\n");
		printer->Indent();
		printer->Print(vars, "return srv.($Service$Server).$Method$(ctx, req.(*$Request$))\n");
		printer->Outdent();
		printer->Print("}\n");
		printer->Print("return interceptor(ctx, in, info, handler)\n");
		printer->Outdent();
		printer->Print("}\n");
		return;
	}
	vars["StreamType"] = vars["ServiceUnexported"] + vars["Method"] + "Server";
	printer->Print(vars, "func $Handler$(srv interface{}, stream $grpc$.ServerStream) error {\n");
	printer->Indent();
	if (ServerOnlyStreaming(method)) {
		printer->Print(vars, "m := new($Request$)\n");
    vars["Error_Check"] = "err := stream.RecvMsg(m); err != nil";
    GenerateError(printer, vars, false);
		printer->Print(vars, "return srv.($Service$Server).$Method$(m, &$StreamType${stream})\n");
	} else {
		printer->Print(vars, "return srv.($Service$Server).$Method$(&$StreamType${stream})\n");
	}
	printer->Outdent();
	printer->Print("}\n\n");

	bool genSend = method->BidiStreaming() || ServerOnlyStreaming(method);
	bool genRecv = method->BidiStreaming() || ClientOnlyStreaming(method);
	bool genSendAndClose = ClientOnlyStreaming(method);

	printer->Print(vars, "type $Service$_$Method$Server interface {\n");
	printer->Indent();
	if (genSend) {
		printer->Print(vars, "Send(*$Response$) error\n");
	}
	if (genRecv) {
		printer->Print(vars, "Recv() (*$Request$, error)\n");
	}
	if (genSendAndClose) {
		printer->Print(vars, "SendAndClose(*$Response$) error\n");
	}
	printer->Print(vars, "$grpc$.ServerStream\n");
	printer->Outdent();
	printer->Print("}\n\n");

	printer->Print(vars, "type $StreamType$ struct {\n");
	printer->Indent();
	printer->Print(vars, "$grpc$.ServerStream\n");
	printer->Outdent();
	printer->Print("}\n\n");

	if (genSend) {
		printer->Print(vars, "func (x *$StreamType$) Send(m *$Response$) error {\n");
		printer->Indent();
		printer->Print("return x.ServerStream.SendMsg(m)\n");
		printer->Outdent();
		printer->Print("}\n\n");
	}
	if (genRecv) {
		printer->Print(vars, "func (x *$StreamType$) Recv() (*$Request$, error) {\n");
		printer->Indent();
		printer->Print(vars, "m := new($Request$)\n");
    vars["Error_Check"] = "err := x.ServerStream.RecvMsg(m); err != nil";
    GenerateError(printer, vars);
		printer->Print("return m, nil\n");
		printer->Outdent();
		printer->Print("}\n\n");
	}
	if (genSendAndClose) {
		printer->Print(vars, "func (x *$StreamType$) SendAndClose(m *$Response$) error {\n");
		printer->Indent();
		printer->Print("return x.ServerStream.SendMsg(m)\n");
		printer->Outdent();
		printer->Print("}\n\n");
	}

}

// Generates Client method signature source
void GenerateClientMethodSignature(const grpc_generator::Method *method, grpc_generator::Printer *printer,
                                   std::map<grpc::string, grpc::string> vars) {
	vars["Method"] = exportName(method->name());
	vars["Request"] = ", in *" + ((vars["CustomMethodIO"] == "") ? method->get_input_type_name() : vars["CustomMethodIO"]);
	if (ClientOnlyStreaming(method) || method->BidiStreaming()) {
		vars["Request"] = "";
	}
	vars["Response"] = "*" + method->get_output_type_name();
	if (ClientOnlyStreaming(method) || method->BidiStreaming() || ServerOnlyStreaming(method)) {
		vars["Response"] = vars["Service"] + "_" + vars["Method"] + "Client" ;
	}
	printer->Print(vars, "$Method$(ctx $context$.Context$Request$,\n\topts ...$grpc$.CallOption) ($Response$, error)$Ending$");
}

// Generates Client method source
void GenerateClientMethod(const grpc_generator::Method *method, grpc_generator::Printer *printer,
                          std::map<grpc::string, grpc::string> vars) {
	printer->Print(vars, "func (c *$ServiceUnexported$Client) ");
  vars["Ending"] = " {\n";
	GenerateClientMethodSignature(method, printer, vars);
	printer->Indent();
	vars["Method"] = exportName(method->name());
	vars["Request"] = (vars["CustomMethodIO"] == "") ? method->get_input_type_name() : vars["CustomMethodIO"];
	vars["Response"] = method->get_output_type_name();
	vars["FullMethodName"] = "/" + vars["ServicePrefix"] + vars["Service"] + "/" + vars["Method"];
	if (method->NoStreaming()) {
		printer->Print(vars, "out := new($Response$)\n");
		printer->Print(vars, "err := c.cc.Invoke(ctx, \"$FullMethodName$\", in, out, opts...)\n");
    vars["Error_Check"] = "err != nil";
    GenerateError(printer, vars);
		printer->Print("return out, nil\n");
		printer->Outdent();
		printer->Print("}\n\n");
		return;
	}
	vars["StreamType"] = vars["ServiceUnexported"] + vars["Method"] + "Client";
	printer->Print(vars, "stream, err := c.cc.NewStream(ctx, &$MethodDesc$, \"$FullMethodName$\", opts...)\n");
  vars["Error_Check"] = "err != nil";
  GenerateError(printer, vars);

	printer->Print(vars, "x := &$StreamType${stream}\n");
	if (ServerOnlyStreaming(method)) {
    vars["Error_Check"] = "err := x.ClientStream.SendMsg(in); err != nil";
    GenerateError(printer, vars);
    vars["Error_Check"] = "err := x.ClientStream.CloseSend(); err != nil";
    GenerateError(printer, vars);
	}
	printer->Print("return x, nil\n");
	printer->Outdent();
	printer->Print("}\n\n");

	bool genSend = method->BidiStreaming() || ClientOnlyStreaming(method);
	bool genRecv = method->BidiStreaming() || ServerOnlyStreaming(method);
	bool genCloseAndRecv = ClientOnlyStreaming(method);

	//Stream interface
	printer->Print(vars, "type $Service$_$Method$Client interface {\n");
	printer->Indent();
	if (genSend) {
		printer->Print(vars, "Send(*$Request$) error\n");
	}
	if (genRecv) {
		printer->Print(vars, "Recv() (*$Response$, error)\n");
	}
	if (genCloseAndRecv) {
		printer->Print(vars, "CloseAndRecv() (*$Response$, error)\n");
	}
	printer->Print(vars, "$grpc$.ClientStream\n");
	printer->Outdent();
	printer->Print("}\n\n");

	//Stream Client
	printer->Print(vars, "type $StreamType$ struct {\n");
	printer->Indent();
	printer->Print(vars, "$grpc$.ClientStream\n");
	printer->Outdent();
	printer->Print("}\n\n");

	if (genSend) {
		printer->Print(vars, "func (x *$StreamType$) Send(m *$Request$) error {\n");
		printer->Indent();
		printer->Print("return x.ClientStream.SendMsg(m)\n");
		printer->Outdent();
		printer->Print("}\n\n");
	}

	if (genRecv) {
		printer->Print(vars, "func (x *$StreamType$) Recv() (*$Response$, error) {\n");
		printer->Indent();
		printer->Print(vars, "m := new($Response$)\n");
    vars["Error_Check"] = "err := x.ClientStream.RecvMsg(m); err != nil";
    GenerateError(printer, vars);
		printer->Print("return m, nil\n");
		printer->Outdent();
		printer->Print("}\n\n");
	}

	if (genCloseAndRecv) {
		printer->Print(vars, "func (x *$StreamType$) CloseAndRecv() (*$Response$, error) {\n");
		printer->Indent();
    vars["Error_Check"] = "err := x.ClientStream.CloseSend(); err != nil";
    GenerateError(printer, vars);
		printer->Print(vars, "m := new($Response$)\n");
    vars["Error_Check"] = "err := x.ClientStream.RecvMsg(m); err != nil";
    GenerateError(printer, vars);
		printer->Print("return m, nil\n");
		printer->Outdent();
		printer->Print("}\n\n");
	}
}

// Generates client API for the service
void GenerateService(const grpc_generator::Service *service, grpc_generator::Printer* printer,
                     std::map<grpc::string, grpc::string> vars) {
	vars["Service"] = exportName(service->name());
	// Client Interface
	printer->Print(vars, "// Client API for $Service$ service\n");
	printer->Print(vars, "type $Service$Client interface {\n");
	printer->Indent();
  vars["Ending"] = "\n";
	for (int i = 0; i < service->method_count(); i++) {
		GenerateClientMethodSignature(service->method(i).get(), printer, vars);
	}
	printer->Outdent();
	printer->Print("}\n\n");

	// Client structure
	vars["ServiceUnexported"] = unexportName(vars["Service"]);
	printer->Print(vars, "type $ServiceUnexported$Client struct {\n");
	printer->Indent();
	printer->Print(vars, "cc $grpc$.ClientConnInterface\n");
	printer->Outdent();
	printer->Print("}\n\n");

	// NewClient
	printer->Print(vars, "func New$Service$Client(cc $grpc$.ClientConnInterface) $Service$Client {\n");
	printer->Indent();
	printer->Print(vars, "return &$ServiceUnexported$Client{cc}");
	printer->Outdent();
	printer->Print("\n}\n\n");

	int unary_methods = 0, streaming_methods = 0;
	vars["ServiceDesc"] = "_" + vars["Service"] + "_serviceDesc";
	for (int i = 0; i < service->method_count(); i++) {
		auto method = service->method(i);
		if (method->NoStreaming()) {
			vars["MethodDesc"] = vars["ServiceDesc"] + ".Method[" + as_string(unary_methods) + "]";
			unary_methods++;
		} else {
			vars["MethodDesc"] = vars["ServiceDesc"] + ".Streams[" + as_string(streaming_methods) + "]";
			streaming_methods++;
		}
		GenerateClientMethod(method.get(), printer, vars);
	}

	//Server Interface
	printer->Print(vars, "// Server API for $Service$ service\n");
	printer->Print(vars, "type $Service$Server interface {\n");
	printer->Indent();
  vars["Ending"] = "\n";
	for (int i = 0; i < service->method_count(); i++) {
		GenerateServerMethodSignature(service->method(i).get(), printer, vars);
	}
  printer->Print(vars, "mustEmbedUnimplemented$Service$Server()\n");
	printer->Outdent();
	printer->Print("}\n\n");

  printer->Print(vars, "type Unimplemented$Service$Server struct {\n");
  printer->Print("}\n\n");

  vars["Ending"] = " {\n";
  for (int i = 0; i < service->method_count(); i++) {
    auto method = service->method(i);
    vars["Method"] = exportName(method->name());
    vars["Nil"] = method->NoStreaming() ? "nil, " : "";
    printer->Print(vars, "func (Unimplemented$Service$Server) ");
    GenerateServerMethodSignature(method.get(), printer, vars);
    printer->Indent();
    printer->Print(vars, "return $Nil$status.Errorf(codes.Unimplemented, \"method $Method$ not implemented\")\n");
    printer->Outdent();
    printer->Print("}\n");
    printer->Print("\n");
  }

  printer->Print(vars, "func (Unimplemented$Service$Server) mustEmbedUnimplemented$Service$Server() {}");
  printer->Print("\n\n");

  printer->Print(vars, "type Unsafe$Service$Server interface {\n");
  printer->Indent();
  printer->Print(vars, "mustEmbedUnimplemented$Service$Server()\n");
  printer->Outdent();
  printer->Print("}\n\n");
	// Server registration.
	printer->Print(vars, "func Register$Service$Server(s $grpc$.ServiceRegistrar, srv $Service$Server) {\n");
	printer->Indent();
	printer->Print(vars, "s.RegisterService(&$ServiceDesc$, srv)\n");
	printer->Outdent();
	printer->Print("}\n\n");

	for (int i = 0; i < service->method_count(); i++) {
		GenerateServerMethod(service->method(i).get(), printer, vars);
	}


	//Service Descriptor
	printer->Print(vars, "var $ServiceDesc$ = $grpc$.ServiceDesc{\n");
	printer->Indent();
	printer->Print(vars, "ServiceName: \"$ServicePrefix$$Service$\",\n");
	printer->Print(vars, "HandlerType: (*$Service$Server)(nil),\n");
	printer->Print(vars, "Methods: []$grpc$.MethodDesc{\n");
	printer->Indent();
	for (int i = 0; i < service->method_count(); i++) {
		auto method = service->method(i);
		vars["Method"] = exportName(method->name());
		vars["Handler"] = "_" + vars["Service"] + "_" + vars["Method"] + "_Handler";
		if (method->NoStreaming()) {
			printer->Print("{\n");
			printer->Indent();
			printer->Print(vars, "MethodName: \"$Method$\",\n");
			printer->Print(vars, "Handler:    $Handler$,\n");
			printer->Outdent();
			printer->Print("},\n");
		}
	}
	printer->Outdent();
	printer->Print("},\n");
	printer->Print(vars, "Streams: []$grpc$.StreamDesc{\n");
	printer->Indent();
	for (int i = 0; i < service->method_count(); i++) {
		auto method = service->method(i);
		vars["Method"] = exportName(method->name());
		vars["Handler"] = "_" + vars["Service"] + "_" + vars["Method"] + "_Handler";
		if (!method->NoStreaming()) {
			printer->Print("{\n");
			printer->Indent();
			printer->Print(vars, "StreamName:    \"$Method$\",\n");
			printer->Print(vars, "Handler:       $Handler$,\n");
			if (ClientOnlyStreaming(method.get())) {
				printer->Print("ClientStreams: true,\n");
			} else if (ServerOnlyStreaming(method.get())) {
				printer->Print("ServerStreams: true,\n");
			} else {
				printer->Print("ServerStreams: true,\n");
				printer->Print("ClientStreams: true,\n");
			}
			printer->Outdent();
			printer->Print("},\n");
		}
	}
	printer->Outdent();
	printer->Print("},\n");
	printer->Outdent();
	printer->Print("}\n");

}


// Returns source for the service
grpc::string GenerateServiceSource(grpc_generator::File *file,
                                   const grpc_generator::Service *service,
                                   grpc_go_generator::Parameters *parameters) {
	grpc::string out;
	auto p = file->CreatePrinter(&out, '\t');
  p->SetIndentationSize(1);
	auto printer = p.get();
	std::map<grpc::string, grpc::string> vars;
	vars["Package"] = parameters->package_name;
	vars["ServicePrefix"] = parameters->service_prefix;
  if (!parameters->service_prefix.empty())
    vars["ServicePrefix"].append(".");
	vars["grpc"] = "grpc";
	vars["context"] = "context";
	GenerateImports(file, printer, vars);
	if (parameters->custom_method_io_type != "") {
		vars["CustomMethodIO"] = parameters->custom_method_io_type;
	}
	GenerateService(service, printer, vars);
	return out;
}
}// Namespace grpc_go_generator
