/*
 * 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.ml.math.impls.vector;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.stream.IntStream;
import junit.framework.TestCase;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.internal.util.IgniteUtils;
import org.apache.ignite.ml.math.IdentityValueMapper;
import org.apache.ignite.ml.math.Vector;
import org.apache.ignite.ml.math.distributed.VectorKeyMapper;
import org.apache.ignite.ml.math.exceptions.UnsupportedOperationException;
import org.apache.ignite.ml.math.functions.Functions;
import org.apache.ignite.ml.math.impls.MathTestConstants;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.apache.ignite.testframework.junits.common.GridCommonTest;

/**
 * Tests for {@link CacheVector}.
 */
@GridCommonTest(group = "Distributed Models")
public class CacheVectorTest extends GridCommonAbstractTest {
    /** Number of nodes in grid */
    private static final int NODE_COUNT = 3;
    /** Cache name. */
    private static final String CACHE_NAME = "test-cache";
    /** Cache size. */
    private static final int size = MathTestConstants.STORAGE_SIZE;

    /** Grid instance. */
    private Ignite ignite;
    /** Default key mapper. */
    private VectorKeyMapper<Integer> keyMapper = new TestKeyMapper();

    /**
     * Default constructor.
     */
    public CacheVectorTest() {
        super(false);
    }

    /** {@inheritDoc} */
    @Override protected void beforeTestsStarted() throws Exception {
        for (int i = 1; i <= NODE_COUNT; i++)
            startGrid(i);
    }

    /** {@inheritDoc} */
    @Override protected void afterTestsStopped() throws Exception {
        stopAllGrids();
    }

    /** {@inheritDoc} */
    @Override protected void beforeTest() throws Exception {
        ignite = grid(NODE_COUNT);

        ignite.configuration().setPeerClassLoadingEnabled(true);
    }

    /** {@inheritDoc} */
    @Override protected void afterTest() throws Exception {
        ignite.destroyCache(CACHE_NAME);
    }

    /** */
    public void testGetSet() throws Exception {
        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        for (int i = 0; i < size; i++) {
            double random = Math.random();
            cacheVector.set(i, random);
            assertEquals("Unexpected value.", random, cacheVector.get(i), 0d);
        }
    }

    /** */
    public void testMap() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        initVector(cacheVector);

        cacheVector.map(value -> 110d);

