package org.semanticweb.owlapitools.decomposition;

import static org.semanticweb.owlapi.util.OWLAPIPreconditions.verifyNotNull;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;

import javax.annotation.Nullable;

import org.semanticweb.owlapi.model.OWLAxiom;
import org.semanticweb.owlapi.model.OWLEntity;

import uk.ac.manchester.cs.owlapi.modularity.ModuleType;

/**
 * Atomic decomposer for an ontology.
 */
public class Decomposer {

    /**
     * atomic structure to build
     */
    @Nullable
    private AtomList atomList = null;
    /**
     * modularizer to build modules
     */
    private final Modularizer modularizer;
    /**
     * tautologies of the ontology
     */
    private final List<AxiomWrapper> tautologies = new ArrayList<>();
    /**
     * fake atom that represents the whole ontology
     */
    @Nullable
    private OntologyAtom rootAtom = null;
    /**
     * module type for current AOS creation
     */
    private ModuleType type;
    private final List<AxiomWrapper> axioms;

    /**
     * @param axioms axiom wrappers to decompose
     * @param c locality checker to use
     */
    public Decomposer(List<AxiomWrapper> axioms, LocalityChecker c) {
        this.axioms = axioms;
        modularizer = buildModulariser(axioms, c);
    }

    /**
     * @param axioms axioms
     * @param c locality checker
     * @return new modularizer
     */
    public static Modularizer buildModulariser(List<AxiomWrapper> axioms, LocalityChecker c) {
        Modularizer m = new Modularizer(c);
        m.preprocessOntology(axioms);
        return m;
    }

    /**
     * @return the modularizer for this decomposer
     */
    public Modularizer getModularizer() {
        return modularizer;
    }

    /**
     * restore all tautologies back
     */
    private void restoreTautologies() {
        for (AxiomWrapper p : tautologies) {
            p.setUsed(true);
        }
    }

    /**
     * remove tautologies (axioms that are always local) from the ontology temporarily
     */
    private void removeTautologies() {
        // we might use it for another decomposition
        tautologies.clear();
        for (AxiomWrapper p : axioms) {
            if (p.isUsed()) {
                // check whether an axiom is local wrt its own signature
                modularizer.extract(p, new Signature(p.getAxiom().signature()), type);
                if (modularizer.isTautology(p.getAxiom(), type)) {
                    tautologies.add(p);
                    p.setUsed(false);
                }
            }
        }
    }

    /**
     * @return all tautologies wrapped
     */
    public List<AxiomWrapper> getTautologies() {
        return tautologies;
    }

    /**
     * @param sig signature
     * @param parent parent atom
     * @return module for given axiom AX; use parent atom's module as a base for the module search
     */
    private Optional<OntologyAtom> buildModule(Signature sig, OntologyAtom parent) {
        // build a module for a given signature
        modularizer.extract(parent.getModule(), sig, type);
        Collection<AxiomWrapper> module = modularizer.getModule();
        // if module is empty (empty bottom atom) -- do nothing
        if (module.isEmpty()) {
            return Optional.empty();
        }
        // check if the module corresponds to a PARENT one; modules are the same
        // iff their sizes are the same
        if (parent != rootAtom && module.size() == parent.getModule().size()) {
            return Optional.of(parent);
        }
        // create new atom with that module
        assert atomList != null;
        OntologyAtom atom = atomList.newAtom();
        atom.setModule(module);
        return Optional.of(atom);
    }

    /**
     * @param ax axiom
     * @param parent parent atom
     * @return create atom for given axiom AX; use parent atom's module as a base for the module
     *         search
     */
    private OntologyAtom createAtom(AxiomWrapper ax, OntologyAtom parent) {
        // check whether axiom already has an atom
        Optional<OntologyAtom> atom = ax.getAtom();
        if (atom.isPresent()) {
            return atom.get();
        }
        // build an atom: use a module to find atomic dependencies
        atom = buildModule(new Signature(ax.getAxiom().signature()), parent);
        // no empty modules should be here
        assert atom.isPresent();
        // register axiom as a part of an atom
        atom.get().addAxiom(ax);
        // if atom is the same as parent -- nothing more to do
        if (atom.get() == parent) {
            return parent;
        }
        // not the same as parent: for all atom's axioms check their atoms and
        // make ATOM depend on them
        /** do cycle via set to keep the order */
        for (AxiomWrapper q : atom.get().getModule()) {
            // #endif
            if (!q.equals(ax)) {
                atom.get().addDepAtom(createAtom(q, atom.get()));
            }
        }
        return atom.get();
    }

    /**
     * @return atom list class
     */
    public AtomList getAOS() {
        return verifyNotNull(atomList);
    }

    /**
     * @param t module type
     * @return the atomic structure for given module type T
     */
    public AtomList getAOS(ModuleType t) {
        // remember the type of the module
        type = t;
        // prepare a new AO structure
        atomList = new AtomList();
        // init semantic locality checker
        modularizer.preprocessOntology(axioms);
        // we don't need tautologies here
        removeTautologies();
        // init the root atom
        rootAtom = new OntologyAtom();
        rootAtom.setModule(new HashSet<>(axioms));
        // build the "bottom" atom for an empty signature
        assert rootAtom != null;
        Optional<OntologyAtom> bottomAtom = buildModule(new Signature(), rootAtom);
        if (bottomAtom.isPresent()) {
            bottomAtom.get().addAxioms(bottomAtom.get().getModule());
        }
        // create atoms for all the axioms in the ontology
        for (AxiomWrapper p : axioms) {
            if (p.isUsed() && !p.getAtom().isPresent()) {
                assert rootAtom != null;
                createAtom(p, rootAtom);
            }
        }
        // restore tautologies in the ontology
        restoreTautologies();
        rootAtom = null;
        // reduce graph
        assert atomList != null;
        atomList.reduceGraph();
        return verifyNotNull(atomList);
    }

    /**
     * @param signature the signature to use
     * @param moduletype the module type
     * @return a set of axioms that corresponds to the atom with the id INDEX
     */
    public Set<OWLAxiom> getNonLocal(Stream<OWLEntity> signature, ModuleType moduletype) {
        // init signature
        Signature sig = new Signature(signature);
        sig.setLocality(false);
        // do check
        modularizer.getLocalityChecker().setSignatureValue(sig);
        Set<OWLAxiom> result = new HashSet<>();
        for (AxiomWrapper p : axioms) {
            if (!modularizer.getLocalityChecker().local(p.getAxiom())) {
                result.add(p.getAxiom());
            }
        }
        return result;
    }

    /**
     * @param signature the signature to use
     * @param moduletype module type
     * @param useSemantics true if semantic locality should be used
     * @return a set of axioms that corresponds to the atom with the id INDEX
     */
    public Collection<AxiomWrapper> getModule(Stream<OWLEntity> signature, boolean useSemantics,
        ModuleType moduletype) {
        // init signature
        Signature sig = new Signature(signature);
        sig.setLocality(false);
        modularizer.extract(axioms, sig, moduletype);
        return modularizer.getModule();
    }
}
