// ---------------------------------------------------------------------
//
// Copyright (C) 2016 - 2018 by the deal.II authors
//
// This file is part of the deal.II library.
//
// The deal.II library is free software; you can use it, redistribute
// it, and/or modify it under the terms of the GNU Lesser General
// Public License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
// The full text of the license can be found in the file LICENSE at
// the top level of the deal.II distribution.
//
// ---------------------------------------------------------------------

#include <deal.II/base/point.h>
#include <deal.II/base/function_spherical.h>
#include <deal.II/base/geometric_utilities.h>

#include <cmath>
#include <algorithm>

DEAL_II_NAMESPACE_OPEN
namespace Functions
{

  // other implementations/notes:
  // https://github.com/apache/commons-math/blob/master/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/SphericalCoordinates.java
  // http://mathworld.wolfram.com/SphericalCoordinates.html

  /*derivation of Hessian in Maxima as function of tensor products of unit vectors:

  depends(ur,[theta,phi]);
  depends(utheta,theta);
  depends(uphi,[theta,phi]);
  depends(f,[r,theta,phi]);
  declare([f,r,theta,phi], scalar)$
  dotscrules: true;
  grads(a):=ur.diff(a,r)+(1/r)*uphi.diff(a,phi)+(1/(r*sin(phi)))*utheta.diff(a,theta);


  H : factor(grads(grads(f)));
  H2: subst([diff(ur,theta)=sin(phi)*utheta,
       diff(utheta,theta)=-cos(phi)*uphi-sin(phi)*ur,
       diff(uphi,theta)=cos(phi)*utheta,
       diff(ur,phi)=uphi,
       diff(uphi,phi)=-ur],
      H);
  H3: trigsimp(fullratsimp(H2));


  srules : [diff(f,r)=sg0,
          diff(f,theta)=sg1,
          diff(f,phi)=sg2,
          diff(f,r,2)=sh0,
          diff(f,theta,2)=sh1,
          diff(f,phi,2)=sh2,
          diff(f,r,1,theta,1)=sh3,
          diff(f,r,1,phi,1)=sh4,
          diff(f,theta,1,phi,1)=sh5,
          cos(phi)=cos_phi,
          cos(theta)=cos_theta,
          sin(phi)=sin_phi,
          sin(theta)=sin_theta
        ]$

  c_utheta2 : distrib(subst(srules, ratcoeff(expand(H3), utheta.utheta)));
  c_utheta_ur : (subst(srules, ratcoeff(expand(H3), utheta.ur)));
  (subst(srules, ratcoeff(expand(H3), ur.utheta))) - c_utheta_ur;
  c_utheta_uphi : (subst(srules, ratcoeff(expand(H3), utheta.uphi)));
  (subst(srules, ratcoeff(expand(H3), uphi.utheta))) - c_utheta_uphi;
  c_ur2 : (subst(srules, ratcoeff(expand(H3), ur.ur)));
  c_ur_uphi : (subst(srules, ratcoeff(expand(H3), ur.uphi)));
  (subst(srules, ratcoeff(expand(H3), uphi.ur))) - c_ur_uphi;
  c_uphi2 : (subst(srules, ratcoeff(expand(H3), uphi.uphi)));


  where (used later to do tensor products):

  ur     : [cos(theta)*sin(phi), sin(theta)*sin(phi), cos(phi)];
  utheta : [-sin(theta), cos(theta), 0];
  uphi   : [cos(theta)*cos(phi), sin(theta)*cos(phi), -sin(phi)];

  with the following proof of substitution rules above:

  -diff(ur,theta)+sin(phi)*utheta;
  trigsimp(-diff(utheta,theta)-cos(phi)*uphi-sin(phi)*ur);
  -diff(uphi,theta)+cos(phi)*utheta;
  -diff(ur,phi)+uphi;
  -diff(uphi,phi)-ur;

   */

  namespace
  {

