/*
    Copyright (c) 2010-2012, GEM Foundation.

    OpenQuake is free software: you can redistribute it and/or modify it
    under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    OpenQuake is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with OpenQuake.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.gem.calc;

import static org.junit.Assert.*;

import java.util.Arrays;

import org.junit.Test;
import org.opensha.commons.geo.BorderType;
import org.opensha.commons.geo.Location;
import org.opensha.commons.geo.LocationList;
import org.opensha.sha.earthquake.rupForecastImpl.GEM1.GEM1ERF;
import org.opensha.sha.imr.param.SiteParams.Vs30_TypeParam.Vs30Type;
import org.opensha.sha.util.TectonicRegionType;

import static org.gem.calc.CalcTestHelper.*;
import static org.gem.calc.DisaggregationTestHelper.*;
import static org.gem.calc.DisaggregationCalculator.closestLocation;
import static org.gem.calc.DisaggregationCalculator.inRange;
import static org.gem.calc.DisaggregationCalculator.normalize;
import static org.gem.calc.DisaggregationCalculator.assertNonZeroStdDev;
import static org.gem.calc.CalcUtils.InputValidationException;

public class DisaggregationCalculatorTest
{

    public static final double AREA_SRC_DISCRETIZATION = 0.01;
    public static final int NUM_MFD_PTS = 41;
    public static final BorderType BORDER_TYPE = BorderType.MERCATOR_LINEAR;
    /**
     * If any of the bin edges passed to the constructor are null,
     * an IllegalArgumentException should be thrown.
     */
    @Test(expected=InputValidationException.class)
    public void testConstructorOneNull()
    {
        new DisaggregationCalculator(
                new Double[10], new Double[10], null,
                new Double[10]);
    }

    /**
     * Same the test above, except with all null input.
     */
    @Test(expected=InputValidationException.class)
    public void testConstructorManyNull()
    {
        new DisaggregationCalculator(null, null, null, null);
    }

    /**
     * If any of the bin edges passed to the constructor have a length < 2,
     * an IllegalArgumentException should be thrown.
     */
    @Test(expected=InputValidationException.class)
    public void testConstructorOneTooShort()
    {
        new DisaggregationCalculator(
                new Double[2], new Double[2], new Double[1],
                new Double[2]);
    }

    /**
     * Same as the test above, except all input arrays are too short.
     */
    @Test(expected=InputValidationException.class)
    public void testConstructorAllTooShort()
    {
        new DisaggregationCalculator(
                new Double[1], new Double[1], new Double[1],
                new Double[1]);
    }

    @Test(expected=InputValidationException.class)
    public void testConstructorUnsortedBinEdges()
    {
        Double[] unsorted = {1.1, 1.0};
        new DisaggregationCalculator(
                LAT_BIN_LIMS, LON_BIN_LIMS, unsorted,
                EPS_BIN_LIMS);
    }

    /**
     * Test constructor with known-good input.
     * (No errors should be thrown.)
     */
    @Test
    public void testConstructorGoodInput()
    {
        new DisaggregationCalculator(
                LAT_BIN_LIMS, LON_BIN_LIMS, MAG_BIN_LIMS,
                EPS_BIN_LIMS);
    }

    @Test
    public void testComputeMatrix()
    {
        DisaggregationCalculator disCalc = new DisaggregationCalculator(
                LAT_BIN_LIMS, LON_BIN_LIMS, MAG_BIN_LIMS,
                EPS_BIN_LIMS);

        GEM1ERF erf = makeTestERF(AREA_SRC_DISCRETIZATION, NUM_MFD_PTS, BORDER_TYPE);

        double minMag = (Double) erf.getParameter(GEM1ERF.MIN_MAG_NAME).getValue();

        double[][][][][] result = disCalc.computeMatrix(
                makeTestSite(), erf, makeTestImrMap(), POE,
                makeHazardCurve(LOG_IMLS, AREA_SRC_DISCRETIZATION, erf), minMag).getMatrix();

        // The expected results were generated by code independent of this
        // calculator.
        assertArrayEquals(EXPECTED, result, 0.00000009);
    }

    @Test(expected=RuntimeException.class)
    public void testComputeMatrixNonPoissonianErf()
    {
        DisaggregationCalculator disCalc = new DisaggregationCalculator(
                LAT_BIN_LIMS, LON_BIN_LIMS, MAG_BIN_LIMS,
                EPS_BIN_LIMS);

        disCalc.computeMatrix(
                null, new NonPoissonianERF(), null, 0.0, null, 0.0);
    }

    /**
     * Test for the simplified computeMatrix method (basically, with more
     * primitive input).
     *
     * The results should be same as the other computeMatrix method;
     * this is just to exercise different (but equivalent) input.
     *
     * The reason we want to test this is because a more primitive
     * method is easier to call through the Python-Java bridge.
     */
    @Test
    public void testComputeMatrix2()
    {
        DisaggregationCalculator disCalc = new DisaggregationCalculator(
                LAT_BIN_LIMS, LON_BIN_LIMS, MAG_BIN_LIMS,
                EPS_BIN_LIMS);

        GEM1ERF erf = makeTestERF(AREA_SRC_DISCRETIZATION, NUM_MFD_PTS, BORDER_TYPE);

        String vs30Type = Vs30Type.Measured.toString();
        double lat, lon, vs30Value, depthTo1pt0, depthTo2pt5;
        lat = 0.0;
        lon = 0.0;
        vs30Value = 760.0;
        depthTo1pt0 = 100.0;
        depthTo2pt5 = 1.0;
        DisaggregationResult result = disCalc.computeMatrix(
                lat, lon, erf, makeTestImrMap(), POE, LOG_IMLS, vs30Type, vs30Value,
                depthTo1pt0, depthTo2pt5);

        // The expected results were generated by code independent of this
        // calculator.
        assertArrayEquals(EXPECTED, result.getMatrix(), 0.00000009);
    }

    @Test(expected=InputValidationException.class)
    public void testComputeMatrixThrowsOnInvalidVs30Type()
    {
        DisaggregationCalculator disCalc = new DisaggregationCalculator(
                LAT_BIN_LIMS, LON_BIN_LIMS, MAG_BIN_LIMS,
                EPS_BIN_LIMS);
        // vs30Type is case-sensitive
        disCalc.computeMatrix(0, 0, null, null, 0, null, "measured", 0, 0, 0);
    }

    private static void assertArrayEquals(
            double[][][][][] expected,
            double[][][][][] actual,
            double delta) {
        for (int i = 0; i < expected.length; i++)
        {
            for (int j = 0; j < expected[i].length; j++)
            {
                for (int k = 0; k < expected[i][j].length; k++)
                {
                    for (int l = 0; l < expected[i][j][k].length; l++)
                    {
                        for (int m = 0; m < expected[i][j][k][l].length; m++)
                        {
                            double e = expected[i][j][k][l][m];
                            double a = actual[i][j][k][l][m];
                            assertEquals(e, a, delta);
                        }
                    }
                }
            }
        }
    }

    @Test
    public void testGetBinIndices()
    {
        DisaggregationCalculator disCalc = new DisaggregationCalculator(
                LAT_BIN_LIMS, LON_BIN_LIMS, MAG_BIN_LIMS,
                EPS_BIN_LIMS);

        int[] expected = {0, 2, 1, 3, 3};
        double lat, lon, mag, epsilon;
        lat = -0.6;
        lon = 0.0;
        mag = 6.5;
        epsilon = 3.49;
        TectonicRegionType trt = TectonicRegionType.SUBDUCTION_SLAB;

        int[] actual = disCalc.getBinIndices(lat, lon, mag, epsilon, trt);

        assertTrue(Arrays.equals(expected, actual));
    }

    @Test(expected=IllegalArgumentException.class)
    public void testGetBinIndicesOutOfRange()
    {
        DisaggregationCalculator disCalc = new DisaggregationCalculator(
                LAT_BIN_LIMS, LON_BIN_LIMS, MAG_BIN_LIMS,
                EPS_BIN_LIMS);

        double lat, lon, mag, epsilon;
        lat = -0.6;
        lon = 0.0;
        mag = 6.5;
        epsilon = 3.5;  // out of range
        TectonicRegionType trt = TectonicRegionType.SUBDUCTION_SLAB;

        disCalc.getBinIndices(lat, lon, mag, epsilon, trt);
    }

    @Test
    public void testClosestLocation()
    {
        Location target = new Location(90.0, 180.0);

        LocationList locList = new LocationList();
        Location loc1, loc2, loc3;
        loc1 = new Location(0.0, 0.0);
        loc2 = new Location(90.0, 179.9);
        loc3 = new Location(90.0, -180.0);
        locList.add(loc1);
        locList.add(loc2);
        locList.add(loc3);

        assertEquals(loc3, closestLocation(locList, target));
    }

    @Test
    public void testInRange()
    {
        Double[] bins = {10.0, 20.0, 30.0};

        // boundaries:
        assertTrue(inRange(bins, 10.0));
        assertFalse(inRange(bins, 30.0));

        // in range:
        assertTrue(inRange(bins, 29.9));

        // out of range
        assertFalse(inRange(bins, 31.0));
    }

    @Test
    public void testNormalize()
    {
        double[][][][][] input =
            {
                {
                    {
                        {{0, 5.0}, {0, 0}},
                        {{0, 0}, {0, 0}}
                    },
                    {
                        {{0, 0}, {0, 0}},
                        {{0, 0}, {0, 0}}
                    }
                },
                {
                    {
                        {{0, 0}, {10.0, 0}},
                        {{0, 0}, {0, 0}}
                    },
                    {
                        {{0, 0}, {0, 0}},
                        {{0, 25.0}, {0, 0}}
                    }
                }
            };
        double normFactor = 40.0;  // the sum of all values in the matrix

        double[][][][][] expected =
            {
                {
                    {
                        {{0, 0.125}, {0, 0}},
                        {{0, 0}, {0, 0}}
                    },
                    {
                        {{0, 0}, {0, 0}},
                        {{0, 0}, {0, 0}}
                    }
                },
                {
                    {
                        {{0, 0}, {0.25, 0}},
                        {{0, 0}, {0, 0}}
                    },
                    {
                        {{0, 0}, {0, 0}},
                        {{0, 0.625}, {0, 0}}
                    }
                }
            };

        assertTrue(Arrays.deepEquals(expected, normalize(input, normFactor)));
    }

    @Test
    public void testAssertNonZeroStdDev()
    {
        assertNonZeroStdDev(makeTestImrMap());
    }

    @Test(expected=RuntimeException.class)
    public void testAssertNonZeroStdDevBadData()
    {
        assertNonZeroStdDev(makeTestImrMapZeroStdDev());
    }
}
