/*
    GNU Octave level-set package.
    Copyright (C) 2014-2016  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 the gamma chaining information returned by ls_find_geometry.  */

#include "Utils.hpp"

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

#include <cassert>
#include <stdexcept>
#include <utility>
#include <vector>

/* Type used for building up intersection-point chains.  */
typedef std::vector<unsigned> IndexChain;
/* Type for indexing a (boundary element, edge) value.  */
typedef std::pair<unsigned, unsigned> EdgeIndex;

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

/**
 * Simple utility class for the information collected with the boundary
 * element segments.  This class contains all data relevant for one segment,
 * and can convert it to the Octave struct in the end.
 */
class BdryelSegmentEntry
{

private:

  /* Start point and edge.  Already 1-based.  */
  unsigned startPt, startEdge;
  /* End point and edge.  Already 1-based.  */
  unsigned endPt, endEdge;

  /** The enclosed inner points.  Already 1-based indices.  */
  IndexChain inners;

public:

  /**
   * Construct it with set "empty" values.  This is necessary because
   * we want to use this class later in a vector for collecting the
   * different segments of a single boundary element.
   */
  inline BdryelSegmentEntry ()
    : startPt(0), startEdge(0), endPt(0), endEdge(0), inners()
  {}

  // Copying and assignment per default.
  BdryelSegmentEntry (const BdryelSegmentEntry&) = default;
  BdryelSegmentEntry& operator= (const BdryelSegmentEntry&) = default;

  // Initialise start and end points (p and e are 0-based).

  inline void
  setStart (unsigned p, unsigned e)
  {
    assert (startPt == 0 && startEdge == 0);
    assert (e < 4);
    startPt = p + 1;
    startEdge = e + 1;
  }

  inline void
  setEnd (unsigned p, unsigned e)
  {
    assert (endPt == 0 && endEdge == 0);
    assert (e < 4);
    endPt = p + 1;

    /* Note that the edge stored for a point is always the one in the
       boundary element in which the point *starts* an edge.  Thus for
       the end point, we have to take the opposite edge, since we are
       interested in the edge relative to the *other* boundary element
       (where p is the end point).  */
    endEdge = (e + 2) % 4 + 1;
  }

  /**
   * Add a new inner point.
   */
  inline void
  addInner (unsigned p)
  {
    inners.push_back (p + 1);
  }

  /**
   * Fill in the ispts.onedge structure entries represented by this segment.
   * @param onedge The onedge array to fill in.
   * @param bdryel The boundary element index this segment is part of.
   */
  void fillOnEdge (NDArray& onedge, unsigned bdryel) const;

  /**
   * Finalise and convert to Octave structure.
   * @return Resulting Octave structure value.
   */
  octave_scalar_map toStruct () const;

};

/**
 * Fill in the ispts.onedge structure entries represented by this segment.
 * @param onedge The onedge array to fill in.
 * @param bdryel The boundary element index this segment is part of.
 */
void
BdryelSegmentEntry::fillOnEdge (NDArray& onedge, unsigned bdryel) const
{
  assert (startPt > 0 && endPt > 0);
  assert (startEdge >= 1 && startEdge <= 4);
  assert (endEdge >= 1 && endEdge <= 4);
  
#define FILL_CHECK_NA(lhs, rhs) \
  if (!lo_ieee_is_NA (lhs)) \
    throw std::runtime_error ("onedge already filled in"); \
  lhs = (rhs);

  FILL_CHECK_NA (onedge(startPt - 1, 0, 0), bdryel + 1)
  FILL_CHECK_NA (onedge(startPt - 1, 0, 1), startEdge)
  FILL_CHECK_NA (onedge(endPt - 1, 1, 0), bdryel + 1)
  FILL_CHECK_NA (onedge(endPt - 1, 1, 1), endEdge)

#undef FILL_CHECK_NA
}

/**
 * Finalise and convert to Octave structure.
 * @return Resulting Octave structure value.
 */
