use anyhow::Context;
use surrealdb_protocol::fb::v1 as proto_fb;

use super::{FromFlatbuffers, ToFlatbuffers};
use crate::Geometry;

impl ToFlatbuffers for Geometry {
	type Output<'bldr> = flatbuffers::WIPOffset<proto_fb::Geometry<'bldr>>;

	#[inline]
	fn to_fb<'bldr>(
		&self,
		builder: &mut flatbuffers::FlatBufferBuilder<'bldr>,
	) -> anyhow::Result<Self::Output<'bldr>> {
		match self {
			Geometry::Point(point) => {
				let geometry = point.to_fb(builder)?;
				Ok(proto_fb::Geometry::create(
					builder,
					&proto_fb::GeometryArgs {
						geometry_type: proto_fb::GeometryType::Point,
						geometry: Some(geometry.as_union_value()),
					},
				))
			}
			Geometry::Line(line_string) => {
				let geometry = line_string.to_fb(builder)?;
				Ok(proto_fb::Geometry::create(
					builder,
					&proto_fb::GeometryArgs {
						geometry_type: proto_fb::GeometryType::LineString,
						geometry: Some(geometry.as_union_value()),
					},
				))
			}
			Geometry::Polygon(polygon) => {
				let geometry = polygon.to_fb(builder)?;
				Ok(proto_fb::Geometry::create(
					builder,
					&proto_fb::GeometryArgs {
						geometry_type: proto_fb::GeometryType::Polygon,
						geometry: Some(geometry.as_union_value()),
					},
				))
			}
			Geometry::MultiPoint(multi_point) => {
				let geometry = multi_point.to_fb(builder)?;
				Ok(proto_fb::Geometry::create(
					builder,
					&proto_fb::GeometryArgs {
						geometry_type: proto_fb::GeometryType::MultiPoint,
						geometry: Some(geometry.as_union_value()),
					},
				))
			}
			Geometry::MultiLine(multi_line_string) => {
				let geometry = multi_line_string.to_fb(builder)?;
				Ok(proto_fb::Geometry::create(
					builder,
					&proto_fb::GeometryArgs {
						geometry_type: proto_fb::GeometryType::MultiLineString,
						geometry: Some(geometry.as_union_value()),
					},
				))
			}
			Geometry::MultiPolygon(multi_polygon) => {
				let geometry = multi_polygon.to_fb(builder)?;
				Ok(proto_fb::Geometry::create(
					builder,
					&proto_fb::GeometryArgs {
						geometry_type: proto_fb::GeometryType::MultiPolygon,
						geometry: Some(geometry.as_union_value()),
					},
				))
			}
			Geometry::Collection(geometries) => {
				let mut geometries_vec = Vec::with_capacity(geometries.len());
				for geometry in geometries {
					geometries_vec.push(geometry.to_fb(builder)?);
				}
				let geometries_vector = builder.create_vector(&geometries_vec);

				let collection = proto_fb::GeometryCollection::create(
					builder,
					&proto_fb::GeometryCollectionArgs {
						geometries: Some(geometries_vector),
					},
				);

				Ok(proto_fb::Geometry::create(
					builder,
					&proto_fb::GeometryArgs {
						geometry_type: proto_fb::GeometryType::Collection,
						geometry: Some(collection.as_union_value()),
					},
				))
			}
		}
	}
}

impl FromFlatbuffers for Geometry {
	type Input<'a> = proto_fb::Geometry<'a>;