        for (int i = 0; i < size; i++)
            assertEquals("Unexpected value.", cacheVector.get(i), 110d, 0d);
    }

    /** */
    public void testMapBiFunc() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        initVector(cacheVector);

        cacheVector.map(Functions.PLUS, 1d);

        for (int i = 0; i < size; i++)
            assertEquals("Unexpected value.", cacheVector.get(i), 1d, 0d);
    }

    /** */
    public void testSum() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        initVector(cacheVector);

        assertEquals("Unexpected value.", cacheVector.sum(), 0d, 0d);

        cacheVector.assign(1d);

        assertEquals("Unexpected value.", cacheVector.sum(), size, 0d);
    }

    /** */
    public void testSumEmptyVector() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        cacheVector.sum();
    }

    /** */
    public void testAssign() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        initVector(cacheVector);

        cacheVector.assign(1d);

        for (int i = 0; i < size; i++)
            assertEquals("Unexpected value.", cacheVector.get(i), 1d, 0d);
    }

    /** */
    public void testAssignRange() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        cacheVector.assign(IntStream.range(0, size).asDoubleStream().toArray());

        for (int i = 0; i < size; i++)
            assertEquals("Unexpected value.", cacheVector.get(i), i, 0d);
    }

    /** */
    public void testAssignVector() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        Vector testVec = new DenseLocalOnHeapVector(IntStream.range(0, size).asDoubleStream().toArray());

        cacheVector.assign(testVec);

        for (int i = 0; i < size; i++)
            assertEquals("Unexpected value.", cacheVector.get(i), testVec.get(i), 0d);
    }

    /** */
    public void testAssignFunc() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        cacheVector.assign(idx -> idx);

        for (int i = 0; i < size; i++)
            assertEquals("Unexpected value.", cacheVector.get(i), i, 0d);
    }

    /** */
    public void testPlus() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        initVector(cacheVector);

        cacheVector.plus(1d);

        for (int i = 0; i < size; i++)
            assertEquals("Unexpected value.", cacheVector.get(i), 1d, 0d);
    }

    /** */
    public void testPlusVec() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        Vector testVec = new DenseLocalOnHeapVector(IntStream.range(0, size).asDoubleStream().toArray());

        try {
            cacheVector.plus(testVec);
            TestCase.fail();
        }
        catch (UnsupportedOperationException ignored) {

        }
    }

    /** */
    public void testDivide() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        final int size = MathTestConstants.STORAGE_SIZE;

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        initVector(cacheVector);
        cacheVector.assign(1d);

        cacheVector.divide(2d);

        for (int i = 0; i < size; i++)
            assertEquals("Unexpected value.", cacheVector.get(i), 1d / 2d, 0d);
    }

    /** */
    public void testTimes() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        final int size = MathTestConstants.STORAGE_SIZE;

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        initVector(cacheVector);
        cacheVector.assign(1d);

        cacheVector.times(2d);

        for (int i = 0; i < size; i++)
            assertEquals("Unexpected value.", cacheVector.get(i), 2d, 0d);
    }

    /** */
    public void testTimesVector() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        cacheVector.assign(1d);
        Vector testVec = new DenseLocalOnHeapVector(IntStream.range(0, size).asDoubleStream().toArray());

        try {
            cacheVector.times(testVec);
            TestCase.fail();
        }
        catch (UnsupportedOperationException ignored) {

        }

    }

    /** */
    public void testMin() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        Vector testVec = new DenseLocalOnHeapVector(IntStream.range(0, size).asDoubleStream().toArray());

        cacheVector.assign(testVec);

        assertEquals("Unexpected value.", cacheVector.minValue(), 0d, 0d);
    }

    /** */
    public void testMax() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        Vector testVec = new DenseLocalOnHeapVector(IntStream.range(0, size).asDoubleStream().toArray());

        cacheVector.assign(testVec);

        assertEquals("Unexpected value.", cacheVector.maxValue(), testVec.get(size - 1), 0d);
    }

    /** */
    public void testLike() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        try {
            cacheVector.like(size);
            TestCase.fail("Unsupported case");
        }
        catch (UnsupportedOperationException ignored) {

        }
    }

    /** */
    public void testLikeMatrix() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        try {
            cacheVector.likeMatrix(size, size);
            TestCase.fail("Unsupported case");
        }
        catch (UnsupportedOperationException ignored) {

        }
    }

    /** */
    public void testCopy() {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        try {
            cacheVector.copy();
            TestCase.fail("Unsupported case");
        }
        catch (UnsupportedOperationException ignored) {

        }
    }

    /** */
    public void testExternalize() throws IOException, ClassNotFoundException {
        IgniteUtils.setCurrentIgniteName(ignite.configuration().getIgniteInstanceName());

        IdentityValueMapper valMapper = new IdentityValueMapper();
        CacheVector<Integer, Double> cacheVector = new CacheVector<>(size, getCache(), keyMapper, valMapper);

        cacheVector.set(1, 1.0);

        ByteArrayOutputStream byteArrOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objOutputStream = new ObjectOutputStream(byteArrOutputStream);

        objOutputStream.writeObject(cacheVector);

        ByteArrayInputStream byteArrInputStream = new ByteArrayInputStream(byteArrOutputStream.toByteArray());
        ObjectInputStream objInputStream = new ObjectInputStream(byteArrInputStream);

        CacheVector objRestored = (CacheVector)objInputStream.readObject();

        assertTrue(MathTestConstants.VAL_NOT_EQUALS, cacheVector.equals(objRestored));
        assertEquals(MathTestConstants.VAL_NOT_EQUALS, objRestored.get(1), 1.0, 0.0);
    }

    /** */
    private void initVector(CacheVector cacheVector) {
        for (int i = 0; i < cacheVector.size(); i++)
            cacheVector.set(i, 0d);
    }

    /** */
    private IgniteCache<Integer, Double> getCache() {
        assert ignite != null;

        CacheConfiguration cfg = new CacheConfiguration();
        cfg.setName(CACHE_NAME);

        IgniteCache<Integer, Double> cache = ignite.getOrCreateCache(CACHE_NAME);

        assert cache != null;
        return cache;
    }

    /** */
    private static class TestKeyMapper implements VectorKeyMapper<Integer> {
        /** {@inheritDoc} */
        @Override public Integer apply(int i) {
            return i;
        }

        /** {@inheritDoc} */
        @Override public boolean isValid(Integer i) {
            return i < size;
        }
    }
}
