/*
 * 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 Long = require('long');
const ComplexObjectType = require('../ObjectType').ComplexObjectType;
const BinaryTypeStorage = require('./BinaryTypeStorage');
const BinaryUtils = require('./BinaryUtils');
const BinaryCommunicator = require('./BinaryCommunicator');
const Errors = require('../Errors');

class BinaryType {
    constructor(name) {
        this._name = name;
        this._id = BinaryType._calculateId(name);
        this._fields = new Map();
        this._schemas = new Map();
        this._isEnum = false;
        this._enumValues = null;
    }

    get id() {
        return this._id;
    }

    get name() {
        return this._name;
    }

    get fields() {
        return [...this._fields.values()];
    }

    getField(fieldId) {
        return this._fields.get(fieldId);
    }

    hasField(fieldId) {
        return this._fields.has(fieldId);
    }

    removeField(fieldId) {
        return this._fields.delete(fieldId);
    }

    setField(field) {
        this._fields.set(field.id, field);
    }

    hasSchema(schemaId) {
        return this._schemas.has(schemaId);
    }

    addSchema(schema) {
        if (!this.hasSchema(schema.id)) {
            this._schemas.set(schema.id, schema);
        }
    }

    getSchema(schemaId) {
        return this._schemas.get(schemaId);
    }

    merge(binaryType, binarySchema) {
        let fieldId;
        for (let field of binaryType.fields) {
            fieldId = field.id;
            if (this.hasField(fieldId)) {
                if (this.getField(fieldId).typeCode !== field.typeCode) {
                    throw Errors.IgniteClientError.serializationError(
                        true, Util.format('type conflict for field "%s" of complex object type "%s"'),
                        field.name, this._name);
                }
            }
            else {
                this.setField(field);
            }
        }
        this.addSchema(binarySchema);
    }

    clone() {
        const result = new BinaryType();
        result._name = this._name;
        result._id = this._id;
        result._fields = new Map(this._fields.entries());
        result._schemas = new Map(this._schemas.entries());
        result._isEnum = this._isEnum;
        return result;
    }

    isValid() {
        for (let field of this._fields.values()) {
            if (!field.isValid()) {
                return false;
            }
        }
        return this._name !== null;
    }

    static _calculateId(name) {
        return BinaryUtils.hashCodeLowerCase(name);
    }

    async _write(buffer) {
        // type id
        buffer.writeInteger(this._id);
        // type name
        BinaryCommunicator.writeString(buffer, this._name);
        // affinity key field name
        BinaryCommunicator.writeString(buffer, null);
        // fields count
        buffer.writeInteger(this._fields.size);
        // fields
        for (let field of this._fields.values()) {
            await field._write(buffer);
        }
        await this._writeEnum(buffer);
        // schemas count
        buffer.writeInteger(this._schemas.size);
        for (let schema of this._schemas.values()) {
            await schema._write(buffer);
        }
    }

    async _writeEnum(buffer) {
        buffer.writeBoolean(this._isEnum);
        if (this._isEnum) {
            const length = this._enumValues ? this._enumValues.length : 0;
            buffer.writeInteger(length);
            if (length > 0) {
                for (let [key, value] of this._enumValues) {
                    BinaryCommunicator.writeString(buffer, key);
                    buffer.writeInteger(value);
                }
            }
        }
    }

    async _read(buffer) {
        // type id
        this._id = buffer.readInteger();
        // type name
        this._name = BinaryCommunicator.readString(buffer);
        // affinity key field name
        BinaryCommunicator.readString(buffer);
        // fields count
        const fieldsCount = buffer.readInteger();
        // fields
        let field;
        for (let i = 0; i < fieldsCount; i++) {
            field = new BinaryField(null, null);
            await field._read(buffer);
            this.setField(field);
        }
        await this._readEnum(buffer);
        // schemas count
        const schemasCount = buffer.readInteger();
        // schemas
        let schema;
        for (let i = 0; i < schemasCount; i++) {
            schema = new BinarySchema();
            await schema._read(buffer);
            this.addSchema(schema);
        }
    }

    async _readEnum(buffer) {
        this._isEnum = buffer.readBoolean();
        if (this._isEnum) {
            const valuesCount = buffer.readInteger();
            this._enumValues = new Array(valuesCount);
            for (let i = 0; i < valuesCount; i++) {
                this._enumValues[i] = [BinaryCommunicator.readString(buffer), buffer.readInteger()];
            }
        }
    }
}

/** FNV1 hash offset basis. */
const FNV1_OFFSET_BASIS = 0x811C9DC5;
/** FNV1 hash prime. */
const FNV1_PRIME = 0x01000193;

