// Copyright (c) 2015-2020  Geometry Factory
// All rights reserved.
//
// This file is part of CGAL (www.cgal.org)
//
// $URL: https://github.com/CGAL/cgal/blob/v6.1.2/Stream_support/include/CGAL/IO/OBJ.h $
// $Id: include/CGAL/IO/OBJ.h 37e2b278910 $
// SPDX-License-Identifier: LGPL-3.0-or-later OR LicenseRef-Commercial
//
// Author(s) : Lutz Kettner
//             Andreas Fabri
//             Maxime Gimeno

#ifndef CGAL_IO_OBJ_H
#define CGAL_IO_OBJ_H

#include <CGAL/IO/OBJ/File_writer_wavefront.h>
#include <CGAL/IO/Generic_writer.h>
#include <CGAL/IO/io.h>
#include <CGAL/IO/helpers.h>

#include <CGAL/Container_helper.h>

#include <boost/range/value_type.hpp>
#include <CGAL/Named_function_parameters.h>

#include <algorithm>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <type_traits>

namespace CGAL {

////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
// Read

namespace IO {
namespace internal {

template <typename PointRange, typename PolygonRange, typename VertexNormalOutputIterator, typename VertexTextureOutputIterator>
bool read_OBJ(std::istream& is,
              PointRange& points,
              PolygonRange& polygons,
              VertexNormalOutputIterator,
              VertexTextureOutputIterator,
              const bool verbose = false)
{
  if(!is.good())
  {
    if(verbose)
      std::cerr<<"File doesn't exist."<<std::endl;
    return false;
  }

  typedef typename boost::range_value<PointRange>::type                               Point;

  set_ascii_mode(is); // obj is ASCII only

  int mini(1), maxi(-1);
  std::string s;
  Point p;

  std::string line;
  bool tex_found(false), norm_found(false);
  while(getline(is, line))
  {
    // get last non-whitespace, non-null character
    auto last = std::find_if(line.rbegin(), line.rend(), [](char c) { return c != '\0' && !std::isspace(c); });
    if(last == line.rend())
      continue; // line is empty or only whitespace

    // keep reading lines as long as the last non-whitespace, non-null character is a backslash
    while(last != line.rend() && *last == '\\')
    {
      // remove everything from the backslash (included)
      line = line.substr(0, line.size() - (last - line.rbegin()) - 1);

      std::string next_line;
      if(!getline(is, next_line))
        break;

      line += next_line;
      last = std::find_if(line.rbegin(), line.rend(), [](char c) { return c != '\0' && !std::isspace(c); });
    }

    CGAL_assertion(!line.empty());

    std::istringstream iss(line);
    if(!(iss >> s))
      continue;

    if(s == "v")
    {
      if(!(iss >> p))
      {
        if(verbose)
          std::cerr << "error while reading OBJ vertex." << std::endl;
        return false;
      }

      points.push_back(p);
    }
    else if(s == "vt")
    {
      tex_found = true;
    }
    else if(s == "vn")
    {
      norm_found = true;
    }
    else if(s == "f")
    {
      int i;
      polygons.emplace_back();
      while(iss >> i)
      {
        if(i < 1)
        {
          const std::size_t n = polygons.back().size();
          ::CGAL::internal::resize(polygons.back(), n + 1);
          polygons.back()[n] = static_cast<int>(points.size()) + i; // negative indices are relative references
          if(i < mini)
            mini = i;
        }
        else
        {
          const std::size_t n = polygons.back().size();
          ::CGAL::internal::resize(polygons.back(), n + 1);
          polygons.back()[n] = i - 1;
          if(i-1 > maxi)
            maxi = i-1;
        }

        // the format can be "f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3 ..." and we only read vertex ids for now,
        // so skip to the next vertex, but be tolerant about which whitespace is used
        if (!std::isspace(iss.peek())) {
          std::string ignoreme;
          iss >> ignoreme;
        }
      }

      if(iss.bad())
      {
        if(verbose)
          std::cerr << "error while reading OBJ face." << std::endl;
        return false;
      }
    }
    else if(s.front() == '#')
    {
      // this is a commented line, ignored
    }
    else if(s == "vp" ||
            // Display
            s == "bevel" || s == "lod" || s == "ctech" || s == "c_interp" || s == "usemap" || s == "usemtl" ||
            s == "stech" || s == "d_interp" || s == "mtllib" || s == "shadow_obj" || s == "trace_obj" ||
            // groups
            s == "o" || s == "g" || s == "s" ||
            // Free
            s == "p" || s == "cstype" || s == "deg" || s == "step" || s == "bmat" || s == "con" ||
            s == "curv" || s == "curv2" || s == "surf" || s == "parm" || s == "trim" || s == "hole" ||
            s == "scrv" || s == "sp" || s == "end" ||
            s == "con" || s == "surf_1" || s == "q0_1" || s == "q1_1" || s == "curv2d_1" ||
            s == "surf_2" || s == "q0_2" || s == "q1_2" || s == "curv2d_2" ||
            // superseded statements
            s == "bsp" || s == "bzp" || s == "cdc" || s == "cdp" || s == "res")
    {
      // valid, but unsupported
    }
    else
    {
      if(verbose)
        std::cerr << "Error: unrecognized line: " << s << std::endl;
      return false;
    }
  }

  if(norm_found && verbose)
    std::cout << "NOTE: normals were found in this file, but were discarded." << std::endl;
  if(tex_found && verbose)
    std::cout << "NOTE: textures were found in this file, but were discarded." << std::endl;

  if(points.empty() || polygons.empty())
  {
    if(verbose)
      std::cerr << "warning: empty file?" << std::endl;
    return false;
  }

  if(maxi > static_cast<int>(points.size()) || mini < -static_cast<int>(points.size()))
  {
    if(verbose)
      std::cerr << "error: invalid face index" << std::endl;
    return false;
  }

  return !is.bad();
}

} // namespace internal

/// \ingroup PkgStreamSupportIoFuncsOBJ
///
/// \brief reads the content of `is` into `points` and `polygons`, using the \ref IOStreamOBJ.
///
/// \attention The polygon soup is not cleared, and the data from the stream are appended.
///
/// \tparam PointRange a model of the concepts `RandomAccessContainer` and `BackInsertionSequence`
///                    whose value type is the point type
/// \tparam PolygonRange a model of the concepts `SequenceContainer` and `BackInsertionSequence`
///                      whose `value_type` is itself a model of the concepts `SequenceContainer`
///                      and `BackInsertionSequence` whose `value_type` is an unsigned integer type
///                      convertible to `std::size_t`
/// \tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters"
///
/// \param is the input stream
/// \param points points of the soup of polygons
/// \param polygons a range of polygons. Each element in it describes a polygon
///        using the indices of the points in `points`.
/// \param np optional \ref bgl_namedparameters "Named Parameters" described below
///
/// \cgalNamedParamsBegin
///   \cgalParamNBegin{verbose}
///     \cgalParamDescription{indicates whether output warnings and error messages should be printed or not.}
///     \cgalParamType{Boolean}
///     \cgalParamDefault{`false`}
///   \cgalParamNEnd
/// \cgalNamedParamsEnd
///
/// \returns `true` if the reading was successful, `false` otherwise.
template <typename PointRange, typename PolygonRange, typename CGAL_NP_TEMPLATE_PARAMETERS>
bool read_OBJ(std::istream& is,
              PointRange& points,
              PolygonRange& polygons,
              const CGAL_NP_CLASS& np = parameters::default_values()
#ifndef DOXYGEN_RUNNING
              , std::enable_if_t<internal::is_Range<PolygonRange>::value>* = nullptr
#endif
              )
{
  const bool verbose = parameters::choose_parameter(parameters::get_parameter(np, internal_np::verbose), false);

  return internal::read_OBJ(is, points, polygons,
                            CGAL::Emptyset_iterator(), CGAL::Emptyset_iterator(),
                            verbose);
}

/// \ingroup PkgStreamSupportIoFuncsOBJ
///
/// \brief reads the content of the file `fname` into `points` and `polygons`, using the \ref IOStreamOBJ.
///
/// \attention The polygon soup is not cleared, and the data from the file are appended.
///
/// \tparam PointRange a model of the concept `RandomAccessContainer` whose value type is the point type.
/// \tparam PolygonRange a model of the concepts `SequenceContainer` and `BackInsertionSequence`
///                      whose `value_type` is itself a model of the concept `SequenceContainer`
///                      and `BackInsertionSequence` whose `value_type` is an unsigned integer type
///                      convertible to `std::size_t`
/// \tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters"
///
/// \param fname the path to the input file
/// \param points points of the soup of polygons
/// \param polygons a range of polygons. Each element in it describes a polygon
///        using the indices of the points in `points`.
/// \param np optional \ref bgl_namedparameters "Named Parameters" described below
///
/// \cgalNamedParamsBegin
///   \cgalParamNBegin{verbose}
///     \cgalParamDescription{indicates whether output warnings and error messages should be printed or not.}
///     \cgalParamType{Boolean}
///     \cgalParamDefault{`false`}
///   \cgalParamNEnd
/// \cgalNamedParamsEnd
///
/// \returns `true` if the reading was successful, `false` otherwise.
template <typename PointRange, typename PolygonRange, typename CGAL_NP_TEMPLATE_PARAMETERS>
bool read_OBJ(const std::string& fname,
              PointRange& points,
              PolygonRange& polygons,
              const CGAL_NP_CLASS& np = parameters::default_values()
#ifndef DOXYGEN_RUNNING
              , std::enable_if_t<internal::is_Range<PolygonRange>::value>* = nullptr
#endif
              )
{
  std::ifstream is(fname);
  CGAL::IO::set_mode(is, CGAL::IO::ASCII);
  return read_OBJ(is, points, polygons, np);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
// Write

/*!
 * \ingroup PkgStreamSupportIoFuncsOBJ
 *
 * \brief writes the content of `points` and `polygons` in `os`, using the \ref IOStreamOBJ.
 *
 * \tparam PointRange a model of the concept `RandomAccessContainer` whose value type is the point type
 * \tparam PolygonRange a model of the concept `SequenceContainer`
 *                      whose `value_type` is itself a model of the concept `SequenceContainer`
 *                      whose `value_type` is an unsigned integer type convertible to `std::size_t`
 * \tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters"
 *
 * \param os the output stream
 * \param points points of the soup of polygons
 * \param polygons a range of polygons. Each element in it describes a polygon
 *        using the indices of the points in `points`.
 * \param np optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below
 *
 * \cgalNamedParamsBegin
 *   \cgalParamNBegin{stream_precision}
 *     \cgalParamDescription{a parameter used to set the precision (i.e. how many digits are generated) of the output stream}
 *     \cgalParamType{int}
 *     \cgalParamDefault{the precision of the stream `os`}
 *   \cgalParamNEnd
 * \cgalNamedParamsEnd
 *
 * \return `true` if the writing was successful, `false` otherwise.
 */
template <typename PointRange,
          typename PolygonRange,
          typename CGAL_NP_TEMPLATE_PARAMETERS>
bool write_OBJ(std::ostream& os,
               const PointRange& points,
               const PolygonRange& polygons,
               const CGAL_NP_CLASS& np = parameters::default_values()
#ifndef DOXYGEN_RUNNING
               , std::enable_if_t<internal::is_Range<PolygonRange>::value>* = nullptr
#endif
               )
{
  set_ascii_mode(os); // obj is ASCII only
  Generic_writer<std::ostream, File_writer_wavefront> writer(os);
  return writer(points, polygons, np);
}

/*!
 * \ingroup PkgStreamSupportIoFuncsOBJ
 *
 * \brief writes the content of `points` and `polygons` in a file named `fname`, using the \ref IOStreamOBJ.
 *
 * \tparam PointRange a model of the concept `RandomAccessContainer` whose value type is the point type
 * \tparam PolygonRange a model of the concept `SequenceContainer`
 *                      whose `value_type` is itself a model of the concept `SequenceContainer`
 *                      whose `value_type` is an unsigned integer type convertible to `std::size_t`
 * \tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters"
 *
 * \param fname the path to the output file
 * \param points points of the soup of polygons
 * \param polygons a range of polygons. Each element in it describes a polygon
 *        using the indices of the points in `points`.
 * \param np optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below
 *
 * \cgalNamedParamsBegin
 *   \cgalParamNBegin{stream_precision}
 *     \cgalParamDescription{a parameter used to set the precision (i.e. how many digits are generated) of the output stream}
 *     \cgalParamType{int}
 *     \cgalParamDefault{`6`}
 *   \cgalParamNEnd
 * \cgalNamedParamsEnd
 *
 * \return `true` if the writing was successful, `false` otherwise.
 */
template <typename PointRange,
          typename PolygonRange,
          typename CGAL_NP_TEMPLATE_PARAMETERS>
bool write_OBJ(const std::string& fname,
               const PointRange& points,
               const PolygonRange& polygons,
               const CGAL_NP_CLASS& np = parameters::default_values()
#ifndef DOXYGEN_RUNNING
               , std::enable_if_t<internal::is_Range<PolygonRange>::value>* = nullptr
#endif
               )
{
  std::ofstream os(fname);
  CGAL::IO::set_mode(os, CGAL::IO::ASCII);

  return write_OBJ(os, points, polygons, np);
}

} // namespace IO

} // namespace CGAL

#endif // CGAL_IO_OBJ_H
