/*
 * Copyright (c) 2026 University of Insubria <https://www.uninsubria.it>
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or (at
 * your option) any later version. All we ask is that proper credit is given
 * for our work, which includes - but is not limited to - adding the above
 * copyright notice to the beginning of your source code files, and to any
 * copyright notice that you may distribute with programs based on this work.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.
 * <https://fsf.org/>
 */

package qsardes.descriptor;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.openscience.cdk.graph.Cycles;
import org.openscience.cdk.graph.matrix.TopologicalMatrix;
import org.openscience.cdk.interfaces.IAtom;
import org.openscience.cdk.interfaces.IAtomContainer;
import static org.openscience.cdk.interfaces.IBond.Order.DOUBLE;
import static org.openscience.cdk.interfaces.IBond.Order.SINGLE;
import static org.openscience.cdk.interfaces.IBond.Order.TRIPLE;
import org.openscience.cdk.qsar.AtomValenceTool;
import org.openscience.cdk.tools.periodictable.PeriodicTable;
import qsardes.molecule.AbstractMoleculeBuilder;
import qsardes.molecule.Molecule;

/**
 * Extended topochemical index descriptors abstract class.
 *
 * @author Nicola Chirico
 */
public abstract class DescriptorExtendedTopochemIndex implements Descriptor
{
    protected DescriptorExtendedTopochemIndex()
    {
    }

    @Override
    public String toString()
    {
        return "Extended topochemical index";
    }

    @Override
    public DescriptorType get_descriptor_type()
    {
        return DescriptorType.valueOf("DESCRIPTOR");
    }

    @Override
    public List<String> get_descriptor_names()
    {
        return Arrays.asList(
            "ETA_Alpha",      "ETA_AlphaP",     "ETA_dAlpha_A",
            "ETA_dAlpha_B",

            "ETA_Epsilon_1",  "ETA_Epsilon_2",  "ETA_Epsilon_3",
            "ETA_Epsilon_4",  "ETA_Epsilon_5",

            "ETA_dEpsilon_A", "ETA_dEpsilon_B", "ETA_dEpsilon_C",
            "ETA_dEpsilon_D",

            "ETA_Psi_1",      "ETA_dPsi_A",     "ETA_dPsi_B",

            "ETA_Shape_P",    "ETA_Shape_Y",    "ETA_Shape_X",

            "ETA_Beta",       "ETA_BetaP",      "ETA_Beta_s",
            "ETA_BetaP_s",    "ETA_Beta_ns",    "ETA_BetaP_ns",
            "ETA_dBeta",      "ETA_dBetaP",     "ETA_Beta_ns_d",
            "ETA_BetaP_ns_d",

            "ETA_Eta",        "ETA_EtaP",       "ETA_Eta_R",
            "ETA_Eta_F",      "ETA_EtaP_F",     "ETA_Eta_L",
            "ETA_EtaP_L",     "ETA_Eta_R_L",    "ETA_Eta_F_L",
            "ETA_EtaP_F_L",   "ETA_Eta_B",      "ETA_EtaP_B",
            "ETA_Eta_B_RC",   "ETA_EtaP_B_RC");
    }

    @Override
    public Map<String, String> get_descriptor_values(
            AbstractMoleculeBuilder mol_build)
    {
        prepare_descr_map();

        try {
            mol = new Molecule(mol_build);

            standardize_nitro_groups();

            create_cache_data();

            calc_alpha_descr();
            calc_epsilon_descr();
            calc_psi_descr();
            calc_shape_descr();

            create_beta_cache_data();
            create_beta_ns_cache_data();

            calc_beta_descr();
            calc_eta_descr(mol_build);
        } catch (Exception ex) {
            System.err.println(ex);
        }

        return build_out_map();
    }

    protected abstract void standardize_nitro_groups();

    protected abstract double get_eta_eta_b(int atom_count, double eta_r);

    private void prepare_descr_map()
    {
        get_descriptor_names().forEach(
                item -> descr_map.put(item, Double.NaN));
    }

