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

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.KryoSerializable;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.internal.binary.BinaryMarshaller;
import org.apache.ignite.internal.util.lang.GridTuple;
import org.apache.ignite.internal.util.typedef.CI1;
import org.apache.ignite.internal.util.typedef.CIX1;
import org.apache.ignite.internal.util.typedef.CO;
import org.apache.ignite.internal.util.typedef.COX;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgniteInClosure;
import org.apache.ignite.lang.IgniteOutClosure;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.junit.Test;

/**
 * Marshallers benchmark.
 */
public class GridMarshallerPerformanceTest extends GridCommonAbstractTest {
    /** Number of iterations per test. */
    private static final int ITER_CNT = 1 * 1000 * 1000;

    /**
     * Flag that sets whether collections should be marshalled and
     * unmarshalled using writeObject/readObject methods.
     *
     * Set to false to correctly compare with ByteBuffer.
     */
    private static final boolean MARSHAL_COLS_AS_OBJECTS = false;

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testSerialization() throws Exception {
        final ByteArrayOutputStream out = new ByteArrayOutputStream();

        IgniteInClosure<TestObject> writer = new CIX1<TestObject>() {
            @Override public void applyx(TestObject obj) throws IgniteCheckedException {
                out.reset();

                ObjectOutputStream objOut = null;

                try {
                    objOut = new ObjectOutputStream(out);

                    objOut.writeObject(obj);
                }
                catch (IOException e) {
                    throw new IgniteCheckedException(e);
                }
                finally {
                    U.close(objOut, log);
                }
            }
        };

        IgniteOutClosure<TestObject> reader = new COX<TestObject>() {
            @Override public TestObject applyx() throws IgniteCheckedException {
                ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());

                ObjectInputStream objIn = null;

                try {
                    objIn = new ObjectInputStream(in);

                    return (TestObject)objIn.readObject();
                }
                catch (ClassNotFoundException | IOException e) {
                    throw new IgniteCheckedException(e);
                }
                finally {
                    U.close(objIn, log);
                }
            }
        };

