/* $Id$ */

/**
 * \file 
 * $Revision$
 * $Date$
 * 
 * Copyright (C) 2018 The University of Sydney, Australia
 *
 * This file is part of GPlates.
 *
 * GPlates is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, version 2, as published by
 * the Free Software Foundation.
 *
 * GPlates 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, write to Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#ifndef GPLATES_APP_LOGIC_RESOLVEDSUBSEGMENTRANGEINSECTION_H
#define GPLATES_APP_LOGIC_RESOLVEDSUBSEGMENTRANGEINSECTION_H

#include <utility>
#include <vector>
#include <boost/operators.hpp>
#include <boost/optional.hpp>
#include <boost/variant.hpp>

#include "ReconstructionGeometry.h"

#include "maths/AngularDistance.h"
#include "maths/GeometryIntersect.h"
#include "maths/GeometryOnSphere.h"
#include "maths/GreatCircleArc.h"
#include "maths/PointOnSphere.h"
#include "maths/PolylineOnSphere.h"


namespace GPlatesAppLogic
{
	/**
	 * The sub-segment range of an entire topological section geometry that contributes to a
	 * resolved topological geometry.
	 *
	 * The sub-segment is the result of intersecting a section with its two adjacent sections
	 * (such as in a topological boundary) which usually results in some vertices from the
	 * section bounded by a start and an end intersection, or in some cases only a single intersection,
	 * or even no intersections (resulting in the sub-segment being the entire section geometry).
	 *
	 * This class keeps track of the range of vertex *indices* so that any quantities associated with
	 * the section vertices can be tracked (such as vertex plate IDs and velocities).
	 */
	class ResolvedSubSegmentRangeInSection
	{
	public:

		/**
		 * Location of intersection within a specific section (eg, current or previous sections).
		 *
		 * Intersections only apply to section polylines, or polygons (treated as exterior ring polylines).
		 */
		class Intersection :
				public boost::less_than_comparable<Intersection>
		{
		public:

			/**
			 * Create an intersection generated by intersecting two polylines with the GeometryIntersect module.
			 *
			 * @a section_polyline_is_first_geometry determines which polyline to reference in @a intersection.
			 */
			static
			Intersection
			create(
					const GPlatesMaths::GeometryIntersect::Intersection &intersection,
					const GPlatesMaths::PolylineOnSphere &section_polyline,
					bool section_polyline_is_first_geometry);

			/**
			 * Create intersection *at* first vertex (if @a at_start is true) or
			 * last vertex (if @a at_start is false) of section geometry.
			 *
			 * Note that @a section_geometry must be a point, multi-point or polyline.
			 *
			 * Normally we don't have an intersection for a point or multi-point since they're not
			 * considered intersectable, but it is still useful in some situations such as generating
			 * a sub-segment from a start rubber-band point to the start of a section (or from end of a
			 * section to end rubber-band point) by placing an intersection at start (end) of the section.
			 *
			 * @throws PreconditionViolationError if @a section_geometry is a polygon.
			 */
			static
			Intersection
			create_at_section_start_or_end(
					const GPlatesMaths::GeometryOnSphere &section_geometry,
					bool at_start);

			/**
			 * Create an intersection in the segment defined by @a segment_index and @a section_geometry (see
			 * @a get_segment) where the interpolate ratio is specified for an *inner* segment (not the actual segment).
			 *
			 * The inner segment is defined by an interpolation ratio (in the actual segment) for its start and end points.
			 * And the interpolation ratio of the intersection position is in reference to the inner segment.
			 *
			 * Note that the inner segment can be reversed with respect to the actual segment
			 * (ie, start interpolation ratio can be larger than the end interpolation ratio).
			 */
			static
			Intersection
			create_from_inner_segment(
					const GPlatesMaths::PointOnSphere &intersection_position,
					const GPlatesMaths::GeometryOnSphere &section_geometry,
					unsigned int segment_index,
					const double &start_inner_segment_interpolate_ratio_in_segment,
					const double &end_inner_segment_interpolate_ratio_in_segment,
					const double &interpolate_ratio_in_inner_segment);

			/**
			 * Returns the GCA segment in the specified section geometry at the specified segment index.
			 *
			 * Returns none if 'segment_index' represents the fictitious one-past-the-last segment.
			 * This always applies to a point geometry (need two points to make a GCA segment).
			 *
			 * Note that @a section_geometry must be a point, multi-point or polyline.
			 *
			 * For multi-point geometries a GCA segment is created between points indexed by
			 * @a segment_index and @a segment_index + 1. This is unusual since a multi-point by
			 * nature has no GCA segments. However, the multi-point section could be part of a
			 * resolved topological line and hence the points in the multi-point essentially result
			 * in GCA segments of the resolved line, and the resolved line can in turn be part of a
			 * resolved topological boundary/polygon (and hence get intersected in the middle of a GCA segment)
			 * - this is only really used when finding the sub-sub-segments that make up a resolved line sub-segment.
			 *
			 * @throws PreconditionViolationError if @a section_geometry is a polygon.
			 */
			static
			boost::optional<GPlatesMaths::GreatCircleArc>
			get_segment(
					const GPlatesMaths::GeometryOnSphere &section_geometry,
					unsigned int segment_index);


			/**
			 * Less than operator - all other operators (<=, >, >=) provided by boost::less_than_comparable.
			 *
			 * Note: This does not include epsilon testing of angle (when both intersections on same segment).
			 */
			bool
			operator<(
					const Intersection &rhs) const
			{
				return segment_index < rhs.segment_index ||
					(segment_index == rhs.segment_index &&
						angle_in_segment.is_precisely_less_than(rhs.angle_in_segment));
			}


			//! Intersection position.
			GPlatesMaths::PointOnSphere position;

			/**
			 * Index into the segments (great circle arcs) of the section polyline.
			 *
			 * NOTE: A segment index can be equal to the number of segments in the section polyline.
			 *       This represents an intersection with the *last* vertex in the section polyline.
			 *       
			 *       In other words, the segment index can be the fictitious *one-past-the-last* segment.
			 *       So, in this case, care should be taken to not dereference (look up segment in
			 *       section polyline). In this case 'on_segment_start' will be true, and this will
			 *       represent an intersection with the start of the fictitious *one-past-the-last* segment
			 *       which is the same as the end of the last segment (ie, last vertex in polyline).
			 *       In this case the segment index can be thought of as the vertex index.
			 */
			unsigned int segment_index;

			//! Whether intersection is *on* the start of the segment indexed by @a segment_index.
			bool on_segment_start;

			/**
			 * Angle (radians) from segment start point to intersection along segment.
			 *
			 * If @a on_segment_start is true then this will be AngularDistance::ZERO.
			 */
			GPlatesMaths::AngularDistance angle_in_segment;

			/**
			 * Value in range [0, 1] where 0 represents the segment start point and 1 the segment end point.
			 *
			 * So to interpolate quantities use the formula:
			 *
			 *   quantity_lerp = quantity_at_start_point +
			 *			interpolate_ratio * (quantity_at_end_point - quantity_at_start_point)
			 */
			double
			get_interpolate_ratio_in_segment(
					const GPlatesMaths::GeometryOnSphere &section_geometry) const;

		private:
			Intersection(
					const GPlatesMaths::PointOnSphere &position_,
					unsigned int segment_index_,
					bool on_segment_start_,
					const GPlatesMaths::AngularDistance &angle_in_segment_,
					boost::optional<double> interpolate_ratio_in_segment_ = boost::none) :
				position(position_),
				segment_index(segment_index_),
				on_segment_start(on_segment_start_),
				angle_in_segment(angle_in_segment_),
				d_interpolate_ratio_in_segment(interpolate_ratio_in_segment_)
			{  }

			mutable boost::optional<double> d_interpolate_ratio_in_segment;
		};


		/**
		 * Location and information of rubber banding with an adjacent section.
		 *
		 * Rubber banding occurs when there is not intersection with an adjacent section.
		 */
		class RubberBand
		{
		public:

			/**
			 * Create a rubber band halfway between two non-intersecting section geometries.
			 */
			static
			RubberBand
			create(
					const GPlatesMaths::PointOnSphere &current_section_position,
					const GPlatesMaths::PointOnSphere &adjacent_section_position,
					bool is_at_start_of_current_section,
					bool is_at_start_of_adjacent_section,
					const ReconstructionGeometry::non_null_ptr_to_const_type &current_section_reconstruction_geometry,
					const ReconstructionGeometry::non_null_ptr_to_const_type &adjacent_section_reconstruction_geometry);

			/**
			 * Create a rubber band point at @a intersection_position along the great circle arc
			 * between the current and adjacent section points of @a rubber_band.
			 *
			 * An interpolate ratio of 0.0 corresponds to the rubber band's current section point, and
			 * an interpolate ratio of 1.0 corresponds to the rubber band's adjacent section point.
			 *
			 * Note that no check is made to ensure @a intersection_position lies *on* the rubber band great circle arc.
			 */
			static
			RubberBand
			create_from_intersected_rubber_band(
					const RubberBand &rubber_band,
					const GPlatesMaths::PointOnSphere &intersection_position);


			/**
			 * The rubber band position halfway between the adjacent reversed sub-segment and
			 * the current reversed sub-segment.
			 */
			GPlatesMaths::PointOnSphere position;

			/**
			 * Value in range [0, 1] representing where @a position is between @a current_section_position
			 * (represented by 0) and @a adjacent_section_position (represented by 1).
			 *
			 * So to interpolate quantities use the formula:
			 *
			 *   quantity_lerp = quantity_at_current_section_position + interpolate_ratio *
			 *			(quantity_at_adjacent_section_position - quantity_at_current_section_position)
			 *
			 * Normally @a position is at 0.5, but for sub-sub-segments this can be other values in range [0, 1].
			 */
			double interpolate_ratio;

			/**
			 * The position on the current reversed sub-segment (used for rubber-banding).
			 */
			GPlatesMaths::PointOnSphere current_section_position;

			/**
			 * The position on the adjacent reversed sub-segment (used for rubber-banding).
			 */
			GPlatesMaths::PointOnSphere adjacent_section_position;

			/**
			 * Whether the start vertex of the current unreversed section is used to determine rubber-band position.
			 */
			bool is_at_start_of_current_section;

			/**
			 * Whether the start vertex of the adjacent unreversed section is used to determine rubber-band position.
			 */
			bool is_at_start_of_adjacent_section;

			/**
			 * The reconstruction geometry that the current section was obtained from.
			 */
			ReconstructionGeometry::non_null_ptr_to_const_type current_section_reconstruction_geometry;

			/**
			 * The reconstruction geometry that the adjacent section was obtained from.
			 */
			ReconstructionGeometry::non_null_ptr_to_const_type adjacent_section_reconstruction_geometry;

		private:
			RubberBand(
					const GPlatesMaths::PointOnSphere &position_,
					const double &interpolate_ratio_,
					const GPlatesMaths::PointOnSphere &current_section_position_,
					const GPlatesMaths::PointOnSphere &adjacent_section_position_,
					bool is_at_start_of_current_section_,
					bool is_at_start_of_adjacent_section_,
					const ReconstructionGeometry::non_null_ptr_to_const_type &current_section_reconstruction_geometry_,
					const ReconstructionGeometry::non_null_ptr_to_const_type &adjacent_section_reconstruction_geometry_) :
				position(position_),
				interpolate_ratio(interpolate_ratio_),
				current_section_position(current_section_position_),
				adjacent_section_position(adjacent_section_position_),
				is_at_start_of_current_section(is_at_start_of_current_section_),
				is_at_start_of_adjacent_section(is_at_start_of_adjacent_section_),
				current_section_reconstruction_geometry(current_section_reconstruction_geometry_),
				adjacent_section_reconstruction_geometry(adjacent_section_reconstruction_geometry_)
			{  }
		};


		/**
		 * Can have an @a Intersection or a @a RubberBand (but not both) at start or end of a section.
		 */
		class IntersectionOrRubberBand
		{
		public:
			explicit
			IntersectionOrRubberBand(
					const Intersection &intersection) :
				d_intersection_or_rubber_band(intersection)
			{  }

			explicit
			IntersectionOrRubberBand(
					const RubberBand &rubber_band) :
				d_intersection_or_rubber_band(rubber_band)
			{  }

			//! If returns none then @a get_rubber_band will return non-none.
			boost::optional<const Intersection &>
			get_intersection() const
			{
				if (const Intersection *intersection = boost::get<Intersection>(&d_intersection_or_rubber_band))
				{
					return *intersection;
				}

				return boost::none;
			}

			//! If returns none then @a get_intersection will return non-none.
			boost::optional<const RubberBand &>
			get_rubber_band() const
			{
				if (const RubberBand *rubber_band = boost::get<RubberBand>(&d_intersection_or_rubber_band))
				{
					return *rubber_band;
				}

				return boost::none;
			}

		private:
			boost::variant<Intersection, RubberBand> d_intersection_or_rubber_band;
		};


		/**
		 * If no start intersection or rubber band then sub-segment starts at beginning of section.
		 * If no end intersection or rubber band then sub-segment ends at end of section.
		 *
		 * A start/end rubber band is an extra point that is not on the main section geometry.
		 * It is usually halfway between the adjacent (reversed) sub-segment and this (reversed) sub-segment.
		 *
		 * Note that if there is both a start and an end rubber band then the start rubber band is usually at
		 * the start and the end rubber band at the end (in which case the entire section geometry also contributes
		 * to the sub-segment). However, as a special case, both rubber bands can be at the start or both can be
		 * at the end (in which case the section geometry is excluded from the sub-segment). This represents
		 * a part of the start (or end) rubber-band segment which usually only happens when a (resolved line)
		 * sub-segment is split into its sub-sub-segments (in which case any arbitrary part of a sub-sub-segment
		 * could contribute to its parent sub-segment).
		 *
		 * Note that @a section_geometry must be a point, multi-point or polyline.
		 *
		 * @throws PreconditionViolationError if @a section_geometry is a polygon.
		 */
		explicit
		ResolvedSubSegmentRangeInSection(
				GPlatesMaths::GeometryOnSphere::non_null_ptr_to_const_type section_geometry,
				boost::optional<IntersectionOrRubberBand> start_intersection_or_rubber_band = boost::none,
				boost::optional<IntersectionOrRubberBand> end_intersection_or_rubber_band = boost::none);


		/**
		 * Returns the section geometry.
		 *
		 * This is the geometry passed into the constructor.
		 * It will be a point, multi-point or polyline (a polygon exterior ring is converted to polyline).
		 */
		GPlatesMaths::GeometryOnSphere::non_null_ptr_to_const_type
		get_section_geometry() const
		{
			return d_section_geometry;
		}

		/**
		 * Returns the number of points in @a get_section_geoemtry.
		 */
		unsigned int
		get_num_points_in_section_geometry() const
		{
			return d_num_points_in_section_geometry;
		}


		/**
		 * Return the (unreversed) sub-segment geometry.
		 *
		 * Note that we always include rubber band points (if any) in the returned geometry,
		 * otherwise it would be possible to have no geometry points (see @a get_geometry_points).
		 */
		GPlatesMaths::PolylineOnSphere::non_null_ptr_to_const_type
		get_geometry() const;


		/**
		 * Returns the (unreversed) geometry points.
		 *
		 * Does not clear @a geometry_points - just appends points.
		 *
		 * Note that it's possible to have no geometry points if @a include_rubber_band_points is false.
		 * This can happen when a sub-sub-segment of a resolved line sub-segment is entirely within the
		 * start or end rubber band region of the sub-sub-segment (and hence the sub-sub-segment geometry
		 * is only made up of two rubber band points which, if excluded, would result in no points).
		 * Note that this only applies when both rubber band points are on the same side of the section geometry.
		 *
		 * Conversely if @a include_rubber_band_points is true then there will be at least two points.
		 */
		void
		get_geometry_points(
				std::vector<GPlatesMaths::PointOnSphere> &geometry_points,
				bool include_rubber_band_points = true) const;

		/**
		 * Returns the geometry points as they contribute to the resolved topology.
		 *
		 * These are @a get_geometry_points if @a use_reverse is false,
		 * otherwise they are a reversed version of @a get_geometry_points.
		 *
		 * Does not clear @a geometry_points - just appends points.
		 *
		 * Note that it's possible to have no geometry points if @a include_rubber_band_points is false (see @a get_geometry_points).
		 * And if rubber band points are included then there will be at least two points.
		 */
		void
		get_reversed_geometry_points(
				std::vector<GPlatesMaths::PointOnSphere> &geometry_points,
				bool use_reverse,
				bool include_rubber_band_points = true) const;


		/**
		 * Return the start and end points of the sub-segment range in the section.
		 *
		 * If there are start and/or end intersections or rubber bands then these will be start and/or end points.
		 *
		 * Note that if @a include_rubber_band_points is false and both rubber band points are on the same side
		 * of the section geometry then we would normally end up with no sub-segment points. However in this case
		 * we revert to returning the end points of the section geometry.
		 */
		std::pair<GPlatesMaths::PointOnSphere/*start point*/, GPlatesMaths::PointOnSphere/*end point*/>
		get_end_points(
				bool include_rubber_band_points = true) const;

		/**
		 * Return the start and end points of sub-segment range in section as contributed to resolved topology.
		 *
		 * If there are start and/or end intersections or rubber bands then these will be start and/or end points.
		 *
		 * These are @a get_end_points if @a use_reverse is false,
		 * otherwise they are a reversed version of @a get_end_points.
		 *
		 * Note that if @a include_rubber_band_points is false and both rubber band points are on the same side
		 * of the section geometry then we would normally end up with no sub-segment points. However in this case
		 * we revert to returning the end points of the section geometry.
		 */
		std::pair<GPlatesMaths::PointOnSphere/*start point*/, GPlatesMaths::PointOnSphere/*end point*/>
		get_reversed_end_points(
				bool use_reverse,
				bool include_rubber_band_points = true) const;


		/**
		 * Return the number of points in the sub-segment (including optional intersection or rubber band points).
		 *
		 * Note that it's possible to return zero if @a include_rubber_band_points is false (see @a get_geometry_points).
		 * Conversely if @a include_rubber_band_points is true then there will be at least two points.
		 */
		unsigned int
		get_num_points(
				bool include_rubber_band_points = true) const;


		/**
		 * Index of first vertex of section geometry that contributes to sub-segment.
		 *
		 * If zero then sub-segment start matches start of section.
		 */
		unsigned int
		get_start_section_vertex_index() const
		{
			return d_start_section_vertex_index;
		}

		/**
		 * Index of *one-past-the-last* vertex of section geometry that contributes to sub-segment.
		 *
		 * If equal to the number of vertices in section then sub-segment end matches end of section.
		 *
		 * NOTE: This index is *one-past-the-last* index and so should be used like begin/end iterators.
		 */
		unsigned int
		get_end_section_vertex_index() const
		{
			return d_end_section_vertex_index;
		}


		/**
		 * Optional intersection or rubber band signifying start of sub-segment.
		 *
		 * Note that there cannot be both a start intersection and a start rubber band.
		 *
		 * If no start intersection (or rubber band) then sub-segment start matches start of section.
		 *
		 * NOTE: This could be an intersection with the previous or next section.
		 */
		boost::optional<IntersectionOrRubberBand>
		get_start_intersection_or_rubber_band() const;

		/**
		 * Optional intersection signifying start of sub-segment.
		 *
		 * Note that there cannot be both a start intersection and a start rubber band.
		 *
		 * If no start intersection (or rubber band) then sub-segment start matches start of section.
		 *
		 * NOTE: This could be an intersection with the previous or next section.
		 */
		const boost::optional<Intersection> &
		get_start_intersection() const
		{
			return d_start_intersection;
		}

		/**
		 * Optional rubber band signifying start of sub-segment.
		 *
		 * Note that there cannot be both a start rubber band and a start intersection.
		 *
		 * If no start rubber band (or intersection) then sub-segment start matches start of section.
		 *
		 * NOTE: This could be a rubber band with the previous *or* next section.
		 */
		const boost::optional<RubberBand> &
		get_start_rubber_band() const
		{
			return d_start_rubber_band;
		}


		/**
		 * Optional intersection or rubber band signifying end of sub-segment.
		 *
		 * Note that there cannot be both a end intersection and a end rubber band.
		 *
		 * If no end intersection (or rubber band) then sub-segment end matches end of section.
		 *
		 * NOTE: This could be an intersection with the previous or next section.
		 */
		boost::optional<IntersectionOrRubberBand>
		get_end_intersection_or_rubber_band() const;

		/**
		 * Optional intersection signifying end of sub-segment.
		 *
		 * Note that there cannot be both an end intersection and an end rubber band.
		 *
		 * If no end intersection (or rubber band) then sub-segment end matches end of section.
		 *
		 * NOTE: This could be an intersection with the previous or next section.
		 */
		const boost::optional<Intersection> &
		get_end_intersection() const
		{
			return d_end_intersection;
		}

		/**
		 * Optional intersection signifying end of sub-segment.
		 *
		 * Note that there cannot be both an end rubber band and an end intersection.
		 *
		 * If no end rubber band (or intersection) then sub-segment end matches end of section.
		 *
		 * NOTE: This could be a rubber band with the previous *or* next section.
		 */
		const boost::optional<RubberBand> &
		get_end_rubber_band() const
		{
			return d_end_rubber_band;
		}

	private:
		GPlatesMaths::GeometryOnSphere::non_null_ptr_to_const_type d_section_geometry;
		unsigned int d_num_points_in_section_geometry;

		unsigned int d_start_section_vertex_index;
		unsigned int d_end_section_vertex_index;

		boost::optional<Intersection> d_start_intersection;
		boost::optional<Intersection> d_end_intersection;

		boost::optional<RubberBand> d_start_rubber_band;
		boost::optional<RubberBand> d_end_rubber_band;

		//! Cache our calculation of the sub-segment geometry (when it's requested).
		mutable boost::optional<GPlatesMaths::PolylineOnSphere::non_null_ptr_to_const_type> d_sub_segment_geometry;
	};
}

#endif // GPLATES_APP_LOGIC_RESOLVEDSUBSEGMENTRANGEINSECTION_H
