/*
 * 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 qsardes.molecule.Molecule;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.openscience.cdk.atomtype.EStateAtomTypeMatcher;
import org.openscience.cdk.exception.CDKException;
import org.openscience.cdk.graph.matrix.TopologicalMatrix;
import org.openscience.cdk.interfaces.IAtom;
import org.openscience.cdk.interfaces.IAtomContainer;
import org.openscience.cdk.ringsearch.AllRingsFinder;
import org.openscience.cdk.tools.manipulator.AtomContainerManipulator;
import qsardes.molecule.AbstractMoleculeBuilder;

/**
 * Electrotopological state index descriptors abstract class.
 *
 * @author Nicola Chirico
 */
public abstract class DescriptorElectrTopolStateIndex implements Descriptor
{
    protected DescriptorElectrTopolStateIndex()
    {
    }

    @Override
    public String toString()
    {
        return "Electrotopological state index";
    }

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

    @Override
    public List<String> get_descriptor_names()
    {
        List<String> list = new ArrayList<>();

        list.addAll(create_estate_names_list("n"));
        list.addAll(create_estate_names_list("S"));
        list.addAll(create_estate_names_list("min"));
        list.addAll(create_estate_names_list("max"));

        list.addAll(misc_name_list);
        list.addAll(h_misc_name_list);

        return list;
    }

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

        try {
            create_chem_estate_map();

            mol = new Molecule(mol_build);

            var iac = mol.get_atom_container();

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

            mol.convert_impl_to_expl_hydrogens();

            e_match = create_etype_matcher(iac);

            var est_map = calculate_estates_map(topo_matr);
            var h_est_map = calculate_h_estates_map(topo_matr);

            add_estates_values(topo_matr, est_map);
            add_misc_estates_values(topo_matr, est_map);

            add_h_estates_values(h_est_map);
            add_misc_h_estates_values(h_est_map);

            create_descr_num_map();
            create_descr_sum_map();
            create_descr_min_map();
            create_descr_max_map();
            create_descr_mis_map();
        } catch (Exception ex) {
            System.err.println(ex);
        }