    private void create_cache_data()
    {
        IAtomContainer iac = mol.get_atom_container();

        cache_alpha_sum = 0.0;

        cache_atom_valence_map = new HashMap<>();
        cache_alpha_map = new HashMap<>();
        cache_epsilon_map = new HashMap<>();

        for (var atom : iac.atoms()) {
            Integer valence = AtomValenceTool.getValence(atom);

            cache_atom_valence_map.put(atom, valence);

            double alpha = calc_alpha(atom, valence);

            cache_alpha_map.put(atom, alpha);
            cache_epsilon_map.put(atom, (0.3 * valence) - alpha);

            cache_alpha_sum += cache_alpha_map.get(atom);
        }
    }

    private double calc_alpha(IAtom atom, Integer valence)
    {
        int quant_num = PeriodicTable.getPeriod(atom.getSymbol());

        return ((double) (atom.getAtomicNumber() - valence) / (double) valence)
                * (1.0 / (double) (quant_num - 1));
    }

    private void calc_alpha_descr()
    {
        IAtomContainer iac = mol.get_atom_container();

        final double ATOM_COUNT = (double) iac.getAtomCount();

        descr_map.put("ETA_Alpha", cache_alpha_sum);
        descr_map.put("ETA_AlphaP", cache_alpha_sum / ATOM_COUNT);

        Double d_alpha_a = (cache_alpha_sum - (ATOM_COUNT / 2.0)) / ATOM_COUNT;

        descr_map.put("ETA_dAlpha_A", d_alpha_a > 0.0 ? d_alpha_a : 0.0);

        Double d_alpha_b = ((ATOM_COUNT / 2.0) - cache_alpha_sum) / ATOM_COUNT;

        descr_map.put("ETA_dAlpha_B", d_alpha_b > 0.0 ? d_alpha_b : 0.0);
    }

    private void calc_epsilon_descr()
    {
        IAtomContainer iac = mol.get_atom_container();

        final int ATOM_COUNT = iac.getAtomCount();

        double eps_sum = 0.0;
        double eps_sum_eh = 0.0;
        double eps_sum_r = 0.0;

        double eps_s_sum = 0.0;
        int eps_s_sum_count = 0;

        double eps_xh_sum = 0.0;
        int eps_xh_count = 0;

        int tot_impl_h_count = 0;
        int tot_alk_impl_h_count = 0;

        for (var atom : iac.atoms()) {
            double epsilon = cache_epsilon_map.get(atom);

            eps_sum += epsilon;
            eps_sum_eh += epsilon;

            int impl_h_count = atom.getImplicitHydrogenCount();

            tot_impl_h_count += impl_h_count;

            double impl_h_epsilon = 0.3 * (double) impl_h_count;

            eps_sum += impl_h_epsilon;

            int alk_impl_h_count = 4 - atom.getFormalNeighbourCount()
                    + impl_h_count;

            tot_alk_impl_h_count += alk_impl_h_count;

            eps_sum_r += 0.7 + (0.3 * (double) alk_impl_h_count);

            if (atom.getAtomicNumber() == 6) {
                var eps_s_res = calc_epsilon_sat_h(iac, atom);

                eps_s_sum += 0.7 + impl_h_epsilon + eps_s_res.epsilon();
                eps_s_sum_count += 1 + impl_h_count + eps_s_res.count();
            } else {
                eps_s_sum += epsilon + impl_h_epsilon;
                eps_s_sum_count += 1 + impl_h_count;

                eps_xh_sum += impl_h_epsilon;
                eps_xh_count += impl_h_count;
            }
        }

        descr_map.put("ETA_Epsilon_1", eps_sum
                / (double) (ATOM_COUNT + tot_impl_h_count));

        descr_map.put("ETA_Epsilon_2", eps_sum_eh / (double) (ATOM_COUNT));

        descr_map.put("ETA_Epsilon_3", eps_sum_r
                / (double) (ATOM_COUNT + tot_alk_impl_h_count));

        descr_map.put("ETA_Epsilon_4", eps_s_sum
                / (double) (eps_s_sum_count));

        descr_map.put("ETA_Epsilon_5", (eps_sum_eh + eps_xh_sum)
                / (double) (ATOM_COUNT + eps_xh_count));

        descr_map.put("ETA_dEpsilon_A", descr_map.get("ETA_Epsilon_1")
                - descr_map.get("ETA_Epsilon_3"));

        descr_map.put("ETA_dEpsilon_B", descr_map.get("ETA_Epsilon_1")
                - descr_map.get("ETA_Epsilon_4"));

        descr_map.put("ETA_dEpsilon_C", descr_map.get("ETA_Epsilon_3")
                - descr_map.get("ETA_Epsilon_4"));

        descr_map.put("ETA_dEpsilon_D", descr_map.get("ETA_Epsilon_2")
                - descr_map.get("ETA_Epsilon_5"));
    }

