# Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
# reserved. Use of this source code is governed by a BSD-style license that
# can be found in the LICENSE file.

from __future__ import absolute_import
from __future__ import print_function
from file_util import *
import os
import re
import shutil
import string
import sys
import textwrap
import time
import itertools
import hashlib

# Determines string type for python 2 and python 3.
if sys.version_info[0] == 3:
  string_type = str
else:
  string_type = basestring


class cef_api_hash:
  """ CEF API hash calculator """

  def __init__(self, headerdir, debugdir=None, verbose=False):
    if headerdir is None or len(headerdir) == 0:
      raise AssertionError("headerdir is not specified")

    self.__headerdir = headerdir
    self.__debugdir = debugdir
    self.__verbose = verbose
    self.__debug_enabled = not (self.__debugdir is
                                None) and len(self.__debugdir) > 0

    self.platforms = ["windows", "mac", "linux"]

    self.platform_files = {
        # List of includes_win_capi from cef_paths2.gypi.
        "windows": [
            "internal/cef_app_win.h",
            "internal/cef_types_win.h",
        ],
        # List of includes_mac_capi from cef_paths2.gypi.
        "mac": [
            "internal/cef_types_mac.h",
        ],
        # List of includes_linux_capi from cef_paths2.gypi.
        "linux": [
            "internal/cef_types_linux.h",
        ]
    }

    self.included_files = []

    # List of include/ and include/internal/ files from cef_paths2.gypi.
    self.excluded_files = [
        # includes_common
        "cef_api_hash.h",
        "cef_base.h",
        "cef_config.h",
        "cef_version.h",
        "internal/cef_export.h",
        "internal/cef_ptr.h",
        "internal/cef_string_wrappers.h",
        "internal/cef_time_wrappers.h",
        "internal/cef_types_wrappers.h",
        # includes_win
        "cef_sandbox_win.h",
        "internal/cef_win.h",
        # includes_mac
        "cef_application_mac.h",
        "cef_sandbox_mac.h",
        "internal/cef_mac.h",
        # includes_linux
        "internal/cef_linux.h",
    ]

  def calculate(self):
    filenames = [
        filename for filename in self.__get_filenames()
        if not filename in self.excluded_files
    ]

    objects = []
    for filename in filenames:
      if self.__verbose:
        print("Processing " + filename + "...")
      content = read_file(os.path.join(self.__headerdir, filename), True)
      platforms = list([
          p for p in self.platforms if self.__is_platform_filename(filename, p)
      ])

      # Parse cef_string.h happens in special case: grab only defined CEF_STRING_TYPE_xxx declaration
      content_objects = None
      if filename == "internal/cef_string.h":
        content_objects = self.__parse_string_type(content)
      else:
        content_objects = self.__parse_objects(content)

      for o in content_objects:
        o["text"] = self.__prepare_text(o["text"])
        o["platforms"] = platforms
        o["filename"] = filename
        objects.append(o)

    # objects will be sorted including filename, to make stable universal hashes
    objects = sorted(objects, key=lambda o: o["name"] + "@" + o["filename"])

    if self.__debug_enabled:
      namelen = max([len(o["name"]) for o in objects])
      filenamelen = max([len(o["filename"]) for o in objects])
      dumpsig = []
      for o in objects:
        dumpsig.append(
            format(o["name"], str(namelen) + "s") + "|" + format(
                o["filename"], "" + str(filenamelen) + "s") + "|" + o["text"])
      self.__write_debug_file("objects.txt", dumpsig)

    revisions = {}

    for platform in itertools.chain(["universal"], self.platforms):
      sig = self.__get_final_sig(objects, platform)
      if self.__debug_enabled:
        self.__write_debug_file(platform + ".sig", sig)
      revstr = hashlib.sha1(sig.encode('utf-8')).hexdigest()
      revisions[platform] = revstr

    return revisions

  def __parse_objects(self, content):
    """ Returns array of objects in content file. """
    objects = []
    content = re.sub(r"//.*\n", "", content)

    # function declarations
    for m in re.finditer(
        r"\nCEF_EXPORT\s+?.*?\s+?(\w+)\s*?\(.*?\)\s*?;",
        content,
        flags=re.DOTALL):
      object = {"name": m.group(1), "text": m.group(0).strip()}
      objects.append(object)

    # structs
    for m in re.finditer(
        r"\ntypedef\s+?struct\s+?(\w+)\s+?\{.*?\}\s+?(\w+)\s*?;",
        content,
        flags=re.DOTALL):
      object = {"name": m.group(2), "text": m.group(0).strip()}
      objects.append(object)

    # enums
    for m in re.finditer(
        r"\ntypedef\s+?enum\s+?\{.*?\}\s+?(\w+)\s*?;", content, flags=re.DOTALL):
      object = {"name": m.group(1), "text": m.group(0).strip()}
      objects.append(object)

    # typedefs
    for m in re.finditer(r"\ntypedef\s+?.*?\s+(\w+);", content, flags=0):
      object = {"name": m.group(1), "text": m.group(0).strip()}
      objects.append(object)

    return objects

  def __parse_string_type(self, content):
    """ Grab defined CEF_STRING_TYPE_xxx """
    objects = []
    for m in re.finditer(
        r"\n\s*?#\s*?define\s+?(CEF_STRING_TYPE_\w+)\s+?.*?\n", content,
        flags=0):
      object = {
          "name": m.group(1),
          "text": m.group(0),
      }
      objects.append(object)
    return objects

  def __prepare_text(self, text):
    text = text.strip()
    text = re.sub(r"\s+", " ", text)
    text = re.sub(r"\(\s+", "(", text)
    return text

  def __get_final_sig(self, objects, platform):
    sig = []

    for o in objects:
      if platform == "universal" or platform in o["platforms"]:
        sig.append(o["text"])

    return "\n".join(sig)

  def __get_filenames(self):
    """ Returns file names to be processed, relative to headerdir """
    headers = [
        os.path.join(self.__headerdir, filename)
        for filename in self.included_files
    ]

    capi_dir = os.path.join(self.__headerdir, "capi")
    headers = itertools.chain(headers, get_files(os.path.join(capi_dir, "*.h")))

    # Also include capi sub-directories.
    for root, dirs, files in os.walk(capi_dir):
      for name in dirs:
        headers = itertools.chain(headers,
                                  get_files(os.path.join(root, name, "*.h")))

    headers = itertools.chain(
        headers, get_files(os.path.join(self.__headerdir, "internal", "*.h")))

    for v in self.platform_files.values():
      headers = itertools.chain(headers,
                                [os.path.join(self.__headerdir, f) for f in v])

    normalized = [
        os.path.relpath(filename, self.__headerdir) for filename in headers
    ]
    normalized = [f.replace('\\', '/').lower() for f in normalized]

    return list(set(normalized))

  def __is_platform_filename(self, filename, platform):
    if platform == "universal":
      return True
    if not platform in self.platform_files:
      return False
    listed = False
    for p in self.platforms:
      if filename in self.platform_files[p]:
        if p == platform:
          return True
        else:
          listed = True
    return not listed

  def __write_debug_file(self, filename, content):
    make_dir(self.__debugdir)
    outfile = os.path.join(self.__debugdir, filename)
    dir = os.path.dirname(outfile)
    make_dir(dir)
    if not isinstance(content, string_type):
      content = "\n".join(content)
    write_file(outfile, content)


