# Copyright (c) 2012 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.

from copy import copy
import logging
import re

from file_system import FileNotFoundError
from third_party.json_schema_compiler.model import PropertyType


def _ClassifySchemaNode(node_name, node):
  """Attempt to classify |node_name| in an API, determining whether |node_name|
  refers to a type, function, event, or property in |api|.
  """
  if '.' in node_name:
    node_name, rest = node_name.split('.', 1)
  else:
    rest = None
  for key, group in [('types', 'type'),
                     ('functions', 'method'),
                     ('events', 'event'),
                     ('properties', 'property')]:
    for item in getattr(node, key, {}).itervalues():
      if item.simple_name == node_name:
        if rest is not None:
          ret = _ClassifySchemaNode(rest, item)
          if ret is not None:
            return ret
        else:
          return group, node_name
  return None


def _MakeKey(namespace, ref):
  key = '%s/%s' % (namespace, ref)
  # AppEngine doesn't like keys > 500, but there will be some other stuff
  # that goes into this key, so truncate it earlier.  This shoudn't be
  # happening anyway unless there's a bug, such as http://crbug.com/314102.
  max_size = 256
  if len(key) > max_size:
    logging.error('Key was >%s characters: %s' % (max_size, key))
    key = key[:max_size]
  return key


class ReferenceResolver(object):
  """Resolves references to $ref's by searching through the APIs to find the
  correct node. See document_renderer.py for more information on $ref syntax.
  """
  def __init__(self, api_models, object_store):
    self._api_models = api_models
    self._object_store = object_store

  def _GetRefLink(self, ref, api_list, namespace):
    # Check nodes within each API the ref might refer to.
    parts = ref.split('.')
    for i in xrange(1, len(parts)):
      api_name = '.'.join(parts[:i])
      if api_name not in api_list:
        continue
      try:
        api_model = self._api_models.GetModel(api_name).Get()
      except FileNotFoundError:
        continue
      name = '.'.join(parts[i:])
      # Attempt to find |name| in the API.
      node_info = _ClassifySchemaNode(name, api_model)
      if node_info is None:
        # Check to see if this ref is a property. If it is, we want the ref to
        # the underlying type the property is referencing.
        for prop in api_model.properties.itervalues():
          # If the name of this property is in the ref text, replace the
          # property with its type, and attempt to classify it.
          if prop.name in name and prop.type_.property_type == PropertyType.REF:
            name_as_prop_type = name.replace(prop.name, prop.type_.ref_type)
            node_info = _ClassifySchemaNode(name_as_prop_type, api_model)
            if node_info is not None:
              name = name_as_prop_type
              text = ref.replace(prop.name, prop.type_.ref_type)
              break
        if node_info is None:
          continue
      else:
        text = ref
      category, node_name = node_info
      if namespace is not None and text.startswith('%s.' % namespace):
        text = text[len('%s.' % namespace):]
      api_model = self._api_models.GetModel(api_name).Get()
      filename = api_model.documentation_options.get('documented_in', api_name)
      return {
        'href': '%s#%s-%s' % (filename, category, name.replace('.', '-')),
        'text': text,
        'name': node_name
      }

    # If it's not a reference to an API node it might just be a reference to an
    # API. Check this last so that links within APIs take precedence over links
    # to other APIs.
    if ref in api_list:
      return {
        'href': '%s' % ref,
        'text': ref,
        'name': ref
      }

    return None

  def GetLink(self, ref, namespace=None, title=None):
    """Resolve $ref |ref| in namespace |namespace| if not None, returning None
    if it cannot be resolved.
    """
    db_key = _MakeKey(namespace, ref)
    link = self._object_store.Get(db_key).Get()
    if link is None:
      api_list = self._api_models.GetNames()
      link = self._GetRefLink(ref, api_list, namespace)
      if link is None and namespace is not None:
        # Try to resolve the ref in the current namespace if there is one.
        link = self._GetRefLink('%s.%s' % (namespace, ref), api_list, namespace)
      if link is None:
        return None
      self._object_store.Set(db_key, link)

    if title is not None:
      link = copy(link)
      link['text'] = title

    return link

  def SafeGetLink(self, ref, namespace=None, title=None):
    """Resolve $ref |ref| in namespace |namespace|, or globally if None. If it
    cannot be resolved, pretend like it is a link to a type.
    """
    ref_data = self.GetLink(ref, namespace=namespace, title=title)
    if ref_data is not None:
      return ref_data
    logging.error('$ref %s could not be resolved in namespace %s.' %
        (ref, namespace))
    type_name = ref.rsplit('.', 1)[-1]
    return {
      'href': '#type-%s' % type_name,
      'text': title or ref,
      'name': ref
    }