    private void calc_psi_descr()
    {
        IAtomContainer iac = mol.get_atom_container();

        final int ATOM_COUNT = iac.getAtomCount();

        descr_map.put("ETA_Psi_1", (cache_alpha_sum / (double) ATOM_COUNT)
                / descr_map.get("ETA_Epsilon_2"));

        Double dpsi_a = 0.714 - descr_map.get("ETA_Psi_1");

        descr_map.put("ETA_dPsi_A", dpsi_a > 0.0 ? dpsi_a : 0.0);

        Double dpsi_b = descr_map.get("ETA_Psi_1") - 0.714;

        descr_map.put("ETA_dPsi_B", dpsi_b > 0.0 ? dpsi_b : 0.0);
    }

    private void calc_shape_descr()
    {
        IAtomContainer iac = mol.get_atom_container();

        double alpha_p_sum = 0.0;
        double alpha_y_sum = 0.0;
        double alpha_x_sum = 0.0;

        for (var atom : iac.atoms()) {
            var conn_atom_list = iac.getConnectedAtomsList(atom);

            switch (conn_atom_list.size()) {
                case 1 ->
                    alpha_p_sum += cache_alpha_map.get(atom);
                case 3 ->
                    alpha_y_sum += cache_alpha_map.get(atom);
                case 4 ->
                    alpha_x_sum += cache_alpha_map.get(atom);
            }
        }

        descr_map.put("ETA_Shape_P", alpha_p_sum / cache_alpha_sum);
        descr_map.put("ETA_Shape_Y", alpha_y_sum / cache_alpha_sum);
        descr_map.put("ETA_Shape_X", alpha_x_sum / cache_alpha_sum);
    }

    private void create_beta_cache_data()
    {
        IAtomContainer iac = mol.get_atom_container();

        create_beta_s_cache_data();
        create_beta_ns_cache_data();

        cache_beta_map = new HashMap<>();

        for (var atom : iac.atoms()) {
            cache_beta_map.put(atom, cache_beta_s_map.get(atom)
                    + cache_beta_ns_map.get(atom).delta
                    + cache_beta_ns_map.get(atom).pi_count);
        }
    }

    private void create_beta_s_cache_data()
    {
        IAtomContainer iac = mol.get_atom_container();

        cache_beta_s_map = new HashMap<>();

        for (var atom : iac.atoms()) {
            double eps_atom = cache_epsilon_map.get(atom);

            double sigma_sum = 0.0;

            for (var conn_atom : iac.getConnectedAtomsList(atom)) {
                double eps_conn_atom = cache_epsilon_map.get(conn_atom);

                if (StrictMath.abs(eps_atom - eps_conn_atom) > 0.3) {
                    sigma_sum += 0.75;
                } else {
                    sigma_sum += 0.5;
                }

            }

            cache_beta_s_map.put(atom, sigma_sum);
        }
    }

    private void create_beta_ns_cache_data()
    {
        IAtomContainer iac = mol.get_atom_container();

        cache_beta_ns_map = new HashMap<>();

        for (var atom : iac.atoms()) {
            double eps_atom = cache_epsilon_map.get(atom);

            double pi_count = 0.0;

            for (var conn_atom : iac.getConnectedAtomsList(atom)) {
                var bond = iac.getBond(atom, conn_atom);

                if (bond.getOrder() == SINGLE) {
                    continue;
                }

                double y_ij = 1.0;

                if (bond.isAromatic()) {
                    y_ij = 2.0;
                } else if (StrictMath.abs(eps_atom
                        - cache_epsilon_map.get(conn_atom)) > 0.3) {
                    y_ij = 1.5;
                }

                pi_count += y_ij * (bond.getOrder() == TRIPLE ? 2.0 : 1.0);
            }

            cache_beta_ns_map.put(atom,
                    new BetaNs(pi_count, calc_delta(iac, atom)));
        }
    }

