/*
 * 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.molecule;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.openscience.cdk.exception.CDKException;
import org.openscience.cdk.exception.Intractable;
import org.openscience.cdk.graph.Cycles;
import org.openscience.cdk.interfaces.IAtom;
import org.openscience.cdk.interfaces.IAtomContainer;
import org.openscience.cdk.interfaces.IAtomType.Hybridization;
import org.openscience.cdk.interfaces.IBond;
import org.openscience.cdk.interfaces.IChemObject;
import org.openscience.cdk.interfaces.IRingSet;
import org.openscience.cdk.ringsearch.RingPartitioner;
import org.openscience.cdk.tools.AtomTypeAwareSaturationChecker;

/*
 * Adapted from PaDEL-Descriptor version 2.21
 * Author: Chun Wei Yap
 * Class: PaDELDearomatize
 * Availability: http://yapcwsoft.com/dd/padeldescriptor
 *
 * @author Nicola Chirico
 */
final class Dearomatizer
{
    Dearomatizer(IAtomContainer iac)
    {
        this.iac = iac;
    }

    void dearomatize() throws Intractable, CDKException
    {
        var rings = RingPartitioner.partitionRings(
                Cycles.essential().find(iac).toRingSet());

        for (var ring : rings) {
            remove_not_aromatic_rings(ring);

            if (dearomatize_666_rings(ring)) {
                continue;
            }

            if (dearomatize_566_rings(ring)) {
                continue;
            }

            if (dearomatize_66_rings(ring)) {
                continue;
            }

            if (dearomatize_56_rings(ring)) {
                continue;
            }

            if (dearomatize_6_ring(ring)) {
                continue;
            }

            dearomatize_5_ring(ring);
        }

        try {
            new AtomTypeAwareSaturationChecker().decideBondOrder(iac);
        } catch (Exception ex) {
        }

        dearomatize_conjugation();
    }

    private boolean dearomatize_conjugation()
    {
        // Find the start atom of the conjugation.
        IAtom start_atom = null;

        for (var atom : iac.atoms()) {
            if (atom.getFlag(IChemObject.AROMATIC)) {
                start_atom = get_conjugated_start_atom(atom, null,
                        new HashSet<>());

                /*
                   chemodes note: this check has been added because
                   if start_atom is null count_conjugated_atoms fails
                 */
                if (start_atom != null) {
                    // Conjugation should involve more than 2 atoms.
                    if (count_conjugated_atoms(start_atom,
                            null, new HashSet<>(), 1) < 3) {
                        start_atom = null;
                    }
                }

                break;
            }
        }

        if (start_atom != null) {
            convert_conjugation(start_atom, null, false);
        }

        return true;
    }

    private IAtom get_conjugated_start_atom(IAtom current, IAtom previous,
            Set<IAtom> seen)
    {
        seen.add(current);

        var neighbours = iac.getConnectedAtomsList(current);

        for (var neighbour : neighbours) {
            if ((neighbour != previous)
                    && neighbour.getFlag(IChemObject.AROMATIC)) {
                if (seen.contains(neighbour)) {
                    return null;
                } else {
                    return get_conjugated_start_atom(neighbour, current, seen);
                }
            }
        }

        // Cannot find any more neighouring aromatic atom
        // so current atom must be starting atom.
        return current;
    }

    private int count_conjugated_atoms(IAtom current, IAtom previous,
            Set<IAtom> seen, int count)
    {
        seen.add(current);

        var neighbours = iac.getConnectedAtomsList(current);

        for (var neighbour : neighbours) {
            if ((neighbour != previous)
                    && neighbour.getFlag(IChemObject.AROMATIC)) {
                if (seen.contains(neighbour)) {
                    return 0;
                } else {
                    return count_conjugated_atoms(neighbour, current,
                            seen, count++);
                }
            }
        }

        // Cannot find any more neighouring aromatic atom
        // so current atom must be starting atom.
        return count;
    }

