/*****************************************************************************
 *
 * This file is part of Mapnik (c++ mapping toolkit)
 *
 * Copyright (C) 2021 Artem Pavlenko
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 *****************************************************************************/
#ifndef MAPNIK_VERTEX_CACHE_HPP
#define MAPNIK_VERTEX_CACHE_HPP

// mapnik
#include <mapnik/pixel_position.hpp>
#include <mapnik/debug.hpp>
#include <mapnik/config.hpp>
#include <mapnik/util/noncopyable.hpp>

#pragma GCC diagnostic push
#include <mapnik/warning_ignore_agg.hpp>
#include "agg_basics.h"
#pragma GCC diagnostic pop

// stl
#include <vector>
#include <memory>
#include <map>

namespace mapnik
{

class vertex_cache;
using vertex_cache_ptr = std::unique_ptr<vertex_cache>;

// Caches all path points and their lengths. Allows easy moving in both directions.
class MAPNIK_DECL vertex_cache : util::noncopyable
{
    struct segment
    {
        segment(double x, double y, double _length) : pos(x, y), length(_length) {}
        pixel_position pos; //Last point of this segment, first point is implicitly defined by the previous segement in this vector
        double length;
    };

    // The first segment always has the length 0 and just defines the starting point.
    struct segment_vector
    {
        segment_vector() : vector(), length(0.) {}
        void add_segment(double x, double y, double len) {
            if (len == 0. && !vector.empty()) return; //Don't add zero length segments
            vector.emplace_back(x, y, len);
            length += len;
        }
        using iterator = std::vector<segment>::iterator;
        std::vector<segment> vector;
        double length;
    };

public:
    // This class has no public members to avoid acciedential modification.
    // It should only be used with save_state/restore_state.
    class state
    {
        segment_vector::iterator current_segment;
        double position_in_segment;
        pixel_position current_position;
        pixel_position segment_starting_point;
        double position_;
        friend class vertex_cache;
    public:
        pixel_position const& position() const { return current_position; }
    };

    class scoped_state : util::noncopyable
    {
    public:
        scoped_state(vertex_cache &pp) : pp_(pp), state_(pp.save_state()), restored_(false) {}
        void restore() { pp_.restore_state(state_); restored_ = true; }
        ~scoped_state() { if (!restored_) pp_.restore_state(state_); }
        state const& get_state() const { return state_; }
    private:
        vertex_cache &pp_;
        state state_;
        bool restored_;
    };

    ///////////////////////////////////////////////////////////////////////

    template <typename T> vertex_cache(T &path);
    vertex_cache(vertex_cache && rhs);

    double length() const { return current_subpath_->length; }


    pixel_position const& current_position() const { return current_position_; }
    double angle(double width=0.);
    double current_segment_angle();
    double linear_position() const { return position_; }


    // Returns a parallel line in the specified distance.
    vertex_cache & get_offseted(double offset, double region_width);


    // Skip a certain amount of space.
    // This functions automatically calculate new points if the position is not exactly
    // on a point on the path.

    bool forward(double length);
    // Go backwards.
    bool backward(double length);
    // Move in any direction (based on sign of length). Returns false if it reaches either end of the path.
    bool move(double length);
    // Move to given distance.
    bool move_to_distance(double distance);
    // Work on next subpath. Returns false if the is no next subpath.
    bool next_subpath();

    // Compatibility with standard path interface
    void rewind(unsigned);
    unsigned vertex(double *x, double *y);

    // State
    state save_state() const;
    void restore_state(state const& s);
    // Go back to initial state.
    void reset();

    // position on this line closest to the target position
    double position_closest_to(pixel_position const &target_pos);

private:
    void rewind_subpath();
    bool next_segment();
    bool previous_segment();
    void find_line_circle_intersection(
        double cx, double cy, double radius,
        double x1, double y1, double x2, double y2,
        double & ix, double & iy) const;
    // Position as calculated by last move/forward/next call.
    pixel_position current_position_;
    // First pixel of current segment.
    pixel_position segment_starting_point_;
    // List of all subpaths.
    std::vector<segment_vector> subpaths_;
    // Currently active subpath.
    std::vector<segment_vector>::iterator current_subpath_;
    // Current segment for normal operation (move()).
    segment_vector::iterator current_segment_;
    // Current segment in compatibility mode (vertex(), rewind()).
    segment_vector::iterator vertex_segment_;
    // Currently active subpath in compatibility mode.
    std::vector<segment_vector>::iterator vertex_subpath_;
    // State is initialized (after first call to next_subpath()).
    bool initialized_;
    // Position from start of segment.
    double position_in_segment_;
    // Angle for current segment.
    mutable double angle_;
    // Is the value in angle_ valid?
    // Used to avoid unnecessary calculations.
    mutable bool angle_valid_;
    using offseted_lines_map = std::map<double, vertex_cache_ptr>;
    // Cache of all offseted lines already computed.
    offseted_lines_map offseted_lines_;
    // Linear position, i.e distance from start of line.
    double position_;
};


template <typename T>
vertex_cache::vertex_cache(T & path)
        : current_position_(),
          segment_starting_point_(),
          subpaths_(),
          current_subpath_(),
          current_segment_(),
          vertex_segment_(),
          vertex_subpath_(),
          initialized_(false),
          position_in_segment_(0.),
          angle_(0.),
          angle_valid_(false),
          offseted_lines_(),
          position_(0.)
{
    path.rewind(0);
    unsigned cmd;
    double new_x = 0., new_y = 0., old_x = 0., old_y = 0.;
    bool first = true; //current_subpath_ uninitalized
    while (!agg::is_stop(cmd = path.vertex(&new_x, &new_y)))
    {
        if (agg::is_move_to(cmd))
        {
            //Create new sub path
            subpaths_.emplace_back();
            current_subpath_ = subpaths_.end()-1;
            current_subpath_->add_segment(new_x, new_y, 0);
            first = false;
        }
        else if (agg::is_line_to(cmd))
        {
            if (first)
            {
                MAPNIK_LOG_ERROR(vertex_cache) << "No starting point in path!\n";
                continue;
            }
            double dx = old_x - new_x;
            double dy = old_y - new_y;
            double segment_length = std::sqrt(dx*dx + dy*dy);
            current_subpath_->add_segment(new_x, new_y, segment_length);
        }
        else if (agg::is_closed(cmd) && !current_subpath_->vector.empty())
        {
            segment const & first_segment = current_subpath_->vector[0];
            double dx = old_x - first_segment.pos.x;
            double dy = old_y - first_segment.pos.y;
            double segment_length = std::sqrt(dx*dx + dy*dy);
            current_subpath_->add_segment(first_segment.pos.x, first_segment.pos.y, segment_length);
        }
        old_x = new_x;
        old_y = new_y;
    }
}

}
#endif
