#!/usr/bin/env python
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import sys
import string
import json

blink_protocol_path = sys.argv[1]
browser_protocol_path = sys.argv[2]
output_cc_path = sys.argv[3]
output_h_path = sys.argv[4]

header = """\
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// THIS FILE IS AUTOGENERATED. DO NOT EDIT.
// Generated by
//  content/public/browser/devtools_protocol_handler_generator.py from
//  third_party/WebKit/Source/devtools/protocol.json and
//  content/browser/devtools/browser_protocol.json
"""

template_h = string.Template(header + """\

#ifndef CONTENT_BROWSER_DEVTOOLS_PROTOCOL_DEVTOOLS_PROTOCOL_DISPATCHER_H_
#define CONTENT_BROWSER_DEVTOOLS_PROTOCOL_DEVTOOLS_PROTOCOL_DISPATCHER_H_

#include "content/browser/devtools/protocol/devtools_protocol_client.h"

namespace content {

class DevToolsProtocolDispatcher;

namespace devtools {

extern const char kProtocolVersion[];

bool IsSupportedProtocolVersion(const std::string& version);

template<typename T>
base::Value* CreateValue(const T& param) {
  return new base::FundamentalValue(param);
}

template<class T>
base::Value* CreateValue(scoped_ptr<T>& param) {
  return param.release();
}

template<class T>
base::Value* CreateValue(scoped_refptr<T> param) {
  return param->ToValue().release();
}

template<typename T>
base::Value* CreateValue(const std::vector<T> param) {
  base::ListValue* result = new base::ListValue();
  for (auto& item : param) {
    result->Append(CreateValue(item));
  }
  return result;
}

template<>
base::Value* CreateValue(const std::string& param);

${types}\

}  // namespace devtools

class DevToolsProtocolDispatcher {
 public:
  using Notifier = DevToolsProtocolClient::RawMessageCallback;
  using CommandHandler =
      base::Callback<bool(int, scoped_ptr<base::DictionaryValue>)>;

  explicit DevToolsProtocolDispatcher(const Notifier& notifier);
  ~DevToolsProtocolDispatcher();

  CommandHandler FindCommandHandler(const std::string& method);

${setters}\

 private:
  using Response = DevToolsProtocolClient::Response;
  using CommandHandlers = std::map<std::string, CommandHandler>;

${methods}\

  Notifier notifier_;
  DevToolsProtocolClient client_;
  CommandHandlers command_handlers_;
${fields}\
};

}  // namespace content

#endif  // CONTENT_BROWSER_DEVTOOLS_PROTOCOL_DEVTOOLS_PROTOCOL_DISPATCHER_H_
""")

tmpl_typedef = string.Template("""\
namespace ${domain} {
typedef ${param_type} ${declared_name};
}  // namespace ${domain}
""")

tmpl_struct = string.Template("""\
namespace ${domain} {
template<int MASK>
struct ${declared_name}Builder
    : base::RefCounted<${declared_name}Builder<MASK>> {
 public:
  enum {
    kAllSet = 0,
${fields_enum}\
  };

${methods}\

  static scoped_refptr<${declared_name}Builder<kNoneSet>> Create() {
    return new ${declared_name}Builder<kNoneSet>();
  }

  scoped_ptr<base::DictionaryValue> ToValue() {
    COMPILE_ASSERT(MASK == kAllSet, required_properties_missing);
    return make_scoped_ptr(dict_->DeepCopy());
  }

 private:
  friend struct ${declared_name}Builder<0>;

  ${declared_name}Builder() : dict_(new base::DictionaryValue()) {
  }

  template<class T> T* ThisAs() {
    COMPILE_ASSERT(sizeof(*this) == sizeof(T), cannot_cast);
    return reinterpret_cast<T*>(this);
  }

  scoped_ptr<base::DictionaryValue> dict_;
};

typedef ${declared_name}Builder<0> ${declared_name};

}  // namespace ${domain}
""")

