#include <array>
#include <cassert>
#include <cmath>
#include <cstddef>
#include <limits>
#include <stdexcept>
#include <string>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>

namespace a60 {

using point_2t = std::tuple<double, double>;
using string = std::string;
using vd = std::vector<double>;

namespace carto {

struct frame
{
  struct area
  {
    double _M_width;
    double _M_height;
  };

  area frame_area;
  double moriginx;
  double moriginy;

  frame(const area dimensions, const double x = 0, const double y = 0)
  : frame_area(dimensions), moriginx(x), moriginy(y)
  { }

  frame(const double width, const double height,
        const double x = 0, const double y = 0)
  : frame({width, height}, x, y)
  { }

  double width() const { return frame_area._M_width; }
  double height() const { return frame_area._M_height; }
};

} // namespace carto

namespace io {

struct resources
{
  string data;
};

inline resources&
get_run_time_resources()
{
  static resources value;
  return value;
}

inline string
end_path(const string& value)
{ return value.empty() || value.back() == '/' ? value : value + '/'; }

} // namespace io

} // namespace a60

#include "a60-carto-projection.h"
#include "cart0freak0-voronoi.h"

namespace {

struct expected_point
{
  double latitude;
  double longitude;
  double x;
  double y;
};

void
expect_invalid(const a60::carto::projection_api& projection,
               const double latitude, const double longitude)
{
  bool rejected = false;
  try
    {
      static_cast<void>(
        projection.meridians_to_point_2d(latitude, longitude));
    }
  catch (const std::invalid_argument&)
    {
      rejected = true;
    }
  assert(rejected);
}

} // namespace