    private void calc_beta_descr()
    {
        IAtomContainer iac = mol.get_atom_container();

        double beta_s_sum = calc_beta_s_sum() / 2.0;

        var beta_ns_res = calc_beta_ns_sum();

        double beta_ns_sum = (beta_ns_res.pi_count / 2.0) + beta_ns_res.delta;

        final double ATOM_COUNT = (double) iac.getAtomCount();

        descr_map.put("ETA_Beta", beta_s_sum + beta_ns_sum);
        descr_map.put("ETA_BetaP", (beta_s_sum
                + beta_ns_sum) / ATOM_COUNT);

        descr_map.put("ETA_Beta_s", beta_s_sum);
        descr_map.put("ETA_BetaP_s", beta_s_sum / ATOM_COUNT);

        descr_map.put("ETA_Beta_ns", beta_ns_sum);
        descr_map.put("ETA_BetaP_ns", beta_ns_sum / ATOM_COUNT);

        descr_map.put("ETA_dBeta", beta_ns_sum - beta_s_sum);
        descr_map.put("ETA_dBetaP", (beta_ns_sum
                - beta_s_sum) / ATOM_COUNT);

        descr_map.put("ETA_Beta_ns_d", beta_ns_res.delta);
        descr_map.put("ETA_BetaP_ns_d", beta_ns_res.delta / ATOM_COUNT);
    }

    private void calc_eta_descr(AbstractMoleculeBuilder mol_build)
    {
        IAtomContainer iac = mol.get_atom_container();

        final int ATOM_COUNT = iac.getAtomCount();

        int[][] topo_matr = TopologicalMatrix.getMatrix​(iac);

        var eta = calc_eta(iac, topo_matr);
        var eta_r = calc_eta_ref_alk(iac, topo_matr);

        descr_map.put("ETA_Eta", eta.full);
        descr_map.put("ETA_EtaP", eta.full / (double)ATOM_COUNT);

        descr_map.put("ETA_Eta_R", eta_r.full);

        descr_map.put("ETA_Eta_F", eta_r.full - eta.full);
        descr_map.put("ETA_EtaP_F",
                (eta_r.full - eta.full) / (double)ATOM_COUNT);

        descr_map.put("ETA_Eta_L", eta.local);
        descr_map.put("ETA_EtaP_L", eta.local / (double)ATOM_COUNT);

        descr_map.put("ETA_Eta_R_L", eta_r.local);

        descr_map.put("ETA_Eta_F_L", eta_r.local - eta.local);
        descr_map.put("ETA_EtaP_F_L",
                (eta_r.local - eta.local) / (double)ATOM_COUNT);

        double eta_eta_b = get_eta_eta_b(ATOM_COUNT, eta_r.local);

        descr_map.put("ETA_Eta_B", eta_eta_b);
        descr_map.put("ETA_EtaP_B", eta_eta_b / (double)ATOM_COUNT);

        final int RING_COUNT = Cycles.essential(iac).numberOfCycles();

        descr_map.put("ETA_Eta_B_RC", eta_eta_b
                + (0.086 * (double)RING_COUNT));

        descr_map.put("ETA_EtaP_B_RC", (eta_eta_b
                + (0.086 * (double)RING_COUNT)) / (double)ATOM_COUNT);
    }

    private Eta calc_eta(IAtomContainer iac, int[][] topo_matr)
    {
        double eta = 0.0;
        double eta_local = 0.0;

        for (int i = 0, n = iac.getAtomCount(); i < n; i++) {
            var atom_i = iac.getAtom(i);
            double gamma_i = cache_alpha_map.get(atom_i)
                    / cache_beta_map.get(atom_i);

            for (int j = i + 1, m = iac.getAtomCount(); j < m; j++) {
                var atom_j = iac.getAtom(j);

                double gamma_j = cache_alpha_map.get(atom_j)
                        / cache_beta_map.get(atom_j);

                double dist = (double) topo_matr[i][j];

                eta += StrictMath.sqrt((gamma_i * gamma_j) / (dist * dist));

                if (dist == 1) {
                    eta_local += StrictMath.sqrt(gamma_i * gamma_j);
                }
            }
        }

        return new Eta(eta, eta_local);
    }

