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

package org.apache.ignite.internal.binary;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import org.apache.ignite.internal.util.tostring.GridToStringInclude;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.jetbrains.annotations.Nullable;

/**
 * Binary metadata which is passed over a wire.
 */
public class BinaryMetadata implements Externalizable {
    /** */
    private static final long serialVersionUID = 0L;

    /** */
    private static final int VERSION = 1;

    /** Type ID. */
    @GridToStringInclude(sensitive = true)
    private int typeId;

    /** Type name. */
    @GridToStringInclude(sensitive = true)
    private String typeName;

    /** Recorded object fields. */
    @GridToStringInclude(sensitive = true)
    private Map<String, BinaryFieldMetadata> fields;

    /** Affinity key field name. */
    @GridToStringInclude(sensitive = true)
    private String affKeyFieldName;

    /** Schemas associated with type. */
    @GridToStringInclude
    private Collection<BinarySchema> schemas;

    /** Schema IDs registered for this type */
    private Set<Integer> schemaIds;

    /** Whether this is enum type. */
    private boolean isEnum;

    /** Enum name to ordinal mapping. */
    private Map<String, Integer> nameToOrdinal;

    /** Enum ordinal to name mapping. */
    private Map<Integer, String> ordinalToName;

    /**
     * For {@link Externalizable}.
     */
    public BinaryMetadata() {
        // No-op.
    }

    /**
     * Constructor.
     *
     * @param typeId Type ID.
     * @param typeName Type name.
     * @param fields Fields map.
     * @param affKeyFieldName Affinity key field name.
     * @param schemas Schemas.
     * @param isEnum Enum flag.
     * @param enumMap Enum name to ordinal mapping.
     */
    public BinaryMetadata(int typeId, String typeName, @Nullable Map<String, BinaryFieldMetadata> fields,
        @Nullable String affKeyFieldName, @Nullable Collection<BinarySchema> schemas, boolean isEnum,
        @Nullable Map<String, Integer> enumMap) {
        assert typeName != null;

        this.typeId = typeId;
        this.typeName = typeName;
        this.fields = fields;
        this.affKeyFieldName = affKeyFieldName;
        this.schemas = schemas;

        if (schemas != null) {
            schemaIds = U.newHashSet(schemas.size());

            for (BinarySchema schema : schemas)
                schemaIds.add(schema.schemaId());
        }
        else
            schemaIds = Collections.emptySet();

        this.isEnum = isEnum;

        if (enumMap != null) {
            this.nameToOrdinal = new LinkedHashMap<>(enumMap);

            this.ordinalToName = new LinkedHashMap<>(enumMap.size());

            for (Map.Entry<String, Integer> e: nameToOrdinal.entrySet())
                this.ordinalToName.put(e.getValue(), e.getKey());
        }
    }

    /**
     * @return Type ID.
     */
    public int typeId() {
        return typeId;
    }

    /**
     * @return Type name.
     */
    public String typeName() {
        return typeName;
    }

    /**
     * @return Fields.
     */
    public Collection<String> fields() {
        return fields != null ? fields.keySet() : Collections.<String>emptyList();
    }

    /**
     * @return Fields.
     */
    public Map<String, BinaryFieldMetadata> fieldsMap() {
        return fields != null ? fields : Collections.<String, BinaryFieldMetadata>emptyMap();
    }

    /**
     * @param fieldName Field name.
     * @return Field type name.
     */
    @Nullable public String fieldTypeName(String fieldName) {
        BinaryFieldMetadata meta = fields != null ? fields.get(fieldName) : null;

        return meta != null ? BinaryUtils.fieldTypeName(meta.typeId()) : null;
    }

    /**
     * @return Affinity key field name.
     */
    @Nullable public String affinityKeyFieldName() {
        return affKeyFieldName;
    }

    /**
     * @return Schemas.
     */
    public Collection<BinarySchema> schemas() {
        return schemas != null ? schemas : Collections.<BinarySchema>emptyList();
    }

    /**
     * @param schemaId Schema ID.
     * @return {@code true} if <b>BinaryMetadata</b> instance has schema with ID specified, {@code false} otherwise.
     */
    public boolean hasSchema(int schemaId) {
        if (schemaIds == null)
            return false;
        
        return schemaIds.contains(schemaId);
    }

    /**
     * @return {@code True} if this is enum type.
     */
    public boolean isEnum() {
        return isEnum;
    }

