#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright 2016 The Cartographer Authors
#
# 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.
"""A dumb configuration.rst generator that relies on source comments."""

import io
import os

TARGET = 'docs/source/configuration.rst'
ROOT = 'cartographer'
PREFIX = """.. Copyright 2016 The Cartographer Authors

.. 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.

=============
Configuration
=============

.. DO NOT EDIT! This documentation is AUTOGENERATED, please edit .proto files as
.. needed and run scripts/update_configuration_doc.py.

"""
SUFFIX = """
"""
NODOC = 'Not yet documented.'


class Message(object):
  def __init__(self, name, package, preceding_comments):
    self.name = name
    self.package = package
    self.preceding_comments = preceding_comments
    self.trailing_comments = None
    self.options = []

  def AddTrailingComments(self, comments):
    self.trailing_comments = comments

  def AddOption(self, option_type, name, comments):
    self.options.append((option_type, name, comments))


def ParseProtoFile(proto_file):
  """Computes the list of Message objects of the option messages in a file."""
  line_iter = iter(proto_file)

  # We ignore the license header and search for the 'package' line.
  for line in line_iter:
    line = line.strip()
    if line.startswith('package'):
      assert line[-1] == ';'
      package = line[7:-1].strip()
      break
    else:
      assert '}' not in line

  message_list = []
  while True:
    # Search for the next options message and capture preceding comments.
    message_comments = []
    for line in line_iter:
      line = line.strip()
      if '}' in line:
        # The preceding comments were for a different message it seems.
        message_comments = []
      elif line.startswith('//'):
        # We keep comments preceding an options message.
        comment = line[2:].strip()
        if not comment.startswith('NEXT ID:'):
          message_comments.append(comment)
      elif line.startswith('message') and line.endswith('Options {'):
        message_name = package + '.' + line[7:-1].strip()
        break
    else:
      # We reached the end of file.
      break
    print(" Found '%s'." % message_name)
    message = Message(message_name, package, message_comments)
    message_list.append(message)

    # We capture the contents of this message.
    option_comments = []
    multiline = ''
    for line in line_iter:
      line = line.strip()
      if '}' in line:
        # We reached the end of this message.
        message.AddTrailingComments(option_comments)
        break
      elif line.startswith('//'):
        comment = line[2:].strip()
        if not comment.startswith('NEXT ID:'):
          option_comments.append(comment)
      else:
        assert not line.startswith('required')
        multiline += ' ' + line
        if not multiline.endswith(';'):
          continue
        assert len(multiline) < 200
        option = multiline[:-1].strip().rstrip('0123456789').strip()
        assert option.endswith('=')
        if option.startswith('repeated'):
          option = option[8:]
        option_type, option_name = option[:-1].strip().split();
        print("  Option '%s'." % option_name)
        multiline = ''
        message.AddOption(option_type, option_name, option_comments)
        option_comments = []

  return message_list


def ParseProtoFilesRecursively(root):
  """Recursively parses all proto files into a list of Message objects."""
  message_list = []
  for dirpath, dirnames, filenames in os.walk(root):
    for name in filenames:
      if name.endswith('.proto'):
        path = os.path.join(dirpath, name)
        print("Found '%s'..." % path)
        assert not os.path.islink(path)
        message_list.extend(ParseProtoFile(io.open(path, encoding='UTF-8')))
  return message_list


class ResolutionError(Exception):
  """Raised when resolving a message name fails."""


class Resolver(object):
  def __init__(self, name_set):
    self.name_set = set(iter(name_set))

  def Resolve(self, message_name, package_name):
    if message_name in ('bool', 'double', 'float', 'int32'):
      return message_name
    if message_name.startswith('.'):
      return message_name[1:]
    package = package_name.split('.')
    for levels in range(len(package), -1, -1):
      candidate = '.'.join(package[0:levels]) + '.' + message_name
      if candidate in self.name_set:
        return candidate
    raise ResolutionError(
        'Resolving %s in %s failed.' % (message_name, package_name))


def GenerateDocumentation(output_file, root):
  """Recursively generates documentation, sorts and writes it."""
  message_list = ParseProtoFilesRecursively(root)
  resolver = Resolver(message.name for message in message_list)

  output_dict = {}
  for message in message_list:
    content = [message.name, '=' * len(message.name), '']
    assert message.name not in output_dict
    output_dict[message.name] = content
    if message.preceding_comments:
      content.extend(message.preceding_comments)
      content.append('')
    for option_type, option_name, option_comments in message.options:
      # TODO(whess): For now we exclude InitialTrajectoryPose from the
      # documentation. It is documented itself (since it has no Options suffix)
      # and is not parsed from the Lua files.
      if option_type in ('InitialTrajectoryPose',):
        continue
      content.append(
          resolver.Resolve(option_type, message.package) + ' ' + option_name)
      if not option_comments:
        option_comments.append(NODOC)
      for comment in option_comments:
        content.append('  ' + comment)
      content.append('')
    if message.trailing_comments:
      content.extend(message.trailing_comments)
      content.append('')

  output = ['\n'.join(doc) for key, doc in sorted(list(output_dict.items()))]
  print('\n\n'.join(output), file=output_file)


def main():
  assert not os.path.islink(TARGET) and os.path.isfile(TARGET)
  assert not os.path.islink(ROOT) and os.path.isdir(ROOT)
  output_file = io.open(TARGET, mode='w', encoding='UTF-8', newline='\n')
  output_file.write(PREFIX)
  GenerateDocumentation(output_file, ROOT)
  output_file.write(SUFFIX)
  output_file.close()


if __name__ == "__main__":
  main()