	#[inline]
	fn from_fb(input: Self::Input<'_>) -> anyhow::Result<Self> {
		match input.geometry_type() {
			proto_fb::GeometryType::Point => {
				let point = input
					.geometry_as_point()
					.ok_or_else(|| anyhow::anyhow!("Expected Point geometry"))?;
				Ok(Geometry::Point(geo::Point::from_fb(point)?))
			}
			proto_fb::GeometryType::LineString => {
				let line_string = input
					.geometry_as_line_string()
					.ok_or_else(|| anyhow::anyhow!("Expected LineString geometry"))?;
				Ok(Geometry::Line(geo::LineString::from_fb(line_string)?))
			}
			proto_fb::GeometryType::Polygon => {
				let polygon = input
					.geometry_as_polygon()
					.ok_or_else(|| anyhow::anyhow!("Expected Polygon geometry"))?;
				Ok(Geometry::Polygon(geo::Polygon::from_fb(polygon)?))
			}
			proto_fb::GeometryType::MultiPoint => {
				let multi_point = input
					.geometry_as_multi_point()
					.ok_or_else(|| anyhow::anyhow!("Expected MultiPoint geometry"))?;
				Ok(Geometry::MultiPoint(geo::MultiPoint::from_fb(multi_point)?))
			}
			proto_fb::GeometryType::MultiLineString => {
				let multi_line_string = input
					.geometry_as_multi_line_string()
					.ok_or_else(|| anyhow::anyhow!("Expected MultiLineString geometry"))?;
				Ok(Geometry::MultiLine(geo::MultiLineString::from_fb(multi_line_string)?))
			}
			proto_fb::GeometryType::MultiPolygon => {
				let multi_polygon = input
					.geometry_as_multi_polygon()
					.ok_or_else(|| anyhow::anyhow!("Expected MultiPolygon geometry"))?;
				Ok(Geometry::MultiPolygon(geo::MultiPolygon::from_fb(multi_polygon)?))
			}
			proto_fb::GeometryType::Collection => {
				let collection = input
					.geometry_as_collection()
					.ok_or_else(|| anyhow::anyhow!("Expected GeometryCollection"))?;
				let geometries_reader = collection.geometries().context("Geometries is not set")?;
				let mut geometries = Vec::with_capacity(geometries_reader.len());
				for geometry in geometries_reader {
					geometries.push(Geometry::from_fb(geometry)?);
				}
				Ok(Geometry::Collection(geometries))
			}
			_ => Err(anyhow::anyhow!(
				"Unsupported geometry type for FlatBuffers deserialization: {:?}",
				input.geometry_type()
			)),
		}
	}
}

impl ToFlatbuffers for geo::Point {
	type Output<'bldr> = flatbuffers::WIPOffset<proto_fb::Point<'bldr>>;

	#[inline]
	fn to_fb<'bldr>(
		&self,
		builder: &mut flatbuffers::FlatBufferBuilder<'bldr>,
	) -> anyhow::Result<Self::Output<'bldr>> {
		Ok(proto_fb::Point::create(
			builder,
			&proto_fb::PointArgs {
				x: self.x(),
				y: self.y(),
			},
		))
	}
}

impl FromFlatbuffers for geo::Point {
	type Input<'a> = proto_fb::Point<'a>;

	#[inline]
	fn from_fb(input: Self::Input<'_>) -> anyhow::Result<Self> {
		Ok(geo::Point::new(input.x(), input.y()))
	}
}

impl ToFlatbuffers for geo::Coord {
	type Output<'bldr> = flatbuffers::WIPOffset<proto_fb::Point<'bldr>>;

	#[inline]
	fn to_fb<'bldr>(
		&self,
		builder: &mut flatbuffers::FlatBufferBuilder<'bldr>,
	) -> anyhow::Result<Self::Output<'bldr>> {
		Ok(proto_fb::Point::create(
			builder,
			&proto_fb::PointArgs {
				x: self.x,
				y: self.y,
			},
		))
	}
}

impl FromFlatbuffers for geo::Coord {
	type Input<'a> = proto_fb::Point<'a>;

	#[inline]
	fn from_fb(input: Self::Input<'_>) -> anyhow::Result<Self> {
		Ok(geo::Coord {
			x: input.x(),
			y: input.y(),
		})
	}
}

impl ToFlatbuffers for geo::LineString {
	type Output<'bldr> = flatbuffers::WIPOffset<proto_fb::LineString<'bldr>>;

	#[inline]
	fn to_fb<'bldr>(
		&self,
		builder: &mut flatbuffers::FlatBufferBuilder<'bldr>,
	) -> anyhow::Result<Self::Output<'bldr>> {
		let mut points = Vec::with_capacity(self.0.len());
		for point in &self.0 {
			points.push(point.to_fb(builder)?);
		}
		let points_vector = builder.create_vector(&points);
		Ok(proto_fb::LineString::create(
			builder,
			&proto_fb::LineStringArgs {
				points: Some(points_vector),
			},
		))
	}
}

impl FromFlatbuffers for geo::LineString {
	type Input<'a> = proto_fb::LineString<'a>;

	#[inline]
	fn from_fb(input: Self::Input<'_>) -> anyhow::Result<Self> {
		let mut points = Vec::new();
		for point in input.points().context("Points is not set")? {
			points.push(geo::Coord::from_fb(point)?);
		}
		Ok(Self(points))
	}
}

impl ToFlatbuffers for geo::Polygon {
	type Output<'bldr> = flatbuffers::WIPOffset<proto_fb::Polygon<'bldr>>;

