#!/usr/local/bin/python2.6 -tt
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
#

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import errno
import os
import json

from thrift_compiler import frontend
# Easy access to the enum of t_base_type::t_base
from thrift_compiler.frontend import t_base
# Easy access to the enum of t_field::e_req
from thrift_compiler.frontend import e_req

from thrift_compiler.frontend import t_enum
from thrift_compiler.frontend import t_list
from thrift_compiler.frontend import t_set
from thrift_compiler.frontend import t_map
from thrift_compiler.frontend import t_struct

from thrift_compiler.generate import t_generator

from thrift_compiler.generate.t_schema_context import SchemaOutputContext
from thrift_compiler.generate.t_output import IndentedOutput

# ---------------------------------------------------------------
# Generator
# ---------------------------------------------------------------


class CompilerError(RuntimeError):
    pass

class PropertyBag(object):
    pass

class SchemaGenerator(t_generator.Generator):
    '''
    Generates JSON representation of thrift object schema.
    '''

    short_name = 'schema'
    long_name = 'Schema Version 1'
    supported_flags = {}
    _out_dir_base = 'gen-schema'

    _base_to_typename = {
        t_base.void: 'void',
        t_base.string: 'string',
        t_base.bool: 'bool',
        t_base.byte: 'byte',
        t_base.i16: 'i16',
        t_base.i32: 'i32',
        t_base.i64: 'i64',
        t_base.double: 'double',
        t_base.float: 'float',
    }

    def __init__(self, *args, **kwargs):
        # super constructor
        t_generator.Generator.__init__(self, *args, **kwargs)

    @property
    def out_dir(self):
        return os.path.join(self._program.out_path, self._out_dir_base)

    def in_out_dir(self, filename):
        return os.path.join(self.out_dir, filename)

    def _write_to(self, to):
        return IndentedOutput(open(self.in_out_dir(to), 'w'))

    def _make_context(self, filename):
        'Convenience method to get set up context and output files'

        # open files and instantiate outputs
        schema_file = self._write_to(filename + "_schema.json")

        context = SchemaOutputContext(schema_file)
        return context

    def _get_program_deps(self, tprogram, output, recurse=True):
        output.add(tprogram)
        for include in tprogram.includes:
            if include in output:
                continue

            if recurse:
                self._get_program_deps(include, output, recurse)

    def _get_metadata_for_program(self, tprogram):
        output = PropertyBag()
        output.namespaces = {}
        for key in tprogram.namespaces.keys():
            output.namespaces[key] = tprogram.namespaces[key]
        return output

    def init_generator(self):
        name = self._program.name
        # Make output directory
        try:
            os.mkdir(self.out_dir)
        except OSError as exc:
            if exc.errno == errno.EEXIST and os.path.isdir(self.out_dir):
                pass
            else:
                raise

        self._schema = PropertyBag()
        self._schema.dataTypes = {}
        self._schema.names = {}
        self._schema.services = {}

        metadata = PropertyBag()
        metadata.comment = self._autogen_comment
        metadata.path = self._program.path
        metadata.fileMetadata = {}

        includes = set()
        self._get_program_deps(self._program, includes)

        for program in includes:
            metadata.fileMetadata[
                program.name] = self._get_metadata_for_program(
                    program
                )

        self._schema._metadata = metadata

        context = self._make_context(name)

        self._schema_out = context.schema

    def close_generator(self):
        serialized = json.dumps(
            self._schema,
            default=lambda o: o.__dict__,
            sort_keys=True,
            indent=4,
            separators=(',', ': ')
        )

        print(serialized, file=self._schema_out)

    def _generate_consts(self, constants):
        pass

    def _get_true_type(self, ttype):
        'Get the true type behind a series of typedefs and streams'
        while ttype.is_typedef or ttype.is_stream:
            if ttype.is_typedef:
                ttype = ttype.as_typedef.type
            else:
                ttype = ttype.as_stream.elem_type
        return ttype

    def _is_already_processed(self, ttype):
        return ttype.type_id in self._schema.dataTypes

    def _is_optional(self, f):
        return f.req == e_req.optional

    def _generate_schema_for(self, ttype):
        if self._is_already_processed(ttype):
            return

        id = ttype.type_id
        tname = ttype.full_name

        self._schema.dataTypes[id] = PropertyBag()
        self._schema.names[tname] = id

        info = self._schema.dataTypes[id]
        info.name = tname

        ttype = self._get_true_type(ttype)

        info.type_value = (int(ttype.type_value), str(ttype.type_value))

        type_annotations = ttype.annotations
        if type_annotations is not None and len(type_annotations) != 0:
            info.annotations = {}
            for key in type_annotations.keys():
                info.annotations[key] = type_annotations[key]

        if ttype.is_base_type:
            pass
        elif ttype.is_struct or ttype.is_xception:
            tstruct = ttype if isinstance(ttype, t_struct) else ttype.as_struct
            info.fields = {}
            fieldOrder = 0
            for field in tstruct.members:
                schemaField = PropertyBag()
                schemaField.is_required = not self._is_optional(field)
                schemaField.name = field.name

                field_annotations = field.annotations
                if field_annotations is not None and \
                   len(field_annotations) != 0:
                    schemaField.annotations = {}
                    for key in field_annotations.keys():
                        schemaField.annotations[key] = field_annotations[key]

                schemaField.order = fieldOrder
                schemaField.type_id = str(field.type.type_id)

                fieldOrder = fieldOrder + 1
                info.fields[field.key] = schemaField
                self._generate_schema_for(field.type)
        elif ttype.is_map:
            tmap = ttype if isinstance(ttype, t_map) else ttype.as_map
            info.key_type_id = str(tmap.key_type.type_id)
            info.value_type_id = str(tmap.value_type.type_id)

            self._generate_schema_for(tmap.key_type)
            self._generate_schema_for(tmap.value_type)
        elif ttype.is_set:
            tset = ttype if isinstance(ttype, t_set) else ttype.as_set
            info.value_type_id = str(tset.elem_type.type_id)
            self._generate_schema_for(tset.elem_type)
        elif ttype.is_list:
            tlist = ttype if isinstance(ttype, t_list) else ttype.as_list
            info.value_type_id = str(tlist.elem_type.type_id)
            self._generate_schema_for(tlist.elem_type)
        elif ttype.is_enum:
            tenum = ttype if isinstance(ttype, t_enum) else ttype.as_enum
            info.enum_values = {}
            for const in tenum.constants:
                info.enum_values[const.name] = const.value
        else:
            raise CompilerError(
                "[Schema] Don't know how to handle {} (TypeValue: {})".format(
                    tname, ttype.type_value
                )
            )

    def _generate_service(self, svc):
        service_info = self._schema.services[svc.name] = {}

        for fn in svc.functions:
            fn_info = service_info[fn.name] = PropertyBag()

            if fn.arglist is not None:
                fn_info.message_type_id = str(fn.arglist.type_id)
                self._generate_object(fn.arglist)

            if fn.xceptions is not None:
                #
                # Note that thrift parser generates nameless t_struct
                # objects for function "throws" lists which are passed
                # in to t_function objects.
                #
                # This generates a name for it to avoid name collision
                # with other exception lists.
                #
                if len(fn.xceptions.name) == 0:
                    fn.xceptions.name = str("{}_exceptions".format(fn.name))

                fn_info.exception_type_id = str(fn.xceptions.type_id)
                self._generate_object(fn.xceptions)

    def _generate_enum(self, what):
        self._generate_schema_for(self._get_true_type(what))

    def _generate_object(self, obj):
        self._generate_schema_for(self._get_true_type(obj))

    _generate_map = {
        frontend.t_enum: _generate_enum,
        frontend.t_struct: _generate_object,
        frontend.t_service: _generate_service,
    }

    def _generate(self, what):
        try:
            gen_func = self._generate_map[what.__class__]
            gen_func(self, what)
        except KeyError:
            print("Warning: Did not generate {}".format(what))

    def _generate_comment(self, text):
        return filter(None, text.split('\n'))

    def _gen_forward_declaration(self, param):
        pass

# register the generator factory
t_generator.GeneratorFactory(SchemaGenerator)