    /**
     * Evaluate unit vectors in spherical coordinates
     */
    template <int dim>
    void set_unit_vectors(const double &cos_theta,
                          const double &sin_theta,
                          const double &cos_phi,
                          const double &sin_phi,
                          Tensor<1,dim> &unit_r,
                          Tensor<1,dim> &unit_theta,
                          Tensor<1,dim> &unit_phi)
    {
      unit_r[0]     = cos_theta * sin_phi;
      unit_r[1]     = sin_theta * sin_phi;
      unit_r[2]     = cos_phi;

      unit_theta[0] = -sin_theta;
      unit_theta[1] =  cos_theta;
      unit_theta[2] = 0.;

      unit_phi[0]   = cos_theta * cos_phi;
      unit_phi[1]   = sin_theta * cos_phi;
      unit_phi[2]   = -sin_phi;
    }


    /**
     * calculates out[i][j] += v*(in1[i]*in2[j]+in1[j]*in2[i])
     */
    template <int dim>
    void add_outer_product(SymmetricTensor<2,dim> &out,
                           const double &val,
                           const Tensor<1,dim> &in1,
                           const Tensor<1,dim> &in2)
    {
      if (val != 0.)
        for (unsigned int i = 0; i < dim; i++)
          for (unsigned int j = i; j < dim; j++)
            out[i][j] += (in1[i]*in2[j]+in1[j]*in2[i])*val;
    }

    /**
     * calculates out[i][j] += v*in[i]in[j]
     */
    template <int dim>
    void add_outer_product(SymmetricTensor<2,dim> &out,
                           const double &val,
                           const Tensor<1,dim> &in)
    {
      if (val != 0.)
        for (unsigned int i = 0; i < dim; i++)
          for (unsigned int j = i; j < dim; j++)
            out[i][j] += val*in[i]*in[j];
    }
  }



  template <int dim>
  Spherical<dim>::Spherical(const Point<dim> &p,
                            const unsigned int n_components)
    :
    Function<dim>(n_components),
    coordinate_system_offset(p)
  {
    AssertThrow(dim==3,
                ExcNotImplemented());
  }



  template <int dim>
  double
  Spherical<dim>::value (const Point<dim>   &p_,
                         const unsigned int component) const
  {
    const Point<dim> p = p_ - coordinate_system_offset;
    const std::array<double, dim> sp = GeometricUtilities::Coordinates::to_spherical(p);
    return svalue(sp, component);
  }



  template <int dim>
  Tensor<1,dim>
  Spherical<dim>::gradient (const Point<dim>   &/*p_*/,
                            const unsigned int /*component*/) const

  {
    Assert(false, ExcNotImplemented());
    return {};
  }



  template <>
  Tensor<1,3>
  Spherical<3>::gradient (const Point<3>   &p_,
                          const unsigned int component) const
  {
    constexpr int dim = 3;
    const Point<dim> p = p_ - coordinate_system_offset;
    const std::array<double, dim> sp = GeometricUtilities::Coordinates::to_spherical(p);
    const std::array<double, dim> sg = sgradient(sp, component);

    // somewhat backwards, but we need cos/sin's for unit vectors
    const double cos_theta = std::cos(sp[1]);
    const double sin_theta = std::sin(sp[1]);
    const double cos_phi   = std::cos(sp[2]);
    const double sin_phi   = std::sin(sp[2]);

    Tensor<1,dim> unit_r, unit_theta, unit_phi;
    set_unit_vectors(cos_theta, sin_theta,
                     cos_phi, sin_phi,
                     unit_r, unit_theta, unit_phi);

    Tensor<1,dim> res;

    if (sg[0] != 0.)
      {
        res += unit_r * sg[0];
      }

    if (sg[1] * sin_phi != 0.)
      {
        Assert (sp[0] != 0.,
                ExcDivideByZero());
        res += unit_theta * sg[1] / (sp[0] * sin_phi);
      }

    if (sg[2] != 0.)
      {
        Assert (sp[0] != 0.,
                ExcDivideByZero());
        res += unit_phi * sg[2] / sp[0];
      }

    return res;
  }



