/*
 * 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.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import qsardes.molecule.Molecule;
import qsardes.util.UtilsCalc;
import java.util.List;
import java.util.Map;
import org.openscience.cdk.exception.CDKException;
import org.openscience.cdk.graph.matrix.TopologicalMatrix;
import org.openscience.cdk.interfaces.IAtomContainer;
import qsardes.atomconst.AtomConstAllRochElectrNeg;
import qsardes.atomconst.AtomConstCharge;
import qsardes.atomconst.AtomConstFirstIonizPot;
import qsardes.atomconst.AtomConstFunctionCall;
import qsardes.atomconst.AtomConstIntrState;
import qsardes.atomconst.AtomConstMass;
import qsardes.atomconst.AtomConstNum;
import qsardes.atomconst.AtomConstPaulElectrNeg;
import qsardes.atomconst.AtomConstPolariz;
import qsardes.atomconst.AtomConstSandElectrNeg;
import qsardes.atomconst.AtomConstSigma;
import qsardes.atomconst.AtomConstValence;
import qsardes.atomconst.AtomConstVanWVol;
import qsardes.molecule.AbstractMoleculeBuilder;

/**
 * Autocorrelation descriptors abstract class.
 *
 * @author Nicola Chirico
 */
public abstract class DescriptorAutocorrelation implements Descriptor
{
    protected DescriptorAutocorrelation()
    {
        descr_names = build_descriptor_names();
    }

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

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

    @Override
    public List<String> get_descriptor_names()
    {
        return Collections.unmodifiableList(descr_names);
    }

    @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();

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

