#!/usr/bin/python2.7
# Copyright 2011 Google Inc. All Rights Reserved.
#
#  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.

"""PHP library generator.

This module generates a PHP service class from a given Google APIs Discovery
document. The generated PHP code is intended to be used by the Google
APIs PHP Client (http://code.google.com/p/google-api-php-client).

Features:
- Meta classes generated from the schema definition.
- Type hinting in setter methods and api service methods.
- ApiService classes that offer a PHP interface to the API.
- Proper PHPDoc comments for each class and method in the library (FUTURE).
"""

__author__ = 'chirags@google.com (Chirag Shah)'

import collections
import json
import operator

from googleapis.codegen import api
from googleapis.codegen import api_library_generator
from googleapis.codegen import data_types
from googleapis.codegen import language_model
from googleapis.codegen import utilities
from googleapis.codegen.schema import Schema


class PHPGenerator(api_library_generator.ApiLibraryGenerator):
  """The PHP code generator."""

  def __init__(self, discovery, options=None):
    """Create a new PHPGenerator.

    Args:
      discovery: (dict) The discovery document dictionary.
      options: (dict) A dictionary of options to guide the generator's behavior.
    """
    super(PHPGenerator, self).__init__(PHPApi, discovery, 'php',
                                       language_model=PhpLanguageModel(),
                                       options=options)

  def AnnotateResource(self, the_api, resource):
    """Add the discovery dictionary as data to each resource.

    Override default implementation.

    Prepend the resource class name to each sub-resource since PHP doesn't
    support nested classes.

    Args:
      the_api: (Api) The API this Resource belongs to.
      resource: (Resource) The Resource to annotate.
    """
    resource.json = json.dumps(_StripResource(resource.raw))
    # Escape stray quotes since it will be used in a PHP function call.
    resource.json = resource.json.replace('\'', '\\\'').replace('{}', '')
    for method in resource.values['methods']:
      self.AnnotateMethod(the_api, method, resource)

    if not resource.GetTemplateValue('phpPropName'):
      resource.SetTemplateValue('phpPropName', resource.values['wireName'])

    for r in resource.values['resources']:
      r.values['className'] = (resource.values['className'] +
                               r.values['className'])
      namespaced = '_'.join((resource.GetTemplateValue('phpPropName'),
                             r.values['wireName']))
      namespaced = '_'.join(
          (resource.GetTemplateValue('phpPropName'),
           self.language_model.ToMemberName(r.values['wireName'], None)))
      r.SetTemplateValue('phpPropName', namespaced)
      self.AnnotateResource(the_api, r)

  def AnnotateMethod(self, unused_api, method, resource=None):
    """Format service request method parameters.

    Override default implementation.

    Annotates each method parameter with a type hint if possible.
    Adds the postBody parameter if there's a requestType parameter.
    Generates a list of service parameters used by the client library.

    Args:
      unused_api: (Api) The API this Method belongs to.
      method: (Method) The Method to annotate.
      resource: (Resource) The Resource which owns this Method.
    """
    for param in method.parameters:
      self._SetTypeHint(param)

    if method.parameters or method.values.get('requestType'):
      method.SetTemplateValue('hasParams', True)
    method.SetTemplateValue('name', self._ToMethodName(method.values, resource))

  def AnnotateProperty(self, unused_api, prop, schema):
    """Annotate properties with a PHP type hint.

    Overrides default implementation.

    Args:
      unused_api: (Api) The API this Property belongs to.
      prop: (Property) The Property to annotate.
      schema: (Schema) The Schema this Property belongs to.
    """
    if isinstance(prop.data_type, data_types.ArrayDataType):
      prop.SetTemplateValue('dataType', 'array')
      schema.SetTemplateValue('dataType', 'array')
      collection = self.language_model.ToMemberName(prop['wireName'], 0)
      schema.SetTemplateValue('collectionKey', collection)
    elif not schema.GetTemplateValue('dataType'):
      schema.SetTemplateValue('dataType', 'object')

    if isinstance(prop.data_type, data_types.MapDataType):
      prop.SetTemplateValue('dataType', 'map')

    if not prop.member_name_is_json_name:
      schema.SetTemplateValue('has_gapi', True)

    #  add the prop name
    prop_names = schema.values.get('propNames', [])
    prop_names.append(prop.memberName)
    schema.SetTemplateValue('propNames', prop_names)

    self._SetTypeHint(prop)

  def _GenerateLibrarySource(self, the_api, source_package_writer):
    """Default operations to generate the package.

    Do all the default operations for generating a package.
    1. Walk the template tree to generate the source.
    2. Add in per-language additions to the source
    3. Optionally copy in dependencies
    4. (Side effect) Closes the source_package_writer.

    Args:
      the_api: (Api) The Api instance we are writing a libary for.
      source_package_writer: (LibraryPackage) source output package.
    """
    list_replacements = {
        '___models_': ['model', the_api.ModelClasses()],
        '___resources_': ['resource', the_api.ResourceClasses()],
        '___topLevelModels_': ['model', the_api.TopLevelModelClasses()],
        }
    self.WalkTemplateTree('templates', self._path_replacements,
                          list_replacements,
                          self._top_level_defines, source_package_writer)
    # Call back to the language specific generator to give it a chance to emit
    # special case elements.
    self.GenerateExtraSourceOutput(source_package_writer)

  def _ToMethodName(self, method, resource):
    """Convert a wire format name into a suitable PHP variable name."""
    s = method['wireName']
    if resource and (s.lower() in PhpLanguageModel.PHP_KEYWORDS):
      s += resource['className']
    # Ensure dashes don't show up in method names
    words = s.split('-')
    return ''.join(words[:1] + [w.capitalize() for w in words[1:]])

  def _SetTypeHint(self, prop):
    """Strip primitive types since PHP doesn't support primitive type hints."""
    code_type = prop.code_type
    if code_type and code_type.lower() in PhpLanguageModel.PHP_TYPES:
      prop.values['typeHint'] = ''
      prop.values['typeHintOld'] = ''
    else:
      prop.values['typeHintOld'] = ('%s_%s' %
                                    (self._api.values['owner'].title(),
                                     code_type))
      prop.values['typeHint'] = ('%s_Service_%s_%s' %
                                 (self._api.values['owner'].title(),
                                  self._api.values['className'], code_type))


