/*
  Copyright 2010 SINTEF ICT, Applied Mathematics.
  Copyright 2010 Statoil ASA.

  This file is part of the Open Porous Media project (OPM).

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

  OPM 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 General Public License
  along with OPM.  If not, see <http://www.gnu.org/licenses/>.
*/

/*
  This program computes data for an experimental
  variogram from a cornerpoint geometry with properties.

  This works by choosing pairs of volumes chosen
  randomly, and comparing their distance with the
  difference in their properties (poro or perm)

  Direction for pairing is set from the command line
*/

#include <config.h>

#include <opm/common/utility/platform_dependent/disable_warnings.h>

#include <dune/common/version.hh>
#include <dune/common/parallel/mpihelper.hh>

#include <opm/common/utility/platform_dependent/reenable_warnings.h>

#include <opm/input/eclipse/Units/Units.hpp>

#include <opm/upscaling/CornerpointChopper.hpp>

#include <opm/porsol/common/setupBoundaryConditions.hpp>

#include <opm/upscaling/SinglePhaseUpscaler.hpp>

#include <cmath>
#include <ctime>
#include <fstream>
#include <iomanip>
#include <ios>
#include <iostream>
#include <sstream>

#include <sys/utsname.h>

#include <random>

int main(int argc, char** argv)
try
{
    Dune::MPIHelper::instance(argc, argv);

    Opm::ParameterGroup param(argc, argv);
    std::string gridfilename = param.get<std::string>("gridfilename");
    Opm::CornerPointChopper ch(gridfilename);

    // The cells with i coordinate in [imin, imax) are included, similar for j.
    // The z limits may be changed inside the chopper to match actual min/max z.
    const int* dims = ch.dimensions();
    int imin = param.getDefault("imin", 0);
    int imax = param.getDefault("imax", dims[0]);
    int jmin = param.getDefault("jmin", 0);
    int jmax = param.getDefault("jmax", dims[1]);
    double zmin = param.getDefault("zmin", ch.zLimits().first);
    double zmax = param.getDefault("zmax", ch.zLimits().second);
    int pairs = param.getDefault("pairs", 100);
    std::string direction = param.get<std::string>("direction");
    int ilen = param.getDefault("ilen", 0);
    int jlen = param.getDefault("jlen", 0);
    double zlen = param.getDefault("zlen", 0.0);
    std::mt19937::result_type userseed = param.getDefault("seed", 0);

    int outputprecision = param.getDefault("outputprecision", 8);
    std::string resultfile = param.getDefault<std::string>("resultfile", "");

    double minperm = param.getDefault("minperm", 1e-9);
    double minpermSI = Opm::unit::convert::from(minperm, Opm::prefix::milli*Opm::unit::darcy);
    if (param.has("z_tolerance")) {
        std::cerr << "****** Warning: z_tolerance parameter is obsolete, use PINCH in deck input instead\n";
    }
    double residual_tolerance = param.getDefault("residual_tolerance", 1e-8);
    int linsolver_verbosity = param.getDefault("linsolver_verbosity", 0);
    int linsolver_type = param.getDefault("linsolver_type", 1);

    // Check for unused parameters (potential typos).
    if (param.anyUnused()) {
	std::cout << "*****     WARNING: Unused parameters:     *****\n";
	param.displayUsage();
    }


    if (ilen <= 0) {
        std::cerr << "Error: ilen (" << ilen << ") must be greater than zero\n";
        exit(1);
    }
    if (jlen <= 0) {
        std::cerr << "Error: jlen (" << jlen <<") must be greater than zero\n";
        exit(1);
    }
    if (zlen <= 0.0) {
        std::cerr << "Error: zlen (" << zlen <<") must be greater than zero\n";
        exit(1);
    }

    // Check user supplied variogram direction, either horizontal or vertical
    enum variogram_directions { undefined, horizontal, vertical };
    variogram_directions variogram_direction = undefined;
    std::string distancemetric;
    if ( direction == "vertical" || direction == "vert" ) {
	variogram_direction = vertical;
	distancemetric = "zcorn";
    }
    else { // if (direction == "horizontal" || direction == "horiz") {
	variogram_direction = horizontal;
	distancemetric = "cells";
    }


    if (variogram_direction == undefined) {
        std::cerr << "Error: variogram direction is undefined, user supplied '" << direction << "'.\n";
        exit(1);
    }


    // Check that we do not have any user input
    // that goes outside the coordinates described in
    // the cornerpoint file (runtime-exception will be thrown in case of error)
    ch.verifyInscribedShoebox(imin, ilen, imax,
                              jmin, jlen, jmax,
                              zmin, zlen, zmax);

    std::mt19937 gen;

    // Seed the random number generators with the current time, unless specified on command line
    // Warning: Current code does not allow 0 for the seed!!
    if (userseed == 0) {
        gen.seed(time(NULL));
    }
    else {
        gen.seed(userseed);
    }


    // Note that end is included in interval for uniform_int.
    std::uniform_int_distribution<> disti(imin, imax - ilen);
    std::uniform_int_distribution<> distj(jmin, jmax - jlen);
    std::uniform_real_distribution<> distz(zmin, std::max(zmax - zlen, zmin));
    auto ri = [&disti, &gen] { return disti(gen); };
    auto rj = [&distj, &gen] { return distj(gen); };
    auto rz = [&distz, &gen] { return distz(gen); };

    // Storage for results
    std::vector<double> distances;
    std::vector<double> porodiffs;
    std::vector<double> permxdiffs;
    std::vector<double> permydiffs;
    std::vector<double> permzdiffs;

    for (int pair = 1; pair <= pairs; ++pair) {
        int istart_1 = ri();
        int jstart_1 = rj();
        double zstart_1 = rz();
        ch.chop(istart_1, istart_1 + ilen, jstart_1, jstart_1 + jlen, zstart_1, zstart_1 + zlen, false);

        auto subdeck_1 = ch.subDeck();
        Opm::SinglePhaseUpscaler upscaler_1;
        upscaler_1.init(subdeck_1, Opm::SinglePhaseUpscaler::Fixed, minpermSI,
			residual_tolerance, linsolver_verbosity, linsolver_type, false);
        Opm::SinglePhaseUpscaler::permtensor_t upscaled_K_1 = upscaler_1.upscaleSinglePhase();
        upscaled_K_1 *= (1.0/(Opm::prefix::milli*Opm::unit::darcy));
	double porosity_1 = upscaler_1.upscalePorosity();

        // Pick another location to form a location-pair to be compared
        int istart_2 = 0, jstart_2 = 0;
        double zstart_2 = 0;
        if (variogram_direction == horizontal) {
            istart_2 = ri();
            jstart_2 = rj();
            zstart_2 = zstart_1;
        }
        else if (variogram_direction == vertical) {
            istart_2 = istart_1;
            jstart_2 = jstart_1;
            zstart_2 = rz();
        }
        ch.chop(istart_2, istart_2 + ilen, jstart_2, jstart_2 + jlen, zstart_2, zstart_2 + zlen, false);

        auto subdeck_2 = ch.subDeck();
        Opm::SinglePhaseUpscaler upscaler_2;
        upscaler_2.init(subdeck_2, Opm::SinglePhaseUpscaler::Fixed, minpermSI,
			residual_tolerance, linsolver_verbosity, linsolver_type, false);
        Opm::SinglePhaseUpscaler::permtensor_t upscaled_K_2 = upscaler_2.upscaleSinglePhase();
        upscaled_K_2 *= (1.0/(Opm::prefix::milli*Opm::unit::darcy));
	double porosity_2 = upscaler_2.upscalePorosity();

        if (variogram_direction == horizontal) {
            distances.push_back(sqrt(pow(istart_2 - istart_1,2) + pow(jstart_2 - jstart_1,2)));
	}
        else if (variogram_direction == vertical) {
            distances.push_back(fabs(zstart_2 - zstart_1));
        }
        porodiffs.push_back(fabs(porosity_2 - porosity_1));
        permxdiffs.push_back(fabs(upscaled_K_2(0,0) - upscaled_K_1(0,0)));
	permydiffs.push_back(fabs(upscaled_K_2(1,1) - upscaled_K_1(1,1)));
        permzdiffs.push_back(fabs(upscaled_K_2(2,2) - upscaled_K_1(2,2)));
    }

    // Make stream of output data, to be outputted to screen and optionally to file
    std::stringstream outputtmp;

    outputtmp << "################################################################################################" << std::endl;
    outputtmp << "# Data for experimental variogram" << std::endl;
    outputtmp << "#" << std::endl;
    time_t now = time(NULL);
    outputtmp << "# Finished: " << asctime(localtime(&now));

    utsname hostname;   uname(&hostname);
    outputtmp << "# Hostname: " << hostname.nodename << std::endl;
    outputtmp << "#" << std::endl;
    outputtmp << "# Options used:" << std::endl;
    outputtmp << "#     gridfilename: " << gridfilename << std::endl;
    outputtmp << "#   i; min,len,max: " << imin << " " << ilen << " " << imax << std::endl;
    outputtmp << "#   j; min,len,max: " << jmin << " " << jlen << " " << jmax << std::endl;
    outputtmp << "#   z; min,len,max: " << zmin << " " << zlen << " " << zmax << std::endl;
    outputtmp << "#        direction: " << direction << std::endl;
    outputtmp << "#            pairs: " << pairs << std::endl;
    outputtmp << "################################################################################################" << std::endl;
    outputtmp << "# distance (" << distancemetric << ")      porositydiff                permxdiff               permydiff                permzdiff" << std::endl;

    const int fieldwidth = outputprecision + 8;
    for (int pair = 1; pair <= pairs; ++pair) {
	outputtmp << std::showpoint << std::setw(fieldwidth) << std::setprecision(outputprecision) << distances[pair-1] << '\t' <<
	    std::showpoint << std::setw(fieldwidth) << std::setprecision(outputprecision) << porodiffs[pair-1] << '\t' <<
	    std::showpoint << std::setw(fieldwidth) << std::setprecision(outputprecision) << permxdiffs[pair-1] << '\t' <<
	    std::showpoint << std::setw(fieldwidth) << std::setprecision(outputprecision) << permydiffs[pair-1] << '\t' <<
	    std::showpoint << std::setw(fieldwidth) << std::setprecision(outputprecision) << permzdiffs[pair-1] << '\t' <<
	    std::endl;
    }

    if (resultfile != "") {
	std::cout << "Writing results to " << resultfile << std::endl;
	std::ofstream outfile;
	outfile.open(resultfile.c_str(), std::ios::out | std::ios::trunc);
	outfile << outputtmp.str();
	outfile.close();
    }



    std::cout << outputtmp.str();
}
catch (const std::exception &e) {
    std::cerr << "Program threw an exception: " << e.what() << "\n";
    throw;
}
