/*
 * 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.math.BigDecimal;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.ignite.IgniteException;
import org.apache.ignite.binary.BinaryObject;
import org.apache.ignite.binary.BinaryObjectBuilder;
import org.apache.ignite.binary.BinaryObjectException;
import org.apache.ignite.binary.BinaryType;
import org.apache.ignite.internal.binary.builder.BinaryObjectBuilderImpl;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.SB;
import org.apache.ignite.lang.IgniteUuid;
import org.jetbrains.annotations.Nullable;

/**
 * Internal binary object interface.
 */
public abstract class BinaryObjectExImpl implements BinaryObjectEx {
    /**
     * @return Length.
     */
    public abstract int length();

    /**
     * @return Object start.
     */
    public abstract int start();

    /**
     * @return {@code True} if object is array based.
     */
    public abstract boolean hasArray();

    /**
     * @return Object array if object is array based, otherwise {@code null}.
     */
    public abstract byte[] array();

    /**
     * @return Object offheap address is object is offheap based, otherwise 0.
     */
    public abstract long offheapAddress();

    /**
     * Gets field value.
     *
     * @param fieldId Field ID.
     * @return Field value.
     * @throws org.apache.ignite.binary.BinaryObjectException In case of any other error.
     */
    @Nullable public abstract <F> F field(int fieldId) throws BinaryObjectException;

    /** {@inheritDoc} */
    @Override public int enumOrdinal() throws BinaryObjectException {
        throw new BinaryObjectException("Object is not enum.");
    }

    /** {@inheritDoc} */
    @Override public String enumName() throws BinaryObjectException {
        throw new BinaryObjectException("Object is not enum.");
    }

    /**
     * Get offset of data begin.
     *
     * @return Field value.
     */
    public abstract int dataStartOffset();

    /**
     * Get offset of the footer begin.
     *
     * @return Field value.
     */
    public abstract int footerStartOffset();

    /**
     * Get field by offset.
     *
     * @param order Field offset.
     * @return Field value.
     */
    @Nullable public abstract <F> F fieldByOrder(int order);

    /**
     * Create field comparator.
     *
     * @return Comparator.
     */
    public abstract BinarySerializedFieldComparator createFieldComparator();

    /**
     * Writes field value defined by the given field offset to the given byte buffer.
     *
     * @param fieldOffset Field offset.
     * @return Boolean flag indicating whether the field was successfully written to the buffer, {@code false}
     *      if there is no enough space for the field in the buffer.
     */
    protected abstract boolean writeFieldByOrder(int fieldOffset, ByteBuffer buf);

    /**
     * @param ctx Reader context.
     * @param fieldName Field name.
     * @return Field value.
     */
    @Nullable protected abstract <F> F field(BinaryReaderHandles ctx, String fieldName);

    /**
     * @return {@code True} if object has schema.
     */
    public abstract boolean hasSchema();

    /**
     * Get schema ID.
     *
     * @return Schema ID.
     */
    public abstract int schemaId();

    /**
     * Create schema for object.
     *
     * @return Schema.
     */
    public abstract BinarySchema createSchema();

    /**
     * Get binary context.
     *
     * @return Binary context.
     */
    public abstract BinaryContext context();

    /** {@inheritDoc} */
    @Override public BinaryObjectBuilder toBuilder() throws BinaryObjectException {
        return BinaryObjectBuilderImpl.wrap(this);
    }

    /** {@inheritDoc} */
    @Override public BinaryObject clone() throws CloneNotSupportedException {
        return (BinaryObject)super.clone();
    }

    /** {@inheritDoc} */
    public boolean equals(Object other) {
        if (other == this)
            return true;

        if (!(other instanceof BinaryObject))
            return false;

        BinaryIdentityResolver identity = context().identity(typeId());

        return identity.equals(this, (BinaryObject)other);
    }

    /** {@inheritDoc} */
    @Override public String toString() {
        try {
            BinaryReaderHandles ctx = new BinaryReaderHandles();

            ctx.put(start(), this);

            return toString(ctx, new IdentityHashMap<BinaryObject, Integer>());
        }
        catch (BinaryObjectException e) {
            throw new IgniteException("Failed to create string representation of binary object.", e);
        }
    }

    /**
     * @param ctx Reader context.
     * @param handles Handles for already traversed objects.
     * @return String representation.
     */
    private String toString(BinaryReaderHandles ctx, IdentityHashMap<BinaryObject, Integer> handles) {
        int idHash = System.identityHashCode(this);
        int hash = hashCode();

        BinaryType meta;

        try {
            meta = rawType();
        }
        catch (BinaryObjectException ignore) {
            meta = null;
        }

        if (meta == null || !S.INCLUDE_SENSITIVE)
            return S.toString(S.INCLUDE_SENSITIVE ? BinaryObject.class.getSimpleName() : "BinaryObject",
                "idHash", idHash, false,
                "hash", hash, false,
                "typeId", typeId(), true);

        handles.put(this, idHash);

        SB buf = new SB(meta.typeName());

        if (meta.fieldNames() != null) {
            buf.a(" [idHash=").a(idHash).a(", hash=").a(hash);

            for (String name : meta.fieldNames()) {
                Object val = field(ctx, name);

                buf.a(", ").a(name).a('=');

                appendValue(val, buf, ctx, handles);
            }

            buf.a(']');
        }

        return buf.toString();
    }

