ThemisMapper.java

/*
 * Themis: Java Project Framework
 * Copyright 2026. Tony Washer
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License.  You may obtain a copy
 * of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package io.github.tonywasher.joceanus.themis.solver.mapper;

import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
import io.github.tonywasher.joceanus.themis.exc.ThemisDataException;
import io.github.tonywasher.joceanus.themis.parser.base.ThemisInstance;
import io.github.tonywasher.joceanus.themis.parser.base.ThemisInstance.ThemisClassInstance;
import io.github.tonywasher.joceanus.themis.parser.base.ThemisInstance.ThemisExpressionInstance;
import io.github.tonywasher.joceanus.themis.parser.base.ThemisInstance.ThemisNodeInstance;
import io.github.tonywasher.joceanus.themis.parser.base.ThemisInstance.ThemisTypeInstance;
import io.github.tonywasher.joceanus.themis.parser.expr.ThemisExprFieldAccess;
import io.github.tonywasher.joceanus.themis.parser.expr.ThemisExprMethodCall;
import io.github.tonywasher.joceanus.themis.parser.expr.ThemisExprMethodRef;
import io.github.tonywasher.joceanus.themis.parser.expr.ThemisExprName;
import io.github.tonywasher.joceanus.themis.parser.node.ThemisNodeImport;
import io.github.tonywasher.joceanus.themis.parser.type.ThemisTypeClassInterface;
import io.github.tonywasher.joceanus.themis.solver.proj.ThemisSolverClass;
import io.github.tonywasher.joceanus.themis.solver.proj.ThemisSolverFile;
import io.github.tonywasher.joceanus.themis.solver.proj.ThemisSolverPackage;
import io.github.tonywasher.joceanus.themis.solver.proj.ThemisSolverProject;

import java.util.ArrayList;
import java.util.List;

/**
 * Analysis Mapper.
 */
