## vim: ft=makojava

<%
api = java_api
nat = c_api.get_name
%>

    // ==========
    // Define classes to handle project loading
    // ==========

    /**
     * Exception to represent errors during project manipulation
     */
    public static final class ProjectManagerException extends RuntimeException {
        ProjectManagerException(
            final String message
        ) {
            super(message);
        }
    }

    /**
     * Enum to represent the source file mode for a GPR project
     */
    public static enum SourceFileMode {

        // ----- Enum values -----

        DEFAULT(0),
        ROOT_PROJECT(1),
        WHOLE_PROJECT(2),
        WHOLE_PROJECT_WITH_RUNTIME(3),
        ;

        // ----- Class attributes -----

        /** Singleton that represents the none source file mode */
        public static final SourceFileMode NONE = DEFAULT;

        /** The map from int to enum values */
        private static final Map<Integer, SourceFileMode> map = new HashMap<>();

        // ----- Instance attributes -----

        /** The value of the instance */
        private final int value;

        // ----- Constructors -----

        /**
         * The private constructor
         */
        private SourceFileMode(
            final int value
        ) {
            this.value = value;
        }

        static {
            for(SourceFileMode elem : SourceFileMode.values()) {
                map.put(elem.value, elem);
            }
        }

        // ----- Enum methods -----

        /**
         * Get a source file mode from a native integer value.
         *
         * @param cValue The native value of the enum.
         * @return The Java source file mode.
         * @throws EnumException If the given native value doesn't correspond
         * to an actual enum value.
         */
        public static SourceFileMode fromC(
            final int cValue
        ) throws EnumException {
            if(!map.containsKey(cValue))
                throw new EnumException(
                    "Cannot get SourceFileMode from " + cValue
                );
            return (SourceFileMode) map.get(cValue);
        }

        /**
         * Get the native integer value of the enum instance.
         *
         * @return The native C value.
         */
        public int toC() {
            return this.value;
        }

    }

    /**
     * This class represents a scenario variable for a GPR project file.
     */
    public static final class ScenarioVariable {

        // ----- Class attributes -----

        /** Singleton that represents the none scenario variable. */
        public static final ScenarioVariable NONE =
            new ScenarioVariable(null, null);

        // ----- Instance attributes -----

        /** The name of the variable. */
        public final String name;

        /** The value of the variable. */
        public final String value;

        // ----- Constructors -----

        /**
         * Create a new scenario variable with its name and value.
         *
         * @param name Name of the variable.
         * @param value Value of the variable.
         */
        ScenarioVariable(
            final String name,
            final String value
        ) {
            this.name = name;
            this.value = value;
        }

        /**
         * Public method to create scenario variable.
         * This method raise a runtime exception if name or value is null.
         *
         * @param name Name of the variable.
         * @param value Value of the variable.
         */
        public static ScenarioVariable create(
            final String name,
            final String value
        ) {
            if(name == null) throw new RuntimeException("Scenario variable name cannot be null");
            if(value == null) throw new RuntimeException("Scenario variable value cannot be null");
            return new ScenarioVariable(name, value);
        }

        // ----- Graal C API methods -----

        /**
         * Wrap the given pointer to a native scenario variable.
         *
         * @param pointer The pointer to the native scenario variable.
         * @return The wrapped scenario variable.
         */
        public static ScenarioVariable wrap(
            final Pointer pointer
        ) {
            return wrap((ScenarioVariableNative) pointer.readWord(0));
        }

        /**
         * Wrap the given native scenario variable.
         *
         * @param scenarioVariableNative The native scenario variable.
         * @return The wrapped scenario variable.
         */
        public static ScenarioVariable wrap(
            final ScenarioVariableNative scenarioVariableNative
        ) {
            final CCharPointer nameNative = scenarioVariableNative.get_name();
            final CCharPointer valueNative = scenarioVariableNative.get_value();
            return new ScenarioVariable(
                nameNative.isNull() ?
                    null :
                    toJString(nameNative),
                valueNative.isNull() ?
                    null :
                    toJString(valueNative)
            );
        }

        /**
         * Unwrap the scenario variable in the given native value.
         *
         * @param scenarioVariableNative The native value to fill.
         */
        public void unwrap(
            final ScenarioVariableNative scenarioVariableNative
        ) {
            final CCharPointer nameNative = this.name == null ?
                WordFactory.nullPointer() :
                toCString(this.name);
            final CCharPointer valueNative = this.value == null ?
                WordFactory.nullPointer() :
                toCString(this.value);
            scenarioVariableNative.set_name(nameNative);
            scenarioVariableNative.set_value(valueNative);
        }

        /**
         * Release the given native scenario variable allocated strings.
         *
         * @param scenarioVariableNative The native scenario variable to release.
         */
        public static void release(
            final ScenarioVariableNative scenarioVariableNative
        ) {
            if(scenarioVariableNative.get_name().isNonNull()) {
                UnmanagedMemory.free(scenarioVariableNative.get_name());
            }
            if(scenarioVariableNative.get_value().isNonNull()) {
                UnmanagedMemory.free(scenarioVariableNative.get_value());
            }
        }

        // ----- Override methods -----

        /** @see java.lang.Object#toString() */
        public String toString() {
            return "ScenarioVariable(" +
                this.name + " = " + this.value +
                ")";
        }

    }

    /**
     * This class is used for the GPR project loading.
     */
    public static final class ProjectManager implements AutoCloseable {

        // ----- Class attributes -----

        /** Singleton that represents the none project manager. */
        public static final ProjectManager NONE =
            new ProjectManager(PointerWrapper.nullPointer());

        // ----- Instance attributes -----

        /** Reference to the native value. */
        private final PointerWrapper reference;

        // ----- Constructors -----

        /**
         * Create a new project manager from its native reference.
         *
         * @param reference The reference to the native project manager.
         */
        ProjectManager(
            final PointerWrapper reference
        ) {
            this.reference = reference;
        }

        /**
         * Create a project manager for the given project file.
         *
         * @param projectFile The GPR project file to load.
         * @return The newly created project manager.
         */
        public static ProjectManager create(
            final String projectFile
        ) {
            return create(
                projectFile,
                null,
                "",
                ""
            );
        }

        /**
         * Create a project manager from a project file, target and runtime.
         *
         * @param projectFile The GPR file to load.
         * @param scenarioVariables The scenario variables for the project, it can be null.
         * @param target The target to load.
         * @param runtime The runtime to load.
         * @return The newly created project manager.
         */
        public static ProjectManager create(
            final String projectFile,
            final ScenarioVariable[] scenarioVariables,
            final String target,
            final String runtime
        ) {

            if(ImageInfo.inImageCode()) {
                // Create the scenario variable array
                final Pointer scenarioVariablesNative;
                final int scenarioVariableNativeSize = SizeOf.get(ScenarioVariableNative.class);

                if(scenarioVariables != null && scenarioVariables.length > 0) {
                    final int size = scenarioVariables.length + 1;
                    scenarioVariablesNative = UnmanagedMemory.calloc(
                        size * scenarioVariableNativeSize
                    );
                    for(int i = 0 ; i < scenarioVariables.length ; i++) {
                        final ScenarioVariableNative scenarioVariableNative = (ScenarioVariableNative)
                            scenarioVariablesNative.add(i * scenarioVariableNativeSize);
                        scenarioVariables[i].unwrap(scenarioVariableNative);
                    }
                } else {
                    scenarioVariablesNative = WordFactory.nullPointer();
                }

                // Call the native project loading function
                final CCharPointer projectFileNative = toCString(projectFile);
                final CCharPointer targetNative = toCString(target);
                final CCharPointer runtimeNative = toCString(runtime);
                final Pointer projectPointer = StackValue.get(SizeOf.get(VoidPointer.class));
                projectPointer.writeWord(0, WordFactory.nullPointer());
                final Pointer errorsPointer = StackValue.get(SizeOf.get(VoidPointer.class));
                errorsPointer.writeWord(0, WordFactory.nullPointer());
                NI_LIB.${nat("gpr_project_load")}(
                    projectFileNative,
                    scenarioVariablesNative,
                    targetNative,
                    runtimeNative,
                    projectPointer,
                    errorsPointer
                );

                // Check the langkit exception and cast it into a project manager error
                try {
                    checkException();
                } catch (LangkitException e) {
                    throw new ProjectManagerException(e.getMessage());
                }

                // Get the result of modified values
                final ProjectManagerNative projectManagerNative = (ProjectManagerNative) projectPointer.readWord(0);
                final StringArrayNative errorArrayNative = (StringArrayNative) errorsPointer.readWord(0);

                // Free the scenario variables
                if(scenarioVariablesNative.isNonNull()) {
                    for(int i = 0 ; i < scenarioVariables.length ; i++) {
                        final ScenarioVariableNative scenarioVariableNative = (ScenarioVariableNative)
                            scenarioVariablesNative.add(i * scenarioVariableNativeSize);
                        ScenarioVariable.release(scenarioVariableNative);
                    }
                    UnmanagedMemory.free(scenarioVariablesNative);
                }

                // Free the allocated strings
                UnmanagedMemory.free(projectFileNative);
                UnmanagedMemory.free(targetNative);
                UnmanagedMemory.free(runtimeNative);

                // `errorsPointer` is not allocated if an exception was raised during project file loading
                if (errorArrayNative.isNonNull()) {
                    // Translate the error native array into a Java array
                    final String[] errors = toJStringArray(errorArrayNative);
                    for (String error : errors) {
                        System.err.println("Error during project opening: " + error);
                    }

                    // Free the error array
                    NI_LIB.${nat("free_string_array")}(errorArrayNative);
                }

                // Check the langkit exception and cast it into a project manager error
                try {
                    checkException();
                } catch (LangkitException e) {
                    throw new ProjectManagerException(e.getMessage());
                }

                // Return the Java wrapped project manager
                return wrap(projectManagerNative);
            } else {
                final PointerWrapper reference = JNI_LIB.${nat("gpr_project_load")}(
                    projectFile,
                    scenarioVariables,
                    target,
                    runtime
                );

                // Check the langkit exceptions
                try {
                    checkException();
                } catch (LangkitException e) {
                    throw new ProjectManagerException(e.getMessage());
                }

                // Return the project manager
                return new ProjectManager(reference);
            }

        }

        // ----- Graal C API methods -----

        /**
         * Wrap a native project manager in the Java class.
         *
         * @param pointer The pointer to the native project manager.
         * @return The newly wrapped project manager.
         */
        static ProjectManager wrap(
            final Pointer pointer
        ) {
            return wrap((ProjectManagerNative) pointer.readWord(0));
        }

        /**
         * Wrap a native project manager in the Java class.
         *
         * @param projectManagerNative The native project manager to wrap.
         * @return The newly wrapped project manager.
         */
        static ProjectManager wrap(
            final ProjectManagerNative projectManagerNative
        ) {
            return new ProjectManager(new PointerWrapper(projectManagerNative));
        }

        /**
         * Unwrap the project manager inside the given pointer.
         *
         * @param pointer The pointer to write in.
         */
        public void unwrap(
            final Pointer pointer
        ) {
            pointer.writeWord(0, this.unwrap());
        }

        /**
         * Get the native value of the project manager.
         *
         * @return The native project manager.
         */
        public ProjectManagerNative unwrap() {
            return (ProjectManagerNative) this.reference.ni();
        }

        // ----- Class methods -----

        /**
         * Translate a native string array structure into a Java string
         * array.
         *
         * @param stringArrayNative The native string array structure.
         * @return The Java string array.
         */
        private static String[] toJStringArray(
            final StringArrayNative stringArrayNative
        ) {
            final String[] res = new String[stringArrayNative.get_length()];
            final CCharPointerPointer nativeFilesPointer = stringArrayNative.get_c_ptr();
            for(int i = 0 ; i < res.length ; i++) {
                final CCharPointer nativeFile = nativeFilesPointer.read(i);
                res[i] = toJString(nativeFile);
            }
            return res;
        }

        // -----  Instance methods -----

        /**
         * Create a unit provider for the given subproject.
         *
         * @param subproject The subproject for which to create a unit provider.
         * @return The unit provider for the project manager.
         */
        public UnitProvider getProvider(final String subproject) {
            final UnitProvider result;
            if(ImageInfo.inImageCode()) {
                final CCharPointer subprojectNative =
                    subproject == null ?
                    WordFactory.nullPointer() :
                    toCString(subproject);

                UnitProviderNative unitProviderNative = NI_LIB.${nat('gpr_project_create_unit_provider')}(
                    this.reference.ni(),
                    subprojectNative
                );
                result = UnitProvider.wrap(unitProviderNative);
                if (subproject != null) {
                    UnmanagedMemory.free(subprojectNative);
                }
            } else {
                result = JNI_LIB.${nat("gpr_project_create_unit_provider")}(
                    this,
                    subproject
                );
            }
            return result;
        }

        /**
         * Create a unit provider for root project.
         */
        public UnitProvider getProvider() {
            return this.getProvider(null);
        }

        ${java_doc("libadalang.gpr_project_create_context", 8)}
        public AnalysisContext createContext(
            String subproject,
            EventHandler eventHandler,
            boolean withTrivia,
            int tabStop
        ) {
            if(ImageInfo.inImageCode()) {
                // Prepare C values for native calls
                final CCharPointer subproject_c =
                  subproject == null
                  ? WordFactory.nullPointer()
                  : toCString(subproject);
                EventHandlerNative eventHandler_c =
                  eventHandler == null
                  ? WordFactory.nullPointer()
                  : eventHandler.reference.ni();

                // Manually allocate a C-level analysis context so that we can
                // initialize it ourselves.
                final PointerWrapper context = new PointerWrapper(
                    NI_LIB.${nat("allocate_analysis_context")}()
                );

                // Create the Java wrapper, so that we have one ready for
                // event handler callbacks triggered during context
                // initialization.
                AnalysisContext result =
                    AnalysisContext.fromReference(context, eventHandler, true);

                // "result" has its own ownership share: release ours
                NI_LIB.${nat("context_decref")}(context.ni());

                // TODO: attach "this" to "result" so that the former lives at
                // least as long as the former.

                // Finally, initialize the analysis context. Note that this
                // step may raise an exception: in that case, the analysis
                // context is considered not initialized: release it.
                NI_LIB.${nat("gpr_project_initialize_context")}(
                    this.reference.ni(),
                    context.ni(),
                    subproject_c,
                    eventHandler_c,
                    withTrivia ? 1 : 0,
                    tabStop
                );
                UnmanagedMemory.free(subproject_c);
                final LangkitExceptionNative exc_c =
                    NI_LIB.${nat("get_last_exception")}();
                if (exc_c.isNonNull()) {
                    LangkitException exc = wrapException(exc_c);
                    NI_LIB.${nat("context_decref")}(context.ni());
                    throw exc;
                }

                return result;
            } else {
                return JNI_LIB.${nat("gpr_project_create_context")}(
                    this,
                    subproject,
                    eventHandler,
                    withTrivia,
                    tabStop
                );
            }
        }

        /**
         * Get the files for the given subprojects in a string array.
         *
         * @param mode The file getting mode.
         * @param subprojects The subprojects to consider.
         * @return The array that contains the project files.
         */
        public String[] getFiles(
            final SourceFileMode mode,
            final String[] subprojects
        ) {
            // Verify if the project is null
            if(this.reference.isNull())
                return new String[0];

            String[] result = null;
            LangkitException exc;

            if(ImageInfo.inImageCode()) {
                // Convert the Java array of subprojects ("subprojects"
                // argument) into the required C array
                // (subprojectCount/subprojectsNative).
                final int subprojectCount;
                final CCharPointerPointer subprojectsNative;

                if (subprojects != null && subprojects.length > 0) {
                    subprojectCount = subprojects.length;
                    subprojectsNative =
                        UnmanagedMemory.calloc(
                            subprojectCount * SizeOf.get(CCharPointerPointer.class)
                        );
                } else {
                    subprojectCount = 0;
                    subprojectsNative = WordFactory.nullPointer();
                }

                for (int i = 0; i < subprojectCount; ++i) {
                    subprojectsNative.write(i, toCString(subprojects[i]));
                }

                // Call the C API. Keep track of a potential native exception.
                StringArrayNative sourceFileArray =
                    NI_LIB.${nat('gpr_project_source_files')}(
                        this.reference.ni(),
                        mode.toC(),
                        subprojectsNative,
                        subprojectCount
                    );
                exc = getLastException();

                // If the call was successful, create the Java array result and
                // initialize it, and free the C array result.
                if(exc == null) {
                    result = toJStringArray(sourceFileArray);
                    NI_LIB.${nat("free_string_array")}(sourceFileArray);
                }

                // Release subprojectsNative
                for (int i = 0; i < subprojectCount; ++i) {
                    UnmanagedMemory.free(subprojectsNative.read(i));
                }
                if (subprojectCount > 0) {
                    UnmanagedMemory.free(subprojectsNative);
                }
            } else {
                result = JNI_LIB.${nat("gpr_project_source_files")}(
                    this,
                    mode.toC(),
                    subprojects
                );
                exc = getLastException();
            }

            // If we got an exception, turn it into a ProjectManagerException
            // one.
            if(exc != null) {
                throw new ProjectManagerException(exc.getMessage());
            }

            return result;

        }

        /**
         * Get the files of the root project in a string array.
         *
         * @param mode The file getting mode.
         * @return The array that contains the project files.
         */
        public String[] getFiles(final SourceFileMode mode) {
            return this.getFiles(mode, null);
        }

        /** @see java.lang.AutoCloseable#close() */
        @Override
        public void close() {

            if(ImageInfo.inImageCode()) {
                NI_LIB.${nat("gpr_project_free")}(this.reference.ni());
            } else {
                JNI_LIB.${nat("gpr_project_free")}(this);
            }

        }

    }

    ${java_doc("libadalang.create_auto_provider", 4)}
    public static UnitProvider createAutoProvider(
        final String[] sourceFiles,
        final String charset
    ) {
        if (ImageInfo.inImageCode()) {
            final CCharPointer charsetNative =
                charset == null ?
                WordFactory.nullPointer() :
                toCString(charset);

            // Allocate the C array of C strings that will contain decoded
            // source file names. Make room for one additional null pointer
            // to mark the end of the array.
            final CCharPointerPointer sourceFilesNative =
                UnmanagedMemory.calloc(
                    (sourceFiles.length + 1) * SizeOf.get(CCharPointerPointer.class)
                );

            for (int i = 0; i < sourceFiles.length; ++i) {
                sourceFilesNative.write(i, toCString(sourceFiles[i]));
            }

            // Create the auto provider
            final UnitProviderNative unitProviderNative =
                NI_LIB.${nat('create_auto_provider')}(
                    sourceFilesNative,
                    charsetNative
                );

            // Release all temporarily allocated memory
            for (int i = 0; i < sourceFiles.length; ++i) {
                UnmanagedMemory.free(sourceFilesNative.read(i));
            }

            UnmanagedMemory.free(sourceFilesNative);

            if (charset != null) {
                UnmanagedMemory.free(charsetNative);
            }

            return UnitProvider.wrap(unitProviderNative);
        } else {
            return JNI_LIB.${nat("create_auto_provider")}(
                sourceFiles,
                charset
            );
        }
    }