        return build_output_map();
    }

    protected abstract double get_min();

    protected abstract double get_max();

    protected abstract double get_min_estate(double gmin, double estate,
            boolean is_first_value);

    protected abstract double get_max_estate(double gmax, double estate,
            boolean is_first_value);

    protected abstract Map<String, String> create_descr_min_map();

    protected abstract Map<String, String> create_descr_max_map();

    protected abstract Molecule.IntrState get_intrinsic_state(Molecule mol,
            IAtom atom);

    protected String get_original_kier_name(String ematch_name)
    {
        return ematch_to_kier_name_map.containsKey(ematch_name)
                ? ematch_to_kier_name_map.get(ematch_name)
                : ematch_name;
    }

    protected List<String> get_all_misc_name_list()
    {
        List<String> list = new ArrayList<>();

        list.addAll(misc_name_list);
        list.addAll(h_misc_name_list);

        return list;
    }

    protected Map<String, String> get_min_out_map()
    {
        return min_out_map;
    }

    protected Map<String, String> get_max_out_map()
    {
        return max_out_map;
    }

    protected Map<String, List<String>> get_chem_estate_values_map()
    {
        return Collections.unmodifiableMap(chem_estate_values_map);
    }

    private List<String> create_estate_names_list(String type)
    {
        List<String> list = new ArrayList<>();

        kier_hall_name_list.forEach(name -> list.add(
                type + get_original_kier_name(name)));

        add_atom_type_name_list.forEach(name -> list.add(type + name));
        h_cumul_name_list.forEach(name -> list.add(type + name));
        h_atom_type_name_list.forEach(name -> list.add(type + name));

        return list;
    }

    private void prepare_out_maps()
    {
        fill_out_map(num_out_map, "n");
        fill_out_map(sum_out_map, "S");
        fill_out_map(min_out_map, "min");
        fill_out_map(max_out_map, "max");
        fill_mis_out_map();
    }

    private void fill_out_map(Map<String, String> map, String prefix)
    {
        kier_hall_name_list.forEach(name -> map.put(
                prefix + get_original_kier_name(name), "0"));

        add_atom_type_name_list.forEach(name -> map.put(prefix + name, "0"));
        h_cumul_name_list.forEach(name -> map.put(prefix + name, "0"));
        h_atom_type_name_list.forEach(name -> map.put(prefix + name, "0"));
    }

    private void fill_mis_out_map()
    {
        misc_name_list.forEach(name -> mis_out_map.put(name, "0"));
        h_misc_name_list.forEach(name -> mis_out_map.put(name, "0"));
    }

    private Map<String, String> build_output_map()
    {
        Map<String, String> out_map = new HashMap<>();

        maps_list.forEach(src_map -> out_map.putAll(src_map));

        return out_map;
    }

    private void create_chem_estate_map()
    {
        kier_hall_name_list.forEach(name
                -> chem_estate_values_map.put(name, new ArrayList<>()));

        add_atom_type_name_list.forEach(name
                -> chem_estate_values_map.put(name, new ArrayList<>()));

        h_cumul_name_list.forEach(name
                -> chem_estate_values_map.put(name, new ArrayList<>()));

        h_atom_type_name_list.forEach(name
                -> chem_estate_values_map.put(name, new ArrayList<>()));

        misc_name_list.forEach(name
                -> chem_estate_values_map.put(name, new ArrayList<>()));

        h_misc_name_list.forEach(name
                -> chem_estate_values_map.put(name, new ArrayList<>()));
    }

    private Map<String, String> create_descr_num_map()
    {
        List<String> all_misc_name_list = get_all_misc_name_list();

        for (var item : chem_estate_values_map.entrySet()) {
            String name = get_original_kier_name(item.getKey());

            if (!all_misc_name_list.contains(name)) {
                num_out_map.put("n" + name,
                        String.valueOf(item.getValue().size()));
            }
        }

        return num_out_map;
    }

    private Map<String, String> create_descr_sum_map()
    {
        List<String> all_misc_name_list = get_all_misc_name_list();

        for (var item : chem_estate_values_map.entrySet()) {
            String name = get_original_kier_name(item.getKey());

            List<String> value_list = item.getValue();

            if ((!all_misc_name_list.contains(name))
                    && (!value_list.isEmpty())) {
                Double value = 0.0;

                for (String est_val : value_list) {
                    value += Double.valueOf(est_val);
                }

                sum_out_map.put("S" + name, String.valueOf(value));
            }
        }

        return sum_out_map;
    }

    private Map<String, String> create_descr_mis_map()
    {
        List<String> all_misc_name_list = get_all_misc_name_list();

        for (var item : chem_estate_values_map.entrySet()) {
            String name = item.getKey();

            List<String> value_list = item.getValue();

            if ((all_misc_name_list.contains(name))
                    && (!value_list.isEmpty())) {
                mis_out_map.put(name,
                        String.valueOf(item.getValue().get(0)));
            }
        }

        return mis_out_map;
    }

    private EStateAtomTypeMatcher create_etype_matcher(IAtomContainer iac)
            throws CDKException
    {
        var local_e_match = new EStateAtomTypeMatcher();

        local_e_match.setRingSet(new AllRingsFinder().findAllRings(iac));

        return local_e_match;
    }

    // return: atom -> estate atom type symbol (no S prefix) -> estate value
    private Map<IAtom, Map<String, EstateIndex>> calculate_estates_map(
            int[][] topo_matr)
    {
        Map<IAtom, Map<String, EstateIndex>> map = new HashMap<>();

        IAtomContainer iac = mol.get_atom_container();

        for (int i = 0, n = iac.getAtomCount(); i < n; i++) {
            IAtom atom_i = iac.getAtom(i);

            if (atom_i.getAtomicNumber() == 1) {
                continue;
            }

            double intr_i = get_intrinsic_state(mol, iac.getAtom(i)).i_state();

            double delta_i = 0.0;

            for (int j = 0, m = iac.getAtomCount(); j < m; j++) {
                IAtom atom_j = iac.getAtom(j);

                if (atom_j.getAtomicNumber() == 1) {
                    continue;
                }

                if (i != j) {
                    double intr_j = get_intrinsic_state(mol, atom_j).i_state();

                    double r_ij = (double) (topo_matr[i][j] + 1);

                    delta_i += (intr_i - intr_j) / (r_ij * r_ij);
                }
            }

            String atom_type = e_match.findMatchingAtomType(iac, atom_i)
                    .getAtomTypeName().substring(1);

            map.put(atom_i, Map.of(atom_type,
                    new EstateIndex(intr_i, delta_i)));
        }

        return map;
    }

    private Map<IAtom, Map<String, Double>> calculate_h_estates_map(
            int[][] topo_matr)
    {
        Map<IAtom, Map<String, Double>> map = new HashMap<>();

        IAtomContainer iac = mol.get_atom_container();

        for (int i = 0, n = iac.getAtomCount(); i < n; i++) {
            IAtom atom = iac.getAtom(i);

            if (atom.getAtomicNumber() == 1) {
                continue;
            }

            String atom_type = e_match.findMatchingAtomType(iac, atom)
                    .getAtomTypeName().substring(1);

            map.put(atom, Map.of(atom_type, calculate_h_estate(atom,
                    topo_matr[i])));

        }

        return map;
    }

    private Double calculate_h_estate(IAtom atom_i, int[] topo_vect)
    {
        IAtomContainer iac = mol.get_atom_container();

        double khe_i = mol.calc_intrinsic_state_kier_hall_elneg(atom_i)
                .i_state();

        double sum_j = 0.0;

        for (int j = 0, m = iac.getAtomCount(); j < m; j++) {
            IAtom atom_j = iac.getAtom(j);

            if (atom_j.getAtomicNumber() == 1) {
                continue;
            }

            if (atom_i != atom_j) {
                double khe_j = mol.calc_intrinsic_state_kier_hall_elneg(atom_j)
                        .i_state();

                double r_ij = (double) (topo_vect[j] + 1);

                sum_j += (khe_i - khe_j) / (r_ij * r_ij);
            }
        }

        return -(sum_j + calc_hydrogen_estate_index(iac, atom_i) - khe_i);
    }

    private double calc_hydrogen_estate_index(IAtomContainer iac, IAtom atom_i)
    {
        double khe_i = 0.0;

        for (var conn_a : iac.getConnectedAtomsList(atom_i)) {
            if (conn_a.getAtomicNumber() == 1) {
                khe_i = -0.2;
                break;
            }
        }

        return khe_i;
    }

    private void add_estates_values(int[][] topo_matr,
            Map<IAtom, Map<String, EstateIndex>> estates_map)
    {
        IAtomContainer iac = mol.get_atom_container();

        for (int i = 0, n = iac.getAtomCount(); i < n; i++) {
            IAtom atom = iac.getAtom(i);

            if (atom.getAtomicNumber() == 1) {
                continue;
            }

            var iter = estates_map.get(atom).entrySet().iterator();

            if (iter.hasNext()) {
                var item = iter.next();

                String atom_type = item.getKey();
                Double value = item.getValue().intr_i
                        + item.getValue().delta_i;

                add_kier_hall(atom_type, value);

                add_weak_h_bond_acceptor(atom_type, value);
                add_strong_h_bond_acceptor(atom_type, value);

                add_strength_for_potential_h_bond(atom, i, atom_type,
                        topo_matr, value);
            }
        }
    }

    private void add_h_estates_values(
            Map<IAtom, Map<String, Double>> h_estates_map)
    {
        IAtomContainer iac = mol.get_atom_container();

        for (var atom : iac.atoms()) {
            if (atom.getAtomicNumber() == 1) {
                continue;
            }

            var iter = h_estates_map.get(atom).entrySet().iterator();

            if (iter.hasNext()) {
                var item = iter.next();

                String atom_type = item.getKey();

                Double value = item.getValue();

                add_h_indices(atom_type, value);

                add_strong_h_bond_donor(atom_type, value);

                add_c_h_bonded_to_halogens(iac, atom, value);

                add_h_satu_and_vinyl(atom_type, iac, atom, value);
                add_h_other_and_misc(atom_type, iac, atom, value);
            }
        }
    }

    /*
       Logic and atom type list based on PaDEL-Descriptor.

       From: PaDEL-Descriptor version 2.21
       Author: Chun Wei Yap
       Class: EStateAtomTypeDescriptor
       Method: calculate
       Availability: http://yapcwsoft.com/dd/padeldescriptor
     */
    private void add_h_satu_and_vinyl(String atom_type, IAtomContainer iac,
            IAtom atom, Double value)
    {
        if (List.of("sCH3", "ssCH2", "sssCH").contains(atom_type)) {
            add_h_on_sp3_bonded_to_saturated_c(iac, atom, value);
            add_h_on_sp3_bonded_to_unsaturated_c(iac, atom, value);
        } else if (atom_type.equals("dsCH")) {
            add_h_on_vinyl_c_bonded_to_aromatic_c(iac, atom, value);
        }
    }

    /*
       Logic and atom type list based on PaDEL-Descriptor.

       From: PaDEL-Descriptor version 2.21
       Author: Chun Wei Yap
       Class: EStateAtomTypeDescriptor
       Method: calculate
       Availability: http://yapcwsoft.com/dd/padeldescriptor
     */
    private void add_h_other_and_misc(String atom_type, IAtomContainer iac,
            IAtom atom, Double value)
    {
        if (List.of("aaCH", "dCH2", "dsCH").contains(atom_type)) {
            add_h_other(value);
        } else {
            add_h_misc(iac, atom, value);
        }
    }

    private void add_h_other(Double value)
    {
        chem_estate_values_map.get("Hother").add(String.valueOf(value));
    }

    /*
       Logic and atom type list based on PaDEL-Descriptor.

       From: PaDEL-Descriptor version 2.21
       Author: Chun Wei Yap
       Class: EStateAtomTypeDescriptor
       Method: calculate
       Availability: http://yapcwsoft.com/dd/padeldescriptor
     */
    private void add_h_misc(IAtomContainer iac, IAtom atom, Double value)
    {
        List<String> misc_atoms
                = List.of("B", "Si", "P", "Ge", "As", "Se", "Sn", "Pb");

        if ((AtomContainerManipulator.countHydrogens​(iac, atom) > 0)
                && misc_atoms.contains(atom.getSymbol())) {
            chem_estate_values_map.get("Hmisc").add(String.valueOf(value));
        }
    }

    /*
       Fragments list based on PaDEL-Descriptor.

       From: PaDEL-Descriptor version 2.21
       Author: Chun Wei Yap
       Class: EStateAtomTypeDescriptor
       Method: calculate
       Availability: http://yapcwsoft.com/dd/padeldescriptor
     */
    private void add_h_on_sp3_bonded_to_saturated_c(IAtomContainer iac,
            IAtom atom, Double value)
    {
        List<String> saturated_c
                = List.of("sCH3", "ssCH2", "sssCH", "ssssC");

        add_to_estate_map_if_conn_match(iac, atom, saturated_c,
                "HCsats", value);
    }

    /*
       Fragments list based on PaDEL-Descriptor.

       From: PaDEL-Descriptor version 2.21
       Author: Chun Wei Yap
       Class: EStateAtomTypeDescriptor
       Method: calculate
       Availability: http://yapcwsoft.com/dd/padeldescriptor
     */
    private void add_h_on_sp3_bonded_to_unsaturated_c(IAtomContainer iac,
            IAtom atom, Double value)
    {
        List<String> unsaturated_c
                = List.of("dCH2", "tCH", "dsCH", "ddC", "tsC", "dssC");

        add_to_estate_map_if_conn_match(iac, atom, unsaturated_c,
                "HCsatu", value);
    }

    /*
       Fragments list based on PaDEL-Descriptor.

       From: PaDEL-Descriptor version 2.21
       Author: Chun Wei Yap
       Class: EStateAtomTypeDescriptor
       Method: calculate
       Availability: http://yapcwsoft.com/dd/padeldescriptor
     */
    private void add_h_on_vinyl_c_bonded_to_aromatic_c(IAtomContainer iac,
            IAtom atom, Double value)
    {
        List<String> aromatic_c
                = List.of("aaCH", "saaC", "aaaC");

        add_to_estate_map_if_conn_match(iac, atom, aromatic_c,
                "HAvin", value);
    }

    private void add_misc_estates_values(int[][] topo_matr,
            Map<IAtom, Map<String, EstateIndex>> estates_map)
    {
        add_intr_state_diff(get_delta_i_list(estates_map));
        add_intr_state_diff_2(topo_matr);
        add_intr_state_stats(estates_map);
    }

    private void add_intr_state_diff(List<Double> estates_list)
    {
        var map = calc_intr_state_diff(estates_list);

        chem_estate_values_map.get("MAXDN")
                .add(String.valueOf(map.get("maxdn")));

        chem_estate_values_map.get("MAXDP")
                .add(String.valueOf(map.get("maxdp")));

        chem_estate_values_map.get("DELS")
                .add(String.valueOf(map.get("dels")));
    }

    private List<Double> get_delta_i_list(
            Map<IAtom, Map<String, EstateIndex>> estates_map)
    {
        List<Double> list = new ArrayList<>();

        for (var emap : estates_map.entrySet()) {
            var est_iter = emap.getValue().entrySet().iterator();

            if (est_iter.hasNext()) {
                list.add(est_iter.next().getValue().delta_i);
            }
        }

        return list;
    }

    private void add_intr_state_diff_2(int[][] topo_matr)
    {
        var map = calc_intr_state_diff(get_delta_i_state_2_list(topo_matr));

        chem_estate_values_map.get("MAXDN2")
                .add(String.valueOf(map.get("maxdn")));

        chem_estate_values_map.get("MAXDP2")
                .add(String.valueOf(map.get("maxdp")));

        chem_estate_values_map.get("DELS2")
                .add(String.valueOf(map.get("dels")));
    }

    private List<Double> get_delta_i_state_2_list(int[][] topo_matr)
    {
        List<Double> list = new ArrayList<>();

        IAtomContainer iac = mol.get_atom_container();

        for (int i = 0, n = iac.getAtomCount(); i < n; i++) {
            IAtom atom_i = iac.getAtom(i);

            if (atom_i.getAtomicNumber() == 1) {
                continue;
            }

            double intr_i = mol.calc_intrinsic_state_kier_hall(iac.getAtom(i))
                    .i_state();

            double delta_i = 0.0;

            for (int j = 0, m = iac.getAtomCount(); j < m; j++) {
                IAtom atom_j = iac.getAtom(j);

                if (atom_j.getAtomicNumber() == 1) {
                    continue;
                }

                if (i != j) {
                    double intr_j = mol.calc_intrinsic_state_kier_hall(atom_j)
                            .i_state();

                    double r_ij = (double) (topo_matr[i][j] + 1);

                    delta_i += (intr_i - intr_j) / (r_ij * r_ij);
                }
            }

            list.add(delta_i);
        }

        return list;
    }

    private Map<String, Double> calc_intr_state_diff(List<Double> delta_i_list)
    {
        Double maxdn = 0.0;
        Double maxdp = 0.0;
        Double dels = 0.0;

        for (Double delta_i : delta_i_list) {
            Double abs_delta_i = StrictMath.abs(delta_i);

            if (delta_i < 0.0) {
                maxdn = StrictMath.max(maxdn, abs_delta_i);
            } else {
                maxdp = StrictMath.max(maxdp, abs_delta_i);
            }

            dels += abs_delta_i;
        }

        return Map.of("maxdn", maxdn, "maxdp", maxdp, "dels", dels);
    }

    private void add_intr_state_stats(
            Map<IAtom, Map<String, EstateIndex>> est_map)
    {
        double lipo_aff = 0.0;
        double sumi = 0.0;

        double gmin = get_min();
        double gmax = get_max();

        Map<String, Double> lipoaff_coeff_map
                = Map.ofEntries(
                        Map.entry("sCH3", 0.432), Map.entry("aaaC", 0.334),
                        Map.entry("dCH2", 0.318), Map.entry("ssssC", 0.245),
                        Map.entry("ssCH2", 0.482), Map.entry("sF", 0.137),
                        Map.entry("tCH", 0.123), Map.entry("sSH", 0.309),
                        Map.entry("dsCH", 0.280), Map.entry("dS", 0.314),
                        Map.entry("aaCH", 0.299), Map.entry("ssS", 0.561),
                        Map.entry("sssCH", 0.425), Map.entry("aaS", 0.821),
                        Map.entry("ddC", 0.333), Map.entry("sCl", 0.307),
                        Map.entry("tsC", 0.125), Map.entry("sBr", 0.755),
                        Map.entry("dssC", -0.141), Map.entry("sI", 1.616),
                        Map.entry("saaC", 0.200));

        int count = 0;

        for (var emap : est_map.entrySet()) {
            var est_iter = emap.getValue().entrySet().iterator();

            if (est_iter.hasNext()) {
                var item = est_iter.next();

                String atom_type = item.getKey();

                double intr_i = item.getValue().intr_i;
                double delta_i = item.getValue().delta_i;

                double estate = intr_i + delta_i;

                sumi += intr_i;

                gmin = get_min_estate(gmin, estate, count == 0);
                gmax = get_max_estate(gmax, estate, count == 0);

                if (lipoaff_coeff_map.containsKey(atom_type)) {
                    lipo_aff += lipoaff_coeff_map.get(atom_type) * estate;
                }

                count++;
            }
        }

        chem_estate_values_map.get("sumI").add(String.valueOf(sumi));
        chem_estate_values_map.get("gmin").add(String.valueOf(gmin));
        chem_estate_values_map.get("gmax").add(String.valueOf(gmax));

        chem_estate_values_map.get("meanI").add(String.valueOf(
                count == 0 ? 0 : sumi / (double) count));

        chem_estate_values_map.get("LipoaffinityIndex")
                .add(String.valueOf(lipo_aff));
    }

    private void add_misc_h_estates_values(
            Map<IAtom, Map<String, Double>> h_est_map)
    {
        double hmin = get_min();
        double hmax = get_max();

        boolean is_first_value = true;

        for (var emap : h_est_map.entrySet()) {
            var est_iter = emap.getValue().entrySet().iterator();

            if (est_iter.hasNext()) {
                var item = est_iter.next();

                double intr_h = item.getValue();

                hmin = get_min_estate(hmin, intr_h, is_first_value);
                hmax = get_max_estate(hmax, intr_h, is_first_value);

                is_first_value = false;
            }
        }

        chem_estate_values_map.get("hmin").add(String.valueOf(hmin));
        chem_estate_values_map.get("hmax").add(String.valueOf(hmax));
    }

    private void add_kier_hall(String atom_type, Double value)
    {
        if (kier_hall_name_list.contains(atom_type)) {
            chem_estate_values_map.get(atom_type).add(String.valueOf(value));
        }
    }

    /*
       Logic and atom type list based on PaDEL-Descriptor.

       From: PaDEL-Descriptor version 2.21
       Author: Chun Wei Yap
       Class: EStateAtomTypeDescriptor
       Method: calculate
       Availability: http://yapcwsoft.com/dd/padeldescriptor
     */
    private void add_weak_h_bond_acceptor(String atom_type, Double value)
    {
        List<String> weak_h_acceptor
                = List.of("dCH2", "tCH", "dsCH", "aaCH", "ddC", "tsC", "dssC",
                        "saaC", "aaaC");

        if (weak_h_acceptor.contains(atom_type)) {
            chem_estate_values_map.get("wHBa").add(String.valueOf(value));
        }
    }

    /*
       Logic and atom type list based on PaDEL-Descriptor.

       From: PaDEL-Descriptor version 2.21
       Author: Chun Wei Yap
       Class: EStateAtomTypeDescriptor
       Method: calculate
       Availability: http://yapcwsoft.com/dd/padeldescriptor
     */
    private void add_strong_h_bond_acceptor(String atom_type, Double value)
    {
        if (strong_h_acceptor_list.contains(atom_type)) {
            chem_estate_values_map.get("HBa").add(String.valueOf(value));
        }
    }

    /*
       Logic and atom type list based on PaDEL-Descriptor.

       From: PaDEL-Descriptor version 2.21
       Author: Chun Wei Yap
       Class: EStateAtomTypeDescriptor
       Method: calculate
       Availability: http://yapcwsoft.com/dd/padeldescriptor
     */
    private void add_strong_h_bond_donor(String atom_type, Double value)
    {
        if (strong_h_donors_and_potential_h_bonds_list.contains(atom_type)) {
            chem_estate_values_map.get("HBd").add(String.valueOf(value));
        }
    }

    /*
       Logic and atom type list based on PaDEL-Descriptor.

       From: PaDEL-Descriptor version 2.21
       Author: Chun Wei Yap
       Class: EStateAtomTypeDescriptor
       Method: calculate
       Availability: http://yapcwsoft.com/dd/padeldescriptor
     */
    private void add_c_h_bonded_to_halogens(IAtomContainer iac, IAtom atom,
            Double value)
    {
        if ((!atom.getSymbol().equals("C"))
                || (AtomContainerManipulator.countHydrogens​(iac, atom) < 1)) {
            return;
        }

        for (var conn_atom : iac.getConnectedAtomsList(atom)) {
            switch (conn_atom.getSymbol()) {
                case "F", "Cl" -> {
                    chem_estate_values_map.get("HCHnX")
                            .add(String.valueOf(value));

                    chem_estate_values_map.get("wHBd")
                            .add(String.valueOf(value));

                    return;
                }

                // "At" is not included in PaDEL-Descriptor source file
                case "Br", "I", "At" -> {
                    chem_estate_values_map.get("HCHnX")
                            .add(String.valueOf(value));

                    return;
                }
            }
        }
    }

    /*
       Logic and atom type list based on PaDEL-Descriptor.

       From: PaDEL-Descriptor version 2.21
       Author: Chun Wei Yap
       Class: EStateAtomTypeDescriptor
       Method: calculate
       Availability: http://yapcwsoft.com/dd/padeldescriptor
     */
    private void add_strength_for_potential_h_bond(IAtom atom, int atom_index,
            String atom_type, int[][] topo_matr, Double e_value)
    {
        if (!strong_h_acceptor_list.contains(atom_type)) {
            return;
        }

        IAtomContainer iac = mol.get_atom_container();

        for (int i = 0, n = iac.getAtomCount(); i < n; i++) {
            IAtom atom_i = iac.getAtom(i);

            if (atom_i.getSymbol().equals("H") || (atom_i == atom)) {
                continue;
            }

            int path_len = topo_matr[i][atom_index];

            if ((path_len > 1) && (path_len < 11)) {
                String i_atom_type = e_match.findMatchingAtomType(iac, atom_i)
                        .getAtomTypeName().substring(1);

                if (strong_h_donors_and_potential_h_bonds_list
                        .contains(i_atom_type)) {
                    String h_name = "HBint" + String.valueOf(path_len);

                    Double h_value = calculate_h_estate(atom_i, topo_matr[i]);

                    chem_estate_values_map.get(h_name).add(String.valueOf(
                            e_value * h_value));
                }
            }
        }
    }

    private void add_h_indices(String atom_type, Double value)
    {
        for (String name : h_atom_type_name_list) {
            if (name.substring(1).equals(atom_type)) {
                chem_estate_values_map.get(name).add(String.valueOf(value));
                break;
            }
        }
    }

    private void add_to_estate_map_if_conn_match(IAtomContainer iac, IAtom atom,
            List<String> e_list, String atom_type, Double value)
    {
        for (var conn_atom : iac.getConnectedAtomsList(atom)) {
            if (conn_atom == atom) {
                continue;
            }

            String e_name = e_match.findMatchingAtomType(iac, conn_atom)
                    .getAtomTypeName();

            if (e_list.contains(e_name.substring(1))) {
                chem_estate_values_map.get(atom_type)
                        .add(String.valueOf(value));
                break;
            }
        }
    }

    private EStateAtomTypeMatcher e_match;

    private record EstateIndex(Double intr_i, Double delta_i)
    {
    }

    private final Map<String, String> num_out_map = new HashMap<>();
    private final Map<String, String> sum_out_map = new HashMap<>();
    private final Map<String, String> min_out_map = new HashMap<>();
    private final Map<String, String> max_out_map = new HashMap<>();
    private final Map<String, String> mis_out_map = new HashMap<>();

    private final List<Map<String, String>> maps_list = List.of(
            num_out_map, sum_out_map, min_out_map, max_out_map, mis_out_map);

    // atom type -> list of values found in chemical
    private final Map<String, List<String>> chem_estate_values_map
            = new HashMap<>();

    // eMatcher atom type name -> original Kier atom type name
    private final Map<String, String> ematch_to_kier_name_map
            = Map.ofEntries(
                    Map.entry("saaC",   "aasC"),
                    Map.entry("saaN",   "aasN"),
                    Map.entry("sNpH3",  "sNH3p"),
                    Map.entry("ssNpH2", "ssNH2p"),
                    Map.entry("sssNpH", "sssNHp"));


    private final List<String> kier_hall_name_list = Arrays.asList(
        "sLi",    "ssBe",   "ssssBem", "ssBH",   "sssB",   "ssssBm",
        "sCH3",   "dCH2",   "ssCH2",   "tCH",    "dsCH",   "aaCH",
        "sssCH",  "ddC",    "tsC",     "dssC",   "saaC",   "aaaC",
        "ssssC",  "sNpH3",  "sNH2",    "ssNpH2", "dNH",    "ssNH",
        "aaNH",   "tN",     "sssNpH",  "dsN",    "aaN",    "sssN",
        "ddsN",   "saaN",   "ssssNp",  "sOH",    "dO",     "ssO",
        "aaO",    "sF",     "sSiH3",   "ssSiH2", "sssSiH", "ssssSi",
        "sPH2",   "ssPH",   "sssP",    "dsssP",  "sssssP", "sSH",
        "dS",     "ssS",    "aaS",     "dssS",   "ddssS",  "sCl",
        "sGeH3",  "ssGeH2", "sssGeH",  "ssssGe", "sAsH2",  "ssAsH",
        "sssAs",  "dsssAs", "sssssAs", "sSeH",   "dSe",    "ssSe",
        "aaSe",   "dssSe",  "ddssSe",  "sBr",    "sSnH3",  "ssSnH2",
        "sssSnH", "ssssSn", "sI",      "sPbH3",  "ssPbH2", "sssPbH",
        "ssssPb");

    private final List<String> add_atom_type_name_list = Arrays.asList(
        "sBH2",   "aOm",   "sOm", "ddsP", "ssssssS", "Sm",
        "ddsAs",  "ssssssSe");

    private final List<String> misc_name_list = Arrays.asList(
        "LipoaffinityIndex", "MAXDN", "MAXDP", "DELS", "MAXDN2", "MAXDP2",
        "DELS2",             "sumI",  "meanI", "gmax", "gmin");

    private final List<String> h_atom_type_name_list = Arrays.asList(
        "HsOH",   "HdNH",    "HsSH",    "HsNH2", "HssNH", "HaaNH",
        "HsNpH3", "HssNpH2", "HsssNpH", "HtCH",  "HdCH2", "HdsCH",
        "HaaCH");

    private final List<String> h_cumul_name_list = Arrays.asList(
        "HBd",     "wHBd",   "HBa",    "wHBa",   "HBint2", "HBint3",
        "HBint4",  "HBint5", "HBint6", "HBint7", "HBint8", "HBint9",
        "HBint10", "HCHnX",  "HCsats", "HCsatu", "HAvin",  "Hother",
        "Hmisc");

    private final List<String> h_misc_name_list = Arrays.asList(
        "hmin", "hmax");

    /*
       Based on PaDEL-Descriptor version 2.21
       Author: Chun Wei Yap
       Class: EStateAtomTypeDescriptor
       Availability: http://yapcwsoft.com/dd/padeldescriptor
     */
    private final List<String> strong_h_acceptor_list
            = Arrays.asList(
                    "sOH", "dNH", "sNH2", "ssNH", "aaNH", "sssN", "saaN",
                    "ssO", "aaO", "dO",   "ssS",  "aaS",  "sF",   "sCl");

    /*
       Based on PaDEL-Descriptor version 2.21
       Author: Chun Wei Yap
       Class: EStateAtomTypeDescriptor
       Availability: http://yapcwsoft.com/dd/padeldescriptor
     */
    private final List<String> strong_h_donors_and_potential_h_bonds_list
            = Arrays.asList(
                    "sOH", "dNH", "sNH2", "ssNH", "aaNH", "sSH", "tCH");

    private Molecule mol;
}
