// Chemfiles, a modern library for chemistry file reading and writing
// Copyright (C) Guillaume Fraux and contributors -- BSD license

#include "chemfiles/config.h"  // IWYU pragma: keep
#ifndef CHFL_DISABLE_GEMMI

#include <cassert>
#include <cmath>

#include <array>
#include <vector>
#include <string>
#include <exception>

#include <gemmi/cif.hpp>
#include <gemmi/elem.hpp>
#include <gemmi/small.hpp>
#include <gemmi/smcif.hpp>
#include <gemmi/cifdoc.hpp>
#include <gemmi/unitcell.hpp>

#include "chemfiles/types.hpp"
#include "chemfiles/error_fmt.hpp"
#include "chemfiles/external/optional.hpp"

#include "chemfiles/File.hpp"
#include "chemfiles/Atom.hpp"
#include "chemfiles/Frame.hpp"
#include "chemfiles/Property.hpp"
#include "chemfiles/UnitCell.hpp"
#include "chemfiles/FormatMetadata.hpp"

#include "chemfiles/formats/CIF.hpp"

using namespace chemfiles;

template<> const FormatMetadata& chemfiles::format_metadata<CIFFormat>() {
    static FormatMetadata metadata;
    metadata.name = "CIF";
    metadata.extension = ".cif";
    metadata.description = "Crystallographic Information Framework files";
    metadata.reference = "https://www.iucr.org/resources/cif";

    metadata.read = true;
    metadata.write = true;
    metadata.memory = true;

    metadata.positions = true;
    metadata.velocities = false;
    metadata.unit_cell = true;
    metadata.atoms = true;
    metadata.bonds = false;
    metadata.residues = false;
    return metadata;
}

void CIFFormat::init_() {
    gemmi::cif::Document doc;

    if (file_.mode() == File::WRITE) {
        return;
    }

    if (file_.mode() == File::APPEND) {
        throw file_error("cannot open CIF files in append ('a') mode");
    }

    // Parse the CIF file
    auto content = file_.readall();
    try {
        doc = gemmi::cif::read_string(content);
    } catch (std::exception& e) {
        throw format_error("cannot parse CIF file: {}", e.what());
    }

    // Interpret the content of each block, to remove blocks that contain
    // no atom: those can occur, but are not useful to us (they contain comments
    // and associated experimental data).
    for (const auto &block: doc.blocks) {
        try {
            gemmi::SmallStructure s = gemmi::make_small_structure_from_block(block);
            if (s.sites.size() > 0) {
                structures_.push_back(s);
            }
        } catch (std::exception& e) {
            throw format_error("cannot interpret CIF block: {}", e.what());
        }
    }
}

size_t CIFFormat::nsteps() {
    return structures_.size();
}

void CIFFormat::read_step(const size_t step, Frame& frame) {
    assert(step < structures_.size());

    const auto& structure = structures_[step];
    auto sites = structure.get_all_unit_cell_sites();

    UnitCell cell;
    // Take care of the "special case" used to flag an infinite cell
    if (!(std::abs(structure.cell.a - 1) < 1.e-3
       && std::abs(structure.cell.b - 1) < 1.e-3
       && std::abs(structure.cell.c - 1) < 1.e-3
       && std::abs(structure.cell.alpha - 90) < 1.e-3
       && std::abs(structure.cell.beta - 90) < 1.e-3
       && std::abs(structure.cell.gamma - 90) < 1.e-3)) {
        cell = UnitCell(
            {structure.cell.a, structure.cell.b, structure.cell.c},
            {structure.cell.alpha, structure.cell.beta, structure.cell.gamma}
        );
    }


    frame.set_cell(cell);
    frame.set("name", structure.name);

    for (const auto& site: sites) {
        Atom atom(site.label, site.element.name());
        atom.set("occupancy", site.occ);
        atom.set("atomic_number", site.element.atomic_number());

        gemmi::Fractional fract = site.fract.wrap_to_unit();
        gemmi::Position p = structure.cell.orthogonalize(fract);

        frame.add_atom(std::move(atom), Vector3D(p.x, p.y, p.z));
    }
}

void CIFFormat::read(Frame& frame) {
    read_step(current_step_, frame);
    current_step_++;
}

void CIFFormat::write(const Frame& frame) {
    auto name = frame.get("name");
    if (name && name->kind() == Property::STRING) {
        file_.print("data_{}\n", name->as_string());
    } else {
        file_.print("data_model_{}\n", current_step_);
    }
    file_.print("_audit_creation_method         'generated by Chemfiles'\n");
    file_.print("_symmetry_cell_setting         'triclinic'\n");
    file_.print("_symmetry_space_group_name_H-M 'P 1'\n");
    file_.print("_space_group_IT_number         1\n");

    // We handle infinite cells like Mercury
    // https://www.ccdc.cam.ac.uk/solutions/csd-system/components/mercury/
    // i.e. we define a cubic cell with side 1 ångström
    Matrix3D invmat = Matrix3D::unit();
    Vector3D lengths = {1, 1, 1};
    Vector3D angles = {90, 90, 90};
    if (frame.cell().shape() != UnitCell::INFINITE) {
        invmat = frame.cell().matrix().invert();
        lengths = frame.cell().lengths();
        angles = frame.cell().angles();
    }

    file_.print("_cell_length_a {}\n", lengths[0]);
    file_.print("_cell_length_b {}\n", lengths[1]);
    file_.print("_cell_length_c {}\n", lengths[2]);
    file_.print("_cell_angle_alpha {}\n", angles[0]);
    file_.print("_cell_angle_beta  {}\n", angles[1]);
    file_.print("_cell_angle_gamma {}\n", angles[2]);
    file_.print("loop_\n");
    file_.print("  _symmetry_equiv_pos_as_xyz\n");
    file_.print("  '+x,+y,+z'\n");

    file_.print("\n");
    file_.print("loop_\n");
    file_.print("_atom_site_label\n");
    file_.print("_atom_site_type_symbol\n");
    file_.print("_atom_site_occupancy\n");
    file_.print("_atom_site_fract_x\n");
    file_.print("_atom_site_fract_y\n");
    file_.print("_atom_site_fract_z\n");
    // Cartesian coordinates are rare in CIF file, but allowed. Most programs will ignore them
    // and use fractional coordinates
    file_.print("_atom_site_Cartn_x\n");
    file_.print("_atom_site_Cartn_y\n");
    file_.print("_atom_site_Cartn_z\n");

    const auto& positions = frame.positions();
    for (size_t i = 0; i < frame.size(); ++i) {
        const auto& atom = frame[i];
        auto fract = invmat * positions[i];
        file_.print("{} {} 1.0 {:10.7f} {:10.7f} {:10.7f} {:8.5f} {:8.5f} {:8.5f}\n",
            atom.name(), atom.type(), fract[0], fract[1], fract[2],
            positions[i][0], positions[i][1], positions[i][2]
        );
    }

    current_step_++;
}

#endif // CHFL_DISABLE_GEMMI
