/*
 * Copyright 2014 the original author or authors.
 *
 * 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 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.gradle.api.internal.changedetection.state;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import org.gradle.internal.serialize.Decoder;
import org.gradle.internal.serialize.Encoder;
import org.gradle.internal.serialize.HashCodeSerializer;
import org.gradle.internal.serialize.Serializer;

import java.io.IOException;
import java.util.Map;

class InputPropertiesSerializer implements Serializer<ImmutableMap<String, ValueSnapshot>> {
    private static final int NULL_SNAPSHOT = 0;
    private static final int TRUE_SNAPSHOT = 1;
    private static final int FALSE_SNAPSHOT = 2;
    private static final int STRING_SNAPSHOT = 3;
    private static final int INTEGER_SNAPSHOT = 4;
    private static final int LONG_SNAPSHOT = 5;
    private static final int SHORT_SNAPSHOT = 6;
    private static final int FILE_SNAPSHOT = 7;
    private static final int ENUM_SNAPSHOT = 8;
    private static final int EMPTY_ARRAY_SNAPSHOT = 9;
    private static final int ARRAY_SNAPSHOT = 10;
    private static final int EMPTY_LIST_SNAPSHOT = 11;
    private static final int LIST_SNAPSHOT = 12;
    private static final int SET_SNAPSHOT = 13;
    private static final int MAP_SNAPSHOT = 14;
    private static final int DEFAULT_SNAPSHOT = 15;

    private final HashCodeSerializer serializer = new HashCodeSerializer();

    public ImmutableSortedMap<String, ValueSnapshot> read(Decoder decoder) throws Exception {
        int size = decoder.readSmallInt();
        if (size == 0) {
            return ImmutableSortedMap.of();
        }
        if (size == 1) {
            return ImmutableSortedMap.of(decoder.readString(), readSnapshot(decoder));
        }

        ImmutableSortedMap.Builder<String, ValueSnapshot> builder = ImmutableSortedMap.naturalOrder();
        for (int i = 0; i < size; i++) {
            builder.put(decoder.readString(), readSnapshot(decoder));
        }
        return builder.build();
    }

    private ValueSnapshot readSnapshot(Decoder decoder) throws Exception {
        int type = decoder.readSmallInt();
        int size;
        ValueSnapshot[] elements;
        switch (type) {
            case NULL_SNAPSHOT:
                return NullValueSnapshot.INSTANCE;
            case TRUE_SNAPSHOT:
                return BooleanValueSnapshot.TRUE;
            case FALSE_SNAPSHOT:
                return BooleanValueSnapshot.FALSE;
            case STRING_SNAPSHOT:
                return new StringValueSnapshot(decoder.readString());
            case INTEGER_SNAPSHOT:
                return new IntegerValueSnapshot(decoder.readInt());
            case LONG_SNAPSHOT:
                return new LongValueSnapshot(decoder.readLong());
            case SHORT_SNAPSHOT:
                return new ShortValueSnapshot((short) decoder.readInt());
            case FILE_SNAPSHOT:
                return new FileValueSnapshot(decoder.readString());
            case ENUM_SNAPSHOT:
                return new EnumValueSnapshot(decoder.readString(), decoder.readString());
            case EMPTY_ARRAY_SNAPSHOT:
                return ArrayValueSnapshot.EMPTY;
            case ARRAY_SNAPSHOT:
                size = decoder.readSmallInt();
                elements = new ValueSnapshot[size];
                for (int i = 0; i < size; i++) {
                    elements[i] = readSnapshot(decoder);
                }
                return new ArrayValueSnapshot(elements);
            case EMPTY_LIST_SNAPSHOT:
                return ListValueSnapshot.EMPTY;
            case LIST_SNAPSHOT:
                size = decoder.readSmallInt();
                elements = new ValueSnapshot[size];
                for (int i = 0; i < size; i++) {
                    elements[i] = readSnapshot(decoder);
                }
                return new ListValueSnapshot(elements);
            case SET_SNAPSHOT:
                size = decoder.readSmallInt();
                ImmutableSet.Builder<ValueSnapshot> setBuilder = ImmutableSet.builder();
                for (int i = 0; i < size; i++) {
                    setBuilder.add(readSnapshot(decoder));
                }
                return new SetValueSnapshot(setBuilder.build());
            case MAP_SNAPSHOT:
                size = decoder.readSmallInt();
                ImmutableMap.Builder<ValueSnapshot, ValueSnapshot> mapBuilder = ImmutableMap.builder();
                for (int i = 0; i < size; i++) {
                    mapBuilder.put(readSnapshot(decoder), readSnapshot(decoder));
                }
                return new MapValueSnapshot(mapBuilder.build());
            case DEFAULT_SNAPSHOT:
                return new SerializedValueSnapshot(decoder.readBoolean() ? serializer.read(decoder) : null, decoder.readBinary());
            default:
                throw new IllegalArgumentException("Don't know how to deserialize a snapshot with type tag " + type);
        }
    }

    public void write(Encoder encoder, ImmutableMap<String, ValueSnapshot> properties) throws Exception {
        encoder.writeSmallInt(properties.size());
        for (Map.Entry<String, ValueSnapshot> entry : properties.entrySet()) {
            encoder.writeString(entry.getKey());
            writeEntry(encoder, entry.getValue());
        }
    }

    private void writeEntry(Encoder encoder, ValueSnapshot snapshot) throws IOException {
        if (snapshot == NullValueSnapshot.INSTANCE) {
            encoder.writeSmallInt(NULL_SNAPSHOT);
        } else if (snapshot instanceof StringValueSnapshot) {
            StringValueSnapshot stringSnapshot = (StringValueSnapshot) snapshot;
            encoder.writeSmallInt(STRING_SNAPSHOT);
            encoder.writeString(stringSnapshot.getValue());
        } else if (snapshot instanceof ListValueSnapshot) {
            ListValueSnapshot listSnapshot = (ListValueSnapshot) snapshot;
            if (listSnapshot.getElements().length == 0) {
                encoder.writeSmallInt(EMPTY_LIST_SNAPSHOT);
            } else {
                encoder.writeSmallInt(LIST_SNAPSHOT);
                encoder.writeSmallInt(listSnapshot.getElements().length);
                for (ValueSnapshot valueSnapshot : listSnapshot.getElements()) {
                    writeEntry(encoder, valueSnapshot);
                }
            }
        } else if (snapshot == BooleanValueSnapshot.TRUE) {
            encoder.writeSmallInt(TRUE_SNAPSHOT);
        } else if (snapshot == BooleanValueSnapshot.FALSE) {
            encoder.writeSmallInt(FALSE_SNAPSHOT);
        } else if (snapshot instanceof IntegerValueSnapshot) {
            IntegerValueSnapshot integerSnapshot = (IntegerValueSnapshot) snapshot;
            encoder.writeSmallInt(INTEGER_SNAPSHOT);
            encoder.writeInt(integerSnapshot.getValue());
        } else if (snapshot instanceof LongValueSnapshot) {
            LongValueSnapshot longSnapshot = (LongValueSnapshot) snapshot;
            encoder.writeSmallInt(LONG_SNAPSHOT);
            encoder.writeLong(longSnapshot.getValue());
        } else if (snapshot instanceof ShortValueSnapshot) {
            ShortValueSnapshot shortSnapshot = (ShortValueSnapshot) snapshot;
            encoder.writeSmallInt(SHORT_SNAPSHOT);
            encoder.writeInt(shortSnapshot.getValue());
        } else if (snapshot instanceof FileValueSnapshot) {
            FileValueSnapshot fileSnapshot = (FileValueSnapshot) snapshot;
            encoder.writeSmallInt(FILE_SNAPSHOT);
            encoder.writeString(fileSnapshot.getValue());
        } else if (snapshot instanceof EnumValueSnapshot) {
            EnumValueSnapshot enumSnapshot = (EnumValueSnapshot) snapshot;
            encoder.writeSmallInt(ENUM_SNAPSHOT);
            encoder.writeString(enumSnapshot.getClassName());
            encoder.writeString(enumSnapshot.getName());
        } else if (snapshot instanceof SetValueSnapshot) {
            SetValueSnapshot setSnapshot = (SetValueSnapshot) snapshot;
            encoder.writeSmallInt(SET_SNAPSHOT);
            encoder.writeSmallInt(setSnapshot.getElements().size());
            for (ValueSnapshot valueSnapshot : setSnapshot.getElements()) {
                writeEntry(encoder, valueSnapshot);
            }
        } else if (snapshot instanceof SerializedValueSnapshot) {
            SerializedValueSnapshot valueSnapshot = (SerializedValueSnapshot) snapshot;
            encoder.writeSmallInt(DEFAULT_SNAPSHOT);
            if (valueSnapshot.getImplementationHash() == null) {
                encoder.writeBoolean(false);
            } else {
                encoder.writeBoolean(true);
                serializer.write(encoder, valueSnapshot.getImplementationHash());
            }
            encoder.writeBinary(valueSnapshot.getValue());
        } else if (snapshot instanceof MapValueSnapshot) {
            MapValueSnapshot mapSnapshot = (MapValueSnapshot) snapshot;
            encoder.writeSmallInt(MAP_SNAPSHOT);
            encoder.writeSmallInt(mapSnapshot.getEntries().size());
            for (Map.Entry<ValueSnapshot, ValueSnapshot> entry : mapSnapshot.getEntries().entrySet()) {
                writeEntry(encoder, entry.getKey());
                writeEntry(encoder, entry.getValue());
            }
        } else if (snapshot instanceof ArrayValueSnapshot) {
            ArrayValueSnapshot arraySnapshot = (ArrayValueSnapshot) snapshot;
            if (arraySnapshot.getElements().length == 0) {
                encoder.writeSmallInt(EMPTY_ARRAY_SNAPSHOT);
            } else {
                encoder.writeSmallInt(ARRAY_SNAPSHOT);
                encoder.writeSmallInt(arraySnapshot.getElements().length);
                for (ValueSnapshot valueSnapshot : arraySnapshot.getElements()) {
                    writeEntry(encoder, valueSnapshot);
                }
            }
        } else {
            throw new IllegalArgumentException("Don't know how to serialize a value of type " + snapshot.getClass().getSimpleName());
        }
    }
}