    private void convert_conjugation(IAtom current, IAtom previous,
            boolean is_single)
    {
        current.setFlag(IChemObject.AROMATIC, false);

        if (current.getAtomicNumber() == 8) {
            current.setFormalCharge(0);
            current.setHybridization(Hybridization.SP2);
            current.setAtomTypeName("O.sp2");
        }

        var neighbours = iac.getConnectedAtomsList(current);

        for (var neighbour : neighbours) {
            if ((neighbour != previous)
                    && neighbour.getFlag(IChemObject.AROMATIC)) {
                var bond = iac.getBond(current, neighbour);

                bond.setFlag(IChemObject.AROMATIC, false);

                if (is_single) {
                    bond.setOrder(IBond.Order.SINGLE);
                } else {
                    bond.setOrder(IBond.Order.DOUBLE);
                }

                convert_conjugation(neighbour, current, !is_single);

                break;
            }
        }
    }

    private void remove_not_aromatic_rings(IRingSet rings)
    {
        List<IAtomContainer> rings_to_remove = new ArrayList<>();

        for (var ring : rings.atomContainers()) {
            if (!are_all_atoms_aromatic(ring)) {
                rings_to_remove.add(ring);
            }
        }

        for (var ring : rings_to_remove) {
            rings.removeAtomContainer(ring);
        }
    }

    private boolean dearomatize_666_rings(IRingSet rings)
    {
        if (rings.getAtomContainerCount() != 3) {
            // Stop if not three rings fused together.
            return false;
        }

        var ring_a = rings.getAtomContainer(0);
        var ring_b = rings.getAtomContainer(1);
        var ring_c = rings.getAtomContainer(2);

        // Make ringB the middle ring.
        if (is_middle_ring(ring_a, ring_b, ring_c)) {
            var temp = ring_b;
            ring_b = ring_a;
            ring_a = temp;
        }

        if (is_middle_ring(ring_c, ring_a, ring_b)) {
            var temp = ring_b;
            ring_b = ring_c;
            ring_c = temp;
        }

        int max_atoms_ring_a = ring_a.getAtomCount();
        int max_atoms_ring_b = ring_b.getAtomCount();
        int max_atoms_ring_c = ring_c.getAtomCount();

        if ((max_atoms_ring_a != 6)
                || (max_atoms_ring_b != 6)
                || (max_atoms_ring_c != 6)) {
            // Stop if not a 6-membered ring fused to 6-membered ring fused
            // to another 6-membered ring.
            return false;
        }

        if ((!are_all_atoms_aromatic(ring_a))
                || (!are_all_atoms_aromatic(ring_b))
                || (!are_all_atoms_aromatic(ring_c))) {
            // Not all atoms of the three rings are aromatic.
            return false;
        }

        invalidate_bonds(ring_a);
        invalidate_bonds(ring_b);
        invalidate_bonds(ring_c);

        // Change first 6-membered ring.
        dearomatize_ring(ring_a, null, null);

        // Change second 6-membered ring.
        dearomatize_ring(ring_b, null, null);

        // Change third 6-membered ring.
        dearomatize_ring(ring_c, null, null);

        return true;
    }

