/*
 * 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.
 */

'use strict';

const Util = require('util');
const Errors = require('./Errors');
const ArgumentChecker = require('./internal/ArgumentChecker');

/**
 * Supported Ignite type codes for primitive (simple) types.
 * @typedef ObjectType.PRIMITIVE_TYPE
 * @enum
 * @readonly
 * @property BYTE 1
 * @property SHORT 2
 * @property INTEGER 3
 * @property LONG 4
 * @property FLOAT 5
 * @property DOUBLE 6
 * @property CHAR 7
 * @property BOOLEAN 8
 * @property STRING 9
 * @property UUID 10
 * @property DATE 11
 * @property BYTE_ARRAY 12
 * @property SHORT_ARRAY 13
 * @property INTEGER_ARRAY 14
 * @property LONG_ARRAY 15
 * @property FLOAT_ARRAY 16
 * @property DOUBLE_ARRAY 17
 * @property CHAR_ARRAY 18
 * @property BOOLEAN_ARRAY 19
 * @property STRING_ARRAY 20
 * @property UUID_ARRAY 21
 * @property DATE_ARRAY 22
 * @property ENUM 28
 * @property ENUM_ARRAY 29
 * @property DECIMAL 30
 * @property DECIMAL_ARRAY 31
 * @property TIMESTAMP 33
 * @property TIMESTAMP_ARRAY 34
 * @property TIME 36
 * @property TIME_ARRAY 37
 */
const PRIMITIVE_TYPE = Object.freeze({
    BYTE : 1,
    SHORT : 2,
    INTEGER : 3,
    LONG : 4,
    FLOAT : 5,
    DOUBLE : 6,
    CHAR : 7,
    BOOLEAN : 8,
    STRING : 9,
    UUID : 10,
    DATE : 11,
    BYTE_ARRAY : 12,
    SHORT_ARRAY : 13,
    INTEGER_ARRAY : 14,
    LONG_ARRAY : 15,
    FLOAT_ARRAY : 16,
    DOUBLE_ARRAY : 17,
    CHAR_ARRAY : 18,
    BOOLEAN_ARRAY : 19,
    STRING_ARRAY : 20,
    UUID_ARRAY : 21,
    DATE_ARRAY : 22,
    ENUM : 28,
    ENUM_ARRAY : 29,
    DECIMAL : 30,
    DECIMAL_ARRAY : 31,
    TIMESTAMP : 33,
    TIMESTAMP_ARRAY : 34,
    TIME : 36,
    TIME_ARRAY : 37
});

/**
 * Supported Ignite type codes for non-primitive (composite) types.
 * @typedef ObjectType.COMPOSITE_TYPE
 * @enum
 * @readonly
 * @property OBJECT_ARRAY 23
 * @property COLLECTION 24
 * @property MAP 25
 * @property NULL 101
 * @property COMPLEX_OBJECT 103
 */
const COMPOSITE_TYPE = Object.freeze({
    OBJECT_ARRAY : 23,
    COLLECTION : 24,
    MAP : 25,
    NULL : 101,
    COMPLEX_OBJECT : 103
});

