/*
 * 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 qsardes.molecule.AbstractMoleculeBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jgrapht.alg.shortestpath.AllDirectedPaths;
import org.openscience.cdk.interfaces.IAtom;
import org.openscience.cdk.interfaces.IAtomContainer;
import org.openscience.cdk.interfaces.IBond.Order;

/**
 * Chi path descriptors abstract class.
 *
 * @author Nicola Chirico
 */
public abstract class DescriptorChiPath implements Descriptor
{
    protected DescriptorChiPath()
    {
    }

    @Override
    public String toString()
    {
        return "Chi path";
    }

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

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

        for (String order : order_list) {
            Arrays.asList("SP", "ASP", "VP", "AVP")
                    .forEach(name -> list.add(name + "-" + order));
        }

        return list;
    }

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

        try {
            mol = new Molecule(mol_build);
            calculate_descr();
        } catch (Exception ex) {
            System.err.println(ex);
        }

        return descr_map;
    }

    private void prepare_descr_map()
    {
        final String NAN = String.valueOf(Double.NaN);

        get_descriptor_names().forEach(name -> descr_map.put(name, NAN));
    }

    private void calculate_descr()
    {
        var subgr_map = get_subgr_path_atoms_index(Integer.parseInt(MAX_ORDER));

        for (String order : order_list) {
            final int SG_SIZE = subgr_map.get(Integer.valueOf(order)).size();

            if (SG_SIZE > 0) {
                double value_simple = calc_simple_path_value(
                        subgr_map.get(Integer.valueOf(order)));

                descr_map.put(build_name("SP", order),
                        String.valueOf(value_simple));

                descr_map.put(build_name("ASP", order),
                        String.valueOf(value_simple / (double) SG_SIZE));

                double value_valence = calc_valence_path_value(
                        subgr_map.get(Integer.valueOf(order)));

                descr_map.put(build_name("VP", order),
                        String.valueOf(value_valence));

                descr_map.put(build_name("AVP", order),
                        String.valueOf(value_valence / (double) SG_SIZE));
            } else {
                final String ZERO = String.valueOf(0);

                descr_map.put(build_name("SP", order), ZERO);
                descr_map.put(build_name("ASP", order), ZERO);
                descr_map.put(build_name("VP", order), ZERO);
                descr_map.put(build_name("AVP", order), ZERO);
            }
        }
    }

    private double calc_simple_path_value(List<List<Integer>> paths_list)
    {
        IAtomContainer iac = mol.get_atom_container();

        double value = 0.0;

        for (var path : paths_list) {
            double product = 1.0;

            for (Integer atom_index : path) {
                product *= iac.getConnectedBondsCount(atom_index);
            }

            value += 1.0 / StrictMath.sqrt(product);
        }

        return value;
    }

    private double calc_valence_path_value(List<List<Integer>> paths_list)
    {
        IAtomContainer iac = mol.get_atom_container();

        double value = 0.0;

        for (var path : paths_list) {
            double product = 1.0;

            for (Integer atom_index : path) {
                var atom = iac.getAtom(atom_index);

                /*
                   As pointed by CDK, sulfur and phosphorus in higher oxidation
                   states are special cases where empirical delta V values
                   should be used. See page 19-20 in Molecular Connectivity in
                   Structure-Activity Analysis by Lemont B. Kier and Lowell
                   H. Hall. Research studies press LTD. 1986.
                 */
                Double delta_v_s = get_delta_v_sulfur(iac, atom);

                if (!delta_v_s.isNaN()) {
                    product *= delta_v_s;
                }

                Double delta_v_p = get_delta_v_phosphorus(iac, atom);

                if (!delta_v_p.isNaN()) {
                    product *= delta_v_p;
                }

                if (delta_v_s.isNaN() && delta_v_p.isNaN()) {
                    product *= mol.calc_intrinsic_state_kier_hall_elneg(atom)
                            .delta_v();
                }
            }

            value += 1.0 / StrictMath.sqrt(product);
        }

        return value;
    }

    private Double get_delta_v_sulfur(IAtomContainer iac, IAtom atom)
    {
        if (atom.getSymbol().equals("S")) {
            int double_bond_o_count = 0;

            for (var c_atom : iac.getConnectedAtomsList(atom)) {
                if (c_atom.getSymbol().equals("S")
                        && (c_atom.getBond(atom)
                                .getOrder().equals(Order.SINGLE))) {
                    return 0.89;
                }

                if (c_atom.getSymbol().equals("O")
                        && (c_atom.getBond(atom)
                                .getOrder().equals(Order.DOUBLE))) {
                    double_bond_o_count++;
                }
            }

            switch (double_bond_o_count) {

                case 1 -> {
                    return 1.33;
                }
                case 2 -> {
                    return 2.67;
                }
            }
        }

        return Double.NaN;
    }

    private Double get_delta_v_phosphorus(IAtomContainer iac, IAtom atom)
    {
        if (atom.getSymbol().equals("P")) {
            var conn_atom_list = iac.getConnectedAtomsList(atom);

            int links_countdown = conn_atom_list.size();

            if (links_countdown == 4) {
                for (var c_atom : conn_atom_list) {
                    if (c_atom.getSymbol().equals("O")
                            && (c_atom.getBond(atom)
                                    .getOrder().equals(Order.DOUBLE))) {
                        links_countdown--;
                    }

                    if (c_atom.getBond(atom).getOrder().equals(Order.SINGLE)) {
                        links_countdown--;
                    }
                }

                if (links_countdown == 0) {
                    return 2.22;
                }
            }
        }

        return Double.NaN;
    }

    // Map: order -> list of paths, where paths are lists of atoms index
    private Map<Integer, List<List<Integer>>> get_subgr_path_atoms_index(
            int max_order)
    {
        Map<Integer, List<List<Integer>>> map_base_order
                = get_zero_order_paths();

        if (max_order == 0) {
            return map_base_order;
        }

        if (max_order > 0) {
            Map<Integer, List<List<Integer>>> map_to_max_order
                    = get_remaining_orders_paths_to_map(max_order);

            map_base_order.putAll(map_to_max_order);

            return map_base_order;
        }

        throw new IllegalArgumentException("Invalid order " + max_order
                + ". Max order must be a positive number including 0.");
    }

    private Map<Integer, List<List<Integer>>> get_zero_order_paths()
    {
        Map<Integer, List<List<Integer>>> map = new HashMap<>();

        List<List<Integer>> order_0_list = new ArrayList<>();

        var iac = mol.get_atom_container();

        for (Integer i = 0, n = iac.getAtomCount(); i < n; i++) {
            order_0_list.add(List.of(i));
        }

        map.put(0, order_0_list);

        return map;
    }

    private Map<Integer, List<List<Integer>>>
            get_remaining_orders_paths_to_map(int max_order)
    {
        Map<Integer, List<List<Integer>>> map = new HashMap<>();

        for (Integer i = 1; i <= max_order; i++) {
            map.put(i, new ArrayList<>());
        }

        var paths = new AllDirectedPaths<>(mol.get_graph());

        var iac = mol.get_atom_container();

        for (int i = 0, n = iac.getAtomCount(); i < n; i++) {
            for (int j = i + 1, m = iac.getAtomCount(); j < m; j++) {
                var sub_paths = paths.getAllPaths(i, j, true, max_order);

                for (var path : sub_paths) {
                    List<Integer> vert_list = new ArrayList<>();

                    for (var vert : path.getVertexList()) {
                        vert_list.add(vert);
                    }

                    int order = vert_list.size() - 1;
                    map.get(order).add(vert_list);
                }
            }
        }

        return map;
    }

    private String build_name(String prefix, String order)
    {
        return prefix + "-" + order;
    }

    private final String MAX_ORDER = "7";

    private final List<String> order_list = List.of(
            "0", "1", "2", "3", "4", "5", "6", MAX_ORDER);

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

    private Molecule mol;
}