class PhpLanguageModel(language_model.LanguageModel):
  """A LanguageModel tunded for PHP."""

  language = 'php'

  _SCHEMA_TYPE_TO_PHP_TYPE = {
      'any': 'array',
      'boolean': 'bool',
      'integer': 'int',
      'long': 'string',  # PHP doesn't support long integers.
      'number': 'double',
      'string': 'string',
      'uint32': 'string',  # PHP doesn't support unsigned integers.
      'uint64': 'string',  # PHP doesn't support unsigned integers.
      'int32': 'int',
      'int64': 'string',  # Size of an integer is platform-dependent.
      'double': 'double',
      'float': 'float',
      }

  # Populated from http://php.net/manual/en/reserved.keywords.php
  PHP_KEYWORDS = frozenset((
      'abstract', 'and', 'array', 'as', 'break', 'call', 'callable',
      'case', 'catch', 'cfunction', 'class', 'clone',
      'const', 'continue', 'declare', 'default', 'do',
      'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach',
      'endif', 'endswitch', 'endwhile', 'extends', 'final',
      'finally', 'for', 'foreach', 'function', 'global', 'goto',
      'if', 'implements', 'interface', 'instanceof', 'list',
      'namespace', 'new', 'old_function', 'or', 'parent', 'private',
      'protected', 'public', 'return', 'static', 'switch', 'throw',
      'trait', 'try', 'unset', 'use', 'var', 'while', 'xor', 'yield',
      ))

  PHP_TYPES = frozenset((
      'bool', 'boolean', 'int', 'integer', 'file', 'float', 'double', 'string',
      'array', 'object',
      'null', 'resource',
      ))

  # We can not create classes which match a PHP keyword or built in object type.
  RESERVED_CLASS_NAMES = PHP_KEYWORDS | PHP_TYPES

  array_of_policy = language_model.NamingPolicy(format_string='{name}')
  map_of_policy = language_model.NamingPolicy(format_string='{name}')
  member_policy = language_model.NamingPolicy(
      case_transform=language_model.LOWER_CAMEL_CASE)
  getter_policy = language_model.NamingPolicy(
      case_transform=language_model.UPPER_CAMEL_CASE,
      format_string='get{name}')
  setter_policy = language_model.NamingPolicy(
      case_transform=language_model.UPPER_CAMEL_CASE,
      format_string='set{name}')

  def __init__(self):
    super(PhpLanguageModel, self).__init__(class_name_delimiter='.')

  def GetCodeTypeFromDictionary(self, def_dict):
    """Convert a json primitive type to a suitable PHP type name.

    Overrides the default.

    Args:
      def_dict: (dict) A dictionary describing Json schema for this Property.
    Returns:
      A name suitable for use as a class in PHP.
    """

    # Unique identifier for this schema.
    json_type = def_dict.get('type', 'string')

    # An additional regular expression or key that helps constrain the value.
    # See: http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.23
    json_format = def_dict.get('format')

    # If we know what to do with this format type, then use native type.
    php_type = (self._SCHEMA_TYPE_TO_PHP_TYPE.get(json_format)
                or self._SCHEMA_TYPE_TO_PHP_TYPE.get(json_type, json_type))
    return php_type

  def ToMemberName(self, s, unused_api):
    """Convert a wire format name into a suitable PHP variable name.

    Overrides the default.

    Args:
      s: (string) The wire format name of a member variable.
    Returns:
      A name suitable for use as a member in PHP.
    """
    return s.replace('-', '_').replace('.', '_').replace('/', '__')