/**
 * Base class representing a type of Ignite object.
 *
 * The class has no public constructor. Only subclasses may be instantiated.
 *
 * There are two groups of Ignite object types:
 *
 * - Primitive (simple) types. To fully describe such a type it is enough to specify
 * Ignite type code {@link ObjectType.PRIMITIVE_TYPE} only.
 *
 * - Non-primitive (composite) types. To fully describe such a type
 * Ignite type code {@link ObjectType.COMPOSITE_TYPE} with additional information should be specified.
 * Eg. a kind of map or a kind of collection.
 *
 * This class helps the Ignite client to make a mapping between JavaScript types
 * and types used by Ignite.
 *
 * In many methods the Ignite client does not require to directly specify a type of Ignite object.
 * In this case the Ignite client tries to make automatic mapping between JavaScript types
 * and Ignite object types according to the following mapping tables:
 *
 * ----------------------------------------------------------------------------
 *
 * DEFAULT MAPPING FROM JavaScript type TO Ignite type code.
 *
 * This mapping is used when an application does not explicitly specify an Ignite type
 * for a field and is writing data to that field.
 *
 * <pre>
 * | JavaScript type           | Ignite type code      |
 * | ------------------------- | ----------------------|
 * | number                    | DOUBLE                |
 * | boolean                   | BOOLEAN               |
 * | string                    | STRING                |
 * | Date                      | DATE                  |
 * | Timestamp*                | TIMESTAMP             |
 * | EnumItem*                 | ENUM                  |
 * | Decimal**                 | DECIMAL               |
 * | BinaryObject*             | COMPLEX_OBJECT        |
 * | Array of number           | DOUBLE_ARRAY          |
 * | Array of boolean          | BOOLEAN_ARRAY         |
 * | Array of string           | STRING_ARRAY          |
 * | Array of Date             | DATE_ARRAY            |
 * | Array of Timestamp*       | TIMESTAMP_ARRAY       |
 * | Array of EnumItem*        | ENUM_ARRAY            |
 * | Array of Decimal**        | DECIMAL_ARRAY         |
 * | Array of BinaryObject*    | OBJECT_ARRAY          |
 * | Array of any other Object | OBJECT_ARRAY          |
 * | Set                       | COLLECTION (HASH_SET) |
 * | Map                       | MAP (HASH_MAP)        |
 * | any other Object          | COMPLEX_OBJECT        |
 * </pre>
 *
 * Type of an array content is determined by the type of the first element of the array.
 * Empty array has no default mapping.
 *
 * All other JavaScript types have no default mapping.
 *
 * ----------------------------------------------------------------------------
 *
 * DEFAULT MAPPING FROM Ignite type code TO JavaScript type.
 *
 * This mapping is used when an application does not explicitly specify an Ignite type
 * for a field and is reading data from that field.
 *
 * <pre>
 * | Ignite type code             | JavaScript type                       |
 * | ---------------------------- | --------------------------------------|
 * | BYTE                         | number                                |
 * | SHORT                        | number                                |
 * | INTEGER                      | number                                |
 * | LONG                         | number                                |
 * | FLOAT                        | number                                |
 * | DOUBLE                       | number                                |
 * | DECIMAL                      | Decimal**                             |
 * | BOOLEAN                      | boolean                               |
 * | STRING                       | string                                |
 * | CHAR                         | string (one character)                |
 * | UUID                         | Array of number (16 numbers)          |
 * | DATE                         | Date                                  |
 * | TIME                         | Date                                  |
 * | TIMESTAMP                    | Timestamp*                            |
 * | ENUM                         | EnumItem*                             |
 * | COMPLEX_OBJECT               | BinaryObject*                         |
 * | BYTE_ARRAY                   | Array of number                       |
 * | SHORT_ARRAY                  | Array of number                       |
 * | INTEGER_ARRAY                | Array of number                       |
 * | LONG_ARRAY                   | Array of number                       |
 * | FLOAT_ARRAY                  | Array of number                       |
 * | DOUBLE_ARRAY                 | Array of number                       |
 * | DECIMAL_ARRAY                | Array of Decimal**                    |
 * | BOOLEAN_ARRAY                | Array of boolean                      |
 * | STRING_ARRAY                 | Array of string                       |
 * | CHAR_ARRAY                   | Array of string (one character)       |
 * | UUID_ARRAY                   | Array of Array of number (16 numbers) |
 * | DATE_ARRAY                   | Array of Date                         |
 * | TIME_ARRAY                   | Array of Date                         |
 * | TIMESTAMP_ARRAY              | Array of Timestamp*                   |
 * | ENUM_ARRAY                   | Array of EnumItem*                    |
 * | OBJECT_ARRAY                 | Array                                 |
 * | COLLECTION (USER_COL)        | Array                                 |
 * | COLLECTION (ARR_LIST)        | Array                                 |
 * | COLLECTION (LINKED_LIST)     | Array                                 |
 * | COLLECTION (SINGLETON_LIST)  | Array                                 |
 * | COLLECTION (HASH_SET)        | Set                                   |
 * | COLLECTION (LINKED_HASH_SET) | Set                                   |
 * | COLLECTION (USER_SET)        | Set                                   |
 * | MAP (HASH_MAP)               | Map                                   |
 * | MAP (LINKED_HASH_MAP)        | Map                                   |
 * | NULL                         | null                                  |
 * </pre>
 *
 * ----------------------------------------------------------------------------
 *
 * RETURNED JavaScript types WHEN READING DATA OF THE SPECIFIED Ignite type code.
 *
 * When an application explicitly specifies an Ignite type for a field
 * and is reading data from that field - the following JavaScript types
 * are returned for every concrete Ignite type code -
 *
 * SEE THE PREVIOUS TABLE with the following additional comments:
 *
 * - for COMPLEX_OBJECT the Ignite Client returns a JavaScript Object
 * which is defined by the specified {@link ComplexObjectType}.
 *
 * - the returned Map for MAP is defined by the specified {@link MapObjectType}.
 *
 * - the returned Set or Array for COLLECTION is defined by the specified {@link CollectionObjectType}.
 *
 * - the returned Array for OBJECT_ARRAY is defined by the specified {@link ObjectArrayType}.
 *
 * - NULL cannot be specified as a type of a field but JavaScript null may be returned
 * as a value of a field.
 *
 * ----------------------------------------------------------------------------
 *
 * ALLOWED JavaScript types WHEN WRITING DATA OF THE SPECIFIED Ignite type code.
 *
 * When an application explicitly specifies an Ignite type for a field
 * and is writing data to that field - the following JavaScript types
 * are allowed for every concrete Ignite type code -
 *
 * SEE THE PREVIOUS TABLE with the following additional comments:
 *
 * - for COMPLEX_OBJECT the Ignite Client allows a JavaScript Object
 * which is defined by the specified {@link ComplexObjectType}.
 *
 * - the allowed Map for MAP is defined by the specified {@link MapObjectType}.
 *
 * - the allowed Set or Array for COLLECTION is defined by the specified {@link CollectionObjectType}.
 *
 * - the allowed Array for OBJECT_ARRAY is defined by the specified {@link ObjectArrayType}.
 *
 * - NULL cannot be specified as a type of a field but JavaScript null is allowed
 * as value of a field (but not as a key/value in a cache) or as a value of Array/Set/Map element
 * for all Ignite types, except BYTE, SHORT, INTEGER, LONG, FLOAT, DOUBLE, CHAR, BOOLEAN.
 *
 * - for all *_ARRAY Ignite types an empty JavaScript Array is allowed.
 *
 * ----------------------------------------------------------------------------
 *
 * COMMENTS TO ALL TABLES
 *
 * JavaScript type - is a JavaScript primitive or a JavaScript Object
 * ({@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures})
 *
 * (*) Timestamp, EnumItem and BinaryObject - are JavaScript Objects introduced by the Ignite client.
 *
 * (**) Decimal - is an external JavaScript Object exported into the Ignite client
 * ({@link https://github.com/MikeMcl/decimal.js})
 *
 * Ignite type code - is the type code of an Ignite primitive type ({@link ObjectType.PRIMITIVE_TYPE})
 * or an Ignite composite type ({@link ObjectType.COMPOSITE_TYPE}).
 *
 * ----------------------------------------------------------------------------
 *
 * @hideconstructor
 */

