/*
 * 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.commons.math4.legacy.linear;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Random;

import org.apache.commons.math4.legacy.analysis.UnivariateFunction;
import org.apache.commons.math4.legacy.analysis.function.Sin;
import org.apache.commons.math4.legacy.exception.MathUnsupportedOperationException;
import org.apache.commons.math4.legacy.linear.RealVector.Entry;
import org.apache.commons.math4.core.jdkmath.JdkMath;
import org.junit.Assert;
import org.junit.Test;

/**
 * This is an abstract test of the {@link
 * RealVector#unmodifiableRealVector(RealVector) unmodifiable vector}
 * implementation. These unmodifiable vectors decorate a (modifiable)
 * {@link RealVector}; therefore, a new implementation of this abstract
 * test should be considered for each implementation of
 * {@link RealVector}.
 *
 *
 */
public abstract class UnmodifiableRealVectorAbstractTest {
    /** The dimension of the randomly generated vectors. */
    protected static final int DIM = 100;
    /** Absolute tolerance. */
    protected static final double EPS = 10 * Math.ulp(1d);
    /**
     * The list of methods which are excluded from the general test
     * {@link #testAllButExcluded()}.
     */
    protected static final Set<String> EXCLUDE = new HashSet<>();
    /** The random number generator (always initialized with the same seed. */
    protected static final Random RANDOM;

    static {
        EXCLUDE.add("getEntry");
        EXCLUDE.add("setEntry");
        EXCLUDE.add("addToEntry");
        EXCLUDE.add("getSubVector");
        EXCLUDE.add("setSubVector");
        EXCLUDE.add("iterator");
        EXCLUDE.add("sparseIterator");
        EXCLUDE.add("walkInDefaultOrder");
        EXCLUDE.add("walkInOptimizedOrder");
        EXCLUDE.add("ebeDivide");
        EXCLUDE.add("ebeMultiply");

        // Excluded because they are inherited from "Object".
        for (Method m : Object.class.getMethods()) {
            EXCLUDE.add(m.getName());
        }
        RANDOM = new Random(20110813);
    }

    /**
     * Returns {@code true} if the specified {@code double} are equal (within a
     * given tolerance).
     *
     * @param x First {@code double}.
     * @param y Second {@code double}.
     * @return {@code true} if {@code x} and {@code y} are equal.
     */
    public static boolean equals(final double x, final double y) {
        if (x == y) {
            return true;
        } else if (JdkMath.abs(x) <= EPS) {
            return JdkMath.abs(y) <= EPS;
        } else if (JdkMath.abs(y) <= EPS) {
            return JdkMath.abs(x) <= EPS;
        } else {
            return JdkMath.abs(x - y) <= EPS * JdkMath.min(JdkMath.abs(x), JdkMath.abs(y));
        }
    }