    private Eta calc_eta_ref_alk(IAtomContainer iac, int[][] topo_matr)
    {
        double eta_r = 0.0;
        double eta_local_r = 0.0;

        for (int i = 0, n = iac.getAtomCount(); i < n; i++) {
            double gamma_i = 0.5 / ((double) iac.getConnectedAtomsList(
                    iac.getAtom(i)).size() / 2.0);

            for (int j = i + 1, m = iac.getAtomCount(); j < m; j++) {
                double gamma_j = 0.5 / ((double) iac.getConnectedAtomsList(
                        iac.getAtom(j)).size() / 2.0);

                double dist = (double) topo_matr[i][j];

                eta_r += StrictMath.sqrt((gamma_i * gamma_j) / (dist * dist));

                if (dist == 1) {
                    eta_local_r += StrictMath.sqrt(gamma_i * gamma_j);
                }
            }
        }

        return new Eta(eta_r, eta_local_r);
    }

    private double calc_beta_s_sum()
    {
        double sum = 0.0;

        for (var item : cache_beta_s_map.entrySet()) {
            sum += item.getValue();
        }

        return sum;
    }

    private BetaNs calc_beta_ns_sum()
    {
        double sum = 0.0;
        double delta_sum = 0.0;

        for (var item : cache_beta_ns_map.entrySet()) {
            sum += item.getValue().pi_count;
            delta_sum += item.getValue().delta;
        }

        return new BetaNs(sum, delta_sum);
    }

    private double calc_delta(IAtomContainer iac, IAtom atom)
    {
        double elec_num = cache_atom_valence_map.get(atom)
                - (iac.getBondOrderSum​(atom)
                + (double) atom.getImplicitHydrogenCount());

        if ((elec_num > 0.0) && (!atom.isAromatic()) && (!atom.isInRing())) {
            for (var conn_atom : iac.getConnectedAtomsList(atom)) {
                if (conn_atom.isAromatic()) {
                    return 0.5;
                }
            }
        }

        return 0.0;
    }

    private Epsilon calc_epsilon_sat_h(IAtomContainer iac, IAtom atom)
    {
        if (atom.isAromatic()) {
            return new Epsilon(0.3, 1);
        }

        int satu_count = 0;
        double epsilon_h = 0.0;

        for (var conn_atom : iac.getConnectedAtomsList(atom)) {
            if (conn_atom.getAtomicNumber() == 6) {
                var bond = iac.getBond(atom, conn_atom);

                switch (bond.getOrder()) {
                    case DOUBLE -> {
                        epsilon_h += 0.3;
                        satu_count++;
                    }
                    case TRIPLE -> {
                        epsilon_h += 0.6;
                        satu_count += 2;
                    }
                }
            }
        }

        return new Epsilon(epsilon_h, satu_count);
    }

    private Map<String, String> build_out_map()
    {
        Map<String, String> map = new HashMap<>();

        descr_map.forEach((key, value) -> map.put(key,
                Double.toString(value)));

        return map;
    }

    private record Epsilon(double epsilon,  int count)
    {
    }

    private record BetaNs(double pi_count,  double delta)
    {
    }

    /**
     * Stores eta values for extended topochemical index descriptors.
     *
     * @param full eta value.
     * @param local local eta value.
     *
     * @author Nicola Chirico
     */
    protected record Eta(double full,  double local)
    {
    }

    private final Map<String, Double> descr_map = new HashMap<>();

    private Map<IAtom, Integer> cache_atom_valence_map;

    private Map<IAtom, Double> cache_alpha_map;
    private Map<IAtom, Double> cache_epsilon_map;

    private Map<IAtom, Double> cache_beta_s_map;
    private Map<IAtom, BetaNs> cache_beta_ns_map;

    private Map<IAtom, Double> cache_beta_map;

    private double cache_alpha_sum = Double.NaN;

    private Molecule mol;

    protected Molecule get_mol()
    {
        return mol;
    }
}