class ObjectType {
    static get PRIMITIVE_TYPE() {
        return PRIMITIVE_TYPE;
    }

    static get COMPOSITE_TYPE() {
        return COMPOSITE_TYPE;
    }

    /** Private methods */

    constructor(typeCode) {
        this._typeCode = typeCode;
    }
}

/**
 * Base class representing a non-primitive (composite) type of Ignite object.
 *
 * The class has no public constructor. Only subclasses may be instantiated.
 *
 * @hideconstructor
 * @extends ObjectType
 */
class CompositeType extends ObjectType {
}

/**
 * Supported kinds of map.
 * @typedef MapObjectType.MAP_SUBTYPE
 * @enum
 * @readonly
 * @property HASH_MAP 1
 * @property LINKED_HASH_MAP 2
 */
const MAP_SUBTYPE = Object.freeze({
    HASH_MAP : 1,
    LINKED_HASH_MAP : 2
});

/**
 * Class representing a map type of Ignite object.
 *
 * It is described by COMPOSITE_TYPE.MAP {@link ObjectType.COMPOSITE_TYPE}
 * and one of {@link MapObjectType.MAP_SUBTYPE}.
 *
 * @extends CompositeType
 */
class MapObjectType extends CompositeType {
    static get MAP_SUBTYPE() {
        return MAP_SUBTYPE;
    }