octave_scalar_map
BdryelSegmentEntry::toStruct () const
{
  assert (startPt > 0 && endPt > 0);
  assert (startEdge >= 1 && startEdge <= 4);
  assert (endEdge >= 1 && endEdge <= 4);
  assert (inners.size () >= 1 && inners.size () <= 3);

  octave_scalar_map res;
  res.assign ("startPt", startPt);
  res.assign ("startEdge", startEdge);
  res.assign ("endPt", endPt);
  res.assign ("endEdge", endEdge);
  res.assign ("inners", convertToColumnVector (inners));

  return res;
}

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

/* C++ implementation.  */
DEFUN_DLD (__levelset_geomGamma, args, nargout,
  "  [COMPS, ISPTONEDGE, BDRLELSG]\n"
  "    = geomGamma (PHI, NODELIST, BDRYIND, EDGES, INOUT)\n\n"
  "Internal routine to construct the gamma component chains for the\n"
  "given information.  The arguments should already be calculated by\n"
  "the other geometry routines.\n\n"
  "Returned are the components cell array, the intersection points' onedge\n"
  "array as well as the structure to be kept internally for mesh-building.\n")
{
  try
    {
      if (args.length () != 5 || nargout > 3)
        throw std::runtime_error ("invalid argument counts");

      /* Get arguments.  */
      const Matrix phi = args(0).matrix_value ();
      const Matrix nodelist = args(1).matrix_value ();
      const ColumnVector bdryInd = args(2).column_vector_value ();
      const Matrix edges = args(3).matrix_value ();
      const Matrix inout = args(4).matrix_value ();

      /* Extract and check the dimensions.  */
      const unsigned nNodes = phi.numel ();
      const unsigned nElem = getDimension (nodelist, -1, 4);
      const unsigned nBdryEl = getDimension (bdryInd, -1, 1);
      getDimension (edges, nBdryEl, 4);
      const unsigned nIspts = getDimension (inout, -1, 2);

      /* Each intersection point should occur in the "edges" array of exactly
         two boundary elements.  One of these can be used to "continue"
         following the chain, in order to satisfy our ordering requirement
         (interior of the domain to the left in coordinate interpretation).
         
         For each intersection point, find the corresponding boundary element
         and edge index for which this is the case and store this information
         in a map.  It is a map and not a vector, since we will be removing
         intersection points as we add them to chains.  */

      std::map<unsigned, EdgeIndex> isptStartElem;
      for (unsigned i = 0; i < nBdryEl; ++i)
        for (unsigned j = 0; j < 4; ++j)
          if (!lo_ieee_is_NA (edges(i, j)))
            {
              if (edges(i, j) < 1 || edges(i, j) > nIspts)
                throw std::out_of_range ("edges entry out of bounds");
              const unsigned ispt = edges(i, j) - 1;
              assert (ispt < nIspts);

              if (bdryInd(i) < 1 || bdryInd(i) > nElem)
                throw std::out_of_range ("element index out of bounds");
              const unsigned elem = bdryInd(i) - 1;
              assert (elem < nElem);

              /* See if the intersection point's inner node matches the
                 element's nodelist entry at the correct side of the edge.
                 For a N edge intersected, we have a valid "starting point"
                 if the inner node is at NE (in coordinate grid interpretation).
                 Given the orderings of nodelist and edges, this means
                 that the "correct" point is at the same index in nodelist
                 as the edge is in edges.  */
              const unsigned inpt = inout(ispt, 0);
              if (inpt == nodelist(elem, j))
                {
                  if (isptStartElem.find (ispt) != isptStartElem.end ())
                    throw std::runtime_error ("multiple starts found for ispt");

                  const EdgeIndex entry(i, j);
                  isptStartElem.insert (std::make_pair (ispt, entry));
                }
            }
      if (isptStartElem.size () != nIspts)
        throw std::runtime_error ("wrong number of starting points found");

      /* Now, build up the result.  We start at an arbitrary unused
         intersection point, and follow the chain until we're back at
         it.  This is done until no more intersection points are available.  */

      std::vector<IndexChain> chains;
      std::vector<std::vector<BdryelSegmentEntry>> bdryelSegs(nBdryEl);

      while (!isptStartElem.empty ())
        {
          const auto startIter = isptStartElem.begin ();
          const auto startEntry = *startIter;
          isptStartElem.erase (startIter);

          IndexChain curChain;
          std::pair<unsigned, EdgeIndex> curEntry = startEntry;
          while (true)
            {
              /* Add to chain.  Take care of 1-based indices for Octave.  */
              curChain.push_back (curEntry.first + 1);

              /* Find the next edge with an intersection point.  We get the
                 correct handling of narrow pairs by trying to make the
                 enclosed area "as small as possible".  I. e., we turn as much
                 "left" as possible, until we find an intersection point.  This
                 means that we have to traverse the edges in *reverse*
                 direction (since increasing indices are counter-clockwise
                 in the coordinate grid interpretation).

                 With this choice of ordering, it should also always
                 be the case that points over which we step are inner
                 points only.  */

              const unsigned bdryEl = curEntry.second.first;
              const unsigned fullEl = bdryInd(bdryEl) - 1;
              const unsigned edge = curEntry.second.second;
              assert (bdryEl < nBdryEl && edge < 4);
              assert (edges(bdryEl, edge) == curEntry.first + 1);

              BdryelSegmentEntry seg;
              seg.setStart (curEntry.first, edge);

              unsigned nextPt = nIspts;
              for (unsigned i = edge + 3; i != edge; --i)
                {
                  const unsigned innerPtInd = (i + 1) % 4;
                  const unsigned innerPt = nodelist(fullEl, innerPtInd) - 1;
                  if (innerPt >= nNodes)
                    throw std::runtime_error ("nodelist entry out-of-bounds");
                  if (phi(innerPt) >= 0.0)
                    throw std::runtime_error ("innerPt is not actually inside");
                  assert (i != edge + 3
                          || innerPt == inout(curEntry.first) - 1);
                  seg.addInner (innerPt);

                  if (!lo_ieee_is_NA (edges(bdryEl, i % 4)))
                    {
                      nextPt = edges(bdryEl, i % 4) - 1;
                      break;
                    }
                }
              assert (nextPt <= nIspts);
              if (nextPt == nIspts)
                throw std::runtime_error ("no next ispt found");

              /* If we have a closed loop, finish it.  We have to finish
                 the boundary element segment, too.  */
              if (nextPt == startEntry.first)
                {
                  seg.setEnd (nextPt, startEntry.second.second);
                  bdryelSegs[bdryEl].push_back (seg);
                  break;
                }

              /* Do some book keeping before adding it to the chain at
                 the beginning of the next iteration.  */
              const auto nextIter = isptStartElem.find (nextPt);
              if (nextIter == isptStartElem.end ())
                throw std::runtime_error ("next point is not available");
              curEntry = *nextIter;
              isptStartElem.erase (nextIter);

              /* Finish the boundary element segment.  */
              seg.setEnd (nextPt, curEntry.second.second);
              bdryelSegs[bdryEl].push_back (seg);
            }

          if (curChain.size () < 4)
            throw std::runtime_error ("too short gamma chain found");
          chains.push_back (curChain);
        }

      /* Build up the return value as a cell-array.  */
      Cell c(chains.size (), 1);
      for (unsigned i = 0; i < chains.size (); ++i)
        c(i) = convertToColumnVector (chains[i]);

      /* Build the intersection points' "onedge" array.  */
      dim_vector dims{static_cast<octave_idx_type> (nIspts), 2, 3};
      NDArray onedge(dims);
      onedge.fill (octave_NA);
      assert (bdryelSegs.size () == nBdryEl);
      for (unsigned i = 0; i < nBdryEl; ++i)
        for (const auto& s : bdryelSegs[i])
          s.fillOnEdge (onedge, i);

      /* Build boundary element segments as cell-array.  */
      Cell bs(nBdryEl, 1);
      for (unsigned i = 0; i < nBdryEl; ++i)
        {
          const unsigned nSeg = bdryelSegs[i].size ();
          assert (nSeg == 1 || nSeg == 2);

          Cell e(nSeg, 1);
          for (unsigned j = 0; j < nSeg; ++j)
            e(j) = bdryelSegs[i][j].toStruct ();

          bs(i) = e;
        }

      /* Return.  */
      octave_value_list res;
      res(0) = c;
      res(1) = onedge;
      res(2) = bs;

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