! Fortran interface to moyoc, the C bindings for moyo.
!
! Conventions for crossing the C boundary:
! - `basis(:, i)` is the `i`th basis vector (the Fortran array is the
!   transpose view of the row-major C `basis[3][3]`, so columns are vectors).
! - `positions(:, i)` are the fractional coordinates of the `i`th site.
! - 3x3 matrices stored in datasets (`std_linear`, `rotations(:, :, k)`, ...)
!   are likewise transposed with respect to their C/Rust definition.
! - Functions returning `type(c_ptr)` give NULL (`c_associated` is false) on
!   failure; map the result with `c_f_pointer` and release it with the
!   matching `*_free` routine.
! - C strings (`type(c_ptr)` components) are converted with `moyo_to_string`.
module moyo
    use, intrinsic :: iso_c_binding, only: c_ptr, c_char, c_double, c_bool, &
                                           c_int32_t, c_size_t, c_null_char, &
                                           c_associated, c_f_pointer
    implicit none
    private

    ! ------------------------------------------------------------------------
    ! Setting preferences (values of the C `MoyoSetting` enum)
    ! ------------------------------------------------------------------------
    !> Hall number specified by the `hall_number` argument
    integer(c_int32_t), parameter, public :: MOYO_SETTING_HALL_NUMBER = 0
    !> The setting of the smallest Hall number
    integer(c_int32_t), parameter, public :: MOYO_SETTING_SPGLIB = 1
    !> Unique axis b, cell choice 1 for monoclinic, hexagonal axes for
    !> rhombohedral, and origin choice 2 for centrosymmetric space groups
    integer(c_int32_t), parameter, public :: MOYO_SETTING_STANDARD = 2

    ! Values of the C `MoyoLayerSetting` enum
    !> Layer Hall number specified by the `hall_number` argument (1 - 116)
    integer(c_int32_t), parameter, public :: MOYO_LAYER_SETTING_HALL_NUMBER = 0
    !> The setting of the smallest layer Hall number for each layer group
    integer(c_int32_t), parameter, public :: MOYO_LAYER_SETTING_SPGLIB = 1
    !> BCS standard choice (origin choice 2 for layer groups 52, 62, 64)
    integer(c_int32_t), parameter, public :: MOYO_LAYER_SETTING_STANDARD = 2

    ! ------------------------------------------------------------------------
    ! Interoperable derived types mirroring moyoc.h
    ! ------------------------------------------------------------------------
    !> Symmetry operations. `rotations` points to `num_operations` int32 3x3
    !> matrices and `translations` to `num_operations` real vectors; map them
    !> with `c_f_pointer(ops%rotations, rot, [3, 3, int(ops%num_operations)])`.
    type, bind(c), public :: MoyoOperations
        type(c_ptr) :: rotations
        type(c_ptr) :: translations
        integer(c_int32_t) :: num_operations
    end type MoyoOperations

    !> Magnetic symmetry operations; `time_reversals` points to
    !> `num_operations` `logical(c_bool)` values.
    type, bind(c), public :: MoyoMagneticOperations
        type(c_ptr) :: rotations
        type(c_ptr) :: translations
        type(c_ptr) :: time_reversals
        integer(c_int32_t) :: num_operations
    end type MoyoMagneticOperations

    !> Crystal structure. `basis(:, i)` is the `i`th basis vector;
    !> `positions` points to `num_atoms` fractional coordinate triplets and
    !> `numbers` to `num_atoms` atomic numbers.
    type, bind(c), public :: MoyoCell
        real(c_double) :: basis(3, 3)
        type(c_ptr) :: positions
        type(c_ptr) :: numbers
        integer(c_int32_t) :: num_atoms
    end type MoyoCell

    !> Collinear magnetic structure; `magnetic_moments` points to
    !> `cell%num_atoms` scalar moments.
    type, bind(c), public :: MoyoCollinearMagneticCell
        type(MoyoCell) :: cell
        type(c_ptr) :: magnetic_moments
    end type MoyoCollinearMagneticCell

    !> Non-collinear magnetic structure; `magnetic_moments` points to
    !> `cell%num_atoms` Cartesian moment vectors.
    type, bind(c), public :: MoyoNonCollinearMagneticCell
        type(MoyoCell) :: cell
        type(c_ptr) :: magnetic_moments
    end type MoyoNonCollinearMagneticCell

    !> A dataset of the symmetry analysis, created by `moyo_dataset_new` and
    !> freed by `moyo_dataset_free`. See moyoc.h for field documentation.
    type, bind(c), public :: MoyoDataset
        integer(c_int32_t) :: number
        integer(c_int32_t) :: hall_number
        type(c_ptr) :: hm_symbol
        integer(c_int32_t) :: num_atoms
        type(MoyoOperations) :: operations
        type(c_ptr) :: orbits
        type(c_ptr) :: wyckoffs
        type(c_ptr) :: site_symmetry_symbols
        type(MoyoCell) :: std_cell
        real(c_double) :: std_linear(3, 3)
        real(c_double) :: std_origin_shift(3)
        real(c_double) :: std_rotation_matrix(3, 3)
        type(c_ptr) :: pearson_symbol
        type(MoyoCell) :: prim_std_cell
        real(c_double) :: prim_std_linear(3, 3)
        real(c_double) :: prim_std_origin_shift(3)
        type(c_ptr) :: mapping_std_prim
        real(c_double) :: symprec
        real(c_double) :: angle_tolerance
    end type MoyoDataset

    !> A dataset of the layer-group symmetry analysis, created by
    !> `moyo_layer_dataset_new` and freed by `moyo_layer_dataset_free`.
    type, bind(c), public :: MoyoLayerDataset
        integer(c_int32_t) :: number
        integer(c_int32_t) :: hall_number
        type(c_ptr) :: hm_symbol
        integer(c_int32_t) :: num_atoms
        type(MoyoOperations) :: operations
        type(c_ptr) :: orbits
        type(c_ptr) :: wyckoffs
        type(c_ptr) :: site_symmetry_symbols
        type(MoyoCell) :: std_cell
        real(c_double) :: std_linear(3, 3)
        real(c_double) :: std_origin_shift(3)
        real(c_double) :: std_rotation_matrix(3, 3)
        type(c_ptr) :: pearson_symbol
        type(MoyoCell) :: prim_std_cell
        real(c_double) :: prim_std_linear(3, 3)
        real(c_double) :: prim_std_origin_shift(3)
        type(c_ptr) :: mapping_std_prim
        real(c_double) :: symprec
        real(c_double) :: angle_tolerance
    end type MoyoLayerDataset

    !> A dataset of the magnetic symmetry analysis of a collinear magnetic
    !> structure, created by `moyo_collinear_magnetic_dataset_new` and freed
    !> by `moyo_collinear_magnetic_dataset_free`.
    type, bind(c), public :: MoyoCollinearMagneticDataset
        integer(c_int32_t) :: uni_number
        integer(c_int32_t) :: num_atoms
        type(MoyoMagneticOperations) :: magnetic_operations
        type(c_ptr) :: orbits
        type(MoyoCollinearMagneticCell) :: std_mag_cell
        real(c_double) :: std_linear(3, 3)
        real(c_double) :: std_origin_shift(3)
        real(c_double) :: std_rotation_matrix(3, 3)
        type(MoyoCollinearMagneticCell) :: prim_std_mag_cell
        real(c_double) :: prim_std_linear(3, 3)
        real(c_double) :: prim_std_origin_shift(3)
        type(c_ptr) :: mapping_std_prim
        real(c_double) :: symprec
        real(c_double) :: angle_tolerance
        real(c_double) :: mag_symprec
    end type MoyoCollinearMagneticDataset

    !> A dataset of the magnetic symmetry analysis of a non-collinear magnetic
    !> structure, created by `moyo_noncollinear_magnetic_dataset_new` and
    !> freed by `moyo_noncollinear_magnetic_dataset_free`.
    type, bind(c), public :: MoyoNonCollinearMagneticDataset
        integer(c_int32_t) :: uni_number
        integer(c_int32_t) :: num_atoms
        type(MoyoMagneticOperations) :: magnetic_operations
        type(c_ptr) :: orbits
        type(MoyoNonCollinearMagneticCell) :: std_mag_cell
        real(c_double) :: std_linear(3, 3)
        real(c_double) :: std_origin_shift(3)
        real(c_double) :: std_rotation_matrix(3, 3)
        type(MoyoNonCollinearMagneticCell) :: prim_std_mag_cell
        real(c_double) :: prim_std_linear(3, 3)
        real(c_double) :: prim_std_origin_shift(3)
        type(c_ptr) :: mapping_std_prim
        real(c_double) :: symprec
        real(c_double) :: angle_tolerance
        real(c_double) :: mag_symprec
    end type MoyoNonCollinearMagneticDataset

    !> Hall symbol database entry, created by `moyo_hall_symbol_entry_new`
    !> and freed by `moyo_hall_symbol_entry_free`.
    type, bind(c), public :: MoyoHallSymbolEntry
        integer(c_int32_t) :: hall_number
        integer(c_int32_t) :: number
        integer(c_int32_t) :: arithmetic_number
        type(c_ptr) :: setting
        type(c_ptr) :: hall_symbol
        type(c_ptr) :: hm_short
        type(c_ptr) :: hm_full
        type(c_ptr) :: centering
    end type MoyoHallSymbolEntry

    !> Layer Hall symbol database entry, created by
    !> `moyo_layer_hall_symbol_entry_new` and freed by
    !> `moyo_layer_hall_symbol_entry_free`.
    type, bind(c), public :: MoyoLayerHallSymbolEntry
        integer(c_int32_t) :: hall_number
        integer(c_int32_t) :: number
        integer(c_int32_t) :: arithmetic_number
        type(c_ptr) :: setting
        type(c_ptr) :: hall_symbol
        type(c_ptr) :: hm_short
        type(c_ptr) :: hm_full
        type(c_ptr) :: centering
    end type MoyoLayerHallSymbolEntry

    !> Space-group type information, created by `moyo_space_group_type_new`
    !> and freed by `moyo_space_group_type_free`.
    type, bind(c), public :: MoyoSpaceGroupType
        integer(c_int32_t) :: number
        type(c_ptr) :: hm_short
        type(c_ptr) :: hm_full
        integer(c_int32_t) :: arithmetic_number
        type(c_ptr) :: arithmetic_symbol
        type(c_ptr) :: geometric_crystal_class
        type(c_ptr) :: crystal_system
        type(c_ptr) :: bravais_class
        type(c_ptr) :: lattice_system
        type(c_ptr) :: crystal_family
    end type MoyoSpaceGroupType

    !> Layer-group type information, created by `moyo_layer_group_type_new`
    !> and freed by `moyo_layer_group_type_free`.
    type, bind(c), public :: MoyoLayerGroupType
        integer(c_int32_t) :: number
        type(c_ptr) :: hm_short
        type(c_ptr) :: hm_full
        integer(c_int32_t) :: arithmetic_number
        type(c_ptr) :: arithmetic_symbol
        type(c_ptr) :: geometric_crystal_class
        type(c_ptr) :: bravais_class
        type(c_ptr) :: lattice_system
    end type MoyoLayerGroupType

    !> Magnetic space-group type information, created by
    !> `moyo_magnetic_space_group_type_new` and freed by
    !> `moyo_magnetic_space_group_type_free`.
    type, bind(c), public :: MoyoMagneticSpaceGroupType
        integer(c_int32_t) :: uni_number
        integer(c_int32_t) :: litvin_number
        type(c_ptr) :: bns_number
        type(c_ptr) :: og_number
        integer(c_int32_t) :: number
        integer(c_int32_t) :: construct_type
    end type MoyoMagneticSpaceGroupType

    !> Point group identified by `moyo_point_group_new` and freed by
    !> `moyo_point_group_free`. `prim_trans_mat` is the transpose view of the
    !> row-major C matrix.
    type, bind(c), public :: MoyoPointGroup
        integer(c_int32_t) :: arithmetic_number
        integer(c_int32_t) :: prim_trans_mat(3, 3)
    end type MoyoPointGroup

    !> Space group identified by `moyo_space_group_new` and freed by
    !> `moyo_space_group_free`. `linear` is the transpose view of the
    !> row-major C matrix.
    type, bind(c), public :: MoyoSpaceGroup
        integer(c_int32_t) :: number
        integer(c_int32_t) :: hall_number
        integer(c_int32_t) :: linear(3, 3)
        real(c_double) :: origin_shift(3)
    end type MoyoSpaceGroup

    !> Layer group identified by `moyo_layer_group_new` and freed by
    !> `moyo_layer_group_free`. `linear` is the transpose view of the
    !> row-major C matrix.
    type, bind(c), public :: MoyoLayerGroup
        integer(c_int32_t) :: number
        integer(c_int32_t) :: hall_number
        integer(c_int32_t) :: linear(3, 3)
        real(c_double) :: origin_shift(3)
    end type MoyoLayerGroup

    !> Magnetic space group identified by `moyo_magnetic_space_group_new` and
    !> freed by `moyo_magnetic_space_group_free`. `linear` is the transpose
    !> view of the row-major C matrix.
    type, bind(c), public :: MoyoMagneticSpaceGroup
        integer(c_int32_t) :: uni_number
        integer(c_int32_t) :: linear(3, 3)
        real(c_double) :: origin_shift(3)
    end type MoyoMagneticSpaceGroup

    public :: moyo_version
    public :: moyo_dataset_new, moyo_dataset_free
    public :: moyo_layer_dataset_new, moyo_layer_dataset_free
    public :: moyo_collinear_magnetic_dataset_new, moyo_collinear_magnetic_dataset_free
    public :: moyo_noncollinear_magnetic_dataset_new, moyo_noncollinear_magnetic_dataset_free
    public :: moyo_operations_from_number, moyo_operations_from_layer_number
    public :: moyo_operations_free
    public :: moyo_magnetic_operations_from_uni_number, moyo_magnetic_operations_free
    public :: moyo_hall_symbol_entry_new, moyo_hall_symbol_entry_free
    public :: moyo_layer_hall_symbol_entry_new, moyo_layer_hall_symbol_entry_free
    public :: moyo_space_group_type_new, moyo_space_group_type_free
    public :: moyo_layer_group_type_new, moyo_layer_group_type_free
    public :: moyo_magnetic_space_group_type_new, moyo_magnetic_space_group_type_free
    public :: moyo_point_group_new, moyo_point_group_free
    public :: moyo_space_group_new, moyo_space_group_free
    public :: moyo_layer_group_new, moyo_layer_group_free
    public :: moyo_magnetic_space_group_new, moyo_magnetic_space_group_free
    public :: moyo_to_string

    interface
        !> Version string of moyoc; convert with `moyo_to_string`. The
        !> returned string is statically allocated and must not be freed.
        function moyo_version() bind(c, name="moyo_version") result(version)
            import :: c_ptr
            type(c_ptr) :: version
        end function moyo_version

        !> Analyze the symmetry of the given cell; returns a pointer to a
        !> `MoyoDataset` or NULL on failure. Pass a negative
        !> `angle_tolerance` to use the default tolerance; `hall_number` is
        !> only used with `MOYO_SETTING_HALL_NUMBER`.
        function moyo_dataset_new(basis, positions, numbers, num_atoms, symprec, &
                                  angle_tolerance, setting, hall_number, rotate_basis) &
            bind(c, name="moyo_dataset_new") result(dataset)
            import :: c_ptr, c_double, c_int32_t, c_bool
            real(c_double), intent(in) :: basis(3, 3)
            real(c_double), intent(in) :: positions(3, *)
            integer(c_int32_t), intent(in) :: numbers(*)
            integer(c_int32_t), value :: num_atoms
            real(c_double), value :: symprec
            real(c_double), value :: angle_tolerance
            integer(c_int32_t), value :: setting
            integer(c_int32_t), value :: hall_number
            logical(c_bool), value :: rotate_basis
            type(c_ptr) :: dataset
        end function moyo_dataset_new

        !> Free a dataset created by `moyo_dataset_new`.
        subroutine moyo_dataset_free(dataset) bind(c, name="moyo_dataset_free")
            import :: c_ptr
            type(c_ptr), value :: dataset
        end subroutine moyo_dataset_free

        !> Analyze the layer-group symmetry of the given cell; returns a
        !> pointer to a `MoyoLayerDataset` or NULL on failure. The third
        !> basis vector must be the aperiodic stacking direction.
        function moyo_layer_dataset_new(basis, positions, numbers, num_atoms, symprec, &
                                        angle_tolerance, setting, hall_number, rotate_basis) &
            bind(c, name="moyo_layer_dataset_new") result(dataset)
            import :: c_ptr, c_double, c_int32_t, c_bool
            real(c_double), intent(in) :: basis(3, 3)
            real(c_double), intent(in) :: positions(3, *)
            integer(c_int32_t), intent(in) :: numbers(*)
            integer(c_int32_t), value :: num_atoms
            real(c_double), value :: symprec
            real(c_double), value :: angle_tolerance
            integer(c_int32_t), value :: setting
            integer(c_int32_t), value :: hall_number
            logical(c_bool), value :: rotate_basis
            type(c_ptr) :: dataset
        end function moyo_layer_dataset_new

        !> Free a dataset created by `moyo_layer_dataset_new`.
        subroutine moyo_layer_dataset_free(dataset) bind(c, name="moyo_layer_dataset_free")
            import :: c_ptr
            type(c_ptr), value :: dataset
        end subroutine moyo_layer_dataset_free

        !> Analyze the magnetic symmetry of a collinear magnetic cell;
        !> returns a pointer to a `MoyoCollinearMagneticDataset` or NULL on
        !> failure. Pass a negative `mag_symprec` to reuse `symprec`.
        function moyo_collinear_magnetic_dataset_new(basis, positions, numbers, &
                                                     magnetic_moments, num_atoms, symprec, &
                                                     angle_tolerance, mag_symprec, is_axial, &
                                                     rotate_basis) &
            bind(c, name="moyo_collinear_magnetic_dataset_new") result(dataset)
            import :: c_ptr, c_double, c_int32_t, c_bool
            real(c_double), intent(in) :: basis(3, 3)
            real(c_double), intent(in) :: positions(3, *)
            integer(c_int32_t), intent(in) :: numbers(*)
            real(c_double), intent(in) :: magnetic_moments(*)
            integer(c_int32_t), value :: num_atoms
            real(c_double), value :: symprec
            real(c_double), value :: angle_tolerance
            real(c_double), value :: mag_symprec
            logical(c_bool), value :: is_axial
            logical(c_bool), value :: rotate_basis
            type(c_ptr) :: dataset
        end function moyo_collinear_magnetic_dataset_new

        !> Free a dataset created by `moyo_collinear_magnetic_dataset_new`.
        subroutine moyo_collinear_magnetic_dataset_free(dataset) &
            bind(c, name="moyo_collinear_magnetic_dataset_free")
            import :: c_ptr
            type(c_ptr), value :: dataset
        end subroutine moyo_collinear_magnetic_dataset_free

        !> Analyze the magnetic symmetry of a non-collinear magnetic cell
        !> (`magnetic_moments(:, i)` is the Cartesian moment of site `i`);
        !> returns a pointer to a `MoyoNonCollinearMagneticDataset` or NULL
        !> on failure.
        function moyo_noncollinear_magnetic_dataset_new(basis, positions, numbers, &
                                                        magnetic_moments, num_atoms, symprec, &
                                                        angle_tolerance, mag_symprec, is_axial, &
                                                        rotate_basis) &
            bind(c, name="moyo_noncollinear_magnetic_dataset_new") result(dataset)
            import :: c_ptr, c_double, c_int32_t, c_bool
            real(c_double), intent(in) :: basis(3, 3)
            real(c_double), intent(in) :: positions(3, *)
            integer(c_int32_t), intent(in) :: numbers(*)
            real(c_double), intent(in) :: magnetic_moments(3, *)
            integer(c_int32_t), value :: num_atoms
            real(c_double), value :: symprec
            real(c_double), value :: angle_tolerance
            real(c_double), value :: mag_symprec
            logical(c_bool), value :: is_axial
            logical(c_bool), value :: rotate_basis
            type(c_ptr) :: dataset
        end function moyo_noncollinear_magnetic_dataset_new

        !> Free a dataset created by `moyo_noncollinear_magnetic_dataset_new`.
        subroutine moyo_noncollinear_magnetic_dataset_free(dataset) &
            bind(c, name="moyo_noncollinear_magnetic_dataset_free")
            import :: c_ptr
            type(c_ptr), value :: dataset
        end subroutine moyo_noncollinear_magnetic_dataset_free

        !> Symmetry operations for a space-group ITA number (1 - 230);
        !> returns a pointer to `MoyoOperations` or NULL on invalid input.
        function moyo_operations_from_number(number, setting, hall_number, primitive) &
            bind(c, name="moyo_operations_from_number") result(operations)
            import :: c_ptr, c_int32_t, c_bool
            integer(c_int32_t), value :: number
            integer(c_int32_t), value :: setting
            integer(c_int32_t), value :: hall_number
            logical(c_bool), value :: primitive
            type(c_ptr) :: operations
        end function moyo_operations_from_number

        !> Symmetry operations for a layer-group number (1 - 80);
        !> returns a pointer to `MoyoOperations` or NULL on invalid input.
        function moyo_operations_from_layer_number(number, setting, hall_number, primitive) &
            bind(c, name="moyo_operations_from_layer_number") result(operations)
            import :: c_ptr, c_int32_t, c_bool
            integer(c_int32_t), value :: number
            integer(c_int32_t), value :: setting
            integer(c_int32_t), value :: hall_number
            logical(c_bool), value :: primitive
            type(c_ptr) :: operations
        end function moyo_operations_from_layer_number

        !> Free operations returned by `moyo_operations_from_number` or
        !> `moyo_operations_from_layer_number` (not operations embedded in a
        !> dataset).
        subroutine moyo_operations_free(operations) bind(c, name="moyo_operations_free")
            import :: c_ptr
            type(c_ptr), value :: operations
        end subroutine moyo_operations_free

        !> Magnetic symmetry operations for a UNI number (1 - 1651); returns
        !> a pointer to `MoyoMagneticOperations` or NULL on invalid input.
        function moyo_magnetic_operations_from_uni_number(uni_number, primitive) &
            bind(c, name="moyo_magnetic_operations_from_uni_number") result(magnetic_operations)
            import :: c_ptr, c_int32_t, c_bool
            integer(c_int32_t), value :: uni_number
            logical(c_bool), value :: primitive
            type(c_ptr) :: magnetic_operations
        end function moyo_magnetic_operations_from_uni_number

        !> Free magnetic operations returned by
        !> `moyo_magnetic_operations_from_uni_number` (not operations
        !> embedded in a dataset).
        subroutine moyo_magnetic_operations_free(magnetic_operations) &
            bind(c, name="moyo_magnetic_operations_free")
            import :: c_ptr
            type(c_ptr), value :: magnetic_operations
        end subroutine moyo_magnetic_operations_free

        !> Hall symbol entry for `hall_number` (1 - 530); returns a pointer
        !> to `MoyoHallSymbolEntry` or NULL if out of range.
        function moyo_hall_symbol_entry_new(hall_number) &
            bind(c, name="moyo_hall_symbol_entry_new") result(entry)
            import :: c_ptr, c_int32_t
            integer(c_int32_t), value :: hall_number
            type(c_ptr) :: entry
        end function moyo_hall_symbol_entry_new

        !> Free an entry created by `moyo_hall_symbol_entry_new`.
        subroutine moyo_hall_symbol_entry_free(entry) &
            bind(c, name="moyo_hall_symbol_entry_free")
            import :: c_ptr
            type(c_ptr), value :: entry
        end subroutine moyo_hall_symbol_entry_free

        !> Layer Hall symbol entry for `hall_number` (1 - 116); returns a
        !> pointer to `MoyoLayerHallSymbolEntry` or NULL if out of range.
        function moyo_layer_hall_symbol_entry_new(hall_number) &
            bind(c, name="moyo_layer_hall_symbol_entry_new") result(entry)
            import :: c_ptr, c_int32_t
            integer(c_int32_t), value :: hall_number
            type(c_ptr) :: entry
        end function moyo_layer_hall_symbol_entry_new

        !> Free an entry created by `moyo_layer_hall_symbol_entry_new`.
        subroutine moyo_layer_hall_symbol_entry_free(entry) &
            bind(c, name="moyo_layer_hall_symbol_entry_free")
            import :: c_ptr
            type(c_ptr), value :: entry
        end subroutine moyo_layer_hall_symbol_entry_free

        !> Space-group type information for ITA `number` (1 - 230); returns
        !> a pointer to `MoyoSpaceGroupType` or NULL if out of range.
        function moyo_space_group_type_new(number) &
            bind(c, name="moyo_space_group_type_new") result(space_group_type)
            import :: c_ptr, c_int32_t
            integer(c_int32_t), value :: number
            type(c_ptr) :: space_group_type
        end function moyo_space_group_type_new

        !> Free a space-group type created by `moyo_space_group_type_new`.
        subroutine moyo_space_group_type_free(space_group_type) &
            bind(c, name="moyo_space_group_type_free")
            import :: c_ptr
            type(c_ptr), value :: space_group_type
        end subroutine moyo_space_group_type_free

        !> Layer-group type information for layer-group `number` (1 - 80);
        !> returns a pointer to `MoyoLayerGroupType` or NULL if out of range.
        function moyo_layer_group_type_new(number) &
            bind(c, name="moyo_layer_group_type_new") result(layer_group_type)
            import :: c_ptr, c_int32_t
            integer(c_int32_t), value :: number
            type(c_ptr) :: layer_group_type
        end function moyo_layer_group_type_new

        !> Free a layer-group type created by `moyo_layer_group_type_new`.
        subroutine moyo_layer_group_type_free(layer_group_type) &
            bind(c, name="moyo_layer_group_type_free")
            import :: c_ptr
            type(c_ptr), value :: layer_group_type
        end subroutine moyo_layer_group_type_free

        !> Magnetic space-group type information for `uni_number`
        !> (1 - 1651); returns a pointer to `MoyoMagneticSpaceGroupType` or
        !> NULL if out of range.
        function moyo_magnetic_space_group_type_new(uni_number) &
            bind(c, name="moyo_magnetic_space_group_type_new") result(magnetic_space_group_type)
            import :: c_ptr, c_int32_t
            integer(c_int32_t), value :: uni_number
            type(c_ptr) :: magnetic_space_group_type
        end function moyo_magnetic_space_group_type_new

        !> Free a magnetic space-group type created by
        !> `moyo_magnetic_space_group_type_new`.
        subroutine moyo_magnetic_space_group_type_free(magnetic_space_group_type) &
            bind(c, name="moyo_magnetic_space_group_type_free")
            import :: c_ptr
            type(c_ptr), value :: magnetic_space_group_type
        end subroutine moyo_magnetic_space_group_type_free

        !> Identify the point group of `num_operations` primitive rotations;
        !> returns a pointer to `MoyoPointGroup` or NULL on failure.
        !> `prim_rotations` points to the rotation matrices (e.g.
        !> `ops%rotations` or `c_loc` of an `integer(c_int32_t)` array whose
        !> `(:, :, k)` slices are the transposed matrices). Pass `c_null_ptr`
        !> as `basis` to assume an identity basis, or `c_loc` of a
        !> `real(c_double) :: basis(3, 3)` whose columns are the basis vectors.
        function moyo_point_group_new(prim_rotations, num_operations, basis) &
            bind(c, name="moyo_point_group_new") result(point_group)
            import :: c_ptr, c_int32_t
            type(c_ptr), value :: prim_rotations
            integer(c_int32_t), value :: num_operations
            type(c_ptr), value :: basis
            type(c_ptr) :: point_group
        end function moyo_point_group_new

        !> Free a point group created by `moyo_point_group_new`.
        subroutine moyo_point_group_free(point_group) &
            bind(c, name="moyo_point_group_free")
            import :: c_ptr
            type(c_ptr), value :: point_group
        end subroutine moyo_point_group_free

        !> Identify the space group from `num_operations` primitive rotations
        !> and translations; returns a pointer to `MoyoSpaceGroup` or NULL on
        !> failure. Pass `c_null_ptr` as `basis` to assume an identity basis
        !> and a negative `epsilon` to use the default tolerance (1e-4);
        !> `hall_number` is only used with `MOYO_SETTING_HALL_NUMBER`.
        function moyo_space_group_new(prim_rotations, prim_translations, num_operations, &
                                      basis, setting, hall_number, epsilon) &
            bind(c, name="moyo_space_group_new") result(space_group)
            import :: c_ptr, c_int32_t, c_double
            type(c_ptr), value :: prim_rotations
            type(c_ptr), value :: prim_translations
            integer(c_int32_t), value :: num_operations
            type(c_ptr), value :: basis
            integer(c_int32_t), value :: setting
            integer(c_int32_t), value :: hall_number
            real(c_double), value :: epsilon
            type(c_ptr) :: space_group
        end function moyo_space_group_new

        !> Free a space group created by `moyo_space_group_new`.
        subroutine moyo_space_group_free(space_group) &
            bind(c, name="moyo_space_group_free")
            import :: c_ptr
            type(c_ptr), value :: space_group
        end subroutine moyo_space_group_free

        !> Identify the layer group from `num_operations` primitive
        !> layer-cell rotations and translations; returns a pointer to
        !> `MoyoLayerGroup` or NULL on failure. Pass `c_null_ptr` as `basis`
        !> to assume an identity basis and a negative `epsilon` to use the
        !> default tolerance (1e-4); `hall_number` is only used with
        !> `MOYO_LAYER_SETTING_HALL_NUMBER`.
        function moyo_layer_group_new(prim_rotations, prim_translations, num_operations, &
                                      basis, setting, hall_number, epsilon) &
            bind(c, name="moyo_layer_group_new") result(layer_group)
            import :: c_ptr, c_int32_t, c_double
            type(c_ptr), value :: prim_rotations
            type(c_ptr), value :: prim_translations
            integer(c_int32_t), value :: num_operations
            type(c_ptr), value :: basis
            integer(c_int32_t), value :: setting
            integer(c_int32_t), value :: hall_number
            real(c_double), value :: epsilon
            type(c_ptr) :: layer_group
        end function moyo_layer_group_new

        !> Free a layer group created by `moyo_layer_group_new`.
        subroutine moyo_layer_group_free(layer_group) &
            bind(c, name="moyo_layer_group_free")
            import :: c_ptr
            type(c_ptr), value :: layer_group
        end subroutine moyo_layer_group_free

        !> Identify the magnetic space group from `num_operations` primitive
        !> magnetic operations; returns a pointer to `MoyoMagneticSpaceGroup`
        !> or NULL on failure. Pass `c_null_ptr` as `basis` to assume an
        !> identity basis and a negative `epsilon` to use the default
        !> tolerance (1e-4).
        function moyo_magnetic_space_group_new(prim_rotations, prim_translations, &
                                               prim_time_reversals, num_operations, &
                                               basis, epsilon) &
            bind(c, name="moyo_magnetic_space_group_new") result(magnetic_space_group)
            import :: c_ptr, c_int32_t, c_double
            type(c_ptr), value :: prim_rotations
            type(c_ptr), value :: prim_translations
            type(c_ptr), value :: prim_time_reversals
            integer(c_int32_t), value :: num_operations
            type(c_ptr), value :: basis
            real(c_double), value :: epsilon
            type(c_ptr) :: magnetic_space_group
        end function moyo_magnetic_space_group_new

        !> Free a magnetic space group created by
        !> `moyo_magnetic_space_group_new`.
        subroutine moyo_magnetic_space_group_free(magnetic_space_group) &
            bind(c, name="moyo_magnetic_space_group_free")
            import :: c_ptr
            type(c_ptr), value :: magnetic_space_group
        end subroutine moyo_magnetic_space_group_free
    end interface

    interface
        function moyo_c_strlen(s) bind(c, name="strlen") result(length)
            import :: c_ptr, c_size_t
            type(c_ptr), value :: s
            integer(c_size_t) :: length
        end function moyo_c_strlen
    end interface

contains

    !> Convert a NUL-terminated C string (`type(c_ptr)`) into a Fortran
    !> character value. Returns an empty string for a NULL pointer.
    function moyo_to_string(c_str) result(f_str)
        type(c_ptr), intent(in) :: c_str
        character(len=:), allocatable :: f_str
        character(kind=c_char), pointer :: chars(:)
        integer :: i, length

        if (.not. c_associated(c_str)) then
            f_str = ""
            return
        end if

        length = int(moyo_c_strlen(c_str))
        call c_f_pointer(c_str, chars, [length])
        allocate (character(len=length) :: f_str)
        do i = 1, length
            f_str(i:i) = chars(i)
        end do
    end function moyo_to_string

end module moyo