    /**
     * @param val Value to append.
     * @param buf Buffer to append to.
     * @param ctx Reader context.
     * @param handles Handles for already traversed objects.
     */
    @SuppressWarnings("unchecked")
    private void appendValue(Object val, SB buf, BinaryReaderHandles ctx,
        IdentityHashMap<BinaryObject, Integer> handles) {
        if (val instanceof byte[])
            buf.a(Arrays.toString((byte[])val));
        else if (val instanceof short[])
            buf.a(Arrays.toString((short[])val));
        else if (val instanceof int[])
            buf.a(Arrays.toString((int[])val));
        else if (val instanceof long[])
            buf.a(Arrays.toString((long[])val));
        else if (val instanceof float[])
            buf.a(Arrays.toString((float[])val));
        else if (val instanceof double[])
            buf.a(Arrays.toString((double[])val));
        else if (val instanceof char[])
            buf.a(Arrays.toString((char[])val));
        else if (val instanceof boolean[])
            buf.a(Arrays.toString((boolean[])val));
        else if (val instanceof BigDecimal[])
            buf.a(Arrays.toString((BigDecimal[])val));
        else if (val instanceof IgniteUuid)
            buf.a(val);
        else if (val instanceof BinaryObjectExImpl) {
            BinaryObjectExImpl po = (BinaryObjectExImpl)val;

            Integer idHash0 = handles.get(val);

            if (idHash0 != null) {  // Circular reference.
                BinaryType meta0 = po.rawType();

                assert meta0 != null;

                buf.a(meta0.typeName()).a(" [hash=").a(idHash0).a(", ...]");
            }
            else
                buf.a(po.toString(ctx, handles));
        }
        else if (val instanceof Object[]) {
            Object[] arr = (Object[])val;

            buf.a('[');

            for (int i = 0; i < arr.length; i++) {
                Object o = arr[i];

                appendValue(o, buf, ctx, handles);

                if (i < arr.length - 1)
                    buf.a(", ");
            }
        }
        else if (val instanceof Iterable) {
            Iterable<Object> col = (Iterable<Object>)val;

            buf.a(col.getClass().getSimpleName()).a(" {");

            Iterator it = col.iterator();

            while (it.hasNext()) {
                Object o = it.next();

                appendValue(o, buf, ctx, handles);

                if (it.hasNext())
                    buf.a(", ");
            }

            buf.a('}');
        }
        else if (val instanceof Map) {
            Map<Object, Object> map = (Map<Object, Object>)val;

            buf.a(map.getClass().getSimpleName()).a(" {");

            Iterator<Map.Entry<Object, Object>> it = map.entrySet().iterator();

            while (it.hasNext()) {
                Map.Entry<Object, Object> e = it.next();

                appendValue(e.getKey(), buf, ctx, handles);

                buf.a('=');

                appendValue(e.getValue(), buf, ctx, handles);

                if (it.hasNext())
                    buf.a(", ");
            }

            buf.a('}');
        }
        else
            buf.a(val);
    }

    /**
     * Check if object graph has circular references.
     *
     * @return {@code true} if object has circular references.
     */
    public boolean hasCircularReferences() {
        try {
            BinaryReaderHandles ctx = new BinaryReaderHandles();

            ctx.put(start(), this);

            return hasCircularReferences(ctx, new IdentityHashMap<BinaryObject, Integer>());
        }
        catch (BinaryObjectException e) {
            throw new IgniteException("Failed to check binary object for circular references", e);
        }
    }

    /**
     * @param ctx Reader context.
     * @param handles Handles for already traversed objects.
     * @return {@code true} if has circular reference.
     */
    private boolean hasCircularReferences(BinaryReaderHandles ctx, IdentityHashMap<BinaryObject, Integer> handles) {
        BinaryType meta;

        try {
            meta = rawType();
        }
        catch (BinaryObjectException ignore) {
            meta = null;
        }

        if (meta == null)
            return false;

        int idHash = System.identityHashCode(this);

        handles.put(this, idHash);

        if (meta.fieldNames() != null) {
            ctx.put(start(), this);

            for (String name : meta.fieldNames()) {
                Object val = field(ctx, name);

                if (val instanceof BinaryObjectExImpl) {
                    BinaryObjectExImpl po = (BinaryObjectExImpl)val;

                    Integer idHash0 = handles.get(val);

                    // Check for circular reference.
                    if (idHash0 != null || po.hasCircularReferences(ctx, handles))
                        return true;
                }
            }
        }

        return false;
    }
}