        runTest("Serialization", writer, reader);
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testGridMarshaller() throws Exception {
        final GridTuple<byte[]> tuple = new GridTuple<>();

        final BinaryMarshaller marsh = createStandaloneBinaryMarshaller();

        IgniteInClosure<TestObject> writer = new CIX1<TestObject>() {
            @Override public void applyx(TestObject obj) throws IgniteCheckedException {
                tuple.set(marsh.marshal(obj));
            }
        };

        IgniteOutClosure<TestObject> reader = new COX<TestObject>() {
            @Override public TestObject applyx() throws IgniteCheckedException {
                return marsh.unmarshal(tuple.get(), null);
            }
        };

        runTest("GridMarshaller", writer, reader);
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testByteBuffer() throws Exception {
        final ByteBuffer buf = ByteBuffer.allocate(1024);

        IgniteInClosure<TestObject> writer = new CI1<TestObject>() {
            @Override public void apply(TestObject obj) {
                buf.clear();

                obj.write(buf);
            }
        };

        IgniteOutClosure<TestObject> reader = new CO<TestObject>() {
            @Override public TestObject apply() {
                buf.flip();

                return TestObject.read(buf);
            }
        };

        runTest("ByteBuffer", writer, reader);
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testKryo() throws Exception {
        final Kryo kryo = new Kryo();

        final ByteArrayOutputStream out = new ByteArrayOutputStream();

        IgniteInClosure<TestObject> writer = new CI1<TestObject>() {
            @Override public void apply(TestObject obj) {
                out.reset();

                Output kryoOut = null;

                try {
                    kryoOut = new Output(out);

                    kryo.writeObject(kryoOut, obj);
                }
                finally {
                    U.close(kryoOut, log);
                }
            }
        };

        IgniteOutClosure<TestObject> reader = new CO<TestObject>() {
            @Override public TestObject apply() {
                Input kryoIn = null;

                try {
                    kryoIn = new Input(new ByteArrayInputStream(out.toByteArray()));

                    return kryo.readObject(kryoIn, TestObject.class);
                }
                finally {
                    U.close(kryoIn, log);
                }
            }
        };

        runTest("Kryo", writer, reader);
    }

    /**
     * @param name Test name.
     * @param writer Writer closure.
     * @param reader Reader closure.
     * @throws Exception In case of error.
     */
    private void runTest(String name, IgniteInClosure<TestObject> writer,
        IgniteOutClosure<TestObject> reader) throws Exception {
        ArrayList<Float> list = new ArrayList<>();

        list.add(10.0f);
        list.add(20.0f);
        list.add(30.0f);
        list.add(40.0f);
        list.add(50.0f);

        HashMap<Integer, Character> map = new HashMap<>();

        map.put(1, 'a');
        map.put(2, 'b');
        map.put(3, 'c');
        map.put(4, 'd');
        map.put(5, 'e');

        TestObject obj = new TestObject(123, 1234L, true,
            new long[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
            new double[] {0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0},
            list, map);

        for (int i = 0; i < 3; i++) {
            long start = System.currentTimeMillis();

            for (int j = 0; j < ITER_CNT; j++) {
                writer.apply(obj);

                TestObject newObj = reader.apply();

                assertEquals(obj, newObj);
            }

            long duration = System.currentTimeMillis() - start;

            System.out.format("%d %s => %,d ms\n", i, name, duration);
        }
    }

    /**
     * Test object.
     */
    private static class TestObject implements Externalizable, KryoSerializable {
        /** Integer value. */
        private int intVal;

        /** Long value. */
        private long longVal;

        /** Boolean value. */
        private boolean boolVal;

        /** Array of longs. */
        private long[] longArr;

        /** Array of doubles. */
        private double[] dblArr;

        /** ArrayList. */
        private ArrayList<Float> list;

        /** HashMap. */
        private HashMap<Integer, Character> map;

        /** Self reference. */
        private TestObject selfRef;

        /**
         * Required by {@link Externalizable}.
         */
        public TestObject() {
            // No-op.
        }

        /**
         * @param intVal Integer value.
         * @param longVal Long value.
         * @param boolVal Boolean value.
         * @param longArr Array of longs.
         * @param dblArr Array of doubles.
         * @param list Collection.
         * @param map Map.
         */
        TestObject(int intVal, long longVal, boolean boolVal, long[] longArr, double[] dblArr,
            ArrayList<Float> list, HashMap<Integer, Character> map) {
            this.intVal = intVal;
            this.longVal = longVal;
            this.boolVal = boolVal;
            this.longArr = longArr;
            this.dblArr = dblArr;
            this.list = list;
            this.map = map;

            selfRef = this;
        }

        /**
         * Writes this object to {@link ByteBuffer}.
         *
         * @param buf Buffer.
         */
        @SuppressWarnings("ForLoopReplaceableByForEach")
        void write(ByteBuffer buf) {
            buf.putInt(intVal);
            buf.putLong(longVal);
            buf.put((byte)(boolVal ? 1 : 0));

            buf.putInt(longArr.length);

            for (long l : longArr)
                buf.putLong(l);

            buf.putInt(dblArr.length);

            for (double d : dblArr)
                buf.putDouble(d);

            buf.putInt(list.size());

            for (int i = 0; i < list.size(); i++)
                buf.putFloat(list.get(i));

            buf.putInt(map.size());

            for (Map.Entry<Integer, Character> e : map.entrySet()) {
                buf.putInt(e.getKey());
                buf.putChar(e.getValue());
            }
        }

        /**
         * Reads from {@link ByteBuffer}.
         *
         * @param buf Buffer.
         * @return Object.
         */
        static TestObject read(ByteBuffer buf) {
            int intVal = buf.getInt();
            long longVal = buf.getLong();
            boolean boolVal = buf.get() == 1;

            int longArrSize = buf.getInt();

            long[] longArr = new long[longArrSize];

            for (int i = 0; i < longArrSize; i++)
                longArr[i] = buf.getLong();

            int dblArrSize = buf.getInt();

            double[] dblArr = new double[dblArrSize];

            for (int i = 0; i < dblArrSize; i++)
                dblArr[i] = buf.getDouble();

            int listSize = buf.getInt();

            ArrayList<Float> list = new ArrayList<>(listSize);

            for (int i = 0; i < listSize; i++)
                list.add(buf.getFloat());

            int mapSize = buf.getInt();

            HashMap<Integer, Character> map = new HashMap<>(mapSize);

            for (int i = 0; i < mapSize; i++)
                map.put(buf.getInt(), buf.getChar());

            TestObject o = new TestObject(intVal, longVal, boolVal, longArr, dblArr, list, map);

            o.selfRef = o;

            return o;
        }

        /** {@inheritDoc} */
        @SuppressWarnings("ForLoopReplaceableByForEach")
        @Override public void writeExternal(ObjectOutput out) throws IOException {
            out.writeInt(intVal);
            out.writeLong(longVal);
            out.writeBoolean(boolVal);
            out.writeObject(longArr);
            out.writeObject(dblArr);

            if (MARSHAL_COLS_AS_OBJECTS) {
                out.writeObject(list);
                out.writeObject(map);
            }
            else {
                out.writeInt(list.size());

                for (int i = 0; i < list.size(); i++)
                    out.writeFloat(list.get(i));

                out.writeInt(map.size());

                for (Map.Entry<Integer, Character> e : map.entrySet()) {
                    out.writeInt(e.getKey());
                    out.writeChar(e.getValue());
                }
            }

            out.writeObject(selfRef);
        }

        /** {@inheritDoc} */
        @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            intVal = in.readInt();
            longVal = in.readLong();
            boolVal = in.readBoolean();
            longArr = (long[])in.readObject();
            dblArr = (double[])in.readObject();

            if (MARSHAL_COLS_AS_OBJECTS) {
                list = (ArrayList<Float>)in.readObject();
                map = (HashMap<Integer, Character>)in.readObject();
            }
            else {
                int listSize = in.readInt();

                list = new ArrayList<>(listSize);

                for (int i = 0; i < listSize; i++)
                    list.add(in.readFloat());

                int mapSize = in.readInt();

                map = new HashMap<>(mapSize);

                for (int i = 0; i < mapSize; i++)
                    map.put(in.readInt(), in.readChar());
            }

            selfRef = (TestObject)in.readObject();
        }

        /** {@inheritDoc} */
        @SuppressWarnings("ForLoopReplaceableByForEach")
        @Override public void write(Kryo kryo, Output out) {
            kryo.writeObject(out, intVal);
            kryo.writeObject(out, longVal);
            kryo.writeObject(out, boolVal);
            kryo.writeObject(out, longArr);
            kryo.writeObject(out, dblArr);

            if (MARSHAL_COLS_AS_OBJECTS) {
                kryo.writeObject(out, list);
                kryo.writeObject(out, map);
            }
            else {
                kryo.writeObject(out, list.size());

                for (int i = 0; i < list.size(); i++)
                    kryo.writeObject(out, list.get(i));

                kryo.writeObject(out, map.size());

                for (Map.Entry<Integer, Character> e : map.entrySet()) {
                    kryo.writeObject(out, e.getKey());
                    kryo.writeObject(out, e.getValue());
                }
            }

            kryo.writeObject(out, selfRef);
        }

        /** {@inheritDoc} */
        @SuppressWarnings("unchecked")
        @Override public void read(Kryo kryo, Input in) {
            intVal = kryo.readObject(in, Integer.class);
            longVal = kryo.readObject(in, Long.class);
            boolVal = kryo.readObject(in, Boolean.class);
            longArr = kryo.readObject(in, long[].class);
            dblArr = kryo.readObject(in, double[].class);

            if (MARSHAL_COLS_AS_OBJECTS) {
                list = kryo.readObject(in, ArrayList.class);
                map = kryo.readObject(in, HashMap.class);
            }
            else {
                int listSize = kryo.readObject(in, Integer.class);

                list = new ArrayList<>(listSize);

                for (int i = 0; i < listSize; i++)
                    list.add(kryo.readObject(in, Float.class));

                int mapSize = kryo.readObject(in, Integer.class);

                map = new HashMap<>(mapSize);

                for (int i = 0; i < mapSize; i++)
                    map.put(kryo.readObject(in, Integer.class), kryo.readObject(in, Character.class));
            }

            selfRef = kryo.readObject(in, TestObject.class);
        }

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

            if (other == null || getClass() != other.getClass())
                return false;

            TestObject obj = (TestObject)other;

            assert this == selfRef;
            assert obj == obj.selfRef;

            return boolVal == obj.boolVal && intVal == obj.intVal && longVal == obj.longVal &&
                Arrays.equals(dblArr, obj.dblArr) && Arrays.equals(longArr, obj.longArr) &&
                list.equals(obj.list) && map.equals(obj.map);
        }
    }
}
