using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
// ReSharper disable once CheckNamespace
namespace NpgsqlTypes;
///
/// Represents a PostgreSQL cube data type.
///
///
/// See https://www.postgresql.org/docs/current/cube.html
///
public readonly struct NpgsqlCube : IEquatable
{
// Store the coordinates as a value tuple array
readonly double[] _lowerLeft;
readonly double[] _upperRight;
///
/// The lower left coordinates of the cube.
///
public IReadOnlyList LowerLeft => _lowerLeft;
///
/// The upper right coordinates of the cube.
///
public IReadOnlyList UpperRight => _upperRight;
///
/// The number of dimensions of the cube.
///
public int Dimensions => _lowerLeft.Length;
///
/// True if the cube is a point, that is, the two defining corners are the same.
///
public bool IsPoint { get; }
///
/// Makes a cube with upper right and lower left coordinates as defined by the two arrays, which must be of the same length.
///
/// This is an internal constructor to optimize the number of allocations.
/// The lower left values.
/// The upper right values.
///
/// Thrown if the number of dimensions in the upper left and lower right values do not match.
///
internal NpgsqlCube(double[] lowerLeft, double[] upperRight)
{
if (lowerLeft.Length != upperRight.Length)
throw new ArgumentException($"Not a valid cube: Different point dimensions in {lowerLeft} and {upperRight}.");
IsPoint = lowerLeft.SequenceEqual(upperRight);
_lowerLeft = lowerLeft;
_upperRight = upperRight;
}
///
/// Makes a one dimensional cube with both coordinates the same.
///
/// The point coordinate.
public NpgsqlCube(double coord)
{
IsPoint = true;
_lowerLeft = [coord];
_upperRight = _lowerLeft;
}
///
/// Makes a one dimensional cube.
///
/// The lower left value.
/// The upper right value.
public NpgsqlCube(double lowerLeft, double upperRight)
{
IsPoint = lowerLeft.CompareTo(upperRight) == 0;
_lowerLeft = [lowerLeft];
_upperRight = IsPoint ? _lowerLeft : [upperRight];
}
///
/// Makes a zero-volume cube using the coordinates defined by the array.
///
/// The coordinates.
public NpgsqlCube(IEnumerable coords)
{
// Always create a defensive copy to prevent external mutation
_lowerLeft = coords.ToArray();
IsPoint = true;
_upperRight = _lowerLeft;
}
///
/// Makes a cube with upper right and lower left coordinates as defined by the two arrays, which must be of the same length.
///
/// The lower left values.
/// The upper right values.
///
/// Thrown if the number of dimensions in the upper left and lower right values do not match
/// or if the cube exceeds the maximum dimensions (100).
///
public NpgsqlCube(IEnumerable lowerLeft, IEnumerable upperRight) :
this(lowerLeft.ToArray(), upperRight.ToArray())
{ }
///
/// Makes a new cube by adding a dimension on to an existing cube, with the same values for both endpoints of the new coordinate.
/// This is useful for building cubes piece by piece from calculated values.
///
/// The existing cube.
/// The coordinate to add.
public NpgsqlCube(NpgsqlCube cube, double coord)
{
IsPoint = cube.IsPoint;
if (IsPoint)
{
_lowerLeft = cube._lowerLeft.Append(coord).ToArray();
_upperRight = _lowerLeft;
}
else
{
_lowerLeft = cube._lowerLeft.Append(coord).ToArray();
_upperRight = cube._upperRight.Append(coord).ToArray();
}
}
///
/// Makes a new cube by adding a dimension on to an existing cube.
/// This is useful for building cubes piece by piece from calculated values.
///
/// The existing cube.
/// The lower left value.
/// The upper right value.
public NpgsqlCube(NpgsqlCube cube, double lowerLeft, double upperRight)
{
IsPoint = cube.IsPoint && lowerLeft.CompareTo(upperRight) == 0;
if (IsPoint)
{
_lowerLeft = cube._lowerLeft.Append(lowerLeft).ToArray();
_upperRight = _lowerLeft;
}
else
{
_lowerLeft = cube._lowerLeft.Append(lowerLeft).ToArray();
_upperRight = cube._upperRight.Append(upperRight).ToArray();
}
}
///
/// Makes a new cube from an existing cube, using a list of dimension indexes from an array.
/// Can be used to extract the endpoints of a single dimension, or to drop dimensions, or to reorder them as desired.
///
/// The list of dimension indexes.
/// A new cube.
///
///
/// var cube = new NpgsqlCube(new[] { 1, 3, 5 }, new[] { 6, 7, 8 }); // '(1,3,5),(6,7,8)'
/// cube.ToSubset(1); // '(3),(7)'
/// cube.ToSubset(2, 1, 0, 0); // '(5,3,1,1),(8,7,6,6)'
///
///
public NpgsqlCube ToSubset(params int[] indexes)
{
var lowerLeft = new double[indexes.Length];
var upperRight = new double[indexes.Length];
for (var i = 0; i < indexes.Length; i++)
{
lowerLeft[i] = _lowerLeft[indexes[i]];
upperRight[i] = _upperRight[indexes[i]];
}
return new NpgsqlCube(lowerLeft, upperRight);
}
///
public bool Equals(NpgsqlCube other) => Dimensions == other.Dimensions
&& _lowerLeft.SequenceEqual(other._lowerLeft)
&& _upperRight.SequenceEqual(other._upperRight);
///
public override bool Equals(object? obj) => obj is NpgsqlCube other && Equals(other);
///
public static bool operator ==(NpgsqlCube x, NpgsqlCube y) => x.Equals(y);
///
public static bool operator !=(NpgsqlCube x, NpgsqlCube y) => !(x == y);
///
public override int GetHashCode()
{
var hashCode = new HashCode();
for (var i = 0; i < Dimensions; i++)
{
hashCode.Add(_lowerLeft[i]);
hashCode.Add(_upperRight[i]);
}
return hashCode.ToHashCode();
}
///
/// Writes the cube in PostgreSQL's text format.
///
void Write(StringBuilder stringBuilder)
{
var leftBuilder = new StringBuilder();
var rightBuilder = new StringBuilder();
leftBuilder.Append('(');
rightBuilder.Append('(');
for (var i = 0; i < Dimensions; i++)
{
leftBuilder.Append(CultureInfo.InvariantCulture, $"{_lowerLeft[i]:G17}");
rightBuilder.Append(CultureInfo.InvariantCulture, $"{_upperRight[i]:G17}");
if (i >= Dimensions - 1) continue;
leftBuilder.Append(", ");
rightBuilder.Append(", ");
}
leftBuilder.Append(')');
rightBuilder.Append(')');
if (IsPoint)
{
stringBuilder.Append(leftBuilder);
}
else
{
stringBuilder.Append(leftBuilder);
stringBuilder.Append(',');
stringBuilder.Append(rightBuilder);
}
}
///
/// Writes the cube in PostgreSQL's text format.
///
public override string ToString()
{
var sb = new StringBuilder();
Write(sb);
return sb.ToString();
}
}