tmpl_builder_setter_req = string.Template("""\
  scoped_refptr<${declared_name}Builder<MASK & ~k${Param}>>
  set_${param}(${pass_type} ${param}) {
    COMPILE_ASSERT(MASK & k${Param}, already_set);
    dict_->Set("${proto_param}", CreateValue(${param}));
    return ThisAs<${declared_name}Builder<MASK & ~k${Param}>>();
  }
""")

tmpl_builder_setter_opt = string.Template("""\
  scoped_refptr<${declared_name}Builder<MASK>>
  set_${param}(${pass_type} ${param}) {
    dict_->Set("${proto_param}", CreateValue(${param}));
    return this;
  }
""")

tmpl_builder_enum = string.Template("""\
    k${Param} = 1 << ${ordinal},
""")

tmpl_builder_none_set = string.Template("""\
    kNoneSet = ${all_fields}
""")

tmpl_enum = string.Template("""\
namespace ${domain} {
namespace ${subdomain} {
${values}\
}  // namespace ${subdomain}
}  // namespace ${domain}
""")

tmpl_enum_value = string.Template("""\
extern const char k${Param}${Value}[];
""")

tmpl_enum_value_def = string.Template("""\
const char k${Param}${Value}[] = "${value}";
""")

tmpl_handler = string.Template("""\
namespace ${domain} {
class ${Domain}Handler;
}  // namespace domain
""")

tmpl_client = string.Template("""\
namespace ${domain} {
class Client : public DevToolsProtocolClient {
 public:
  explicit Client(const RawMessageCallback& raw_message_callback);
  virtual ~Client();

${methods}\
};
}  // namespace ${domain}
""")

tmpl_event = string.Template("""\
  void ${Command}(
      scoped_refptr<${Command}Params> params);
""")

tmpl_response = string.Template("""\
  void Send${Command}Response(
      DevToolsCommandId command_id,
      scoped_refptr<${Command}Response> params);
""")

tmpl_setter = string.Template("""\
  void Set${Domain}Handler(
      devtools::${domain}::${Domain}Handler* ${domain}_handler);
""")

tmpl_callback = string.Template("""\
  bool On${Domain}${Command}(
      DevToolsCommandId command_id,
      scoped_ptr<base::DictionaryValue> params);
""")

tmpl_field = string.Template("""\
  devtools::${domain}::${Domain}Handler* ${domain}_handler_;
""")

template_cc = string.Template(header + """\

#include "content/browser/devtools/protocol/devtools_protocol_handler.h"

#include "base/bind.h"
#include "base/strings/string_number_conversions.h"
${includes}\

namespace content {

DevToolsProtocolDispatcher::DevToolsProtocolDispatcher(
    const Notifier& notifier)
    : notifier_(notifier),
      client_(notifier),
      ${fields_init} {
}

DevToolsProtocolDispatcher::~DevToolsProtocolDispatcher() {
}

DevToolsProtocolDispatcher::CommandHandler
DevToolsProtocolDispatcher::FindCommandHandler(const std::string& method) {
  CommandHandlers::iterator it = command_handlers_.find(method);
  return it == command_handlers_.end() ? CommandHandler() : it->second;
}

${methods}\

namespace devtools {

const char kProtocolVersion[] = "${major}.${minor}";

bool IsSupportedProtocolVersion(const std::string& version) {
  std::vector<std::string> tokens;
  Tokenize(version, ".", &tokens);
  int major, minor;
  return tokens.size() == 2 &&
      base::StringToInt(tokens[0], &major) && major == ${major} &&
      base::StringToInt(tokens[1], &minor) && minor <= ${minor};
}

template<>
base::Value* CreateValue(const std::string& param) {
  return new base::StringValue(param);
}

${types}\

}  // namespace devtools

}  // namespace content
""")

tmpl_include = string.Template("""\
#include "content/browser/devtools/protocol/${domain}_handler.h"
""")

tmpl_field_init = string.Template("${domain}_handler_(nullptr)")

tmpl_setter_impl = string.Template("""\
void DevToolsProtocolDispatcher::Set${Domain}Handler(
    devtools::${domain}::${Domain}Handler* ${domain}_handler) {
  DCHECK(!${domain}_handler_);
  ${domain}_handler_ = ${domain}_handler;
${initializations}\
}
""")

