#!/usr/bin/python
#

# Copyright (C) 2011 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


"""Script to generate RPC code.

"""

# pylint: disable=C0103
# [C0103] Invalid name

import sys
import re
import itertools
import textwrap
from cStringIO import StringIO

from ganeti import utils
from ganeti import compat
from ganeti import build


_SINGLE = "single-node"
_MULTI = "multi-node"

#: Expected length of a rpc definition
_RPC_DEF_LEN = 8


def _WritePreamble(sw):
  """Writes a preamble for the RPC wrapper output.

  """
  sw.Write("# This code is automatically generated at build time.")
  sw.Write("# Do not modify manually.")
  sw.Write("")
  sw.Write("\"\"\"Automatically generated RPC client wrappers.")
  sw.Write("")
  sw.Write("\"\"\"")
  sw.Write("")
  sw.Write("from ganeti import rpc_defs")
  sw.Write("")


def _WrapCode(line):
  """Wraps Python code.

  """
  return textwrap.wrap(line, width=70, expand_tabs=False,
                       fix_sentence_endings=False, break_long_words=False,
                       replace_whitespace=True,
                       subsequent_indent=utils.ShellWriter.INDENT_STR)


def _WriteDocstring(sw, name, timeout, kind, args, desc):
  """Writes a docstring for an RPC wrapper.

  """
  sw.Write("\"\"\"Wrapper for RPC call '%s'", name)
  sw.Write("")
  if desc:
    sw.Write(desc)
    sw.Write("")

  note = ["This is a %s call" % kind]
  if timeout and not callable(timeout):
    note.append(" with a timeout of %s" % utils.FormatSeconds(timeout))
  sw.Write("@note: %s", "".join(note))

  if kind == _SINGLE:
    sw.Write("@type node: string")
    sw.Write("@param node: Node name")
  else:
    sw.Write("@type node_list: list of string")
    sw.Write("@param node_list: List of node names")

  if args:
    for (argname, _, argtext) in args:
      if argtext:
        docline = "@param %s: %s" % (argname, argtext)
        for line in _WrapCode(docline):
          sw.Write(line)
  sw.Write("")
  sw.Write("\"\"\"")


def _WriteBaseClass(sw, clsname, calls):
  """Write RPC wrapper class.

  """
  sw.Write("")
  sw.Write("class %s(object):", clsname)
  sw.IncIndent()
  try:
    sw.Write("# E1101: Non-existent members")
    sw.Write("# R0904: Too many public methods")
    sw.Write("# pylint: disable=E1101,R0904")

    if not calls:
      sw.Write("pass")
      return

    sw.Write("_CALLS = rpc_defs.CALLS[%r]", clsname)
    sw.Write("")

    for v in calls:
      if len(v) != _RPC_DEF_LEN:
        raise ValueError("Procedure %s has only %d elements, expected %d" %
                         (v[0], len(v), _RPC_DEF_LEN))

    for (name, kind, _, timeout, args, _, _, desc) in calls:
      funcargs = ["self"]

      if kind == _SINGLE:
        funcargs.append("node")
      elif kind == _MULTI:
        funcargs.append("node_list")
      else:
        raise Exception("Unknown kind '%s'" % kind)

      funcargs.extend(map(compat.fst, args))

      funcargs.append("_def=_CALLS[%r]" % name)

      funcdef = "def call_%s(%s):" % (name, utils.CommaJoin(funcargs))
      for line in _WrapCode(funcdef):
        sw.Write(line)

      sw.IncIndent()
      try:
        _WriteDocstring(sw, name, timeout, kind, args, desc)

        buf = StringIO()
        buf.write("return ")

        # In case line gets too long and is wrapped in a bad spot
        buf.write("( ")

        buf.write("self._Call(_def, ")
        if kind == _SINGLE:
          buf.write("[node]")
        else:
          buf.write("node_list")

        buf.write(", [%s])" %
                  # Function arguments
                  utils.CommaJoin(map(compat.fst, args)))

        if kind == _SINGLE:
          buf.write("[node]")
        buf.write(")")

        for line in _WrapCode(buf.getvalue()):
          sw.Write(line)
      finally:
        sw.DecIndent()
      sw.Write("")
  finally:
    sw.DecIndent()


def main():
  """Main function.

  """
  buf = StringIO()
  sw = utils.ShellWriter(buf)

  _WritePreamble(sw)

  for filename in sys.argv[1:]:
    sw.Write("# Definitions from '%s'", filename)

    module = build.LoadModule(filename)

    # Call types are re-defined in definitions file to avoid imports. Verify
    # here to ensure they're equal to local constants.
    assert module.SINGLE == _SINGLE
    assert module.MULTI == _MULTI

    dups = utils.FindDuplicates(itertools.chain(*map(lambda value: value.keys(),
                                                     module.CALLS.values())))
    if dups:
      raise Exception("Found duplicate RPC definitions for '%s'" %
                      utils.CommaJoin(sorted(dups)))

    for (clsname, calls) in module.CALLS.items():
      _WriteBaseClass(sw, clsname, calls.values())

  print buf.getvalue()


if __name__ == "__main__":
  main()