/*
 * 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.elements.ElementsEnum;
import qsardes.molecule.Molecule;
import qsardes.molecule.AbstractMoleculeBuilder;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.openscience.cdk.graph.PathTools;
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.QUADRUPLE;
import static org.openscience.cdk.interfaces.IBond.Order.SINGLE;
import static org.openscience.cdk.interfaces.IBond.Order.TRIPLE;
import org.openscience.cdk.tools.periodictable.PeriodicTable;

/**
 * Information content descriptors abstract class.
 *
 * @author Nicola Chirico
 */
public abstract class DescriptorInformationContent implements Descriptor
{
    protected DescriptorInformationContent()
    {
    }

    @Override
    public String toString()
    {
        return "Information content";
    }

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

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

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

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

        order_list.forEach(order -> {
            list.add(build_name("IC", order));
            list.add(build_name("CIC", order));
            list.add(build_name("TIC", order));
            list.add(build_name("BIC", order));
            list.add(build_name("SIC", order));
            list.add(build_name("MIC", order));
            list.add(build_name("ZMIC", order));
        });

        return list;
    }

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

        get_descriptor_names().forEach(name -> {
            if (name.startsWith("IC")) ic_map.put(name, NAN);
            if (name.startsWith("CIC")) cic_map.put(name, NAN);
            if (name.startsWith("TIC")) tic_map.put(name, NAN);
            if (name.startsWith("BIC")) bic_map.put(name, NAN);
            if (name.startsWith("SIC")) sic_map.put(name, NAN);
            if (name.startsWith("MIC")) mic_map.put(name, NAN);
            if (name.startsWith("ZMIC")) zmic_map.put(name, NAN);
        });
    }

    private void calculate_descr()
    {
        var equiv_classes = get_equiv_classes();

        final int ATOM_NUM = mol.get_atom_container().getAtomCount();

        final double LOG_2A = StrictMath.log((double) ATOM_NUM)
                / StrictMath.log(2.0);

        final double LOG_2_BORD_SUM = StrictMath.log(get_bonds_order_sum())
                / StrictMath.log(2.0);

        for (String order : order_list) {
            double ic = calculate_ic(equiv_classes, order);

            ic_map.put(build_name("IC", order),
                    String.valueOf(ic));

            cic_map.put(build_name("CIC", order),
                    String.valueOf(LOG_2A - ic));

            tic_map.put(build_name("TIC", order),
                    String.valueOf((double) ATOM_NUM * ic));

            bic_map.put(build_name("BIC", order),
                    String.valueOf(ic / LOG_2_BORD_SUM));

            sic_map.put(build_name("SIC", order),
                    String.valueOf(ic / LOG_2A));

            mic_map.put(build_name("MIC", order), String.valueOf(
                    calculate_mic(equiv_classes, order, MicType.MASS)));

            zmic_map.put(build_name("ZMIC", order), String.valueOf(
                    calculate_mic(equiv_classes, order, MicType.ZNUM)));
        }
    }

    private double get_bonds_order_sum()
    {
        double sum = 0.0;

        for (var bond : mol.get_atom_container().bonds()) {
            if (bond.isAromatic()) {
                sum += 1.5;
            } else {
                switch (bond.getOrder()) {
                    case SINGLE ->
                        sum += 1.0;
                    case DOUBLE ->
                        sum += 2.0;
                    case TRIPLE ->
                        sum += 3.0;
                    case QUADRUPLE ->
                        sum += 4.0;
                }
            }
        }

        return sum;
    }

    private double calculate_ic(Map<String, List<Map<Integer, Integer>>>
            equiv_classes, String order)
    {
        double ic = 0.0;

        int TOT_ATOMS = mol.get_atom_container().getAtomCount();

        for (var map : equiv_classes.get(order)) {
            var count = map.entrySet().iterator().next().getKey();

            double prob = count.doubleValue() / (double) TOT_ATOMS;
            ic -= prob * (StrictMath.log(prob) / StrictMath.log(2.0));
        }

        return ic;
    }

    private double calculate_mic(Map<String, List<Map<Integer, Integer>>>
            equiv_classes, String order, MicType mic_type)
    {
        double mic = 0.0;

        int TOT_ATOMS = mol.get_atom_container().getAtomCount();

        for (var map : equiv_classes.get(order)) {
            var item = map.entrySet().iterator().next();

            double count = (double) item.getKey();

            Integer atom_num = item.getValue();

            var elem = ElementsEnum.valueOf(PeriodicTable.getSymbol(atom_num));

            double prob = count / (double) TOT_ATOMS;

            switch (mic_type) {
                case MASS -> {
                    mic -= elem.get_mass() * prob
                            * (StrictMath.log(prob) / StrictMath.log(2.0));
                }

                case ZNUM -> {
                    mic -= (double) elem.get_atom_num() * count * prob
                            * (StrictMath.log(prob) / StrictMath.log(2.0));
                }
            }
        }

        return mic;
    }

    // Map: order -> list of maps of order counting -> first atom atomic num.
    private Map<String, List<Map<Integer, Integer>>> get_equiv_classes()
    {
        var iac = mol.get_atom_container();

        // Table: order, coordinate string, count
        Table<String, String, Integer> coord_table = HashBasedTable.create();

        for (String order : order_list) {
            final int order_num = Integer.parseInt(order);

            Set<String> unique_coord_set = new HashSet<>();

            for (var atom : iac.atoms()) {
                ArrayList<String> coord_list = new ArrayList<>();

                PathTools.getPathsOfLength​(iac, atom, order_num).forEach(
                        atom_list -> coord_list
                                .addAll(build_coord_list(iac, atom_list)));

                Collections.sort(coord_list);

                if (!coord_list.isEmpty()) {
                    coord_list.add(":" + atom.getAtomicNumber());

                    var builder = new StringBuilder();

                    for (var coord : coord_list) {
                        if (!unique_coord_set.contains(coord)) {
                            unique_coord_set.add(coord);
                        }
                        builder.append(coord);
                    }

                    String coord = builder.toString();

                    if (coord_table.get(order, coord) == null) {
                        coord_table.put(order, coord, 1);
                    } else {
                        coord_table.put(order, coord,
                                coord_table.get(order, coord) + 1);
                    }
                }
            }
        }

        return convert_to_class_map(coord_table);
    }

    private List<String> build_coord_list(IAtomContainer iac,
            List<IAtom> atom_list)
    {
        List<String> coord_list = new ArrayList<>();

        if (atom_list.size() < 1) {
            return coord_list;
        }

        if (atom_list.size() == 1) {
            coord_list.add(String.valueOf("|"
                    + atom_list.get(0).getAtomicNumber() + "|"));
            return coord_list;
        }

        for (int i = 1, n = atom_list.size(); i < n; i++) {
            var atom_i = atom_list.get(i);
            var atom_p = atom_list.get(i - 1);

            var builder = new StringBuilder();

            builder.append("|");

            builder.append(String.valueOf(
                    atom_i.getAtomicNumber()));

            builder.append("|");

            builder.append(String.valueOf(iac.getConnectedBondsCount(atom_i)));

            builder.append("|");

            builder.append(get_aromatic_info(iac, atom_p, atom_i));

            builder.append("|");

            coord_list.add(builder.toString());
        }

        return coord_list;
    }

    protected abstract String get_aromatic_info(IAtomContainer iac,
            IAtom atom_p, IAtom atom_i);

    private Map<String, List<Map<Integer, Integer>>> convert_to_class_map(
            Table<String, String, Integer> coord_table)
    {
        Map<String, List<Map<Integer, Integer>>> map = new HashMap<>();

        for (String order : order_list) {
            List<Map<Integer, Integer>> list = new ArrayList<>();

            coord_table.row(order).forEach((coord, count) -> {
                Map<Integer, Integer> map_item = new HashMap<>();

                map_item.put(count,
                        Integer.valueOf(
                                coord.substring(coord.indexOf(":") + 1)));

                list.add(map_item);
            });

            map.put(order, list);
        }

        return map;
    }

    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 String build_name(String name, String order)
    {
        return name + order;
    }

    private enum MicType
    {
        MASS, ZNUM
    }

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

    private final Map<String, String> ic_map = new HashMap<>();
    private final Map<String, String> cic_map = new HashMap<>();
    private final Map<String, String> tic_map = new HashMap<>();
    private final Map<String, String> mic_map = new HashMap<>();
    private final Map<String, String> zmic_map = new HashMap<>();

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

    private final List<Map<String, String>> maps_list = List.of(
            ic_map, cic_map, tic_map, bic_map, sic_map, mic_map, zmic_map);

    private Molecule mol;
}
