/*
 * Copyright (C) 2005-2022 Centre National d'Etudes Spatiales (CNES)
 *
 * This file is part of Orfeo Toolbox
 *
 *     https://www.orfeo-toolbox.org/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef otbGeometriesProjectionFilter_h
#define otbGeometriesProjectionFilter_h

#include "otbGeometriesToGeometriesFilter.h"
#include "otbOGRGeometryWrapper.h"
#include "otbImageReference.h"
#include "itkTransform.h"
#include "otbGenericRSTransform.h"
#include "otbImageMetadata.h"

#include "OTBProjectionExport.h"
#include <string>

class OGRCoordinateTransformation;

namespace otb
{

namespace internal
{
/**\ingroup Projection GeometriesFilters
 * Internal functor used to reproject an \c OGRGeometry.
 *
 * \internal
 * As \c OGRGeometry isn't open to new functions through a \em Visitor design
 * pattern, this class uses a nasty hack: it try to downcast to any possible
 * subtype of \c OGRGeometry.
 *
 * \since OTB v 3.14.0
 */
struct OTBProjection_EXPORT ReprojectTransformationFunctor
{
  typedef OGRGeometry TransformedElementType;

  /**\ingroup Projection GeometriesFilters
   * Internal functor used to reproject a \c OGRGeometry: \em By-Copy transformation policy.
   * \since OTB v 3.14.0
   */
  struct ByCopy
  {
    ByCopy(ReprojectTransformationFunctor const& reprojector) : m_Reprojector(reprojector)
    {
    }
    template <typename TGeometry>
    ogr::UniqueGeometryPtr operator()(TGeometry const* in) const;

  private:
    ReprojectTransformationFunctor const& m_Reprojector;
  };

  /**\ingroup Projection GeometriesFilters
   * Internal functor used to reproject a \c OGRGeometry: \em In-Place transformation policy.
   * \since OTB v 3.14.0
   */
  struct InPlace
  {
    InPlace(ReprojectTransformationFunctor const& reprojector) : m_Reprojector(reprojector)
    {
    }
    template <typename TGeometry>
    void operator()(TGeometry* inout) const;

  private:
    ReprojectTransformationFunctor const& m_Reprojector;
  };


  ogr::UniqueGeometryPtr operator()(OGRGeometry const* in) const;
  void apply_inplace(OGRGeometry* inout) const;

  typedef otb::GenericRSTransform<double, 2, 2> InternalTransformType;
  typedef InternalTransformType::Pointer InternalTransformPointerType;
  /**
   * Setter for the 1-point transformation functor.
   * \param[in] transform  transformation functor to apply on single points.
   * \throw None
   */
  void SetOnePointTransformation(InternalTransformPointerType transform);

private:
  /**
   * Transforms one single point thanks to \c m_Transform.
   * \param[in,out] g  point to transform
   * \throw Whatever is thrown by \c m_Transform::operator()
   */
  void do_transform(OGRPoint& g) const;
  // void do_transform(OGRLinearRing         & g) const;
  /**
   * Transforms all the points from a line-string, thanks to \c m_Transform.
   * \param[in,out] g  line-string to transform
   * \throw Whatever is thrown by \c m_Transform::operator()
   */
  void do_transform(OGRLineString& g) const;
  // void do_transform(OGRCurve              & g) const;
  /**
   * Transforms all the rings from a polygon.
   * \param[in,out] g  polygon to transform
   * \throw Whatever is thrown by \c m_Transform::operator()
   */
  void do_transform(OGRPolygon& g) const;
#if 0
  void do_transform(OGRSurface            & g) const;
  void do_transform(OGRMultiLineString    & g) const;
  void do_transform(OGRMultiPoint         & g) const;
  void do_transform(OGRMultiPolygon       & g) const;
#endif
  /**
   * Transforms all the geometries from a geometreis collection.
   * \param[in,out] g  polygon to transform
   * \throw Whatever is thrown by \c m_Transform::operator()
   */
  void do_transform(OGRGeometryCollection& g) const;

  /** Transformation functor that operates on one single \c OGRPoint.
   */
  InternalTransformPointerType m_Transform;
};
} // internal namespace


/**\ingroup Projection GeometriesFilters
 * \class GeometriesProjectionFilter
 * Projection filter for OGR geometries sets.
 * \since OTB v 3.14.0
 *
 * \param[in] InputGeometriesSet
 * \param[in] InputImageMetadata if the \em InputGeometriesSet doesn't have a
 * projection reference (i.e. a \c OGRSpatialReference), this filter will use
 * the \em InputImageMetadata to describe the positioning of the geometries set.
 *
 * \param[in,out] OutputGeometriesSet This set of geometries needs to be given to
 * the filter (in order to set the exact output file/OGR driver). However the
 * filter is in charge of filling the geometries set.
 * \param[in] OutputProjectionRef wkt description of the \c OGRSpatialReference
 * to project the \em InputGeometriesSet into.
 * \param[in] OutputImageMetadata if no \em OutputProjectionRef is set, the
 * projection will be done according to the \em OutputImageMetadata.
 *
 * \note Unlike \c VectorDataProjectionFilter, we have to explicitly set which
 * to use between projection reference or ImageMetadata.
 *
 * \note This filter does not support \em in-place transformation as the spatial
 * references of the new layer are expected to change.
 *
 * \ingroup OTBProjection
 */
