/*
 * [LoadCodeToProcessMacro.java]
 *
 * Summary: Loads code to process a given custom macro.
 *
 * Copyright: (c) 2008-2017 Roedy Green, Canadian Mind Products, http://mindprod.com
 *
 * Licence: This software may be copied and used freely for any purpose but military.
 *          http://mindprod.com/contact/nonmil.html
 *
 * Requires: JDK 1.6+
 *
 * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/
 *
 * Version History:
 *  1.0 2008-07-26 initial version. Extract and expand code in Include and Replacer.
 *                 Now does cache and looks first in custom package.
 */
/**
 * Loads code to process a given custom macro.
 *
 * @author Roedy Green, Canadian Mind Products
 * @version 1.0 2008-07-26 initial version. Extract and expandNoRef code in Include and Replacer.
 *          Now does cache and looks first in custom package.
 * @since 2008-07-26
 */
package com.mindprod.htmlmacros;

import java.util.HashMap;

/**
 * Loads code to process a given custom macro.
 * <p/>
 * Deals with loading the Class to process a macro, creating a fresh instance for each time a macro needs to be
 * expanded.
 * It maintains a cache of previously loaded Macro Classes, not Macro Instances.
 * Used by Include and Replacer only.
 * <p/>
 * created with Intellij Idea
 *
 * @author Roedy Green, Canadian Mind Products
 * @version 1.0 2008-07-26 initial version. Extract and expandNoRef code in Include and Replacer.
 *          Now does cache and looks first in custom package.
 */
class LoadCodeToProcessMacro
    {
    // ------------------------------ CONSTANTS ------------------------------

    /**
     * how many macros max we might load
     */
    private static final int MACRO_CACHE_CAPACITY = 200;

    /**
     * cache of previously loaded Macro processing code classes.  We create a fresh instance for each Macro processed.
     * Look up Class object(not Macro instance) via unqualified macro name.
     * We could have used the System's cache of loaded classes accessible via
     * ClassLoader.findLoadedClass(String) but the code would be a tad more complicated.
     */
    private static final HashMap<String, Class<? extends Macro>> macroClassCache = new HashMap<String,
            Class<? extends Macro>>(
            MACRO_CACHE_CAPACITY );

    // -------------------------- STATIC METHODS --------------------------

    /**
     * find class to process macro. Look in three places, cache, custom package and main package.
     *
     * @param macroName Single word Macro name. Same as class name to process macro.
     *
     * @return class handle to class to process the macro. Null if does not exist.
     */
    private static Class<? extends Macro>
    findMacroClass( String macroName )
        {
        Class<? extends Macro> macroClass = getCachedMacroClass( macroName );
        if ( macroClass != null )
            {
            return macroClass;
            }
        // not in custom package, look in main package.
        return loadMacroClass( macroName, "com.mindprod.htmlmacros" );
        // return with possibly null result.
        }

    /**
     * get class to process macro from cache of previously loaded classes.
     *
     * @param macroName Single word Macro name. Same as class name to process macro.
     *
     * @return class handle to class to process the macro. Null if not in cache.
     */
    private static Class<? extends Macro> getCachedMacroClass( String macroName )
        {
        return macroClassCache.get( macroName );
        }

    /**
     * get fresh instance of Class to process this macro.  May have to load the class dynamically.
     *
     * @param macroName Single word Macro name. Same as class name to process macro.
     *                  Code may live in either com.mindprod.htmlmacros package or  CUSTOM_MACROS_PACKAGE.
     *
     * @return interface handle to instance of the class to process the macro.
     * @throws InstantiationException if macro class refuses to Instantiate.
     * @throws IllegalAccessException if class does not have public access.
     * @throws ClassNotFoundException if code for class cannot be found or if it does not implement Macro.
     */
    static Macro getMacroProcessorInstance( String macroName ) throws InstantiationException, IllegalAccessException,
            ClassNotFoundException
        {
        Class<? extends Macro> macroClass = findMacroClass( macroName );
        if ( macroClass == null )
            {
            if ( !( macroName.length() > 0
                    && Character.isUpperCase( macroName.charAt( 0 ) ) ) )
                {
                throw new IllegalArgumentException( "macro "
                                                    +
                                                    macroName
                                                    +
                                                    " should start with an upper case letter. Possible missing macro " +
                                                    "name." );
                }
            else
                {
                throw new ClassNotFoundException( "No such macro " + macroName + " or possible coding bug: The code " +
                                                  "that implements the Macro interface to process " + macroName + " " +
                                                  "could not be found." );
                }
            }
        try
            {
            //   This cast will fail if the loaded Macro code does not implement Macro.
            return macroClass.newInstance();
            }
        catch ( ClassCastException e )
            {
            throw new ClassNotFoundException( "Coding bug: The code to process macro " + macroName + " does not " +
                                              "implement the Macro interface." );
            }
        catch ( InstantiationException e )
            {
            // macro is screwed up if it won't instantiate.
            throw new InstantiationException( "Coding bug: The code to process macro " + macroName + " would not " +
                                              "instantiate. It needs a public no-arg constructor." );
            }
        catch ( IllegalAccessException e )
            {
            // macro is screwed up if if does not have no-arg public constructor.
            throw new IllegalAccessException( "Coding bug: The code to process macro " + macroName + " refused access" +
                                              ". It needs a public no-arg constructor." );
            }
        }

    /**
     * load class to process macro.
     *
     * @param macroName   Single word Macro name. Same as class name to process macro.
     * @param packageName name of package where to look for code for this Macro class.
     *
     * @return class handle to class to process the macro. Null if does not exist.
     */
    private static Class<? extends Macro> loadMacroClass( String macroName, String packageName )
        {
        try
            {
            // e.g. parm to Class.forName looks like: "Measure"
            final String binaryClassName = packageName + "." + macroName;
            // Make sure the class we load extends Macro.
            final Class<? extends Macro> macroClass = Class.forName( binaryClassName ).asSubclass( Macro.class );
            if ( macroClass != null )
                {
                // save copy of class object for future use.
                macroClassCache.put( macroName, macroClass );
                }
            return macroClass;
            }
        catch ( ClassCastException e )
            {
            // macro is screwed up, but the code exists.
            throw new ClassCastException( "Coding bug: The code to process macro " + macroName + " refused access. It needs a public no-arg constructor." );
            }
        catch ( Exception e )
            {
            // might have been ClassNotFoundException,  NoClassDefFoundException
            // Any problem is a failure.
            return null;
            }
        }
    }