if __name__ == "__main__":
  from optparse import OptionParser
  import time

  disc = """
    This utility calculates CEF API hash.
    """

  parser = OptionParser(description=disc)
  parser.add_option(
      '--cpp-header-dir',
      dest='cppheaderdir',
      metavar='DIR',
      help='input directory for C++ header files [required]')
  parser.add_option(
      '--debug-dir',
      dest='debugdir',
      metavar='DIR',
      help='intermediate directory for easy debugging')
  parser.add_option(
      '-v',
      '--verbose',
      action='store_true',
      dest='verbose',
      default=False,
      help='output detailed status information')
  (options, args) = parser.parse_args()

  # the cppheader option is required
  if options.cppheaderdir is None:
    parser.print_help(sys.stdout)
    sys.exit()

  # calculate
  c_start_time = time.time()

  calc = cef_api_hash(options.cppheaderdir, options.debugdir, options.verbose)
  revisions = calc.calculate()

  c_completed_in = time.time() - c_start_time

  print("{")
  for k in sorted(revisions.keys()):
    print(format("\"" + k + "\"", ">12s") + ": \"" + revisions[k] + "\"")
  print("}")
  # print
  # print 'Completed in: ' + str(c_completed_in)
  # print

  # print "Press any key to continue...";
  # sys.stdin.readline();