# Copyright (c) 2014 Spotify AB
#
# 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.

"""Tools to convert from Avro schemas to PySchema schemas

Compatibility intended to be production-grade except:
* defaults;
* namespaces (not scrutinized);
* aliases;
* advanced Avro union types (other than the pattern to union with null, that
  translates to nullable i.e. optional).

This module doesn't deal at all with compatibility of data serialization logic
proper, only with schema definitions.
"""

import json

field_map = {
    'string': 'pyschema.Text',
    'float': 'pyschema.Float',
    'double': 'pyschema.Float',
    'int': 'pyschema.Integer',
    'boolean': 'pyschema.Boolean',
    'long': 'pyschema.Integer',
    'bytes': 'pyschema.Bytes',
}

extra_args_map = {
    'float': ['size=4'],
    'int': ['size=4'],
}

complex_field_map = {
    'array': 'pyschema.List',
    'map': 'pyschema.Map',
    'record': 'pyschema.SubRecord',
}


def get_ununionized_field_type(field_type):
    if isinstance(field_type, list):
        if len(field_type) != 2 or field_type[0] != 'null':
            raise NotImplementedError("PySchema doesn't support such advanced union types yet: {0!r}".format(field_type))
        return field_type[1]
    return field_type


def get_name(field):
    if isinstance(field['type'], basestring):
        return field['name']
    if isinstance(field['type'], list):
        return field['type'][0]['name']
    if isinstance(field['type'], dict):
        return field['type']['name']


def is_nullable(field_type):
    return isinstance(field_type, list) and 'null' in field_type


def get_field_type_name(field_type):
    field_type = get_ununionized_field_type(field_type)
    if isinstance(field_type, dict):
        field_type = field_type['type']
    return field_type


def get_sub_field_type(field):
    field_type = get_ununionized_field_type(field['type'])
    type_name = get_field_type_name(field['type'])
    if type_name == 'record':
        return field_type['fields']
    elif type_name == 'array':
        return field_type['items']
    elif type_name == 'map':
        return field_type['values']


def nullable_str(field_type):
    if not is_nullable(field_type):
        return 'nullable=False'
    return ''


def get_sub_fields_name(sub_type):
    sub_map = {'record': 'fields', 'array': 'items', 'map': 'values'}
    return sub_map[sub_type]


def get_sub_field(field):
    field_type = get_field_type_name(field['type'])
    if field_type == 'record':
        return field['fields']
    sub_field = field['type'][get_sub_fields_name(field_type)]
    if isinstance(sub_field, list):
        return sub_field[0]
    return sub_field


def get_field_definition(field, sub_records):
    args = []
    if isinstance(field, basestring):
        field_type_name = field
        args.append('nullable=False')
    else:
        field_type_name = get_field_type_name(field['type'])
        if is_nullable(field['type']):
            args.append('nullable=True')
        else:
            args.append('nullable=False')

    if 'doc' in field:
        args.append('description={0!r}'.format(field['doc'].encode('UTF-8')))

    # simple types
    if field_type_name in field_map:
        args.extend(extra_args_map.get(field_type_name, []))
        pyschema_type = field_map[field_type_name]
    # complex types
    elif field_type_name == 'record':
        args.insert(0, get_name(field))
        sub_rec = get_pyschema_record(field, sub_records)
        sub_records.append(sub_rec)
        pyschema_type = complex_field_map[field_type_name]
    else:
        sub_field = get_sub_field_type(field)
        args.insert(0, get_field_definition(sub_field, sub_records))
        pyschema_type = complex_field_map[field_type_name]
    return '{0}({1})'.format(pyschema_type, ', '.join(args))


def get_pyschema_record(schema, sub_records):
    if isinstance(schema, basestring):
        schema = json.loads(schema)

    name = get_name(schema)

    record_def = [
        "class {0}(pyschema.Record):".format(name),
        "# GENERATED BY {0}".format(__name__),
        "# YOU KNOW YOU WOULDN'T WANT IT ANY OTHER WAY",
        "# SO TAKE ME AS I AM",
    ]
    if 'doc' in schema:
        record_def += [repr(str(schema['doc']))]
    if 'namespace' in schema:
        record_def += ["_namespace = {0!r}".format(str(schema['namespace']))]

    if is_nullable(schema['type']):
        fields = schema['type'][0]['fields']
    else:
        fields = schema['fields']
    for field in fields:
        name = field['name']
        record_def += ["{0} = {1}".format(name, get_field_definition(field, sub_records))]
    return "\n    ".join(record_def) + "\n"
