/*
 *  Copyright 2019 wjybxx
 *
 *  Licensed 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 iBn 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 com.wjybxx.fastjgame.net.binary;

import com.google.protobuf.Parser;
import com.wjybxx.fastjgame.utils.CollectionUtils;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.function.IntFunction;

/**
 * @author wjybxx
 * @version 1.0
 * date - 2020/2/23
 */
class ObjectReaderImp implements ObjectReader {

    private final CodecRegistry codecRegistry;
    private final DataInputStream inputStream;

    ObjectReaderImp(CodecRegistry codecRegistry, DataInputStream inputStream) {
        this.codecRegistry = codecRegistry;
        this.inputStream = inputStream;
    }

    @Override
    public int readInt() throws Exception {
        readTagAndCheck(BinaryTag.INT);
        return inputStream.readInt();
    }

    @Override
    public long readLong() throws Exception {
        readTagAndCheck(BinaryTag.LONG);
        return inputStream.readLong();
    }

    @Override
    public float readFloat() throws Exception {
        readTagAndCheck(BinaryTag.FLOAT);
        return inputStream.readFloat();
    }

    @Override
    public double readDouble() throws Exception {
        readTagAndCheck(BinaryTag.DOUBLE);
        return inputStream.readDouble();
    }

    @Override
    public short readShort() throws Exception {
        readTagAndCheck(BinaryTag.SHORT);
        return inputStream.readShort();
    }

    @Override
    public boolean readBoolean() throws Exception {
        readTagAndCheck(BinaryTag.BOOLEAN);
        return inputStream.readBoolean();
    }

    @Override
    public byte readByte() throws Exception {
        readTagAndCheck(BinaryTag.BYTE);
        return inputStream.readByte();
    }

    @Override
    public char readChar() throws Exception {
        readTagAndCheck(BinaryTag.CHAR);
        return inputStream.readChar();
    }

    private void readTagAndCheck(BinaryTag expectedTag) throws Exception {
        final BinaryTag tag = inputStream.readTag();
        if (tag != expectedTag) {
            throw new IOException("Incompatible wireType, expected: " + expectedTag + ", but read: " + tag);
        }
    }

    @Override
    public String readString() throws Exception {
        final BinaryTag tag = inputStream.readTag();
        if (tag == BinaryTag.NULL) {
            return null;
        }
        checkTag(tag, BinaryTag.STRING);

        return inputStream.readString();
    }

    @Override
    public <T> T readMessage(Parser<T> parser) throws Exception {
        final BinaryTag tag = inputStream.readTag();
        if (tag == BinaryTag.NULL) {
            return null;
        }
        checkTag(tag, BinaryTag.POJO);

        @SuppressWarnings("unchecked") final T message = (T) PojoCodec.readPojoImp(inputStream, codecRegistry);
        return message;
    }

    @Override
    public byte[] readBytes() throws Exception {
        final BinaryTag tag = inputStream.readTag();
        if (tag == BinaryTag.NULL) {
            return null;
        }

        checkTag(tag, BinaryTag.ARRAY);

        return ArrayCodec.readByteArray(inputStream);
    }

    @Nullable
    @Override
    public <C extends Collection<E>, E> C readCollection(@Nonnull IntFunction<C> collectionFactory) throws Exception {
        final BinaryTag tag = inputStream.readTag();
        if (tag == BinaryTag.NULL) {
            return null;
        }

        checkTag(tag, BinaryTag.COLLECTION);

        return CollectionCodec.readCollectionImp(inputStream, collectionFactory, this);
    }

    @Nullable
    @Override
    public <M extends Map<K, V>, K, V> M readMap(@Nonnull IntFunction<M> mapFactory) throws Exception {
        final BinaryTag tag = inputStream.readTag();
        if (tag == BinaryTag.NULL) {
            return null;
        }

        checkTag(tag, BinaryTag.MAP);

        return MapCodec.readMapImpl(inputStream, mapFactory, this);
    }

    @Nullable
    @Override
    public <T> T readArray(@Nonnull Class<?> componentType) throws Exception {
        final BinaryTag tag = inputStream.readTag();
        if (tag == BinaryTag.NULL) {
            return null;
        }

        checkTag(tag, BinaryTag.ARRAY);

        @SuppressWarnings("unchecked") final T array = (T) ArrayCodec.readArrayImpl(inputStream, componentType, this);
        return array;
    }

    public <E> E readEntity(EntityFactory<E> factory, Class<? super E> superClass) throws Exception {
        final BinaryTag tag = inputStream.readTag();
        if (tag == BinaryTag.NULL) {
            return null;
        }

        checkTag(tag, BinaryTag.POJO);

        return PojoCodec.readPolymorphicPojoImpl(inputStream, factory, superClass, this, codecRegistry);

    }

    @Override
    public <T> T readPreDeserializeObject() throws Exception {
        final BinaryTag tag = inputStream.readTag();
        if (tag == BinaryTag.NULL) {
            return null;
        }

        checkTag(tag, BinaryTag.ARRAY);

        final BinaryTag childType = inputStream.readTag();
        checkTag(childType, BinaryTag.BYTE);

        final int length = inputStream.readFixedInt32();
        final DataInputStream childInputStream = inputStream.slice(length);
        final ObjectReaderImp childReader = new ObjectReaderImp(codecRegistry, childInputStream);
        final T value = childReader.readObject();

        // 更新当前输入流的读索引
        inputStream.readIndex(inputStream.readIndex() + childInputStream.readIndex());

        return value;
    }

    private void checkTag(final BinaryTag readTag, final BinaryTag expectedTag) throws Exception {
        if (readTag != expectedTag) {
            throw new IOException("Incompatible wireType, expected: " + expectedTag + ", but read: " + readTag);
        }
    }

    // -----------------------------------------------------------------------------------------------------------

    @Override
    public <T> T readObject() throws Exception {
        final BinaryTag tag = inputStream.readTag();
        @SuppressWarnings("unchecked") final T result = (T) readObjectImpl(tag);
        return result;
    }

    private Object readObjectImpl(BinaryTag tag) throws Exception {
        switch (tag) {
            case NULL:
                return null;

            case BYTE:
                return inputStream.readByte();
            case CHAR:
                return inputStream.readChar();
            case SHORT:
                return inputStream.readShort();
            case INT:
                return inputStream.readInt();
            case BOOLEAN:
                return inputStream.readBoolean();
            case LONG:
                return inputStream.readLong();
            case FLOAT:
                return inputStream.readFloat();
            case DOUBLE:
                return inputStream.readDouble();
            case STRING:
                return inputStream.readString();

            case POJO:
                return PojoCodec.readPojoImp(inputStream, codecRegistry);
            case ARRAY:
                return ArrayCodec.readArrayImpl(inputStream, null, this);
            case MAP:
                return MapCodec.readMapImpl(inputStream, CollectionUtils::newLinkedHashMapWithExpectedSize, this);
            case COLLECTION:
                return CollectionCodec.readCollectionImp(inputStream, ArrayList::new, this);
            default:
                throw new IOException("unexpected tag : " + tag);
        }
    }
}