    private boolean dearomatize_566_rings(IRingSet rings)
    {
        if (rings.getAtomContainerCount() != 3) {
            // Stop if not three rings fused together.
            return false;
        }

        var ring_a = rings.getAtomContainer(0);
        var ring_b = rings.getAtomContainer(1);
        var ring_c = rings.getAtomContainer(2);

        if (ring_a.getAtomCount() > ring_b.getAtomCount()) {
            // Ensure ringA is smaller than ringB and ringC
            ring_a = rings.getAtomContainer(1);
            ring_b = rings.getAtomContainer(0);
        } else if (ring_a.getAtomCount() > ring_c.getAtomCount()) {
            // Ensure ringA is smaller than ringB and ringC
            ring_a = rings.getAtomContainer(2);
            ring_c = rings.getAtomContainer(0);
        }

        // Make ringB the middle ring if ringA is not the middle ring.
        if (is_middle_ring(ring_c, ring_a, ring_b)) {
            var temp = ring_b;
            ring_b = ring_c;
            ring_c = temp;
        }

        int max_atoms_ring_a = ring_a.getAtomCount();
        int max_atoms_ring_b = ring_b.getAtomCount();
        int max_atoms_ring_c = ring_c.getAtomCount();

        if ((max_atoms_ring_a != 5)
                || (max_atoms_ring_b != 6)
                || (max_atoms_ring_c != 6)) {
            // Stop if not a 5-membered ring fused to 6-membered ring
            // fused to another 6-membered ring.
            return false;
        }

        if ((!are_all_atoms_aromatic(ring_a))
                || (!are_all_atoms_aromatic(ring_b))
                || (!are_all_atoms_aromatic(ring_c))) {
            // Not all atoms of the three rings are aromatic.
            return false;
        }

        invalidate_bonds(ring_a);
        invalidate_bonds(ring_b);
        invalidate_bonds(ring_c);

        // Find nH, nR, o or s in 5-membered ring if any.
        // If none are found, start from atom furthest away from fused bond.
        boolean has_start_atom = false;

        var start_atom = ring_a.getAtom(0);

        for (int a = 0; a < max_atoms_ring_a; a++) {
            var atom = ring_a.getAtom(a);

            if (((atom.getAtomicNumber() == 7)
                        && ((atom.getImplicitHydrogenCount() >= 1)
                        || atom.getFormalNeighbourCount() == 3)) // nH or nR
                    || (atom.getAtomicNumber() == 8)
                    || (atom.getAtomicNumber() == 16)) {
                start_atom = atom;
                has_start_atom = true;
                break;
            }
        }

        if (!has_start_atom) {
            for (int a = 0; a < max_atoms_ring_a; a++) {
                var atom = ring_a.getAtom(a);

                if (ring_b.contains(atom)) {
                    // Current atom is part of fused bond
                    var neighbours = ring_a.getConnectedAtomsList(atom);

                    for (var neighbour : neighbours) {
                        if (!ring_b.contains(neighbour)) {
                            var neighbours_1
                                    = ring_a.getConnectedAtomsList(neighbour);

                            for (var neighbour_1 : neighbours_1) {
                                if ((neighbour_1 != atom)
                                        && ring_a.contains(neighbour_1)) {
                                    // Found start atom
                                    start_atom = neighbour_1;
                                    has_start_atom = true;
                                    break;
                                }
                            }
                        }

                        if (has_start_atom) {
                            break;
                        }
                    }
                }

                if (has_start_atom) {
                    break;
                }
            }
        }

        var start_bond = ring_a.getConnectedBondsList(start_atom).get(0);

        // Change 5-membered ring.
        dearomatize_ring(ring_a, start_atom, start_bond);
        convert_N_to_NH(ring_a);

        // Change second 6-membered ring.
        dearomatize_ring(ring_b, null, null);

        // Change third 6-membered ring.
        dearomatize_ring(ring_c, null, null);

        return true;
    }

    private boolean dearomatize_66_rings(IRingSet rings)
    {
        if (rings.getAtomContainerCount() != 2) {
            // Stop if not two rings fused together.
            return false;
        }

        var ring_a = rings.getAtomContainer(0);
        var ring_b = rings.getAtomContainer(1);

        int max_atoms_ring_a = ring_a.getAtomCount();
        int max_atoms_ring_b = ring_b.getAtomCount();

        if ((max_atoms_ring_a != 6)
                || (max_atoms_ring_b != 6)) {
            // Stop if not a 6-membered ring fused to 6-membered ring.
            return false;
        }

        if ((!are_all_atoms_aromatic(ring_a))
                || (!are_all_atoms_aromatic(ring_b))) {
            // Not all atoms of the two rings are aromatic.
            return false;
        }

        invalidate_bonds(ring_a);
        invalidate_bonds(ring_b);

        // Change first 6-membered ring.
        dearomatize_ring(ring_a, null, null);

        // Change second 6-membered ring.
        dearomatize_ring(ring_b, null, null);

        return true;
    }