    /**
     * Public constructor.
     *
     * Optionally specifies a kind of map and types of keys and values in the map.
     *
     * If a kind of map is not specified, MAP_SUBTYPE.HASH_MAP is assumed.
     *
     * If key and/or value type is not specified then during operations the Ignite client
     * will try to make automatic mapping between JavaScript types and Ignite object types -
     * according to the mapping table defined in the description of the {@link ObjectType} class.
     *
     * @param {MapObjectType.MAP_SUBTYPE} [mapSubType=MAP_SUBTYPE.HASH_MAP] - map subtype, one of the
     *   {@link MapObjectType.MAP_SUBTYPE} constants.
     * @param {ObjectType.PRIMITIVE_TYPE | CompositeType} [keyType=null] - type of the keys in the map:
     *   - either a type code of primitive (simple) type
     *   - or an instance of class representing non-primitive (composite) type
     *   - or null (or not specified) that means the type is not specified
     * @param {ObjectType.PRIMITIVE_TYPE | CompositeType} [valueType=null] - type of the values in the map:
     *   - either a type code of primitive (simple) type
     *   - or an instance of class representing non-primitive (composite) type
     *   - or null (or not specified) that means the type is not specified
     *
     * @return {MapObjectType} - new MapObjectType instance
     *
     * @throws {IgniteClientError} if error.
     */
    constructor(mapSubType = MapObjectType.MAP_SUBTYPE.HASH_MAP, keyType = null, valueType = null) {
        super(COMPOSITE_TYPE.MAP);
        const BinaryUtils = require('./internal/BinaryUtils');
        ArgumentChecker.hasValueFrom(mapSubType, 'mapSubType', false, MapObjectType.MAP_SUBTYPE);
        this._subType = mapSubType;
        BinaryUtils.checkObjectType(keyType, 'keyType');
        BinaryUtils.checkObjectType(valueType, 'valueType');
        this._keyType = keyType;
        this._valueType = valueType;
    }
}

/**
 * Supported kinds of collections.
 * @typedef CollectionObjectType.COLLECTION_SUBTYPE
 * @enum
 * @readonly
 * @property USER_SET -1
 * @property USER_COL 0
 * @property ARRAY_LIST 1
 * @property LINKED_LIST 2
 * @property HASH_SET 3
 * @property LINKED_HASH_SET 4
 * @property SINGLETON_LIST 5
 */
const COLLECTION_SUBTYPE = Object.freeze({
    USER_SET : -1,
    USER_COL : 0,
    ARRAY_LIST : 1,
    LINKED_LIST : 2,
    HASH_SET : 3,
    LINKED_HASH_SET : 4,
    SINGLETON_LIST : 5
});

/**
 * Class representing a collection type of Ignite object.
 *
 * It is described by COMPOSITE_TYPE.COLLECTION {@link ObjectType.COMPOSITE_TYPE}
 * and one of {@link CollectionObjectType.COLLECTION_SUBTYPE}.
 *
 * @extends CompositeType
 */
class CollectionObjectType extends CompositeType {
    static get COLLECTION_SUBTYPE() {
        return COLLECTION_SUBTYPE;
    }

    /**
     * Public constructor.
     *
     * Specifies a kind of collection
     * and optionally specifies a type of elements in the collection.
     *
     * If the type of elements is not specified then during operations the Ignite client
     * will try to make automatic mapping between JavaScript types and Ignite object types -
     * according to the mapping table defined in the description of the {@link ObjectType} class.
     *
     * @param {CollectionObjectType.COLLECTION_SUBTYPE} collectionSubType - collection subtype, one of the
     *  {@link CollectionObjectType.COLLECTION_SUBTYPE} constants.
     * @param {ObjectType.PRIMITIVE_TYPE | CompositeType} [elementType=null] - type of elements in the collection:
     *   - either a type code of primitive (simple) type
     *   - or an instance of class representing non-primitive (composite) type
     *   - or null (or not specified) that means the type is not specified
     *
     * @return {CollectionObjectType} - new CollectionObjectType instance
     *
     * @throws {IgniteClientError} if error.
     */
    constructor(collectionSubType, elementType = null) {
        super(COMPOSITE_TYPE.COLLECTION);
        const BinaryUtils = require('./internal/BinaryUtils');
        ArgumentChecker.hasValueFrom(
            collectionSubType, 'collectionSubType', false, CollectionObjectType.COLLECTION_SUBTYPE);
        this._subType = collectionSubType;
        BinaryUtils.checkObjectType(elementType, 'elementType');
        this._elementType = elementType;
    }

    /** Private methods */

    /**
     * @ignore
     */
    static _isSet(subType) {
        return subType === CollectionObjectType.COLLECTION_SUBTYPE.USER_SET ||
            subType === CollectionObjectType.COLLECTION_SUBTYPE.HASH_SET ||
            subType === CollectionObjectType.COLLECTION_SUBTYPE.LINKED_HASH_SET;
    }

    /**
     * @ignore
     */
    _isSet() {
        return CollectionObjectType._isSet(this._subType);
    }
}

/**
 * Class representing an array type of Ignite objects.
 *
 * It is described by COMPOSITE_TYPE.OBJECT_ARRAY {@link ObjectType.COMPOSITE_TYPE}.
 *
 * @extends CompositeType
 */
class ObjectArrayType extends CompositeType {