	#[inline]
	fn to_fb<'bldr>(
		&self,
		builder: &mut flatbuffers::FlatBufferBuilder<'bldr>,
	) -> anyhow::Result<Self::Output<'bldr>> {
		let exterior = self.exterior().to_fb(builder)?;
		let mut interiors = Vec::with_capacity(self.interiors().len());
		for interior in self.interiors() {
			interiors.push(interior.to_fb(builder)?);
		}
		let interiors_vector = builder.create_vector(&interiors);
		Ok(proto_fb::Polygon::create(
			builder,
			&proto_fb::PolygonArgs {
				exterior: Some(exterior),
				interiors: Some(interiors_vector),
			},
		))
	}
}

impl FromFlatbuffers for geo::Polygon {
	type Input<'a> = proto_fb::Polygon<'a>;

	#[inline]
	fn from_fb(input: Self::Input<'_>) -> anyhow::Result<Self> {
		let exterior =
			input.exterior().ok_or_else(|| anyhow::anyhow!("Missing exterior in Polygon"))?;
		let exterior = geo::LineString::from_fb(exterior)?;

		let mut interiors = Vec::new();
		if let Some(interiors_reader) = input.interiors() {
			for interior in interiors_reader {
				interiors.push(geo::LineString::from_fb(interior)?);
			}
		}

		Ok(Self::new(exterior, interiors))
	}
}

impl ToFlatbuffers for geo::MultiPoint {
	type Output<'bldr> = flatbuffers::WIPOffset<proto_fb::MultiPoint<'bldr>>;

	#[inline]
	fn to_fb<'bldr>(
		&self,
		builder: &mut flatbuffers::FlatBufferBuilder<'bldr>,
	) -> anyhow::Result<Self::Output<'bldr>> {
		let mut points = Vec::with_capacity(self.0.len());
		for point in &self.0 {
			points.push(point.to_fb(builder)?);
		}
		let points_vector = builder.create_vector(&points);
		Ok(proto_fb::MultiPoint::create(
			builder,
			&proto_fb::MultiPointArgs {
				points: Some(points_vector),
			},
		))
	}
}

impl FromFlatbuffers for geo::MultiPoint {
	type Input<'a> = proto_fb::MultiPoint<'a>;

	#[inline]
	fn from_fb(input: Self::Input<'_>) -> anyhow::Result<Self> {
		let mut points = Vec::new();
		for point in input.points().context("Points is not set")? {
			points.push(geo::Point::from_fb(point)?);
		}
		Ok(Self(points))
	}
}

impl ToFlatbuffers for geo::MultiLineString {
	type Output<'bldr> = flatbuffers::WIPOffset<proto_fb::MultiLineString<'bldr>>;

	#[inline]
	fn to_fb<'bldr>(
		&self,
		builder: &mut flatbuffers::FlatBufferBuilder<'bldr>,
	) -> anyhow::Result<Self::Output<'bldr>> {
		let mut lines = Vec::with_capacity(self.0.len());
		for line in &self.0 {
			lines.push(line.to_fb(builder)?);
		}
		let lines_vector = builder.create_vector(&lines);
		Ok(proto_fb::MultiLineString::create(
			builder,
			&proto_fb::MultiLineStringArgs {
				lines: Some(lines_vector),
			},
		))
	}
}

impl FromFlatbuffers for geo::MultiLineString {
	type Input<'a> = proto_fb::MultiLineString<'a>;

	#[inline]
	fn from_fb(input: Self::Input<'_>) -> anyhow::Result<Self> {
		let mut lines = Vec::new();
		for line in input.lines().context("Lines is not set")? {
			lines.push(geo::LineString::from_fb(line)?);
		}
		Ok(Self(lines))
	}
}

impl ToFlatbuffers for geo::MultiPolygon {
	type Output<'bldr> = flatbuffers::WIPOffset<proto_fb::MultiPolygon<'bldr>>;

	#[inline]
	fn to_fb<'bldr>(
		&self,
		builder: &mut flatbuffers::FlatBufferBuilder<'bldr>,
	) -> anyhow::Result<Self::Output<'bldr>> {
		let mut polygons = Vec::with_capacity(self.0.len());
		for polygon in &self.0 {
			polygons.push(polygon.to_fb(builder)?);
		}
		let polygons_vector = builder.create_vector(&polygons);
		Ok(proto_fb::MultiPolygon::create(
			builder,
			&proto_fb::MultiPolygonArgs {
				polygons: Some(polygons_vector),
			},
		))
	}
}

impl FromFlatbuffers for geo::MultiPolygon {
	type Input<'a> = proto_fb::MultiPolygon<'a>;

	#[inline]
	fn from_fb(input: Self::Input<'_>) -> anyhow::Result<Self> {
		let mut polygons = Vec::new();
		for polygon in input.polygons().context("Polygons is not set")? {
			polygons.push(geo::Polygon::from_fb(polygon)?);
		}
		Ok(Self(polygons))
	}
}
