#
# Copyright 2019 GridGain Systems, Inc. and Contributors.
#
# Licensed under the GridGain Community Edition License (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.gridgain.com/products/software/community-edition/gridgain-community-edition-license
#
# 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 typing import Union

from pygridgain.connection import Connection, AioConnection
from pygridgain.constants import PROTOCOL_BYTE_ORDER
from pygridgain.datatypes.binary import enum_struct, schema_struct, binary_fields_struct
from pygridgain.datatypes import String, Int, Bool
from pygridgain.queries import Query, query_perform
from pygridgain.queries.op_codes import OP_GET_BINARY_TYPE, OP_PUT_BINARY_TYPE
from pygridgain.utils import entity_id, schema_id
from .result import APIResult
from ..queries.response import BinaryTypeResponse


def get_binary_type(conn: 'Connection', binary_type: Union[str, int], query_id=None) -> APIResult:
    """
    Gets the binary type information by type ID.

    :param conn: connection to GridGain server,
    :param binary_type: binary type name or ID,
    :param query_id: (optional) a value generated by client and returned as-is
     in response.query_id. When the parameter is omitted, a random value
     is generated,
    :return: API result data object.
    """
    return __get_binary_type(conn, binary_type, query_id)


async def get_binary_type_async(conn: 'AioConnection', binary_type: Union[str, int], query_id=None) -> APIResult:
    """
    Async version of get_binary_type.
    """
    return await __get_binary_type(conn, binary_type, query_id)


def __get_binary_type(conn, binary_type, query_id):
    query_struct = Query(
        OP_GET_BINARY_TYPE,
        [
            ('type_id', Int),
        ],
        query_id=query_id,
        response_type=BinaryTypeResponse
    )

    return query_perform(query_struct, conn, query_params={
        'type_id': entity_id(binary_type),
    })


def put_binary_type(connection: 'Connection', type_name: str, affinity_key_field: str = None,
                    is_enum=False, schema: dict = None, query_id=None) -> APIResult:
    """
    Registers binary type information in cluster.

    :param connection: connection to GridGain server,
    :param type_name: name of the data type being registered,
    :param affinity_key_field: (optional) name of the affinity key field,
    :param is_enum: (optional) register enum if True, binary object otherwise.
     Defaults to False,
    :param schema: (optional) when register enum, pass a dict of enumerated
     parameter names as keys and an integers as values. When register binary
     type, pass a dict of field names: field types. Binary type with no fields
     is OK,
    :param query_id: (optional) a value generated by client and returned as-is
     in response.query_id. When the parameter is omitted, a random value
     is generated,
    :return: API result data object.
    """
    return __put_binary_type(connection, type_name, affinity_key_field, is_enum, schema, query_id)


async def put_binary_type_async(connection: 'AioConnection', type_name: str, affinity_key_field: str = None,
                                is_enum=False, schema: dict = None, query_id=None) -> APIResult:
    """
    Async version of put_binary_type.
    """
    return await __put_binary_type(connection, type_name, affinity_key_field, is_enum, schema, query_id)


def __post_process_put_binary(type_id):
    def internal(result):
        if result.status == 0:
            result.value = {
                'type_id': type_id,
                'schema_id': schema_id,
            }
        return result
    return internal


def __put_binary_type(connection, type_name, affinity_key_field, is_enum, schema, query_id):
    # prepare data
    if schema is None:
        schema = {}
    type_id = entity_id(type_name)
    data = {
        'type_name': type_name,
        'type_id': type_id,
        'affinity_key_field': affinity_key_field,
        'binary_fields': [],
        'is_enum': is_enum,
        'schema': [],
    }
    s_id = None
    if is_enum:
        data['enums'] = []
        for literal, ordinal in schema.items():
            data['enums'].append({
                'literal': literal,
                'type_id': ordinal,
            })
    else:
        # assemble schema and calculate schema ID in one go
        s_id = schema_id(schema)
        for field_name, data_type in schema.items():
            # TODO: check for allowed data types
            field_id = entity_id(field_name)
            data['binary_fields'].append({
                'field_name': field_name,
                'type_id': int.from_bytes(
                    data_type.type_code,
                    byteorder=PROTOCOL_BYTE_ORDER
                ),
                'field_id': field_id,
            })

    data['schema'].append({
        'schema_id': s_id,
        'schema_fields': [
            {'schema_field_id': entity_id(x)} for x in schema
        ],
    })

    # do query
    if is_enum:
        query_struct = Query(
            OP_PUT_BINARY_TYPE,
            [
                ('type_id', Int),
                ('type_name', String),
                ('affinity_key_field', String),
                ('binary_fields', binary_fields_struct),
                ('is_enum', Bool),
                ('enums', enum_struct),
                ('schema', schema_struct),
            ],
            query_id=query_id,
        )
    else:
        query_struct = Query(
            OP_PUT_BINARY_TYPE,
            [
                ('type_id', Int),
                ('type_name', String),
                ('affinity_key_field', String),
                ('binary_fields', binary_fields_struct),
                ('is_enum', Bool),
                ('schema', schema_struct),
            ],
            query_id=query_id,
        )
    return query_perform(query_struct, connection, query_params=data,
                         post_process_fun=__post_process_put_binary(type_id))
