/*
    GNU Octave level-set package.
    Copyright (C) 2014  Daniel Kraft <d@domob.eu>

    This program 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.

    This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/* Calculate boundary and intersection-point related stuff of the information
   returned by ls_find_geometry.  */

#include "Utils.hpp"

#include <octave/oct.h>
#include <octave/ov-struct.h>
#include <octave/lo-ieee.h>

#include <algorithm>
#include <cassert>
#include <map>
#include <stdexcept>
#include <utility>

/* ************************************************************************** */
/* IntersectionPoint class.  */

/**
 * Encapsulate an intersection point.
 */
class IntersectionPoint
{

public:

  /** Type used as index into map to uniquely identify the point.  */
  typedef std::pair<unsigned, unsigned> keyT;

  /**
   * Type of this intersection point in terms of how the edge it lies on
   * is oriented.  This is used internally for some decisions.
   */
  enum Type
  {
    WESTIN_EASTOUT,
    EASTIN_WESTOUT,
    SOUTHIN_NORTHOUT,
    NORTHIN_SOUTHOUT
  };

private:

  /** Index of defining inside point.  */
  unsigned inInd;
  /** Index of defining outside point.  */
  unsigned outInd;

  /** Type to describe a boundary element edge this lies on.  */
  typedef std::pair<unsigned, unsigned> edgeT;
  /** Edges this point lies on (should be two in the end).  */
  std::vector<edgeT> edges;

  /** Type of the intersection point.  */
  Type type;

  /** Edge fraction value.  */
  double frac;

  /** x coordinate wrt inner point.  */
  double coordX;
  /** y coordinate wrt inner point.  */
  double coordY;

  /**
   * Store index of this intersection point in the ordering we want to
   * use in the end.
   */
  int index;

public:

  /**
   * Construct the intersection point, given inside- and outside point
   * defining it as well as the other parameters.
   * @param in Inside point index.
   * @param out Outside point index.
   * @param L Rows of grid.
   * @param M Columns of grid.
   * @param phi Level-set function.
   * @param h Grid spacing.
   * @param fracTol Tolerance for pushing points away from nodes.
   */
  IntersectionPoint (unsigned in, unsigned out, unsigned L, unsigned M,
                     const Matrix& phi, double h, double fracTol);

  // No default constructor, copying is ok.
  IntersectionPoint () = delete;
  IntersectionPoint (const IntersectionPoint&) = default;
  IntersectionPoint& operator= (const IntersectionPoint&) = default;

  /**
   * Retrieve the map key for this point.
   * @return Key uniquely identifying this point.
   */
  inline keyT
  getKey () const
  {
    return std::make_pair (inInd, outInd);
  }

  /**
   * Add an edge this point lies on to the list.
   * @param el The bdryelement index.
   * @param ed The edge number.
   */
  inline void
  addEdge (unsigned el, unsigned ed)
  {
    assert (ed <= 3);
    edges.push_back (edgeT (el, ed));
  }

  /**
   * Get type of this intersection point.
   * @return This intersection point's type.
   */
  inline Type
  getType () const
  {
    return type;
  }

  /**
   * Set this intersection point's index.
   * @param ind New index.
   */
  inline void
  setIndex (unsigned ind)
  {
    assert (index == -1);
    index = ind;
  }

  /**
   * Fill in the bdryel.edges row corresponding to this point.
   * @param mat Matrix to fill into.
   */
  inline void
  fillEdges (Matrix& mat) const
  {
    if (edges.size () != 2)
      throw std::runtime_error ("boundary cuts through hold-all domain");

    assert (index >= 0);
    for (const auto& e : edges)
      {
        assert (lo_ieee_is_NA (mat(e.first, e.second)));
        mat(e.first, e.second) = index + 1;
      }
  }

  /**
   * Fill in the inout matrix row.
   * @param mat Matrix to fill in.
   */
  inline void
  fillInOut (Matrix& mat) const
  {
    assert (index >= 0);
    mat(index, 0) = inInd + 1;
    mat(index, 1) = outInd + 1;
  }

  /**
   * Fill in the frac entry.
   * @param vec Vector to fill in.
   */
  inline void
  fillFrac (ColumnVector& vec) const
  {
    assert (index >= 0);
    vec(index) = frac;
  }

  /**
   * Fill in the coord_in_orig matrix row.
   * @param mat Matrix to fill in.
   */
  inline void
  fillInCoord (Matrix& mat) const
  {
    assert (index >= 0);
    mat(index, 0) = coordX;
    mat(index, 1) = coordY;
  }

};