            calculate_descr(topo_matr);
        } catch (Exception ex) {
            System.err.println(ex);
        }

        return build_output_map();
    }

    protected abstract String get_default_value();

    protected abstract List<String> get_weights_list();

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

        List<String> all_ext_list = new ArrayList<>();
        List<String> noc_ext_list = new ArrayList<>();
        List<String> noz_ext_list = new ArrayList<>();

        var weights_list = get_weights_list();

        for (var lag : lag_list) {
            for (var w_item : WeightEnum.values()) {
                String weight = w_item.get_ext_str();

                if (weights_list.contains(weight)) {
                    String ext = lag + weight;

                    all_ext_list.add(ext);

                    if (w_item != WeightEnum.C) {
                        noc_ext_list.add(ext);
                    }

                    if (!lag.equals("0")) {
                        noz_ext_list.add(ext);
                    }
                }
            }
        }

        noc_ext_list.forEach(ext -> list.add("ATS" + ext));
        noc_ext_list.forEach(ext -> list.add("AATS" + ext));

        all_ext_list.forEach(ext -> list.add("ATSC" + ext));
        all_ext_list.forEach(ext -> list.add("AATSC" + ext));

        noz_ext_list.forEach(ext -> list.add("MATS" + ext));
        noz_ext_list.forEach(ext -> list.add("GATS" + ext));


        return list;
    }

    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 prepare_descr_map()
    {
        final String NAN = String.valueOf(Double.NaN);

        descr_names.forEach(name -> {
            if (name.startsWith("AATSC")) aatsc_map.put(name, NAN);
            else if (name.startsWith("AATS")) aats_map.put(name, NAN);
            else if (name.startsWith("ATSC")) atsc_map.put(name, NAN);
            else if (name.startsWith("ATS")) ats_map.put(name, NAN);
            else if (name.startsWith("MATS")) mats_map.put(name, NAN);
            else if (name.startsWith("GATS")) gats_map.put(name, NAN);
        });
    }

    private void calculate_descr(int[][] topo_matr) throws CDKException
    {
        for (var lag : lag_list) {
            calculate_lag_descr(topo_matr, lag);
        }
    }

    private void calculate_lag_descr(int[][] topo_matr, String lag)
            throws CDKException
    {
        IAtomContainer iac = mol.get_atom_container();

        var weights_list = get_weights_list();

        for (var w_item : WeightEnum.values()) {
            String ext = w_item.get_ext_str();

            if (!weights_list.contains(ext)) {
                continue;
            }

            double[] weight = w_item.get_func_call().call(mol).const_vect();

            var br_mo = calc_broto_moreau(iac, topo_matr, weight, lag);

            double[] cent_weight = utils_calc.calc_centered_values(weight);

            var cent_br_mo = calc_broto_moreau(iac, topo_matr, cent_weight,
                    lag);

            if (w_item != WeightEnum.C) {
                ats_map.put("ATS" + lag + ext, String.valueOf(br_mo.value));
            }

            atsc_map.put("ATSC" + lag + ext, String.valueOf(cent_br_mo.value));

            if (br_mo.k_delta > 0) {
                if (w_item != WeightEnum.C) {
                    aats_map.put("AATS" + lag + ext,
                            String.valueOf(br_mo.value
                                    / (double) br_mo.k_delta));
                }

                aatsc_map.put("AATSC" + lag + ext,
                        String.valueOf(cent_br_mo.value
                                / (double) cent_br_mo.k_delta));
            }  else {
                if (w_item != WeightEnum.C) {
                    aats_map.put("AATS" + lag + ext, get_default_value());
                }

                aatsc_map.put("AATSC" + lag + ext, get_default_value());
            }

            if (!lag.equals("0")) {
                var moran = calc_moran(iac, topo_matr, weight, lag);
                mats_map.put("MATS" + lag + ext,
                        String.valueOf(moran.value));

                var geary = calc_geary(iac, topo_matr, weight, lag);
                gats_map.put("GATS" + lag + ext,
                        String.valueOf(geary.value));
            }
        }
    }

    private AutocorrResult calc_broto_moreau(IAtomContainer iac,
            int[][] topo_matr, double[] weight, String lag)
    {
        final int LAG_NUM = Integer.parseInt(lag);

        double value = 0.0;

        int k_delta = 0;

        if (LAG_NUM == 0) {
            for (int i = 0, n = iac.getAtomCount(); i < n; i++) {
                value += weight[i] * weight[i];
                k_delta++;
            }
        } else {
            for (int i = 0, n = iac.getAtomCount() - 1; i < n; i++) {
                for (int j = i + 1, m = iac.getAtomCount(); j < m; j++) {
                    if (topo_matr[i][j] == LAG_NUM) {
                        value += weight[i] * weight[j];
                        k_delta++;
                    }
                }
            }
        }

        return new AutocorrResult(value, k_delta);
    }

    private AutocorrResult calc_moran(IAtomContainer iac, int[][] topo_matr,
            double[] weight, String lag)
    {
        final int LAG_NUM = Integer.parseInt(lag);

        double value = 0.0;

        int k_delta = 0;

        double av_weight = utils_calc.calc_average(weight);

        final int ATOM_COUNT = iac.getAtomCount();

        for (int i = 0; i < (ATOM_COUNT - 1); i++) {
            for (int j = i + 1; j < ATOM_COUNT; j++) {
                if (topo_matr[i][j] == LAG_NUM) {
                    value += (weight[i] - av_weight) * (weight[j] - av_weight);
                    k_delta++;
                }
            }
        }

        if (k_delta > 0) {
            value /= (double) k_delta;

            double den = 0.0;

            for (int i = 0; i < ATOM_COUNT; i++) {
                double item = weight[i] - av_weight;
                den += item * item;
            }

            value /= den / (double) ATOM_COUNT;
        }

        return new AutocorrResult(value, k_delta);
    }

    private AutocorrResult calc_geary(IAtomContainer iac, int[][] topo_matr,
            double[] weight, String lag)
    {
        final int LAG_NUM = Integer.parseInt(lag);

        double value = 0.0;

        int k_delta = 0;

        final int ATOM_COUNT = iac.getAtomCount();

        for (int i = 0; i < (ATOM_COUNT - 1); i++) {
            for (int j = i + 1; j < ATOM_COUNT; j++) {
                if (topo_matr[i][j] == LAG_NUM) {
                    double item = weight[i] - weight[j];
                    value += item * item;
                    k_delta++;
                }
            }
        }

        if (k_delta > 0) {
            value /= 2.0 * (double) k_delta;

            double av_weight = utils_calc.calc_average(weight);

            double den = 0.0;

            for (int i = 0; i < ATOM_COUNT; i++) {
                double item = weight[i] - av_weight;
                den += item * item;
            }

            value /= den / (double) (ATOM_COUNT - 1);
        }

        return new AutocorrResult(value, k_delta);
    }

    private enum WeightEnum
    {
        C("c", new AtomConstCharge()),
        D("d", new AtomConstSigma()),
        E("e", new AtomConstSandElectrNeg()),
        I("i", new AtomConstFirstIonizPot()),
        M("m", new AtomConstMass()),
        P("p", new AtomConstPolariz()),
        S("s", new AtomConstIntrState()),
        V("v", new AtomConstVanWVol()),
        Z("Z", new AtomConstNum()),
        DV("dv", new AtomConstValence()),
        PE("pe", new AtomConstPaulElectrNeg()),
        ARE("are", new AtomConstAllRochElectrNeg());

        private WeightEnum(String ext_str, AtomConstFunctionCall func_call)
        {
            this.ext_str = ext_str;
            this.func_call = func_call;
        }

        public String get_ext_str()
        {
            return ext_str;
        }

        public AtomConstFunctionCall get_func_call()
        {
            return func_call;
        }

        private final String ext_str;
        private final AtomConstFunctionCall func_call;
    }

    private record AutocorrResult(Double value, int k_delta)
    {
    }

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

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

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

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

    private final List<Map<String, String>> maps_list = List.of(
            ats_map, aats_map, atsc_map, aatsc_map, mats_map, gats_map);

    private final List<String> descr_names;

    private Molecule mol;

    private final UtilsCalc utils_calc = new UtilsCalc();
}