tmpl_register = string.Template("""\
  command_handlers_["${Domain}.${command}"] =
      base::Bind(
          &DevToolsProtocolDispatcher::On${Domain}${Command},
          base::Unretained(this));
""")

tmpl_init_client = string.Template("""\
  ${domain}_handler_->SetClient(make_scoped_ptr(
      new devtools::${domain}::Client(notifier_)));
""")

tmpl_callback_impl = string.Template("""\
bool DevToolsProtocolDispatcher::On${Domain}${Command}(
    DevToolsCommandId command_id,
    scoped_ptr<base::DictionaryValue> params) {
${prep}\
  Response response = ${domain}_handler_->${Command}(${args});
  scoped_ptr<base::DictionaryValue> protocol_response;
  if (client_.SendError(command_id, response))
    return true;
  if (response.IsFallThrough())
    return false;
  scoped_ptr<base::DictionaryValue> result(new base::DictionaryValue());
${wrap}\
  client_.SendSuccess(command_id, result.Pass());
  return true;
}
""")

tmpl_wrap = string.Template("""\
  result->Set("${proto_param}", devtools::CreateValue(out_${param}));
""")

tmpl_callback_async_impl = string.Template("""\
bool DevToolsProtocolDispatcher::On${Domain}${Command}(
    DevToolsCommandId command_id,
    scoped_ptr<base::DictionaryValue> params) {
${prep}\
  Response response = ${domain}_handler_->${Command}(${args});
  if (client_.SendError(command_id, response))
    return true;
  return !response.IsFallThrough();
}
""")

tmpl_prep_req = string.Template("""\
  ${param_type} in_${param}${init};
  if (!params || !params->Get${Type}("${proto_param}", &in_${param})) {
    client_.SendError(command_id, Response::InvalidParams("${proto_param}"));
    return true;
  }
""")

tmpl_prep_req_list = string.Template("""\
  base::ListValue* list_${param} = nullptr;
  if (!params || !params->GetList("${proto_param}", &list_${param})) {
    client_.SendError(command_id, Response::InvalidParams("${proto_param}"));
    return true;
  }
  std::vector<${item_type}> in_${param};
  for (base::ListValue::const_iterator it =
      list_${param}->begin(); it != list_${param}->end(); ++it) {
    ${item_type} item${item_init};
    if (!(*it)->GetAs${ItemType}(&item)) {
      client_.SendError(command_id, Response::InvalidParams("${proto_param}"));
      return true;
    }
    in_${param}.push_back(item);
  }
""")

tmpl_prep_opt = string.Template("""\
  ${param_type} in_${param}${init};
  bool ${param}_found = params && params->Get${Type}(
      "${proto_param}",
      &in_${param});
""")

tmpl_prep_output = string.Template("""\
  ${param_type} out_${param}${init};
""")

tmpl_arg_req = string.Template("in_${param}")

tmpl_arg_opt = string.Template(
    "${param}_found ? &in_${param} : nullptr")

tmpl_client_impl = string.Template("""\
namespace ${domain} {

Client::Client(const RawMessageCallback& raw_message_callback)
    : DevToolsProtocolClient(raw_message_callback) {
}

Client::~Client() {
}

${methods}\

}  // namespace ${domain}
""")

tmpl_event_impl = string.Template("""\
void Client::${Command}(
    scoped_refptr<${Command}Params> params) {
  SendNotification("${Domain}.${command}",
                   params->ToValue().Pass());
}
""")

tmpl_response_impl = string.Template("""\
void Client::Send${Command}Response(
    DevToolsCommandId command_id,
    scoped_refptr<${Command}Response> params) {
  SendSuccess(command_id, params->ToValue().Pass());
}
""")

tmpl_typename = string.Template("devtools::${domain}::${declared_name}")

def Capitalize(s):
  return s[:1].upper() + s[1:]

def Uncamelcase(s):
  result = ""
  for i, c in enumerate(s):
    if c.isupper():
      if (i > 0) and ((i < len(s)-1) and s[i+1].islower() or s[i-1].islower()):
        result += "_"
      result += c.lower()
    else:
      result += c
  return result