class PHPApi(api.Api):
  """An Api with PHP annotations."""

  def __init__(self, discovery_doc, language=None):
    super(PHPApi, self).__init__(discovery_doc, language)
    # We've been advised by the open source office that the correct legal thing
    # to do is to hard-code a copyright date. Generated clients should always
    # display the year 2014, even if they are generated in subsequent years.
    self.SetTemplateValue('copyright', 'Copyright 2014 Google Inc.\n')

  # pylint: disable=unused-argument
  # The parameter element_type is deliberately unused since PHP doesn't
  # support nested classes.
  def ToClassName(self, s, unused_element, element_type=None):
    """Convert a discovery name to a suitable PHP class name.

    Overrides the default.

    Args:
      s: (string) The wire format name of a class.
      unused_element: (object) The object we need a class name for.
      element_type: (string) The kind of object we need a class name for.
    Returns:
      A name suitable for use as a class in PHP.
    """
    if s.lower() in PhpLanguageModel.RESERVED_CLASS_NAMES:
      # Prepend the service name.
      return utilities.CamelCase(self.values['name']) + utilities.CamelCase(s)
    return utilities.CamelCase(s)

  def ModelClasses(self):
    """Return all the model classes."""
    ret = set(
        s for s in self._schemas.itervalues()
        if isinstance(s, Schema))
    return sorted(ret, key=operator.attrgetter('class_name'))

  def ResourceClasses(self, resources=None):
    """Return all the resource classes."""
    if resources is None:
      resources = self.values['resources']

    all_resources = sorted(resources,
                           key=lambda resource: resource.values['className'])

    for resource in resources:
      all_resources.extend(self.ResourceClasses(resource.values['resources']))

    return all_resources

  def _BuildResourceDefinitions(self):
    """Loop over the resources in the discovery doc and build definitions."""
    self._resources = []
    def_dict = self.values.get('resources') or {}
    for name in sorted(def_dict):
      method_dict = def_dict[name].get('methods', {})
      for n in method_dict:
        # make sure all parameters are of type dict (and not list)
        if not method_dict[n].get('parameters'):
          method_dict[n]['parameters'] = {}
      resource = api.Resource(self, name, def_dict[name], parent=self)
      self._resources.append(resource)

# Properties that should be stripped when serializing parts of the
# discovery document.
_EXTRA_PROPERTIES = ['description', 'enumDescriptions', 'resources', 'pattern',
                     'parameterOrder']


def _StripResource(resource):
  """Return a copy of a resource dict with extraneous properties removed."""
  if not isinstance(resource, dict):
    return resource
  ret = collections.OrderedDict()
  for name, value in resource.iteritems():
    if name not in _EXTRA_PROPERTIES:
      ret[name] = _StripResource(value)
  return ret
