//- ****************************************************************************
//-
//- Copyright 2009 National Technology & Engineering Solutions of Sandia, LLC
//- (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S.
//- Government retains certain rights in this software.
//-
//- BSD Open Source License
//- All rights reserved.
//-
//- Redistribution and use in source and binary forms, with or without
//- modification, are permitted provided that the following conditions are met:
//-
//-   1. Redistributions of source code must retain the above copyright notice,
//-      this list of conditions and the following disclaimer.
//-
//-   2. Redistributions in binary form must reproduce the above copyright
//-      notice, this list of conditions and the following disclaimer in the
//-      documentation and/or other materials provided with the distribution.
//-
//-   3. Neither the name of the copyright holder nor the names of its
//-      contributors may be used to endorse or promote products derived from
//-      this software without specific prior written permission.
//-
//- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
//- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
//- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
//- ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
//- LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
//- CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
//- SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
//- INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
//- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
//- ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
//- POSSIBILITY OF SUCH DAMAGE.
//-
//- ****************************************************************************

#include "Grid.h"
#include "CrustalProfile.h"
#include "GridProfile.h"

#include <iostream>

// **** _BEGIN SLBM NAMESPACE_ **************************************************

namespace slbm {

// **** _STATIC INITIALIZATIONS_************************************************

int CrustalProfile::crustalProfileClassCount = 0;

CrustalProfile::CrustalProfile() : InterpolatedProfile(), fudgeFactor(1.0), cpTPS(NULL)
{
    ++crustalProfileClassCount;
}

CrustalProfile::~CrustalProfile()
{
    --crustalProfileClassCount;
  if (cpTPS) delete cpTPS;
}

CrustalProfile::CrustalProfile(const CrustalProfile &other) 
: InterpolatedProfile(other), fudgeFactor(other.fudgeFactor), cpTPS(0)
{
    ++crustalProfileClassCount;
  if (other.cpTPS) cpTPS = new taup::TauPSite(*other.cpTPS);

    for (int k=0; k<NLAYERS; k++)
    {
        radius[k] = other.radius[k];
        velocity[k] = other.velocity[k];
    }

    inCrust = other.inCrust;
}

CrustalProfile& CrustalProfile::operator=(const CrustalProfile& other)
{
  if (cpTPS) delete cpTPS;
  cpTPS = (taup::TauPSite*) 0;
  if (other.cpTPS) cpTPS = new taup::TauPSite(*other.cpTPS);

    InterpolatedProfile::operator = (other);
    for (int k=0; k<NLAYERS; k++)
    {
        radius[k] = other.radius[k];
        velocity[k] = other.velocity[k];
    }
    fudgeFactor = other.fudgeFactor;
    return *this;
}

bool CrustalProfile::operator==(const CrustalProfile& other)
{
    return 
        InterpolatedProfile::operator == (other)
        && radius == other.radius
        && velocity == other.velocity;
}

void CrustalProfile::setup(Grid& grid, const int& _phase, 
        const double& lat, const double& lon, const double& depth) 
{
    location.setLocation(lat, lon, depth);
    double earthRadius = getEarthRadius();

    phase = _phase;

    if (cpTPS) 
    {
        delete cpTPS;
        cpTPS = NULL;
    }

    grid.findProfile(location, nodes, nodeIds, coefficients);

    // get interpolated radius values for all intervals.
    // Note:  it is assumed that MANTLE == NLAYERS-1
    for (int k=0; k<NLAYERS; k++)
    {
        // sb 10/07/2009: changed this line with 
        // the two that follow and made derivatives
        // and second derivatives smooth.
        //interpRadius(k, radius[k]);
        interpDepth(k, radius[k]);
        radius[k] = earthRadius-radius[k];
        interpVelocity(phase%2, k, velocity[k]);
    }

    // number of layers with non-zero thickness.
    // This list may or may not include the WATER
    // layer, depending on how TOP_LAYER is defined.
    // The mantle is guaranteed to be included.
    nlay = 0;

    // save the indeces of the layers that have
    // non-zero thickness.
    for (int k=TOP_LAYER; k<NLAYERS-1; k++)
    {
        if (k==MIDDLE_CRUST_N)
        {
            // if this layer is the middle crust/n, only add layer to 
            // list of non-zero thickness layers if phase is Pn,Sn
            // and layer is finite thickness
            if ((phase == Pn || phase == Sn) && 
                radius[MIDDLE_CRUST_N]-radius[LOWER_CRUST] > 1e-6)
                            layid[nlay++] = k;
        }
        else if (k==MIDDLE_CRUST_G)
        {
            // if this layer is the middle crust/g, only add layer to 
            // list of non-zero thickness layers if phase is Pg/Lg
            // and layer is finite thickness
            if ((phase == Pg || phase == Lg) && 
                radius[MIDDLE_CRUST_G]-radius[LOWER_CRUST] > 1e-6)
                            layid[nlay++] = k;
        }
        else if (radius[k]-radius[k+1] > 1e-6)
                            layid[nlay++] = k;
    }

    // always include the mantle
    layid[nlay++] = MANTLE;

    // the index of the entry in layid whose value is MIDDLE_CRUST.
    nMiddleCrust = -1; 

    // index of entry in layid that contains the location of this profile.
    topLayid = -1;

    inCrust = location.getRadius() > radius[MANTLE];

    // if the location is in the mantle, then layid should remain empty.
    // if it is in the crust then populate it.
    if (inCrust)
    {
        // location is in the crust.
        // iterate over all the non-zero thickness layers
        for (int k=0; k<nlay; k++)
            // skip ones whose top is shallower than location radius.
            if (radius[layid[k]] < location.getRadius())
            {
                // if we have not yet identified the layer that 
                // the location radius is in, do so now.
                if (topLayid == -1)
                    topLayid = max(0, k-1);

                // keep track of the index in the layid array 
                // that corresponds to the middle crust, or,
                // if the middle crust is zero thickness, then
                // the next non-zero thickness layer below the
                // middle crust.
                if (nMiddleCrust < 0 && layid[k] >= MIDDLE_CRUST_G)
                    nMiddleCrust = k;
            }
    }

    if (phase == Pg || phase == Lg)
    {
        // change the velocity of the lower crust to be equal to the velocity
        // of the middle crust.
        velocity[LOWER_CRUST] = velocity[MIDDLE_CRUST_G];

        // if the velocity of the upper crust is greater than the velocity of the 
        // middle crust minus .1 km/sec, then reduce all velocities above the 
        // middle crust by a fudge factor such that the velocity of the upper crust
        // is equal to the velocity of the middle crust minus 0.1 km/sec.
        fudgeFactor = min(1., (velocity[MIDDLE_CRUST_G] - 0.1)
          /velocity[UPPER_CRUST]);
        if (fudgeFactor < 1.0)
            for (int i=0; i <= UPPER_CRUST; ++i)
                velocity[i] *= fudgeFactor;
    }
    else
        // fudgeFactor is 1.0 for Pn/Sn
        fudgeFactor = 1.0;


    // check for zero velocities along the ray path
    for (int i=0; i<nlay; i++)
        if (velocity[layid[i]] < 1e-6)
        {
            ostringstream os;
            os << setiosflags(ios::fixed) << setiosflags(ios::showpoint) << setprecision(2);
              os << endl << "ERROR in CrustalProfile constructor" << endl
                << "Layer " << layid[i]
                << " has velocity of 0 km/sec." << endl
                << "Phase = " << (phase==Pn ? "Pn" : phase==Sn ? "Sn" : phase==Pg ? "Pg" : phase==Lg ? "Lg" : "?")
                << endl << "Location = " << location.toString() << endl
                << "Version " << SlbmVersion << "  File " << __FILE__ << " line " << __LINE__ << endl << endl;
            throw SLBMException(os.str(),500);
        }

}

void CrustalProfile::xtCrust(
    GreatCircle* greatCircle,
    const double& p,
    double& xTotal,
    double& zTotal,
    double& sTotal,
    double& tTotal)
{
    int j, n;
    if (greatCircle->getHeadWaveInterface() == MANTLE)
        n = nlay-1;
    else if (greatCircle->getHeadWaveInterface() == MIDDLE_CRUST_G)
        n = nMiddleCrust;
    else
    {
        ostringstream os;
        os << setiosflags(ios::fixed) << setiosflags(ios::showpoint) << setprecision(2);
          os << endl << "ERROR in CrustalProfile::xtCrust()" << endl
            << "greatCircle->getHeadWaveInterface() returned " << greatCircle->getHeadWaveInterface()
            << endl << endl
            << toString()
            << endl
            << "Version " << SlbmVersion << "  File " << __FILE__ << " line " << __LINE__ << endl << endl;
        throw SLBMException(os.str(),502);
    }

    // set rayParameter = p, except:
    // if p < 0 or p > critical ray parameter, 
    // set rayParameter = critical ray parameter.
    double rayParameter = (p < 0. ? getPCrit(greatCircle) 
        : min(p, getPCrit(greatCircle)));

    double r, pv;
    bool done = false;

    while (!done)
    {
        done = true;

        // initialize intermediate variables
        double x, z, s, t;
        x = z = s = t = 0.;
        
        // initialize output variables
        xTotal = zTotal = sTotal = tTotal = 0.;

        for (j=topLayid; j<n; j++)
        {
            // the top of the layer.  
            r = (j==topLayid ? location.getRadius() : radius[layid[j]]);

            // set pv = ray parameter * velocity.  pv also = rBottom * sin(i)
            pv = rayParameter*velocity[layid[j]];

            //// if sin(i) > 1.0, then ray does not reach the moho.
            //// Reduce the rayParameter and start all over.
            //if (pv/radius[layid[j+1]] > 1.0)
            //{
            //	rayParameter = radius[layid[j+1]]/velocity[layid[j]] * (1 - 1e-14);
            //	done = false;
            //	break;
            //}

            // if sin(i) > 1.0, then ray does not reach the moho.
            // this is an error, throw exception.
            if (pv/radius[layid[j+1]] > 1.0)
            {
                ostringstream os;
                os << setiosflags(ios::fixed) << setiosflags(ios::showpoint) << setprecision(2);
                  os << endl << "ERROR in CrustalProfile::xtCrust()" << endl
                    << "A crustal layer has velocity greater than the velocity of the top of the mantle." 
                    << endl << endl
                    << toString()
                    << endl
                    << "Version " << SlbmVersion << "  File " << __FILE__ << " line " << __LINE__ << endl << endl;
                throw SLBMException(os.str(),501);
            }

            // horizontal component of the distance travelled by the ray 
            // in this layer in radians.
            x = acos(pv/r) - acos(pv/radius[layid[j+1]]);
            xTotal += x;

            // vertical component
            z = r - radius[layid[j+1]];
            zTotal += z;

            // hypoteneuse (Eq 3.64, Fig. 3.22 in Thorne & Lay, 1995)
            s = sqrt( z*z + radius[layid[j+1]]*radius[layid[j+1]]*x*x );
            sTotal += s;

            // travel time of the ray in this layer, in seconds.
            t = (sqrt(r*r-pv*pv) - sqrt(radius[layid[j+1]]*radius[layid[j+1]]-pv*pv))
                / velocity[layid[j]];
            tTotal += t;

        }
    }
}

void CrustalProfile::xtCrust(
    GreatCircle* greatCircle,
    const double& p,
    int lid[],
    double x[],
    double r[],
    double v[],
    double t[],
    int& npoints)
{
    int j, n;

    if (greatCircle->getHeadWaveInterface() == MANTLE)
        n = nlay-1;
    else if (greatCircle->getHeadWaveInterface() == MIDDLE_CRUST_G)
        n = nMiddleCrust;
    else
    {
        ostringstream os;
        os << setiosflags(ios::fixed) << setiosflags(ios::showpoint) << setprecision(2);
          os << endl << "ERROR in CrustalProfile::xtCrust()" << endl
            << "greatCircle->getHeadWaveInterface() returned " << greatCircle->getHeadWaveInterface()
            << endl << endl
            << toString()
            << endl
            << "Version " << SlbmVersion << "  File " << __FILE__ << " line " << __LINE__ << endl << endl;
        throw SLBMException(os.str(),502);
    }


    npoints = n+1;

    // set rayParameter = p, except:
    // if p < 0 or p > critical ray parameter, 
    // set rayParameter = critical ray parameter.
    double rayParameter = (p < 0. ? getPCrit(greatCircle) 
        : min(p, getPCrit(greatCircle)));

    double dx=0., dt=0., pv;
    bool done = false;

    while (!done)
    {
        done = true;

        for (j=0; j<npoints; j++)
        {
            // the top of the layer.  
            x[j] = (j==0 ? 0. : x[j-1]+dx);
            r[j] = (j==0 ? location.getRadius() : radius[layid[j]]);
            v[j] = velocity[layid[j]];
            t[j] = (j==0 ? 0. : t[j-1]+dt);
            lid[j] = layid[j];

            // set pv = ray parameter * velocity.  pv also = rBottom * sin(i)
            pv = rayParameter*velocity[layid[j]];

            //// if sin(i) > 1.0, then ray does not reach the moho.
            //// Reduce the rayParameter and start all over.
            //if (pv/r[j] > 1.0)
            //{
            //	rayParameter = r[j]/velocity[layid[j]] * (1 - 1e-14);
            //	done = false;
            //	break;
            //}

            // if sin(i) > 1.0, then ray does not reach the moho.
            // this is an error, throw exception.
            if (pv/r[j] > 1.0)
            {
                ostringstream os;
                os << setiosflags(ios::fixed) << setiosflags(ios::showpoint) << setprecision(2);
                  os << endl << "ERROR in CrustalProfile::xtCrust()" << endl
                    << "A crustal layer has velocity greater than the velocity of the top of the mantle." 
                    << endl << endl
                    << toString()
                    << endl
                    << "Version " << SlbmVersion << "  File " << __FILE__ << " line " << __LINE__ << endl << endl;
                throw SLBMException(os.str(),502);
            }

            if (j < nlay-1)
            {
                // horizontal component of the distance travelled by the ray 
                // in this layer in radians.
                dx = acos(pv/r[j]) - acos(pv/radius[layid[j+1]]);

                // travel time of the ray in this layer, in seconds.
                dt = (sqrt(r[j]*r[j]-pv*pv) 
                    - sqrt(radius[layid[j+1]]*radius[layid[j+1]]-pv*pv))
                    /velocity[layid[j]];
            }
        }
    }
}

string CrustalProfile::toString(GreatCircle* greatCircle, double rayParameter)
{
    // number of crustal layers (mantle is not included)
    int n = greatCircle->getHeadWaveInterface(); 

    double earthRadius = getEarthRadius();

    // total travel time and horizontal offset down to head wave interface
    double xTotal, zTotal, sTotal, tTotal;

    // create a Location to hold the pierce point
    Location piercePoint;

    if (inCrust)
    {

        // compute x,z and t.
        xtCrust(greatCircle, rayParameter, xTotal, zTotal, sTotal, tTotal);

        // create an array to hole the vector triple product of 
        // (source cross receiver) cross source   OR
        // (receiver cross source) cross receiver
        double vtp[3];

        if (location == greatCircle->getSourceProfile()->getLocation())
            // this CrustalProfile is the source crustal profile.  compute
            // (source cross receiver) cross source 
            greatCircle->getSourceProfile()->getLocation().vectorTripleProduct(
                greatCircle->getReceiverProfile()->getLocation(), vtp);
        else
            // this CrustalProfile is the receiver crustal profile, compute
            // (receiver cross source) cross receiver
            greatCircle->getReceiverProfile()->getLocation().vectorTripleProduct(
                greatCircle->getSourceProfile()->getLocation(), vtp);

        // populate pierce point with the pierce point appropriate for either
        // the source or the receiver as determined above.
        location.move(vtp, xTotal, piercePoint);
    }

    ostringstream os;

    os << setiosflags(ios::fixed) << setiosflags(ios::showpoint) << setprecision(4);

    int w = 9;

    os  << "             " << setw(w) << "Lat" << setw(w) << "Lon" << setw(w) << "Depth"
            << setw(w) << "R Earth" << setw(w) << "P" << setw(w) << "P crit"
            << endl;

    os << "   Location: " 
        << setprecision(4)
        << setw(w) << location.getLatDegrees()
        << setw(w) << location.getLonDegrees()
        << setprecision(3)
        << setw(w) << location.getDepth()
        << setprecision(2)
        << setw(w) << earthRadius
        << setw(w) << rayParameter
        << setw(w) << getPCrit(greatCircle)
        << setprecision(4)
        << endl;

    os << "   Pierce Pt:" ;
    if (inCrust)
        os << setw(w) << piercePoint.getLatDegrees()
            << setw(w) << piercePoint.getLonDegrees()
            << setprecision(3)
            << setw(w) << earthRadius-radius[n];
    else
        os << "  Ray did not pierce the head wave interface.";
    os << endl << endl;

    w=9;
    os << setprecision(4);

    string layers[9] = {"water", "sediment1", "sediment2", "sediment3",
            "upper_crust", "middle_crust", "middle_crust", "lower_crust", "mantle"};

    os << "   Model Layers:" << endl << endl;

    os << " " << setw(5) << "#" 
        << left 
        << " " << setw(13) << "Layer" 
        << right
        << setw(w-1) << "Vel" 
        << setw(w) << "Top" 
        << setw(w) << "Thick" 
        << endl;

    --w;
    for (int i=0; i<getNIntervals(); i++)
    {
        if (i==MIDDLE_CRUST_G && (phase == Pn || phase == Sn)) continue;
        if (i==MIDDLE_CRUST_N && (phase == Pg || phase == Lg)) continue;

        // layer number
        os << " " << setw(5) << i+1;

        // layer name
        os << " " << setw(13) << left << layers[i] << right;

        // velocity
        os << " " << setprecision(4) << setw(w-1) << velocity[i];

        // depth
        os << " " << setprecision(4) << setw(w) << getDepth(i);

        // thickness
        if (i < getNIntervals()-1)
        {
            double thick;
            if (i==MIDDLE_CRUST_N || i==MIDDLE_CRUST_G)
                thick = getInterfaceRadius(i)-getInterfaceRadius(LOWER_CRUST);
            else
                thick = getInterfaceRadius(i)-getInterfaceRadius(i+1);
            os << " " << setprecision(4) << setw(w) << thick;
        }

        os << endl;
    }

    os << endl << endl
        << "   Applied Layers:" << endl << endl;

    if (nlay == 0)
        os << "     Ray did not traverse the crust." << endl << endl;
    else
    {
        os << " " << setw(5) << "#" 
        << " " << setw(w-1) << "Vel" 
        << " " << setw(w) << "Top" 
        << " " << setw(w) << "Thick" 
        << " " << setw(w) << "i, deg"
        << " " << setw(w) << "X, deg" 
        << " " << setw(w) << "X, km" 
        << " " << setw(w) << "TT" 
        << endl;


        double x[NLAYERS], t[NLAYERS], v[NLAYERS], r[NLAYERS];
        int lid[NLAYERS], npoints;

        xtCrust(greatCircle, rayParameter, lid, x, r, v, t, npoints);

        for (int i=0; i<npoints; i++)
        {
            // layer number
            os << " " << setw(5) << lid[i]+1;

            // layer name
            //os << " " << setw(13) << left << layers[layerid[i]] << right;

            // velocity
            os << " " << setprecision(4) << setw(w-1) << v[i];

            // depth
            os << " " << setprecision(4) << setw(w) << earthRadius-r[i];

            // thickness
            if (i == npoints-1)
                os << " " << setw(w) << " ";
            else
                os << " " << setprecision(4) << setw(w) << r[i]-r[i+1];

            //incidence angle at top of layer.
            os << " " << setprecision(4) << setw(w) 
                << asin(min(1., rayParameter*v[i]/r[i])) * RAD_TO_DEG;

            // x, deg
            os << " " << setprecision(5) << setw(w) << x[i] * RAD_TO_DEG;

            // x, km
            os << " " << setprecision(4) << setw(w) << x[i] * r[i];

            // travel time
            os << " " << setprecision(4) << setw(w) << t[i];

            os << endl;
        }
    }

    return os.str();
}

string CrustalProfile::toString()
{
    double earthRadius = getEarthRadius();

    ostringstream os;

    os << setiosflags(ios::fixed) << setiosflags(ios::showpoint) << setprecision(4);

    int w = 9;

    os  << "             " << setw(w) << "Lat" << setw(w) << "Lon" << setw(w) << "Depth"
            << setw(w) << "R Earth" 
            << endl;

    os << "   Location: " 
        << setprecision(4)
        << setw(w) << location.getLatDegrees()
        << setw(w) << location.getLonDegrees()
        << setprecision(3)
        << setw(w) << location.getDepth()
        << setprecision(2)
        << setw(w) << earthRadius
        //<< setw(w) << rayParameter
        << setprecision(4)
        << endl << endl;

    os << setprecision(6);

    os << "   Fudge factor: " << fudgeFactor << endl << endl;

    string layers[9] = {"water", "sediment1", "sediment2", "sediment3",
            "upper_crust", "middle_crust_n", "middle_crust_g", "lower_crust", "mantle"};

    os << "   Model Layers:" << endl << endl;

    w=9;
    os << setprecision(4);

    os << " " << setw(5) << "#" 
        << left 
        << " " << setw(15) << "Layer" 
        << right
        << setw(w-1) << "Vel" 
        << setw(w) << "Top" 
        << setw(w) << "Thick" 
        << endl;

    --w;
    for (int i=0; i<getNIntervals(); i++)
    {
        int j = i+1;
        if (i==MIDDLE_CRUST_N || i==MIDDLE_CRUST_G) j = LOWER_CRUST;

        if (i < getNIntervals()-1 && getInterfaceRadius(i)-getInterfaceRadius(j) < 1e-6) 
            continue;

        //if (i==MIDDLE_CRUST_G) continue;
        if (i==MIDDLE_CRUST_G && (phase == Pn || phase == Sn)) continue;
        //if (i==MIDDLE_CRUST_N && (phase == Pg || phase == Lg)) continue;

        // layer number
        os << " " << setw(5) << i+1;

        // layer name
        os << " " << setw(15) << left << layers[i] << right;

        // velocity
        os << " " << setprecision(4) << setw(w-1) << velocity[i];

        // depth
        os << " " << setprecision(4) << setw(w) << getDepth(i);

        // thickness
        if (i < getNIntervals()-1)
        {
            //double thick;
            //if (i==MIDDLE_CRUST_N || i==MIDDLE_CRUST_G)
            //	thick = getInterfaceRadius(i)-getInterfaceRadius(LOWER_CRUST);
            //else
            //	thick = getInterfaceRadius(i)-getInterfaceRadius(i+1);
            os << " " << setprecision(4) << setw(w) << getInterfaceRadius(i)-getInterfaceRadius(j);
        }

        os << endl;
    }

    return os.str();
}

size_t CrustalProfile::memSize()
{
    return InterpolatedProfile::memSize()
        + sizeof(location)
        + sizeof(location)
        + sizeof(radius) + getNIntervals()*sizeof(double)
        + sizeof(velocity) + getNIntervals()*sizeof(double)
        ;

}

void CrustalProfile::setTauPSite(taup::TauPSite* tps)
{
  if (cpTPS) delete cpTPS;
    cpTPS = tps;
}

taup::TauPSite* CrustalProfile::getTauPSite()
{
    return cpTPS;
}

int CrustalProfile::getClassCount()
{
  return crustalProfileClassCount;
}

} // end slbm namespace