types = {}
blink_protocol = json.loads(open(blink_protocol_path, "r").read())
browser_protocol = json.loads(open(browser_protocol_path, "r").read())
type_decls = []
type_impls = []
handler_methods = []
handler_method_impls = []

all_domains = blink_protocol["domains"] + browser_protocol["domains"]

for json_domain in all_domains:
  if "types" in json_domain:
    for json_type in json_domain["types"]:
      types["%s.%s" % (json_domain["domain"], json_type["id"])] = json_type

def DeclareStruct(json_properties, mapping):
  methods = []
  fields_enum = []
  enum_items = []
  req_fields_num = 0
  for json_prop in json_properties:
    prop_map = mapping.copy()
    prop_map["proto_param"] = json_prop["name"]
    prop_map["param"] = Uncamelcase(json_prop["name"])
    prop_map["Param"] = Capitalize(json_prop["name"])
    ResolveType(json_prop, prop_map)
    prop_map["declared_name"] = mapping["declared_name"]
    if json_prop.get("optional"):
      methods.append(tmpl_builder_setter_opt.substitute(prop_map))
    else:
      methods.append(tmpl_builder_setter_req.substitute(prop_map))
      enum_items.append("k%s" % prop_map["Param"]);
      fields_enum.append(tmpl_builder_enum.substitute(prop_map,
        ordinal = req_fields_num))
      req_fields_num += 1

  all_fields = "kAllSet"
  if len(enum_items) > 0:
    all_fields = " | ".join(enum_items)
  fields_enum.append(tmpl_builder_none_set.substitute(mapping,
      all_fields = all_fields))
  type_decls.append(tmpl_struct.substitute(mapping,
      methods = "\n".join(methods),
      fields_enum = "".join(fields_enum)))

def ResolveRef(json, mapping):
  dot_pos = json["$ref"].find(".")
  if dot_pos == -1:
    domain_name = mapping["Domain"]
    type_name = json["$ref"]
  else:
    domain_name = json["$ref"][:dot_pos]
    type_name = json["$ref"][dot_pos + 1:]
  json_type = types["%s.%s" % (domain_name, type_name)]
  mapping["declared_name"] = Capitalize(type_name)
  mapping["Domain"] = domain_name
  mapping["domain"] = Uncamelcase(domain_name)
  mapping["param_type"] = tmpl_typename.substitute(mapping)
  if json_type.get("enum"):
    # TODO(vkuzkokov) Implement. Approximate template:
    # namespace ${domain} { const char k${declared_name}${Value}; }
    raise Exception("Named enumerations are not implemented")
  ResolveType(json_type, mapping)
  if not "___struct_declared" in json_type:
    json_type["___struct_declared"] = True;
    if (json_type.get("type") == "object") and ("properties" in json_type):
      DeclareStruct(json_type["properties"], mapping)
    else:
      type_decls.append(tmpl_typedef.substitute(mapping))

def ResolveArray(json, mapping):
  items_map = mapping.copy()
  ResolveType(json["items"], items_map)
  mapping["param_type"] = "std::vector<%s>" % items_map["param_type"]
  mapping["Type"] = "List"
  mapping["pass_type"] = "const %s&" % mapping["param_type"]
  mapping["prep_req"] = tmpl_prep_req_list.substitute(mapping,
      item_type = items_map["param_type"],
      item_init = items_map["init"],
      ItemType = items_map["Type"])
  mapping["arg_out"] = "&out_%s" % mapping["param"]

def ResolveObject(json, mapping):
  mapping["Type"] = "Dictionary"
  if "properties" in json:
    if not "declared_name" in mapping:
      mapping["declared_name"] = ("%s%s" %
          (mapping["Command"], Capitalize(mapping["proto_param"])))
      mapping["param_type"] = ("scoped_refptr<%s>" %
          tmpl_typename.substitute(mapping))
      DeclareStruct(json["properties"], mapping)
    else:
      mapping["param_type"] = ("scoped_refptr<%s>" %
          tmpl_typename.substitute(mapping))
    mapping["pass_type"] = mapping["param_type"]
    mapping["arg_out"] = "&out_%s" % mapping["param"]
  else:
    mapping["param_type"] = "base::DictionaryValue"
    mapping["pass_type"] = "scoped_ptr<base::DictionaryValue>"
    mapping["arg_out"] = "out_%s.get()" % mapping["param"]

