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

import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.apache.ignite.internal.binary.BinaryWriterExImpl;
import org.apache.ignite.internal.binary.GridBinaryMarshaller;
import org.apache.ignite.internal.binary.BinaryUtils;
import org.jetbrains.annotations.Nullable;

/**
 *
 */
class BinaryLazyMap extends AbstractMap<Object, Object> implements BinaryBuilderSerializationAware {
    /** */
    private final BinaryBuilderReader reader;

    /** */
    private final int off;

    /** */
    private Map<Object, Object> delegate;

    /**
     * @param reader Reader.
     * @param off Offset.
     */
    private BinaryLazyMap(BinaryBuilderReader reader, int off) {
        this.reader = reader;
        this.off = off;
    }

    /**
     * @param reader Reader.
     * @return BinaryLazyMap.
     */
    @Nullable public static BinaryLazyMap parseMap(BinaryBuilderReader reader) {
        int off = reader.position() - 1;

        int size = reader.readInt();

        reader.skip(1); // map type.

        for (int i = 0; i < size; i++) {
            reader.skipValue(); // skip key
            reader.skipValue(); // skip value
        }

        return new BinaryLazyMap(reader, off);
    }

    /**
     *
     */
    private void ensureDelegateInit() {
        if (delegate == null) {
            int size = reader.readIntPositioned(off + 1);

            reader.position(off + 1/* flag */ + 4/* size */ + 1/* col type */);

            delegate = new LinkedHashMap<>();

            for (int i = 0; i < size; i++)
                delegate.put(BinaryUtils.unwrapLazy(reader.parseValue()), reader.parseValue());
        }
    }

    /** {@inheritDoc} */
    @Override public void writeTo(BinaryWriterExImpl writer, BinaryBuilderSerializer ctx) {
        if (delegate == null) {
            int size = reader.readIntPositioned(off + 1);

            int hdrSize = 1 /* flag */ + 4 /* size */ + 1 /* col type */;
            writer.write(reader.array(), off, hdrSize);

            reader.position(off + hdrSize);

            for (int i = 0; i < size; i++) {
                ctx.writeValue(writer, reader.parseValue()); // key
                ctx.writeValue(writer, reader.parseValue()); // value
            }
        }
        else {
            writer.writeByte(GridBinaryMarshaller.MAP);
            writer.writeInt(delegate.size());

            byte colType = reader.array()[off + 1 /* flag */ + 4 /* size */];

            writer.writeByte(colType);

            for (Entry<Object, Object> entry : delegate.entrySet()) {
                ctx.writeValue(writer, entry.getKey());
                ctx.writeValue(writer, entry.getValue());
            }
        }
    }

    /** {@inheritDoc} */
    @Override public int size() {
        if (delegate == null)
            return reader.readIntPositioned(off + 1);

        return delegate.size();
    }

    /** {@inheritDoc} */
    @Override public boolean containsKey(Object key) {
        ensureDelegateInit();

        return delegate.containsKey(key);
    }

    /** {@inheritDoc} */
    @Override public boolean containsValue(Object val) {
        return values().contains(val);
    }

    /** {@inheritDoc} */
    @Override public Set<Object> keySet() {
        ensureDelegateInit();

        return delegate.keySet();
    }

    /** {@inheritDoc} */
    @Override public void clear() {
        if (delegate == null)
            delegate = new LinkedHashMap<>();
        else
            delegate.clear();
    }

    /** {@inheritDoc} */
    @Override public Object get(Object key) {
        ensureDelegateInit();

        return BinaryUtils.unwrapLazy(delegate.get(key));
    }

    /** {@inheritDoc} */
    @Override public Object put(Object key, Object val) {
        ensureDelegateInit();

        return BinaryUtils.unwrapLazy(delegate.put(key, val));
    }

    /** {@inheritDoc} */
    @Override public Object remove(Object key) {
        ensureDelegateInit();

        return BinaryUtils.unwrapLazy(delegate.remove(key));
    }

    /** {@inheritDoc} */
    @Override public Set<Entry<Object, Object>> entrySet() {
        ensureDelegateInit();

        return new AbstractSet<Entry<Object, Object>>() {
            @Override public boolean contains(Object o) {
                throw new UnsupportedOperationException();
            }

            @Override public Iterator<Entry<Object, Object>> iterator() {
                return new Iterator<Entry<Object, Object>>() {
                    /** */
                    private final Iterator<Entry<Object, Object>> itr = delegate.entrySet().iterator();

                    @Override public boolean hasNext() {
                        return itr.hasNext();
                    }

                    @Override public Entry<Object, Object> next() {
                        Entry<Object, Object> res = itr.next();

                        final Object val = res.getValue();

                        if (val instanceof BinaryLazyValue) {
                            return new SimpleEntry<Object, Object>(res.getKey(), val) {
                                private static final long serialVersionUID = 0L;

                                @Override public Object getValue() {
                                    return ((BinaryLazyValue)val).value();
                                }
                            };
                        }

                        return res;
                    }

                    @Override public void remove() {
                        itr.remove();
                    }
                };
            }

            @Override public int size() {
                return delegate.size();
            }
        };
    }
}