    private boolean dearomatize_56_rings(IRingSet rings)
    {
        if (rings.getAtomContainerCount() != 2) {
            // Stop if not two rings fused together.
            return false;
        }

        var ring_a = rings.getAtomContainer(0);
        var ring_b = rings.getAtomContainer(1);

        if (ring_a.getAtomCount() > ring_b.getAtomCount()) {
            // Ensure ringA is smaller than ringB
            ring_a = rings.getAtomContainer(1);
            ring_b = rings.getAtomContainer(0);
        }

        int max_atoms_ring_a = ring_a.getAtomCount();
        int max_atoms_ring_b = ring_b.getAtomCount();

        if ((max_atoms_ring_a != 5)
                || (max_atoms_ring_b != 6)) {
            // Stop if not a 5-membered ring fused to 6-membered ring.
            return false;
        }

        if ((!are_all_atoms_aromatic(ring_a))
                || (!are_all_atoms_aromatic(ring_b))) {
            // Not all atoms of the two rings are aromatic.
            return false;
        }

        invalidate_bonds(ring_a);
        invalidate_bonds(ring_b);

        // Find nH, nR, o or s in 5-membered ring if any.
        // If none are found, start from atom furthest away from fused bond.
        boolean has_start_atom = false;

        IAtom start_atom = ring_a.getAtom(0);

        for (int a = 0; a < max_atoms_ring_a; a++) {
            IAtom atom = ring_a.getAtom(a);
            if (((atom.getAtomicNumber() == 7)
                        && ((atom.getImplicitHydrogenCount() >= 1)
                        || (atom.getFormalNeighbourCount() == 3))) // nH or nR
                    || (atom.getAtomicNumber() == 8)
                    || (atom.getAtomicNumber() == 16)) {
                start_atom = atom;
                has_start_atom = true;
                break;
            }
        }

        if (!has_start_atom) {
            for (int a = 0; a < max_atoms_ring_a; a++) {

                var atom = ring_a.getAtom(a);

                if (ring_b.contains(atom)) {
                    // Current atom is part of fused bond
                    var neighbours = ring_a.getConnectedAtomsList(atom);

                    for (var neighbour : neighbours) {
                        if (!ring_b.contains(neighbour)) {
                            var neighbours_1
                                    = ring_a.getConnectedAtomsList(neighbour);

                            for (var neighbour_1 : neighbours_1) {
                                if ((neighbour_1 != atom)
                                        && ring_a.contains(neighbour_1)) {
                                    // Found start atom
                                    start_atom = neighbour_1;
                                    has_start_atom = true;
                                    break;
                                }
                            }
                        }

                        if (has_start_atom) {
                            break;
                        }
                    }
                }

                if (has_start_atom) {
                    break;
                }
            }
        }

        var start_bond = ring_a.getConnectedBondsList(start_atom).get(0);

        // Change 5-membered ring.
        dearomatize_ring(ring_a, start_atom, start_bond);
        convert_N_to_NH(ring_a);

        // Change 6-membered ring.
        dearomatize_ring(ring_b, null, null);

        return true;
    }

    private boolean dearomatize_6_ring(IRingSet rings)
    {
        if (rings.getAtomContainerCount() != 1) {
            // Stop if not one ring.
            return false;
        }

        var ring_a = rings.getAtomContainer(0);
        int max_atoms_ring_a = ring_a.getAtomCount();

        if (max_atoms_ring_a != 6) {
            // Stop if not a 6-membered ring.
            return false;
        }

        if (!are_all_atoms_aromatic(ring_a)) {
            // Not all atoms of the ring are aromatic.
            return false;
        }

        invalidate_bonds(ring_a);

        var start_atom = ring_a.getAtom(0);
        var start_bond = ring_a.getConnectedBondsList(start_atom).get(0);

        // Change 6-membered ring.
        dearomatize_ring(ring_a, start_atom, start_bond);

        return true;
    }

    private boolean dearomatize_5_ring(IRingSet rings)
    {
        if (rings.getAtomContainerCount() != 1) {
            // Stop if not one ring.
            return false;
        }

        var ring_a = rings.getAtomContainer(0);
        int max_atoms_ring_a = ring_a.getAtomCount();

        if (max_atoms_ring_a != 5) {
            // Stop if not a 5-membered ring.
            return false;
        }

        if (!are_all_atoms_aromatic(ring_a)) {
            // Not all atoms of the ring are aromatic.
            return false;
        }

        invalidate_bonds(ring_a);

        // Find nH, nR, o or s in 5-membered ring if any.
        var start_atom = ring_a.getAtom(0);

        for (int a = 0; a < max_atoms_ring_a; a++) {
            var atom = ring_a.getAtom(a);

            if (((atom.getAtomicNumber() == 7)
                        && ((atom.getImplicitHydrogenCount() >= 1)
                        || (atom.getFormalNeighbourCount() == 3))) // nH or nR
                    || (atom.getAtomicNumber() == 8)
                    || (atom.getAtomicNumber() == 16)) {
                start_atom = atom;
                break;
            }
        }

        var start_bond = ring_a.getConnectedBondsList(start_atom).get(0);

        // Change 5-membered ring.
        dearomatize_ring(ring_a, start_atom, start_bond);
        convert_N_to_NH(ring_a);

        return true;
    }