def ResolvePrimitive(json, mapping):
  jsonrpc_type = json["type"]
  if jsonrpc_type == "boolean":
    mapping["param_type"] = "bool"
    mapping["Type"] = "Boolean"
    mapping["init"] = " = false"
  elif jsonrpc_type == "integer":
    mapping["param_type"] = "int"
    mapping["Type"] = "Integer"
    mapping["init"] = " = 0"
  elif jsonrpc_type == "number":
    mapping["param_type"] = "double"
    mapping["Type"] = "Double"
    mapping["init"] = " = 0.0"
  elif jsonrpc_type == "string":
    mapping["param_type"] = "std::string"
    mapping["pass_type"] = "const std::string&"
    mapping["Type"] = "String"
    if "enum" in json:
      values = []
      value_defs = []
      if "declared_name" in mapping:
        mapping["subdomain"] = Uncamelcase(mapping["declared_name"])
      else:
        mapping["subdomain"] = Uncamelcase(mapping["command"])
      mapping["Param"] = Capitalize(mapping["proto_param"])
      for enum_value in json["enum"]:
        values.append(tmpl_enum_value.substitute(mapping,
            Value = Capitalize(enum_value)))
        value_defs.append(tmpl_enum_value_def.substitute(mapping,
            value = enum_value,
            Value = Capitalize(enum_value)))
      type_decls.append(tmpl_enum.substitute(mapping,
          values = "".join(values)))
      type_impls.append(tmpl_enum.substitute(mapping,
          values = "".join(value_defs)))
  else:
    raise Exception("Unknown type: %s" % json_type)
  mapping["prep_req"] = tmpl_prep_req.substitute(mapping)
  if jsonrpc_type != "string":
    mapping["pass_type"] = mapping["param_type"]
  mapping["arg_out"] = "&out_%s" % mapping["param"]

def ResolveType(json, mapping):
  mapping["init"] = ""
  if "$ref" in json:
    ResolveRef(json, mapping)
  elif "type" in json:
    jsonrpc_type = json["type"]
    if jsonrpc_type == "array":
      ResolveArray(json, mapping)
    elif jsonrpc_type == "object":
      ResolveObject(json, mapping)
    else:
      ResolvePrimitive(json, mapping)
  else:
    raise Exception("Unknown type at %s.%s %s" %
        (mapping["Domain"], mapping["command"], mapping["proto_param"]))

setters = []
fields = []

includes = []
fields_init = []