    /**
     * Public constructor.
     *
     * Optionally specifies a type of elements in the array.
     *
     * If the type of elements is not specified then during operations the Ignite client
     * will try to make automatic mapping between JavaScript types and Ignite object types -
     * according to the mapping table defined in the description of the {@link ObjectType} class.
     *
     * @param {ObjectType.PRIMITIVE_TYPE | CompositeType} [elementType=null] - type of the array element:
     *   - either a type code of primitive (simple) type
     *   - or an instance of class representing non-primitive (composite) type
     *   - or null (or not specified) that means the type is not specified
     *
     * @return {ObjectArrayType} - new ObjectArrayType instance
     *
     * @throws {IgniteClientError} if error.
     */
    constructor(elementType = null) {
        super(COMPOSITE_TYPE.OBJECT_ARRAY);
        const BinaryUtils = require('./internal/BinaryUtils');
        BinaryUtils.checkObjectType(elementType, 'elementType');
        this._elementType = elementType;
    }
}

/**
 * Class representing a complex type of Ignite object.
 *
 * It is described by COMPOSITE_TYPE.COMPLEX_OBJECT {@link ObjectType.COMPOSITE_TYPE},
 * by a name of the complex type and by a JavaScript Object which is mapped to/from the Ignite complex type.
 *
 * @extends CompositeType
 */
class ComplexObjectType extends CompositeType {

    /**
     * Public constructor.
     *
     * Specifies a JavaScript Object type which will be mapped to/from the complex type.
     * This specification is done using an instance of the JavaScript Object.
     *
     * If an object of the complex type is going to be received (deserialized),
     * the JavaScript Object must have a constructor without parameters or with optional parameters only.
     *
     * The JavaScript Object defines a set of fields of the complex type.
     *
     * By default, the fields have no types specified. It means during operations the Ignite client
     * will try to make automatic mapping between JavaScript types and Ignite object types -
     * according to the mapping table defined in the description of the {@link ObjectType} class.
     *
     * A type of any field may be specified later by setFieldType() method.
     *
     * By default, the name of the complex type is the name of the JavaScript Object.
     * The name may be explicitely specified using optional typeName parameter in the constructor.
     *
     * @param {object} jsObject - instance of JavaScript Object which will be mapped to/from this complex type.
     * @param {string} [typeName] - name of the complex type.
     *
     * @return {ComplexObjectType} - new ComplexObjectType instance
     *
     * @throws {IgniteClientError} if error.
     */
    constructor(jsObject, typeName = null) {
        super(COMPOSITE_TYPE.COMPLEX_OBJECT);
        ArgumentChecker.notEmpty(jsObject, 'jsObject');
        this._template = jsObject;
        this._objectConstructor = jsObject && jsObject.constructor ?
            jsObject.constructor : Object;
        if (!typeName) {
            typeName = this._objectConstructor.name;
        }
        this._typeName = typeName;
        this._fields = new Map();
        const BinaryUtils = require('./internal/BinaryUtils');
        for (let fieldName of BinaryUtils.getJsObjectFieldNames(this._template)) {
            this._fields.set(fieldName, null);
        }
    }

    /**
     * Specifies a type of the field in the complex type.
     *
     * If the type is not specified then during operations the Ignite client
     * will try to make automatic mapping between JavaScript types and Ignite object types -
     * according to the mapping table defined in the description of the {@link ObjectType} class.
     *
     * @param {string} fieldName - name of the field.
     * @param {ObjectType.PRIMITIVE_TYPE | CompositeType} fieldType - type of the field:
     *   - either a type code of primitive (simple) type
     *   - or an instance of class representing non-primitive (composite) type
     *   - or null (means the type is not specified).
     *
     * @return {ComplexObjectType} - the same instance of the ComplexObjectType.
     *
     * @throws {IgniteClientError} if error.
     */
    setFieldType(fieldName, fieldType) {
        if (!this._fields.has(fieldName)) {
            throw Errors.IgniteClientError.illegalArgumentError(
                Util.format('Field "%s" is absent in the complex object type', fieldName));
        }
        const BinaryUtils = require('./internal/BinaryUtils');
        BinaryUtils.checkObjectType(fieldType, 'fieldType');
        this._fields.set(fieldName, fieldType);
        return this;
    }

    /** Private methods */

    /**
     * @ignore
     */
    _getFieldType(fieldName) {
        return this._fields.get(fieldName);
    }
}

module.exports.ObjectType = ObjectType;
module.exports.CompositeType = CompositeType;
module.exports.MapObjectType = MapObjectType;
module.exports.CollectionObjectType = CollectionObjectType;
module.exports.ComplexObjectType = ComplexObjectType;
module.exports.ObjectArrayType = ObjectArrayType;