    private boolean are_all_atoms_aromatic(IAtomContainer ring)
    {
        for (IAtom atom : ring.atoms()) {
            if (!atom.getFlag(IChemObject.AROMATIC)) {
                return false;
            }
        }
        return true;
    }

    private boolean is_middle_ring(IAtomContainer ring,
            IAtomContainer ring_a, IAtomContainer ring_b)
    {
        boolean has_atom_in_ring_a = false;
        boolean has_atom_in_ring_b = false;

        for (var atom : ring.atoms()) {
            if ((!has_atom_in_ring_a) && ring_a.contains(atom)) {
                has_atom_in_ring_a = true;
            } else if (!has_atom_in_ring_b && ring_b.contains(atom)) {
                has_atom_in_ring_b = true;
            }
        }

        return has_atom_in_ring_a && has_atom_in_ring_b;
    }

    private void remove_aromatic_flag(IAtomContainer ring)
    {
        for (var atom : ring.atoms()) {
            atom.setFlag(IChemObject.AROMATIC, false);
        }

        for (var bond : ring.bonds()) {
            bond.setFlag(IChemObject.AROMATIC, false);
        }
    }

    private void invalidate_bonds(IAtomContainer ring)
    {
        for (var bond : ring.bonds()) {
            bond.setOrder(IBond.Order.QUADRUPLE);
        }
    }

    private void dearomatize_ring(IAtomContainer ring,
            IAtom start_atom, IBond start_bond)
    {
        var cur_atom = start_atom;
        var cur_bond = start_bond;

        int max_bonds = ring.getBondCount();

        if (cur_atom == null) {
            cur_atom = ring.getAtom(0);
        }
        if (cur_bond == null) {
            cur_bond = ring.getConnectedBondsList(cur_atom).get(0);
        }

        // Find an appropriate starting atom and starting bond
        // This will be run for subsequent fused rings and not for the first
        // fused ring to be dearomatize.
        boolean next_is_single = true;
        boolean next_next_is_single = false;

        for (int i = 0; i < max_bonds; i++) {
            var bond = ring.getBond(i);

            if (bond.getOrder() != IBond.Order.QUADRUPLE) {
                cur_bond = bond;
                cur_atom = bond.getAtom(0);

                if (bond.getOrder() == IBond.Order.SINGLE) {
                    next_is_single = true;
                    next_next_is_single = true;
                } else {
                    next_is_single = false;
                    next_next_is_single = true;
                }
            }
        }

        for (int i = 0; i < max_bonds; i++) {
            if (next_is_single || ((i == 1) && next_next_is_single)) {
                cur_bond.setOrder(IBond.Order.SINGLE);
                next_is_single = false;
            } else {
                cur_bond.setOrder(IBond.Order.DOUBLE);
                next_is_single = true;
            }

            cur_atom = cur_bond.getOther(cur_atom);

            var bonds = ring.getConnectedBondsList(cur_atom);

            for (IBond bond : bonds) {
                if (bond != cur_bond) {
                    cur_bond = bond;
                    break;
                }
            }
        }

        remove_aromatic_flag(ring);
    }

    // Change N to sp3 if there is a sp2 N initially in a 5-member ring.
    // This is because after dearomatization, it will become a N
    // with two single bond
    private void convert_N_to_NH(IAtomContainer ring)
    {
        int max_atoms_ring = ring.getAtomCount();

        for (int a = 0; a < max_atoms_ring; ++a) {
            IAtom atom = ring.getAtom(a);
            if ((atom.getAtomicNumber() == 7)
                    && (atom.getHybridization() == Hybridization.SP2)) {
                var bonds = ring.getConnectedBondsList(atom);

                boolean is_all_single_bond = true;

                for (var bond : bonds) {
                    if (bond.getOrder() != IBond.Order.SINGLE) {
                        is_all_single_bond = false;
                        break;
                    }
                }

                if (is_all_single_bond) {
                    atom.setHybridization(Hybridization.PLANAR3);
                    atom.setFormalNeighbourCount(3);
                    atom.setImplicitHydrogenCount(1);
                    atom.setAtomTypeName("N.planar3");
                }
            }
        }
    }

    private final IAtomContainer iac;
}