for json_domain in all_domains:
  domain_map = {}
  domain_map["Domain"] = json_domain["domain"]
  domain_map["domain"] = Uncamelcase(json_domain["domain"])

  initializations = []
  client_methods = []
  client_method_impls = []
  domain_empty = True
  domain_needs_client = False

  if "commands" in json_domain:
    for json_command in json_domain["commands"]:
      if (not ("handlers" in json_command) or
          not ("browser" in json_command["handlers"])):
        continue
      domain_empty = False

      command_map = domain_map.copy()
      command_map["command"] = json_command["name"]
      command_map["Command"] = Capitalize(json_command["name"])

      prep = []
      args = []

      if "parameters" in json_command:
        for json_param in json_command["parameters"]:
          param_map = command_map.copy()
          param_map["proto_param"] = json_param["name"]
          param_map["param"] = Uncamelcase(json_param["name"])
          ResolveType(json_param, param_map)
          if json_param.get("optional"):
            if param_map["Type"] in ["List", "Dictionary"]:
              # TODO(vkuzkokov) Implement transformation of base::ListValue
              # to std::vector and base::DictonaryValue to struct.
              raise Exception(
                  "Optional array and object parameters are not implemented")
            prep.append(tmpl_prep_opt.substitute(param_map))
            args.append(tmpl_arg_opt.substitute(param_map))
          else:
            prep.append(param_map["prep_req"])
            args.append(tmpl_arg_req.substitute(param_map))

      if json_command.get("async"):
        domain_needs_client = True
        json_returns = []
        if "returns" in json_command:
          json_returns = json_command["returns"]
        command_map["declared_name"] = "%sResponse" % command_map["Command"]
        DeclareStruct(json_returns, command_map)
        # TODO(vkuzkokov) Pass async callback instance similar to how
        # InspectorBackendDispatcher does it. This, however, can work
        # only if Blink and Chrome are in the same repo.
        args.insert(0, "command_id")
        handler_method_impls.append(
            tmpl_callback_async_impl.substitute(command_map,
                prep = "".join(prep),
                args = "\n      " + ",\n      ".join(args)))
        client_methods.append(tmpl_response.substitute(command_map))
        client_method_impls.append(tmpl_response_impl.substitute(command_map))
      else:
        wrap = []
        if "returns" in json_command:
          for json_param in json_command["returns"]:
            param_map = command_map.copy()
            param_map["proto_param"] = json_param["name"]
            param_map["param"] = Uncamelcase(json_param["name"])
            if json_param.get("optional"):
              # TODO(vkuzkokov) Implement Optional<T> for value types.
              raise Exception("Optional return values are not implemented")
            ResolveType(json_param, param_map)
            prep.append(tmpl_prep_output.substitute(param_map))
            args.append(param_map["arg_out"])
            wrap.append(tmpl_wrap.substitute(param_map))
        args_str = ""
        if len(args) > 0:
          args_str = "\n      " + ",\n      ".join(args)
        handler_method_impls.append(tmpl_callback_impl.substitute(command_map,
            prep = "".join(prep),
            args = args_str,
            wrap = "".join(wrap)))

      initializations.append(tmpl_register.substitute(command_map))
      handler_methods.append(tmpl_callback.substitute(command_map))

  if "events" in json_domain:
    for json_event in json_domain["events"]:
      if (not ("handlers" in json_event) or
          not ("browser" in json_event["handlers"])):
        continue
      domain_empty = False
      domain_needs_client = True

      event_map = domain_map.copy()
      event_map["command"] = json_event["name"]
      event_map["Command"] = Capitalize(json_event["name"])

      json_parameters = []
      if "parameters" in json_event:
        json_parameters = json_event["parameters"]
      event_map["declared_name"] = "%sParams" % event_map["Command"]
      DeclareStruct(json_parameters, event_map);

      client_methods.append(tmpl_event.substitute(event_map))
      client_method_impls.append(tmpl_event_impl.substitute(event_map))

  if domain_empty:
    continue
  type_decls.append(tmpl_handler.substitute(domain_map))
  setters.append(tmpl_setter.substitute(domain_map))
  fields.append(tmpl_field.substitute(domain_map))
  includes.append(tmpl_include.substitute(domain_map))
  fields_init.append(tmpl_field_init.substitute(domain_map))
  if domain_needs_client:
    type_decls.append(tmpl_client.substitute(domain_map,
        methods = "".join(client_methods)))
    initializations.append(tmpl_init_client.substitute(domain_map))
    type_impls.append(tmpl_client_impl.substitute(domain_map,
        methods = "\n".join(client_method_impls)))
  handler_method_impls.append(tmpl_setter_impl.substitute(domain_map,
      initializations = "".join(initializations)))


output_h_file = open(output_h_path, "w")
output_cc_file = open(output_cc_path, "w")

output_h_file.write(template_h.substitute({},
    types = "\n".join(type_decls),
    setters = "".join(setters),
    methods = "".join(handler_methods),
    fields = "".join(fields)))
output_h_file.close()

output_cc_file.write(template_cc.substitute({},
    major = blink_protocol["version"]["major"],
    minor = blink_protocol["version"]["minor"],
    includes = "".join(sorted(includes)),
    fields_init = ",\n      ".join(fields_init),
    methods = "\n".join(handler_method_impls),
    types = "\n".join(type_impls)))
output_cc_file.close()