class BinarySchema {
    constructor() {
        this._id = BinarySchema._schemaInitialId();
        this._fieldIds = new Set();
        this._isValid = true;
    }

    get id() {
        return this._id;
    }

    get fieldIds() {
        return [...this._fieldIds];
    }

    finalize() {
        if (!this._isValid) {
            this._id = BinarySchema._schemaInitialId();
            for (let fieldId of this._fieldIds) {
                this._id = BinarySchema._updateSchemaId(this._id, fieldId);
            }
            this._isValid = true;
        }
    }

    clone() {
        const result = new BinarySchema();
        result._id = this._id;
        result._fieldIds = new Set(this._fieldIds);
        result._isValid = this._isValid;
        return result;
    }

    addField(fieldId) {
        if (!this.hasField(fieldId)) {
            this._fieldIds.add(fieldId);
            if (this._isValid) {
                this._id = BinarySchema._updateSchemaId(this._id, fieldId);
            }
        }
    }

    removeField(fieldId) {
        if (this._fieldIds.delete(fieldId)) {
            this._isValid = false;
        }
    }

    hasField(fieldId) {
        return this._fieldIds.has(fieldId);
    }

    static _schemaInitialId() {
        return FNV1_OFFSET_BASIS | 0;
    }

    static _updateSchemaId(schemaId, fieldId) {
        schemaId = BinarySchema._updateSchemaIdPart(schemaId, fieldId & 0xFF);
        schemaId = BinarySchema._updateSchemaIdPart(schemaId, (fieldId >> 8) & 0xFF);
        schemaId = BinarySchema._updateSchemaIdPart(schemaId, (fieldId >> 16) & 0xFF);
        schemaId = BinarySchema._updateSchemaIdPart(schemaId, (fieldId >> 24) & 0xFF);
        return schemaId;
    }

    static _updateSchemaIdPart(schemaId, fieldIdPart) {
        schemaId = schemaId ^ fieldIdPart;
        schemaId = Long.fromValue(schemaId).multiply(FNV1_PRIME).getLowBits();
        return schemaId;
    }

    async _write(buffer) {
        this.finalize();
        // schema id
        buffer.writeInteger(this._id);
        // fields count
        buffer.writeInteger(this._fieldIds.size);
        // field ids
        for (let fieldId of this._fieldIds) {
            buffer.writeInteger(fieldId);
        }
    }

    async _read(buffer) {
        // schema id
        this._id = buffer.readInteger();
        // fields count
        const fieldsCount = buffer.readInteger();
        // field ids
        for (let i = 0; i < fieldsCount; i++) {
            this._fieldIds.add(buffer.readInteger());
        }
    }
}

class BinaryField {
    constructor(name, typeCode) {
        this._name = name;
        this._id = BinaryField._calculateId(name);
        this._typeCode = typeCode;
    }

    get id() {
        return this._id;
    }

    get name() {
        return this._name;
    }

    get typeCode() {
        return this._typeCode;
    }

    isValid() {
        return this._name !== null;
    }

    static _calculateId(name) {
        return BinaryUtils.hashCodeLowerCase(name);
    }

    async _write(buffer) {
        // field name
        BinaryCommunicator.writeString(buffer, this._name);
        // type code
        buffer.writeInteger(this._typeCode);
        // field id
        buffer.writeInteger(this._id);
    }

    async _read(buffer) {
        // field name
        this._name = BinaryCommunicator.readString(buffer);
        // type code
        this._typeCode = buffer.readInteger();
        // field id
        this._id = buffer.readInteger();
    }
}

class BinaryTypeBuilder {