int
main()
{
  using namespace a60::carto;
  static_assert(std::is_base_of_v<projection_api, voronoiproj>);
  static_assert(voronoi_detail::vertex_count == 12);
  static_assert(voronoi_detail::face_count == 20);
  static_assert(voronoi_width_to_height_ratio == 48.0 / 25.0);

  const projection_api& api = voronoi_source;
  assert(voronoi_source.pmode == voronoi);
  assert(voronoi_source.pframe.width() == voronoi_source_width);
  assert(voronoi_source.pframe.height() == voronoi_source_height);
  assert(is_voronoi_frame(voronoi_source.pframe));

  // The face sites and shared-edge tree form the complete regular
  // icosahedron used by d3-geo-polygon's geoIcosahedral projection.
  const auto& layout = voronoi_detail::layout();
  const auto parents = voronoi_detail::face_parents();
  assert(parents[0] == -1);
  assert(parents[1] == 7);
  assert(parents[19] == 14);
  for (std::size_t index = 0; index < layout.faces.size(); ++index)
    assert(voronoi_detail::containing_face(layout.faces[index].site)
           == index);

  const auto& face1 = layout.faces[1].transform;
  assert(std::abs(face1.a - 1) < 1e-14);
  assert(std::abs(face1.b) < 1e-14);
  assert(std::abs(face1.c - 1.323169076499215) < 1e-14);
  assert(std::abs(face1.f) < 1e-14);
  const auto& face19 = layout.faces[19].transform;
  assert(std::abs(face19.c - 5.954260844246467) < 2e-14);
  assert(std::abs(face19.f - 1.9098300562505255) < 2e-14);

  // Reference coordinates were generated by d3-geo-polygon v1.12.1's
  // default geoIcosahedral() projection at its 960 x 500 registration.
  constexpr std::array specific_locations {
    expected_point {0, 0, 349.2275614546223, 250},
    expected_point {90, 0, 87.68268436386673, 23.495492209727047},
    expected_point {-90, 0, 174.86431006078521, 476.5045077902729},
    expected_point {0, -180, 785.1356899392149, 250.00000000000006},
    expected_point {0, 180, 785.1356899392149, 250.00000000000006},
    expected_point {40.7128, -74.0060,
                    149.74595984290139, 139.3110834637449},
    expected_point {34.0549, -118.2426,
                    67.02850059353929, 167.3359202201434},
    expected_point {48.8575, 2.3514,
                    384.80376697993484, 121.05946455864901},
    expected_point {-29.8587, 31.0218,
                    418.780813969558, 330.96156487237744},
    expected_point {28.7041, 77.1025,
                    539.993479797133, 172.33343083144914},
    expected_point {35.6895, 139.6917,
                    674.440641591292, 153.2638630600115},
    expected_point {-33.8688, 151.2093,
                    712.516374135553, 331.63365304459103},
    expected_point {21.1444, -157.0226,
                    837.7953719828465, 196.88427092026123},
    expected_point {-23.5558, -46.6396,
                    233.0889867512184, 310.3371749081865},
    expected_point {64.1470, -21.9408,
                    276.40857948517885, 95.66887647730852},
    expected_point {-18.1266, 178.4399,
                    781.238017984519, 297.7133373426773},
    expected_point {-62.2001, 58.9642,
                    509.42741367773317, 399.5226103133416},
  };

  for (const auto& expected : specific_locations)
    {
      const auto [x, y] = api.meridians_to_point_2d(
        expected.latitude, expected.longitude);
      assert(std::abs(x - expected.x) < 2e-9);
      assert(std::abs(y - expected.y) < 2e-9);
    }

  assert(std::abs(voronoi_source.longitude_zero_x
                  - specific_locations[0].x) < 2e-9);
  assert(std::abs(voronoi_source.latitude_zero_y
                  - specific_locations[0].y) < 2e-9);

  // Construct projections from frame/frame_area at arbitrary scales. The
  // registered canvas ratio and every reference coordinate scale uniformly.
  const frame::area compact_dimensions {
    voronoi_width_to_height_ratio * 25, 25
  };
  const frame::area large_dimensions {
    voronoi_width_to_height_ratio * 4000, 4000
  };
  const std::array variable_frames {
    frame {voronoi_width_to_height_ratio, 1},
    frame {48, 25},
    frame {compact_dimensions},
    frame {voronoi_source_width, voronoi_source_height},
    frame {1920, 1000},
    frame {large_dimensions},
    frame {voronoi_width_to_height_ratio * 617.25, 617.25},
  };
  for (const frame& map_frame : variable_frames)
    {
      assert(is_voronoi_frame(map_frame));
      const auto projection = make_voronoi_projection(
        map_frame, "icosahedral-voronoi.png");
      assert(projection.pframe.frame_area._M_width == map_frame.width());
      assert(projection.pframe.frame_area._M_height == map_frame.height());
      assert(projection.pframe.moriginx == 0);
      assert(projection.pframe.moriginy == 0);
      assert(projection.image_filename(projection_base::filled)
             == "icosahedral-voronoi.png");

      const auto [origin_x, origin_y]
        = projection.meridians_to_point_2d(0, 0);
      assert(std::abs(origin_x - projection.longitude_zero_x) < 1e-9);
      assert(std::abs(origin_y - projection.latitude_zero_y) < 1e-9);

      const double factor = map_frame.height() / voronoi_source_height;
      const double tolerance = 2e-9 * std::max(1.0, factor);
      for (const auto& expected : specific_locations)
        {
          const auto [x, y] = projection.meridians_to_point_2d(
            expected.latitude, expected.longitude);
          assert(std::abs(x - expected.x * factor) < tolerance);
          assert(std::abs(y - expected.y * factor) < tolerance);
          assert(x >= 0 && x <= map_frame.width());
          assert(y >= 0 && y <= map_frame.height());
        }
    }

  // Only frame_area controls the projection; placement offsets belong to
  // cartography and are deliberately discarded by the factory.
  const frame positioned_frame {compact_dimensions, 17, 23};
  const auto positioned_projection
    = make_voronoi_projection(positioned_frame);
  assert(positioned_projection.pframe.moriginx == 0);
  assert(positioned_projection.pframe.moriginy == 0);

  a60::io::get_run_time_resources().data = "/projection-data";
  const auto named_projection = make_voronoi_projection(
    pvoronoi_source, "voronoi.png");
  assert(named_projection.image_filename(projection_base::outline)
         == "/projection-data/voronoi.png");
  a60::io::get_run_time_resources().data.clear();

  const auto rejects_frame = [](const frame& invalid_frame)
  {
    assert(!is_voronoi_frame(invalid_frame));
    bool rejected = false;
    try
      {
        static_cast<void>(make_voronoi_projection(invalid_frame));
      }
    catch (const std::invalid_argument&)
      {
        rejected = true;
      }
    assert(rejected);
  };
  rejects_frame(frame {2, 1});
  rejects_frame(frame {960, 500.001});
  rejects_frame(frame {1, 2});
  rejects_frame(frame {0, 0});
  rejects_frame(frame {-48, -25});
  rejects_frame(frame {std::numeric_limits<double>::infinity(), 25});
  rejects_frame(frame {std::numeric_limits<double>::max(),
                       std::numeric_limits<double>::max()});
  rejects_frame(frame {voronoi_width_to_height_ratio,
                       std::numeric_limits<double>::quiet_NaN()});

  // Exercise all geographic quadrants, poles, face boundaries, and map cuts.
  for (int latitude = -90; latitude <= 90; ++latitude)
    for (int longitude = -180; longitude <= 180; ++longitude)
      {
        const auto [x, y] = api.meridians_to_point_2d(latitude, longitude);
        assert(std::isfinite(x));
        assert(std::isfinite(y));
        assert(x >= 0 && x <= voronoi_source_width);
        assert(y >= 0 && y <= voronoi_source_height);
      }

  // -180 and +180 identify the same meridian before the configured rotation.
  for (int latitude = -90; latitude <= 90; latitude += 5)
    {
      const auto west = api.meridians_to_point_2d(latitude, -180);
      const auto east = api.meridians_to_point_2d(latitude, 180);
      assert(std::abs(std::get<0>(west) - std::get<0>(east)) < 1e-12);
      assert(std::abs(std::get<1>(west) - std::get<1>(east)) < 1e-12);
    }

  expect_invalid(api, -90.0001, 0);
  expect_invalid(api, 90.0001, 0);
  expect_invalid(api, 0, -180.0001);
  expect_invalid(api, 0, 180.0001);
  expect_invalid(api, std::numeric_limits<double>::infinity(), 0);
  expect_invalid(api, 0, -std::numeric_limits<double>::infinity());
  expect_invalid(api, std::numeric_limits<double>::quiet_NaN(), 0);
  expect_invalid(api, 0, std::numeric_limits<double>::quiet_NaN());
}