  template <int dim>
  SymmetricTensor<2,dim>
  Spherical<dim>::hessian (const Point<dim> &/*p*/,
                           const unsigned int /*component*/) const
  {
    Assert(false, ExcNotImplemented());
    return {};
  }



  template <>
  SymmetricTensor<2,3>
  Spherical<3>::hessian (const Point<3> &p_,
                         const unsigned int component) const

  {
    constexpr int dim = 3;
    const Point<dim> p = p_ - coordinate_system_offset;
    const std::array<double, dim> sp = GeometricUtilities::Coordinates::to_spherical(p);
    const std::array<double, dim> sg = sgradient(sp, component);
    const std::array<double, 6>   sh = shessian(sp, component);

    // somewhat backwards, but we need cos/sin's for unit vectors
    const double cos_theta = std::cos(sp[1]);
    const double sin_theta = std::sin(sp[1]);
    const double cos_phi   = std::cos(sp[2]);
    const double sin_phi   = std::sin(sp[2]);
    const double r         = sp[0];

    Tensor<1,dim> unit_r, unit_theta, unit_phi;
    set_unit_vectors(cos_theta, sin_theta,
                     cos_phi, sin_phi,
                     unit_r, unit_theta, unit_phi);

    const double sin_phi2 = sin_phi*sin_phi;
    const double r2 = r*r;
    Assert (r != 0.,
            ExcDivideByZero());

    const double c_utheta2     = sg[0]/r +
                                 ((sin_phi!= 0.) ?
                                  (cos_phi*sg[2])/(r2*sin_phi)+sh[1]/(r2*sin_phi2) :
                                  0.);
    const double c_utheta_ur   = ((sin_phi != 0.) ?
                                  (r*sh[3]-sg[1])/(r2*sin_phi) :
                                  0.);
    const double c_utheta_uphi = ((sin_phi != 0.) ?
                                  (sh[5]*sin_phi-cos_phi*sg[1])/(r2*sin_phi2) :
                                  0.);
    const double c_ur2         = sh[0];
    const double c_ur_uphi     = (r*sh[4]-sg[2])/r2;
    const double c_uphi2       = (sh[2]+r*sg[0])/r2;

    // go through each tensor product
    SymmetricTensor<2,dim> res;

    add_outer_product(res,
                      c_utheta2,
                      unit_theta);

    add_outer_product(res,
                      c_utheta_ur,
                      unit_theta,
                      unit_r);

    add_outer_product(res,
                      c_utheta_uphi,
                      unit_theta,
                      unit_phi);

    add_outer_product(res,
                      c_ur2,
                      unit_r);

    add_outer_product(res,
                      c_ur_uphi,
                      unit_r,
                      unit_phi);

    add_outer_product(res,
                      c_uphi2,
                      unit_phi);

    return res;
  }



  template <int dim>
  std::size_t
  Spherical<dim>::memory_consumption () const
  {
    return sizeof(Spherical<dim>);
  }



  template <int dim>
  double
  Spherical<dim>::svalue(const std::array<double, dim> & /* sp */,
                         const unsigned int /*component*/) const
  {
    AssertThrow(false,
                ExcNotImplemented());
    return 0.;
  }



  template <int dim>
  std::array<double, dim>
  Spherical<dim>::sgradient(const std::array<double, dim> & /* sp */,
                            const unsigned int /*component*/) const
  {
    AssertThrow(false,
                ExcNotImplemented());
    return std::array<double,dim>();
  }



  template <int dim>
  std::array<double, 6>
  Spherical<dim>::shessian (const std::array<double, dim> & /* sp */,
                            const unsigned int /*component*/) const
  {
    AssertThrow(false,
                ExcNotImplemented());
    return std::array<double, 6>();
  }



// explicit instantiations
  template class Spherical<1>;
  template class Spherical<2>;
  template class Spherical<3>;

}

DEAL_II_NAMESPACE_CLOSE