/* Define constructor.  */
IntersectionPoint::IntersectionPoint (unsigned in, unsigned out,
                                      unsigned L, unsigned,
                                      const Matrix& phi,
                                      double h, double fracTol)
  : inInd(in), outInd(out), edges(), index(-1)
{
  assert (phi(inInd) < 0.0 && phi(outInd) > 0.0);

  const unsigned diff
    = std::abs (static_cast<int> (in) - static_cast<int> (out));
  if (diff == L)
    {
      if (in < out)
        type = WESTIN_EASTOUT;
      else
        type = EASTIN_WESTOUT;
    }
  else
    {
      assert (diff == 1);
      if (in < out)
        type = NORTHIN_SOUTHOUT;
      else
        type = SOUTHIN_NORTHOUT;
    }

  /* Calculate intersection position on the boundary edge.  Make sure to
     avoid a total collapse of the triangles if phi takes infinite values
     at some points.  */
  frac = getZeroFraction (phi(inInd), phi(outInd));
  assert (frac >= 0.0 && frac <= 1.0);
  if (frac < fracTol)
    frac = fracTol;
  else if (frac > 1.0 - fracTol)
    frac = 1.0 - fracTol;

  switch (type)
    {
    case WESTIN_EASTOUT:
      coordX = frac * h;
      coordY = 0.0;
      break;
    
    case EASTIN_WESTOUT:
      coordX = -frac * h;
      coordY = 0.0;
      break;

    case NORTHIN_SOUTHOUT:
      coordX = 0.0;
      coordY = frac * h;
      break;

    case SOUTHIN_NORTHOUT:
      coordX = 0.0;
      coordY = -frac * h;
      break;

    default:
      assert (false);
    }
}

/* ************************************************************************** */

/** Map of intersection points.  */
typedef std::map<IntersectionPoint::keyT, IntersectionPoint> isptMap;

/* C++ implementation.  */
DEFUN_DLD (__levelset_geomBoundary, args, nargout,
  "  [ISPTS, EDGES] = geomBoundary (PHI, H, FRACTOL, BDRYINDX, NODELIST)\n\n"
  "Calculate geometrical properties of the boundary for the geometry\n"
  "described by PHI and with grid-width H.  BDRYINDX should be a vector\n"
  "of indices of boundary elements, as per elem.index.bdry, which must\n"
  "already be known.  NODELIST should be elem.nodelist.\n\n"
  "ISPTS returns the intersectpts structure of the geometry, and EDGES\n"
  "contains bdryel.edges, which can be calculated as a by-product.\n")
{
  try
    {
      if (args.length () != 5 || nargout > 2)
        throw std::runtime_error ("invalid argument counts");

      /* Get argument and retrieve dimensions.  */
      const Matrix phi = args(0).matrix_value ();
      const double h = args(1).double_value ();
      const double fracTol = args(2).double_value ();
      const ColumnVector bdryindx = args(3).column_vector_value ();
      const Matrix nodelist = args(4).matrix_value ();

      /* Check all the dimensions.  */
      const dim_vector dims = phi.dims ();
      const unsigned L = dims(0);
      const unsigned M = dims(1);
      const unsigned nBdry = getDimension (bdryindx, -1, 1);
      getDimension (nodelist, -1, 4);

      /* Build up the list of intersection points.  */
      isptMap ispts;
      unsigned nextInd = 0;
      for (unsigned i = 0; i < nBdry; ++i)
        {
          const unsigned idx = bdryindx(i) - 1;
          static const unsigned EDGES[4][2] = {{0, 1}, {1, 2}, {2, 3}, {3, 0}};
          for (unsigned j = 0; j < 4; ++j)
            {
              unsigned pt1 = nodelist(idx, EDGES[j][0]) - 1;
              unsigned pt2 = nodelist(idx, EDGES[j][1]) - 1;
              if (phi(pt1) > phi(pt2))
                std::swap (pt1, pt2);

              if (phi(pt1) * phi(pt2) < 0.0)
                {
                  IntersectionPoint p(pt1, pt2, L, M, phi, h, fracTol);
                  auto iter = ispts.find (p.getKey ());
                  if (iter == ispts.end ())
                    {
                      p.setIndex (nextInd++);
                      const auto pair = std::make_pair (p.getKey (), p);
                      const auto res = ispts.insert (pair);
                      assert (res.second);
                      iter = res.first;
                    }

                  iter->second.addEdge (i, j);
                }
              else
                assert ((phi(pt1) > 0.0 && phi(pt2) > 0.0)
                        || (phi(pt1) < 0.0 && phi(pt2) < 0.0));
            }
        }
      assert (nextInd == ispts.size ());

      /* Build Octave matrices from the intersection points.  */
      Matrix edges(nBdry, 4);
      edges.fill (octave_NA);
      const unsigned nIspts = ispts.size ();
      Matrix inout(nIspts, 2);
      ColumnVector frac(nIspts);
      Matrix incoord(nIspts, 2);
      for (const auto& p : ispts)
        {
          p.second.fillEdges (edges);
          p.second.fillInOut (inout);
          p.second.fillFrac (frac);
          p.second.fillInCoord (incoord);
        }

      /* Build result structure.  */
      octave_scalar_map st;
      st.assign ("n", nIspts);
      st.assign ("inout", inout);
      st.assign ("frac", frac);
      st.assign ("incoord", incoord);

      /* Return.  */
      octave_value_list res;
      res(0) = st;
      res(1) = edges;

      return res;
    }
  catch (const std::runtime_error& exc)
    {
      error (exc.what ());
      return octave_value_list ();
    }
}