public class ThemisMapper
        implements AutoCloseable {
    /**
     * Project State.
     */
    private final ThemisMapperProjectState theProject;

    /**
     * Project State.
     */
    private final ThemisMapperFileState theFile;

    /**
     * Project State.
     */
    private final ThemisMapperTypeState theType;

    /**
     * Project State.
     */
    private final ThemisMapperNameState theName;

    /**
     * Constructor.
     *
     * @param pProject the project.
     * @throws OceanusException on error
     */
    public ThemisMapper(final ThemisSolverProject pProject) throws OceanusException {
        /* Create the project state */
        theProject = new ThemisMapperProjectState(pProject);

        /* Create the file state */
        theFile = new ThemisMapperFileState(theProject);
        theType = new ThemisMapperTypeState();
        theName = new ThemisMapperNameState();
    }

    /**
     * PreProcess package.
     *
     * @param pPackage the package
     */
    public void preProcessPackage(final ThemisSolverPackage pPackage) {
        /* Loop through the files in the package */
        for (ThemisSolverFile myFile : pPackage.getFiles()) {
            /* preProcess the file if it has not been done yet */
            if (myFile.needsPreProcess()) {
                preProcessFile(myFile);
            }
        }
    }

    /**
     * Process package.
     *
     * @param pPackage the package
     * @throws OceanusException on error
     */
    public void processPackage(final ThemisSolverPackage pPackage) throws OceanusException {
        /* Loop through the files in the package */
        for (ThemisSolverFile myFile : pPackage.getFiles()) {
            /* Process the file */
            processFile(myFile);
        }

        /* Loop through the files in the package */
        for (ThemisSolverFile myFile : pPackage.getFiles()) {
            /* Process the local references */
            myFile.processLocalReferences();
        }
    }

    /**
     * Reset fileState.
     *
     * @param pFile the file
     */
    private void resetFileState(final ThemisSolverFile pFile) {
        /* Reset the various states */
        theFile.initForFile(pFile);
        theType.reset();
        theName.reset();
    }

    /**
     * preProcess file.
     *
     * @param pFile the file
     */
    private void preProcessFile(final ThemisSolverFile pFile) {
        /* Note that we have pre-processed this file */
        pFile.markPreProcessed();

        /* preProcess the imports */
        preProcessImports(pFile);

        /* Reset the fileState */
        resetFileState(pFile);

        /* Loop through the classes in the file */
        for (ThemisSolverClass myClass : pFile.getClasses()) {
            final ThemisClassInstance myInstance = myClass.getUnderlyingClass();

            /* Loop through the extends */
            for (ThemisTypeInstance myExtends : myInstance.getExtends()) {
                processAncestor(myClass, myExtends);
            }

            /* Loop through the implements */
            for (ThemisTypeInstance myImplements : myInstance.getImplements()) {
                processAncestor(myClass, myImplements);
            }

            /* Process inherited children */
            theFile.processInherited(myClass);
        }
    }


    /**
     * preProcess file.
     *
     * @param pFile the file
     */
    private void preProcessImports(final ThemisSolverFile pFile) {
        /* Loop through all the imports */
        for (ThemisNodeInstance myNode : pFile.getUnderlyingFile().getContents().getImports()) {
            /* If the import is of a project file */
            final ThemisNodeImport myImport = (ThemisNodeImport) myNode;
            final ThemisSolverClass myClass = theProject.getProjectClassMap().get(myImport.getFullName());
            if (myClass != null) {
                /* Make sure that the file has been pre-processed */
                final ThemisSolverFile myFile = (ThemisSolverFile) myClass.getOwningFile();
                if (myFile.needsPreProcess()) {
                    /* PreProcess the file */
                    preProcessFile(myFile);
                }
            }
        }
    }

    /**
     * process ancestor.
     *
     * @param pClass    the class
     * @param pAncestor the ancestor
     */
    private void processAncestor(final ThemisSolverClass pClass,
                                 final ThemisTypeInstance pAncestor) {
        /* Handle ClassInterface Reference */
        if (pAncestor instanceof ThemisTypeClassInterface myRef) {
            final ThemisClassInstance myResolved = theFile.processPossibleReference(myRef.getFullName());
            if (myResolved != null) {
                pClass.addAncestor(myResolved.getFullName());
                myRef.setClassInstance(myResolved);
            }
        }
    }

    /**
     * Process file.
     *
     * @param pFile the file
     * @throws OceanusException on error
     */
    private void processFile(final ThemisSolverFile pFile) throws OceanusException {
        /* Reset the fileState */
        resetFileState(pFile);

        /* Obtain the top-level class element */
        final ThemisSolverClass myBase = pFile.getTopLevel();
        final ThemisInstance myInstance = (ThemisInstance) myBase.getUnderlyingClass();
        processInstance(myInstance);

        /* Propagate the referenced classes */
        pFile.setReferenced(theFile.getReferenced());
    }

    /**
     * Process instance.
     *
     * @param pInstance the instance
     * @throws OceanusException on error
     */
    private void processInstance(final ThemisInstance pInstance) throws OceanusException {
        /* Process stacks */
        final boolean bumpType = theType.processInstance(pInstance);
        final boolean bumpName = theName.processInstance(pInstance);

        /* Process element */
        final boolean doChildren = processElement(pInstance);

        /* Process children */
        if (doChildren) {
            for (ThemisInstance myChild : sortedChildren(pInstance)) {
                processInstance(myChild);
            }
        }

        /* CleanUp stacks */
        if (bumpName) {
            theName.cleanUpAfterInstance();
        }
        if (bumpType) {
            theType.cleanUpAfterInstance();
        }
    }

    /**
     * Process element.
     *
     * @param pElement the element
     * @return process children? true/false
     * @throws OceanusException on error
     */
    private boolean processElement(final ThemisInstance pElement) throws OceanusException {
        /* Handle ClassInstance */
        if (pElement instanceof ThemisClassInstance myInstance) {
            final ThemisSolverClass myClass = theProject.getProjectClassMap().get(myInstance.getFullName());
            theFile.processInherited(myClass);
        }

        /* Handle ClassInterface Reference */
        if (pElement instanceof ThemisTypeClassInterface myRef) {
            processClassReference(myRef);
            return false;
        }

        /* Handle FieldAccess */
        if (pElement instanceof ThemisExprFieldAccess myAccess) {
            processFieldAccess(myAccess);
            return false;
        }

        /* Handle MethodCall */
        if (pElement instanceof ThemisExprMethodCall myCall) {
            processMethodCall(myCall);
            return false;
        }

        /* Handle MethodReference */
        if (pElement instanceof ThemisExprMethodRef myRef) {
            processMethodReference(myRef);
            return false;
        }
        return true;
    }

    /**
     * Process class reference.
     *
     * @param pReference the reference
     * @throws OceanusException on error
     */
    private void processClassReference(final ThemisTypeClassInterface pReference) throws OceanusException {
        /* Process as a possible reference */
        final ThemisClassInstance myResolved = theFile.processPossibleReference(pReference.getFullName());

        /* If we failed to resolve */
        if (myResolved == null) {
            /* Check for type parameters and variable names */
            final ThemisInstance myType = theType.lookUpType(pReference.getName());
            final ThemisInstance myName = theName.lookUpName(pReference.getName());

            /* Report failure */
            final boolean bFound = (myType != null || myName != null);
            if (!bFound) {
                throw new ThemisDataException("Unresolved link: " + pReference.getFullName());
            }
        }
    }

    /**
     * Process field Access.
     *
     * @param pAccess the fieldAccess
     * @throws OceanusException on error
     */
    private void processFieldAccess(final ThemisExprFieldAccess pAccess) throws OceanusException {
        final ThemisExpressionInstance myExpr = pAccess.getScope();
        if (myExpr instanceof ThemisExprName myNameExpr) {
            /* Check to see if the field access is to an explicit class */
            final String myFullName = pAccess.toString();
            final String myNameRef = myNameExpr.toString();
            ThemisClassInstance myResolved = theFile.processPossibleReference(myFullName);

            /* If not a full path look to see whether it is a field of a class */
            if (myResolved == null) {
                myResolved = theFile.processPossibleReference(myNameRef);
            }

            /* If we still failed to resolve */
            if (myResolved == null) {
                /* Check for variable names */
                final ThemisInstance myName = theName.lookUpName(myNameRef);

                /* Report failure */
                if (myName == null) {
                    throw new ThemisDataException("Unresolved fieldAccess: " + myNameRef);
                }
            }
        }
    }

    /**
     * Process method call.
     *
     * @param pCall the methodCall
     * @throws OceanusException on error
     */
    private void processMethodCall(final ThemisExprMethodCall pCall) throws OceanusException {
        final ThemisExpressionInstance myExpr = pCall.getScope();
        if (myExpr instanceof ThemisExprName myNameExpr) {
            /* Check to see if the field access is to an explicit class */
            final String myNameRef = myNameExpr.toString();
            final ThemisClassInstance myResolved = theFile.processPossibleReference(myNameRef);

            /* If we still failed to resolve */
            if (myResolved == null) {
                /* Check for variable names */
                final ThemisInstance myName = theName.lookUpName(myNameRef);

                /* Report failure */
                if (myName == null) {
                    throw new ThemisDataException("Unresolved method call using name: " + myNameRef);
                }
            }
        } else if (myExpr instanceof ThemisExprFieldAccess myFieldExpr) {
            processFieldAccess(myFieldExpr);
        } else if (myExpr instanceof ThemisExprMethodCall myMethodCall) {
            processMethodCall(myMethodCall);
        }
    }

    /**
     * Process method Reference.
     *
     * @param pReference the reference
     * @throws OceanusException on error
     */
    private void processMethodReference(final ThemisExprMethodRef pReference) throws OceanusException {
        final ThemisExpressionInstance myExpr = pReference.getScope();
        if (myExpr instanceof ThemisExprName myNameExpr) {
            /* Check to see if the method reference is to an explicit class */
            final String myNameRef = myNameExpr.toString();
            final ThemisClassInstance myResolved = theFile.processPossibleReference(myNameRef);

            /* If we still failed to resolve */
            if (myResolved == null) {
                /* Check for variable names */
                final ThemisInstance myName = theName.lookUpName(myNameRef);

                /* Report failure */
                if (myName == null) {
                    throw new ThemisDataException("Unresolved methodRef: " + myNameRef);
                }
            }
        }
    }

    /**
     * Obtain sorted list of children.
     *
     * @param pInstance the instanceNode
     * @return the sorted list
     */
    private List<ThemisInstance> sortedChildren(final ThemisInstance pInstance) {
        final List<ThemisInstance> myChildren = new ArrayList<>(pInstance.getChildren());
        myChildren.sort(this::compareTo);
        return myChildren;
    }

    /**
     * Comparator that pushes ClassInstances to bottom of list.
     *
     * @param pFirst  the first node
     * @param pSecond the second node
     * @return -1, 0, 1 according to order
     */
    private int compareTo(final ThemisInstance pFirst,
                          final ThemisInstance pSecond) {
        if (pFirst instanceof ThemisClassInstance) {
            return pSecond instanceof ThemisClassInstance ? 0 : 1;
        }
        return pSecond instanceof ThemisClassInstance ? -1 : 0;
    }

    @Override
    public void close() {
        theProject.close();
    }
}