class OTBProjection_EXPORT GeometriesProjectionFilter : public GeometriesToGeometriesFilter
{
public:
  /**\name Standard ITK typedefs */
  //@{
  typedef GeometriesProjectionFilter    Self;
  typedef GeometriesToGeometriesFilter  Superclass;
  typedef itk::SmartPointer<Self>       Pointer;
  typedef itk::SmartPointer<const Self> ConstPointer;
  //@}

  /**\name Standard macros */
  //@{
  /** Method for creation through the object factory. */
  itkNewMacro(Self);

  /** Run-time type information (and related methods). */
  itkTypeMacro(GeometriesProjectionFilter, GeometriesToGeometriesFilter);
  //@}

  /**\name I/O typedefs */
  //@{
  typedef Superclass::InputGeometriesType InputGeometriesType;
  // typedef Superclass::InputGeometriesPointer  InputGeometriesPointer;
  typedef Superclass::OutputGeometriesType OutputGeometriesType;
  // typedef Superclass::OutputGeometriesPointer OutputGeometriesPointer;
  typedef ogr::ImageReference<double> ImageReference;

  //@}

private:
  /**
   * Hook used to determine the \c OGRSpatialReference when creating a new layer.
   * \param[in] source  source \c Layer for reference (in case it has relevant
   * information).
   * \return a \c OGRSpatialReference that matches the \em OutputProjectionRef
   * of the filter. It's up to the caller to take responsibility of the returned
   * object.
   */
  OGRSpatialReference* DoDefineNewLayerSpatialReference(ogr::Layer const& source) const override;
  /**
   * Hook that actually filters an OGR \c Layer.
   * \param[in]     source      Input layer
   * \param[in,out] destination Output layer
   *
   * Before forwarding the transformation to the \c m_TransformationFunctor,
   * this specialization finishes initializing the inner transformation functor.
   * Indeed some values depend on the current layer to reproject and thus, the
   * inner-filter working on \c ogr::DataSource cannot be globally configured
   * once and for all.
   */
  void DoProcessLayer(ogr::Layer const& source, ogr::Layer& destination) const override;
  /** Hook used to conclude the initialization phase.
   * Global \c ogr::DataSource settings for the \c m_Transform functor are
   * forwarded to the functor. \c ogr::Layer specific settings will be set at
   * the last moment from \c DoProcessLayer().
   */
  void DoFinalizeInitialization() override;

  /**
   * Hook used to define the fields of the new layer.
   * \param[in] source  source \c Layer -- for reference
   * \param[in,out] dest  destination \c Layer
   *
   * Just forwards the fields definition to the \c FieldTransformationPolicy
   * encapsuled in the \c TransformationFunctorDispatcherType.
   */
  void DoDefineNewLayerFields(ogr::Layer const& source, ogr::Layer& dest) const override;

protected:
  /** Default constructor. */
  GeometriesProjectionFilter();
  /** Destructor. */
  ~GeometriesProjectionFilter() override;

  /** Computes output information.
   * \post \c m_OutputProjectionRef contains all its related meta-data
   */
  void GenerateOutputInformation(void) override;

public:
  /**\name Image Reference (origin, spacing) */
  //@{
  void SetInputSpacing(ImageReference::SpacingType const& spacing);
  void SetOutputSpacing(ImageReference::SpacingType const& spacing);
  void SetInputOrigin(ImageReference::OriginType const& origin);
  void SetOutputOrigin(ImageReference::OriginType const& origin);
  //@}

  /**\name ImageMetadata accessors and mutators */
  //@{
  const ImageMetadata* GetInputImageMetadata() const
  {
    return m_InputImageMetadata;
  }

  void SetInputImageMetadata(const ImageMetadata* imd)
  {
    m_InputImageMetadata = imd;
    this->Modified();
  }

  const ImageMetadata* GetOutputImageMetadata() const
  {
    return m_OutputImageMetadata;
  }
  void SetOutputImageMetadata(const ImageMetadata* imd)
    {
      m_OutputImageMetadata = imd;
      this->Modified();
    }
  //@}

  /**\name Projection references accessors and mutators
   * Projection references accessors and mutators.
   * As the Input Projection Reference comes from the Input Geometries set, it
   * can't be set independently.
   */
  //@{
  itkSetStringMacro(OutputProjectionRef);
  itkGetStringMacro(OutputProjectionRef);
  //@}

private:
  /**\name Functor definition */
  //@{
  typedef internal::ReprojectTransformationFunctor          TransformationFunctorType;
  typedef TransformationFunctorType::TransformedElementType TransformedElementType;
  typedef TransformationFunctorDispatcher<TransformationFunctorType, TransformedElementType, FieldCopyTransformation> TransformationFunctorDispatcherType;

  TransformationFunctorDispatcherType m_TransformationFunctor;
  //@}

  /**\name 1 Point Transformation definition */
  //@{
  typedef TransformationFunctorType::InternalTransformType        InternalTransformType;
  typedef TransformationFunctorType::InternalTransformPointerType InternalTransformPointerType;

  InternalTransformPointerType m_Transform;
  //@}

  /**\name Image Reference (origin, spacing) */
  //@{
  ImageReference m_InputImageReference;
  ImageReference m_OutputImageReference;
  //@}

  std::string      m_OutputProjectionRef; // in WKT format!
  const ImageMetadata*   m_InputImageMetadata = nullptr;
  const ImageMetadata*   m_OutputImageMetadata = nullptr;
};
} // end namespace otb

#ifndef OTB_MANUAL_INSTANTIATION
#include "otbGeometriesProjectionFilter.hxx"
#endif

#endif // otbGeometriesProjectionFilter_h
