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

"""Module for parsing TCG TPM2 library specification in HTML format.

This module processes parts 2 and 3 of the specification, extracting
information related to tables defined in the documents, feeding the
information into the Table object for further processing and creating the
appropriate TPM2 objects.
"""

from __future__ import print_function

import HTMLParser
import os
import re
import sys

import tpm_table

table_name = re.compile(r'^\s*Table\s+[0-9]+')


class SpecParser(HTMLParser.HTMLParser):
  """A class for parsing TCG specifications in html format."""

  # The state machine of the parser could be in one of the following states.
  ANCHOR = 0       # Look for table title anchor
  TABLE_NAME = 1   # Look for table title in the data stream
  TABLE_BODY = 2   # Scraping the actual table body
  MAYBE_DONE = 3   # Could be over, unless a single spec table is split in
                   # multiple HTML tables (to continue on the next page)
  SKIP_HEADER = 4  # Ignore the header of the split tables

  def __init__(self):
    """Initialize a parser object to default state."""
    HTMLParser.HTMLParser.__init__(self)
    self._state = self.ANCHOR
    self._title = ''
    self._table = tpm_table.Table()
    self._previous_table_number = 0  # Used to check if there are skipped tables

  def _Normalize(self, data):
    """Normalize HTML data.

    HTML files generated from TCG specifications sometimes include utf8
    characters (like long dashes), which appear only in comments/table titles
    and can be safely ignored.

    Args:
     data: a string representing portion of data from the HTML being parsed.

    Returns:
      a string, the input data with characters above ASCII printable range
                 excluded.
    """
    return ' ' + ''.join(x for x in self.unescape(data) if ord(x) < 128)

  def GetTable(self):
    """Return the Table object containing all information parsed so far."""
    return self._table

  def _SetState(self, new_state):
    if self._state != new_state:
      self._state = new_state
      if new_state == self.TABLE_NAME:
        self._title = ''

  def handle_starttag(self, tag, attrs):
    """Invoked each time a new HTML tag is opened.

    This method drives changes in the parser FSM states, its heuristics are
    derived from the format of the HTML files the TCG specs get converted to.

    Each specification table is preceded with a tittle. The title is wrapped
    in an anchor tag with a property 'name' set to 'bookmark#xxx. The title
    text starts with ' Table [0-9]+ '. Once the table title is detected,
    the state machine switches to looking for the actual HTML table, i.e. tags
    'table', 'tr' and 'td' (the generated specs do not use the 'th' tags).

    Large specification tables can be split into multiple HTML tables (so that
    they fit in a page). This is why the presence of the closing 'table' tag
    is not enough to close the parsing of the current specification table.

    In some cases the next table is defined in the spec immediately after the
    current one - this is when the new anchor tag is used as a signal that the
    previous table has been completely consumed.

    Args:
      tag: a string, the HTML tag
      attrs: a tuple of zero or more two-string tuples, the first element -
             the HTML tag's attribute, the second element - the attribute
             value.
    """
    if tag == 'a':
      if [x for x in attrs if x[0] == 'name' and x[1].startswith('bookmark')]:
        if self._state == self.ANCHOR:
          self._SetState(self.TABLE_NAME)
        elif self._state == self.MAYBE_DONE:
          # Done indeed
          self._table.ProcessTable()
          self._table.Init()
          self._SetState(self.TABLE_NAME)
        elif self._state == self.TABLE_NAME:
          self._title = ''
    elif tag == 'p' and self._state == self.TABLE_NAME and not self._title:
      # This was not a valid table start, back to looking for the right anchor.
      self._SetState(self.ANCHOR)
    elif self._state == self.TABLE_NAME and tag == 'table':
      if not table_name.search(self._title):
        # Table title does not match the expected format - back to square one.
        self._SetState(self.ANCHOR)
        return  # will have to start over
      table_number = int(self._title.split()[1])
      self._previous_table_number += 1
      if table_number > self._previous_table_number:
        print('Table(s) %s missing' % ' '.join(
            '%d' % x for x in
            range(self._previous_table_number, table_number)), file=sys.stderr)
        self._previous_table_number = table_number
      self._table.Init(self._title)
      self._SetState(self.TABLE_BODY)
    elif self._state == self.MAYBE_DONE and tag == 'tr':
      self._SetState(self.SKIP_HEADER)
    elif self._state == self.SKIP_HEADER and tag == 'tr':
      self._SetState(self.TABLE_BODY)
      self._table.NewRow()
    elif self._state == self.TABLE_BODY:
      if tag == 'tr':
        self._table.NewRow()
      elif tag == 'td':
        self._table.NewCell()

  def handle_endtag(self, tag):
    """Invoked each time an HTML tag is closed."""
    if tag == 'table' and self._table.InProgress():
      self._SetState(self.MAYBE_DONE)

  def handle_data(self, data):
    """Process data outside HTML tags."""
    if self._state == self.TABLE_NAME:
      self._title += ' %s' % self._Normalize(data)
    elif self._state == self.TABLE_BODY:
      self._table.AddData(self._Normalize(data))
    elif self._state == self.MAYBE_DONE:
      # Done indeed
      self._table.ProcessTable()
      self._table.Init()
      self._SetState(self.ANCHOR)

  def close(self):
    """Finish processing of the HTML buffer."""
    if self._state in (self.TABLE_BODY, self.MAYBE_DONE):
      self._table.ProcessTable()
    self._state = self.ANCHOR

  def handle_entityref(self, name):
    """Process HTML escape sequence."""
    entmap = {
        'amp': '&',
        'gt': '>',
        'lt': '<',
        'quot': '"',
    }
    if name in entmap:
      if self._state == self.TABLE_BODY:
        self._table.AddData(entmap[name])
      elif self._state == self.TABLE_NAME:
        self._title += entmap[name]


def main(structs_html_file_name):
  """When invoked standalone - dump .h file on the console."""
  parser = SpecParser()
  with open(structs_html_file_name) as input_file:
    html_content = input_file.read()
  parser.feed(html_content)
  parser.close()
  print(parser.GetTable().GetHFile())

if __name__ == '__main__':
  if len(sys.argv) != 2:
    print('%s: One parameter is required, the name of the html file '
          'which is the TPM2 library Part 2 specification' %
          os.path.basename(sys.argv[0]), file=sys.stderr)
    sys.exit(1)
  main(sys.argv[1])