    /**
     * Returns {@code true} if the specified {@code double} arrays are equal
     * (within a given tolerance).
     *
     * @param x First array.
     * @param y Second array.
     * @return {@code true} if {@code x} and {@code y} are equal.
     */
    public static boolean equals(final double[] x, final double[] y) {
        if (x.length != y.length) {
            return false;
        }
        final int n = x.length;
        for (int i = 0; i < n; i++) {
            if (!equals(x[i], y[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns {@code true} if the specified {@code RealVector} are equal
     * (within a given tolerance).
     *
     * @param x First vector.
     * @param y Second vector.
     * @return {@code true} if {@code x} and {@code y} are equal.
     */
    public static boolean equals(final RealVector x, final RealVector y) {
        if (x.getDimension() != y.getDimension()) {
            return false;
        }
        final int n = x.getDimension();
        for (int i = 0; i < n; i++) {
            if (!equals(x.getEntry(i), y.getEntry(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns {@code true} if the specified {@code RealVector} is equal to the
     * specified {@code double} array (within a given tolerance).
     *
     * @param x Vector.
     * @param y Array.
     * @return {@code true} if {@code x} and {@code y} are equal.
     */
    public static boolean equals(final RealVector x, final double[] y) {
        if (x.getDimension() != y.length) {
            return false;
        }
        final int n = x.getDimension();
        for (int i = 0; i < n; i++) {
            if (!equals(x.getEntry(i), y[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns {@code true} if the specified {@code RealMatrix} are equal
     * (within a given tolerance).
     *
     * @param x First matrix.
     * @param y Second matrix.
     * @return {@code true} if {@code x} and {@code y} are equal.
     */
    public static boolean equals(final RealMatrix x, final RealMatrix y) {
        if (x.getRowDimension() != y.getRowDimension()) {
            return false;
        }
        if (x.getColumnDimension() != y.getColumnDimension()) {
            return false;
        }
        final int rows = x.getRowDimension();
        final int cols = x.getColumnDimension();
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (!equals(x.getEntry(i, j), y.getEntry(i, j))) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Returns {@code true} if the specified {@code Object} are equal.
     *
     * @param x First object.
     * @param y Second object.
     * @return {@code true} if {@code x} and {@code y} are equal.
     * @throws IllegalArgumentException if {@code x} and {@code y} could
     * not be compared.
     */
    public static boolean equals(final Object x, final Object y) {
        if (x instanceof Boolean) {
            if (y instanceof Boolean) {
                return ((Boolean) x).booleanValue() == ((Boolean) y)
                        .booleanValue();
            } else {
                return false;
            }
        }
        if (x instanceof Integer) {
            if (y instanceof Integer) {
                return ((Integer) x).intValue() == ((Integer) y).intValue();
            } else {
                return false;
            }
        } else if (x instanceof Double) {
            if (y instanceof Double) {
                return equals(((Double) x).doubleValue(),
                        ((Double) y).doubleValue());
            } else {
                return false;
            }
        } else if (x instanceof double[]) {
            if (y instanceof double[]) {
                return equals((double[]) x, (double[]) y);
            } else if (y instanceof RealVector) {
                return equals((RealVector) y, (double[]) x);
            } else {
                return false;
            }
        } else if (x instanceof RealVector) {
            if (y instanceof double[]) {
                return equals((RealVector) x, (double[]) y);
            } else if (y instanceof RealVector) {
                return equals((RealVector) x, (RealVector) y);
            } else {
                return false;
            }
        } else if (x instanceof RealMatrix) {
            if (y instanceof RealMatrix) {
                return equals((RealMatrix) x, (RealMatrix) y);
            } else {
                return false;
            }
        } else {
            throw new IllegalArgumentException("could not compare " + x + ", "
                    + y);
        }
    }

    /**
     * Creates a new random vector of a specified type. This vector is then to
     * be wrapped in an unmodifiable vector.
     *
     * @return a new random vector.
     */
    public abstract RealVector createVector();

    /**
     * Creates a new random object of the specified type.
     *
     * @param c Class of the object to be created.
     * @return a new random object.
     * @throws IllegalArgumentException if the specified class is not
     * recognized by this method.
     */
    public Object createParameter(final Class<?> c) {
        if (c == Integer.TYPE) {
            return Integer.valueOf(RANDOM.nextInt());
        } else if (c == Double.TYPE) {
            return Double.valueOf(RANDOM.nextDouble());
        } else if (c == double[].class) {
            final double[] v = new double[DIM];
            for (int i = 0; i < DIM; i++) {
                v[i] = RANDOM.nextDouble();
            }
            return v;
        } else if (c.isAssignableFrom(RealVector.class)) {
            return createVector();
        } else if (c.isAssignableFrom(UnivariateFunction.class)) {
            return new Sin();
        } else {
            throw new IllegalArgumentException("could not create " + c);
        }
    }

    /**
     * This is the general test of most methods in the
     * {@link RealVector#unmodifiableRealVector(RealVector) unmodifiable vector}.
     * It works as follows.
     * First, an unmodifiable view of a copy of the specified random vector
     * {@code u} is created: this defines {@code v}. Then the <em>same</em>
     * method {@code m} is invoked on {@code u} and {@code v}, with randomly
     * generated parameters {@code args}.
     * If it turns out that {@code u} has changed after the call of method
     * {@code m}, then this test checks that the call of this method on
     * {@code v} resulted in a {@link MathUnsupportedOperationException}. If
     * {@code u} was not modified, then this test checks that the results
     * returned by the call of method {@code m} on {@code u} and {@code v}
     * returned the same result.
     *
     * @param m Method to be tested.
     * @param u Random vector from which the unmodifiable view is to be
     *constructed.
     * @param args Arguments to be passed to method {@code m}.
     */
    private void callMethod(final Method m,
                            final RealVector u,
                            final Object... args)
        throws IllegalAccessException,
               IllegalArgumentException,
               InvocationTargetException {
        final RealVector uu = u.copy();
        final RealVector v = RealVector.unmodifiableRealVector(u.copy());
        Object exp = m.invoke(u, args);
        if (equals(uu, u)) {
            Object act = m.invoke(v, args);
            Assert.assertTrue(m.toGenericString() + ", unmodifiable vector has changed",
                              equals(uu, v));
            Assert.assertTrue(m.toGenericString() + ", wrong result",
                              equals(exp, act));
        } else {
            boolean flag = false;
            try {
                m.invoke(v, args);
            } catch (InvocationTargetException e) {
                if (e.getCause() instanceof MathUnsupportedOperationException) {
                    flag = true;
                }
            }
            Assert.assertTrue(m.toGenericString()+", exception should have been thrown", flag);
        }
    }

    /**
     * This test calls {@link #callMethod(Method, RealVector, Object...)} on
     * every method defined in interface {@link RealVector}. It generates the
     * appropriate random arguments. Some methods are manually excluded (see
     * {@link #EXCLUDE}), they must be handled by separate tests.
     */
    @Test
    public void testAllButExcluded()
        throws IllegalAccessException,
               IllegalArgumentException,
               InvocationTargetException {
        Method[] method = RealVector.class.getMethods();
        for (int i = 0; i < method.length; i++) {
            Method m = method[i];
            if (!EXCLUDE.contains(m.getName())) {
                RealVector u = (RealVector) createParameter(RealVector.class);
                Class<?>[] paramType = m.getParameterTypes();
                Object[] param = new Object[paramType.length];
                for (int j = 0; j < paramType.length; j++) {
                    param[j] = createParameter(paramType[j]);
                }
                callMethod(m, u, param);
            }
        }
    }

    @Test
    public void testGetEntry() {
        RealVector u = createVector();
        RealVector v = RealVector.unmodifiableRealVector(u);
        for (int i = 0; i < DIM; i++) {
            Assert.assertTrue(equals(u.getEntry(i), v.getEntry(i)));
        }
    }

    @Test(expected = MathUnsupportedOperationException.class)
    public void testSetEntry() {
        RealVector u = createVector();
        RealVector v = RealVector.unmodifiableRealVector(u);
        for (int i = 0; i < DIM; i++) {
            v.setEntry(i, 0d);
        }
    }

    @Test(expected = MathUnsupportedOperationException.class)
    public void testAddToEntry() {
        RealVector u = createVector();
        RealVector v = RealVector.unmodifiableRealVector(u);
        for (int i = 0; i < DIM; i++) {
            v.addToEntry(i, 0d);
        }
    }

    @Test
    public void testGetSubVector() {
        RealVector u = createVector();
        RealVector v = RealVector.unmodifiableRealVector(u);
        for (int i = 0; i < DIM; i++) {
            for (int n = 1; n < DIM - i; n++) {
                RealVector exp = u.getSubVector(i, n);
                RealVector act = v.getSubVector(i, n);
                Assert.assertTrue(equals(exp, act));
            }
        }
    }

    @Test(expected = MathUnsupportedOperationException.class)
    public void testSetSubVector() {
        RealVector u = createVector();
        RealVector v = RealVector.unmodifiableRealVector(u);
        v.setSubVector(0, new ArrayRealVector());
    }

    @Test
    public void testIterator() {
        RealVector u = createVector();
        Iterator<Entry> i = u.iterator();
        RealVector v = RealVector.unmodifiableRealVector(u.copy());
        Iterator<Entry> j = v.iterator();
        boolean flag;
        while (i.hasNext()) {
            Assert.assertTrue(j.hasNext());
            Entry exp = i.next();
            Entry act = j.next();
            Assert.assertTrue(equals(exp.getIndex(), act.getIndex()));
            Assert.assertTrue(equals(exp.getValue(), act.getValue()));
            exp.setIndex(RANDOM.nextInt(DIM));
            act.setIndex(RANDOM.nextInt(DIM));
            flag = false;
            try {
                act.setValue(RANDOM.nextDouble());
            } catch (MathUnsupportedOperationException e) {
                flag = true;
            }
            Assert.assertTrue("exception should have been thrown", flag);
        }
        Assert.assertFalse(j.hasNext());
    }

    @Test
    public void testSparseIterator() {
        RealVector u = createVector();
        Iterator<Entry> i = u.sparseIterator();
        RealVector v = RealVector.unmodifiableRealVector(u.copy());
        Iterator<Entry> j = v.sparseIterator();
        boolean flag;
        while (i.hasNext()) {
            Assert.assertTrue(j.hasNext());
            Entry exp = i.next();
            Entry act = j.next();
            Assert.assertTrue(equals(exp.getIndex(), act.getIndex()));
            Assert.assertTrue(equals(exp.getValue(), act.getValue()));
            exp.setIndex(RANDOM.nextInt(DIM));
            act.setIndex(RANDOM.nextInt(DIM));
            flag = false;
            try {
                act.setValue(RANDOM.nextDouble());
            } catch (MathUnsupportedOperationException e) {
                flag = true;
            }
            Assert.assertTrue("exception should have been thrown", flag);
        }
        Assert.assertFalse(j.hasNext());
    }
}