    static fromTypeName(typeName) {
        let result = new BinaryTypeBuilder();
        result._init(typeName);
        return result;
    }

    static async fromTypeId(communicator, typeId, schemaId) {
        let result = new BinaryTypeBuilder();
        let type = await communicator.typeStorage.getType(typeId, schemaId);
        if (type) {
            result._type = type;
            if (schemaId !== null) {
                result._schema = type.getSchema(schemaId);
                if (!result._schema) {
                    throw Errors.IgniteClientError.serializationError(
                        false, Util.format('schema id "%d" specified for complex object of type "%s" not found',
                            schemaId, type.name));
                }
                result._fromStorage = true;
            }
            else {
                result._schema = new BinarySchema();
            }
            return result;
        }
        result._init(null);
        result._type._id = typeId;
        return result;
    }

    static fromObject(jsObject, complexObjectType = null) {
        if (complexObjectType) {
            return BinaryTypeBuilder.fromComplexObjectType(complexObjectType, jsObject);
        }
        else {
            const result = new BinaryTypeBuilder();
            result._fromComplexObjectType(new ComplexObjectType(jsObject), jsObject);
            return result;
        }
    }

    static fromComplexObjectType(complexObjectType, jsObject) {
        let result = new BinaryTypeBuilder();
        const typeInfo = BinaryTypeStorage.getByComplexObjectType(complexObjectType);
        if (typeInfo) {
            result._type = typeInfo[0];
            result._schema = typeInfo[1];
            result._fromStorage = true;
        }
        else {
            result._fromComplexObjectType(complexObjectType, jsObject);
            BinaryTypeStorage.setByComplexObjectType(complexObjectType, result._type, result._schema);
        }
        return result;
    }

    getTypeId() {
        return this._type.id;
    }

    getTypeName() {
        return this._type.name;
    }

    getSchemaId() {
        return this._schema.id;
    }

    getFields() {
        return this._type.fields;
    }

    getField(fieldId) {
        return this._type._fields.get(fieldId);
    }

    setField(fieldName, fieldTypeCode = null) {
        const fieldId = BinaryField._calculateId(fieldName);
        if (!this._type.hasField(fieldId) || !this._schema.hasField(fieldId) ||
            this._type.getField(fieldId).typeCode !== fieldTypeCode) {
            this._beforeModify();
            this._type.setField(new BinaryField(fieldName, fieldTypeCode));
            this._schema.addField(fieldId);
        }
    }

    removeField(fieldName) {
        const fieldId = BinaryField._calculateId(fieldName);
        if (this._type.hasField(fieldId)) {
            this._beforeModify();
            this._type.removeField(fieldId);
            this._schema.removeField(fieldId);
        }
    }

    async finalize(communicator) {
        this._schema.finalize();
        await communicator.typeStorage.addType(this._type, this._schema);
    }

    constructor() {
        this._type = null;
        this._schema = null;
        this._fromStorage = false;
    }

    _fromComplexObjectType(complexObjectType, jsObject) {
        this._init(complexObjectType._typeName);
        if (complexObjectType._template) {
            this._setFields(complexObjectType, complexObjectType._template, jsObject);
        }
    }

    _init(typeName) {
        this._type = new BinaryType(typeName);
        this._schema = new BinarySchema();
    }

    _beforeModify() {
        if (this._fromStorage) {
            this._type = this._type.clone();
            this._schema = this._schema.clone();
            this._fromStorage = false;
        }
    }

    _setFields(complexObjectType, objectTemplate, jsObject) {
        let fieldType;
        for (let fieldName of BinaryUtils.getJsObjectFieldNames(objectTemplate)) {
            fieldType = complexObjectType._getFieldType(fieldName);
            if (!fieldType && jsObject[fieldName]) {
                fieldType = BinaryUtils.calcObjectType(jsObject[fieldName]);
            }
            this.setField(fieldName, BinaryUtils.getTypeCode(fieldType));
        }
    }
}

module.exports = BinaryType;
module.exports.BinaryField = BinaryField;
module.exports.BinaryTypeBuilder = BinaryTypeBuilder;