    /**
     * Wrap metadata into binary type.
     *
     * @param ctx Binary context.
     * @return Binary type.
     */
    public BinaryTypeImpl wrap(BinaryContext ctx) {
        return new BinaryTypeImpl(ctx, this);
    }

    /** {@inheritDoc} */
    @Override public void writeExternal(ObjectOutput out) throws IOException {
        writeTo(out);
    }

    /**
     * The object implements the writeTo method to save its contents
     * by calling the methods of DataOutput for its primitive values and strings or
     * calling the writeTo method for other objects.
     *
     * @param out the stream to write the object to.
     * @exception IOException Includes any I/O exceptions that may occur.
     */
    public void writeTo(DataOutput out) throws IOException {
        out.writeByte(VERSION);
        out.writeInt(typeId);

        U.writeString(out, typeName);

        if (fields == null)
            out.writeInt(-1);
        else {
            out.writeInt(fields.size());

            for (Map.Entry<String, BinaryFieldMetadata> fieldEntry : fields.entrySet()) {
                U.writeString(out, fieldEntry.getKey());
                fieldEntry.getValue().writeTo(out);
            }
        }

        U.writeString(out, affKeyFieldName);

        if (schemas == null)
            out.writeInt(-1);
        else {
            out.writeInt(schemas.size());

            for (BinarySchema schema : schemas)
                schema.writeTo(out);
        }

        out.writeBoolean(isEnum);

        if (isEnum) {
            Map<String, Integer> map = enumMap();

            out.writeInt(map.size());

            for (Map.Entry<String, Integer> e : map.entrySet()) {
                U.writeString(out, e.getKey());

                out.writeInt(e.getValue());
            }
        }
    }

    /** {@inheritDoc} */
    @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        readFrom(in);
    }

    /**
     * The object implements the readFrom method to restore its
     * contents by calling the methods of DataInput for primitive
     * types and strings or calling readExternal for other objects.  The
     * readFrom method must read the values in the same sequence
     * and with the same types as were written by writeTo.
     *
     * @param in the stream to read data from in order to restore the object.
     * @exception IOException if I/O errors occur.
     */
    public void readFrom(DataInput in) throws IOException {
        in.readByte(); //skip version

        typeId = in.readInt();
        typeName = U.readString(in);

        int fieldsSize = in.readInt();

        if (fieldsSize == -1)
            fields = null;
        else {
            fields = new HashMap<>();

            for (int i = 0; i < fieldsSize; i++) {
                String fieldName = U.readString(in);

                BinaryFieldMetadata fieldMeta = new BinaryFieldMetadata();
                fieldMeta.readFrom(in);

                fields.put(fieldName, fieldMeta);
            }
        }

        affKeyFieldName = U.readString(in);

        int schemasSize = in.readInt();

        if (schemasSize == -1) {
            schemas = null;

            schemaIds = Collections.emptySet();
        }
        else {
            schemas = new ArrayList<>();

            schemaIds = U.newHashSet(schemasSize);

            for (int i = 0; i < schemasSize; i++) {
                BinarySchema schema = new BinarySchema();

                schema.readFrom(in);

                schemas.add(schema);

                schemaIds.add(schema.schemaId());
            }
        }

        isEnum = in.readBoolean();

        if (isEnum) {
            int size = in.readInt();

            if (size >= 0) {
                ordinalToName = new LinkedHashMap<>(size);
                nameToOrdinal = new LinkedHashMap<>(size);

                for (int idx = 0; idx < size; idx++) {
                    String name = U.readString(in);

                    int ord = in.readInt();

                    ordinalToName.put(ord, name);
                    nameToOrdinal.put(name, ord);
                }
            }
        }
    }

    /**
     * Gets enum constant name given its ordinal value.
     *
     * @param ord Enum constant ordinal value.
     * @return Enum constant name.
     */
    public String getEnumNameByOrdinal(int ord) {
        if (ordinalToName == null)
            return null;

        return ordinalToName.get(ord);
    }

    /**
     * Gets enum constant ordinal value given its name.
     *
     * @param name Enum constant name.
     * @return Enum constant ordinal value.
     */
    public Integer getEnumOrdinalByName(String name) {
        assert name != null;

        return nameToOrdinal.get(name);
    }

    /**
     * @return Name to ordinal mapping.
     */
    public Map<String, Integer> enumMap() {
        if (nameToOrdinal == null)
            return Collections.emptyMap();

        return nameToOrdinal;
    }

    /** {@inheritDoc} */
    @Override public String toString() {
        return S.toString(BinaryMetadata.class, this);
    }
}
