ArduinoGenerator.java

/*******************************************************************************
 * jArduino: Arduino C++ Code Generation From Java
 * Copyright 2020 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 net.sourceforge.jarduino.message;

import java.util.List;
import java.util.Map.Entry;

/**
 * Format the c++ sketch.
 */
public final class ArduinoGenerator {
    /**
     * Private constructor.
     */
    private ArduinoGenerator() {
    }

    /**
     * Format system header.
     * @param pSystem the system
     * @param pFilter the filter
     * @return the formatted system
     */
    public static String formatHeader(final ArduinoSystem pSystem,
                                      final ArduinoMsgFilter pFilter) {
        /* Create builder  */
        final StringBuilder myBuilder = new StringBuilder();

        /* Initialise the details */
        final String myResult = updateTemplate(ArduinoTemplate.HEADER, pSystem);

        /* Loop through the messages */
        for (ArduinoMessage myMessage : pSystem.getMessages()) {
            /* If the message is selected */
            if (pFilter.isSelected(myMessage.getId())) {
                /* format the message */
                myBuilder.append(formatMessageHeader(myMessage, pFilter));
            }
        }

        /* Update the template and return  */
        return myResult.replace(ArduinoVar.HDRCLASSES, myBuilder.toString());
    }

    /**
     * Format system header.
     * @param pSystem the system
     * @param pFilter the filter
     * @return the formatted system
     */
    public static String formatBody(final ArduinoSystem pSystem,
                                    final ArduinoMsgFilter pFilter) {
        /* Create builder  */
        final StringBuilder myBuilder = new StringBuilder();

        /* Initialise the details */
        final String myResult = updateTemplate(ArduinoTemplate.BODY, pSystem);

        /* Loop through the messages */
        for (ArduinoMessage myMessage : pSystem.getMessages()) {
            /* If the message is selected */
            if (pFilter.isSelected(myMessage.getId())) {
                /* format the message */
                myBuilder.append(formatMessageBody(myMessage, pFilter));
            }
        }

        /* Update the template and return  */
        return myResult.replace(ArduinoVar.BDYCLASSES, myBuilder.toString())
                .replace(ArduinoVar.MSGCASES, buildMsgCases(pSystem, pFilter));
    }

    /**
     * Format message header.
     * @param pMessage the message
     * @param pFilter the filter
     * @return the formatted message
     */
    private static String formatMessageHeader(final ArduinoMessage pMessage,
                                              final ArduinoMsgFilter pFilter) {
        /* Create builder  */
        final StringBuilder myBuilder = new StringBuilder();

        /* Determine the parts to format */
        final boolean parseMsg = pFilter.isParsed(pMessage.getId());
        final boolean buildMsg = pFilter.isBuilt(pMessage.getId());

        /* Update standard message details */
        String myBase = ArduinoTemplate.HDRCLASS.replace(ArduinoVar.PARSEHDR, parseMsg ? ArduinoTemplate.HDRPARSECLASS : "");
        myBase = myBase.replace(ArduinoVar.BUILDHDR, buildMsg ? ArduinoTemplate.HDRBUILDCLASS : "");
        myBuilder.append(formatClassHeader(myBase, pFilter, pMessage, ArduinoSignal.MULTI_NONE, pMessage.getSignals()));

        /* If we are a multiplex message */
        if (pMessage.hasMultiplex()) {
            /* Loop through the multiplexes */
            for (Entry<Long, List<ArduinoSignal>> myEntry : pMessage.getMultiplexMap().entrySet()) {
                /* Format the multiplex class */
                myBase = ArduinoTemplate.HDRMULTICLASS.replace(ArduinoVar.PARSEHDR, parseMsg ? ArduinoTemplate.HDRPARSECLASS : "");
                myBase = myBase.replace(ArduinoVar.BUILDHDR, buildMsg ? ArduinoTemplate.HDRBUILDMULTICLASS : "");
                myBuilder.append(formatClassHeader(myBase, pFilter, pMessage, myEntry.getKey(), myEntry.getValue()));
            }
        }

        /* Return the result */
        return myBuilder.toString();
    }

    /**
     * Format message body.
     * @param pMessage the message
     * @param pFilter the filter
     * @return the formatted message
     */
    private static String formatMessageBody(final ArduinoMessage pMessage,
                                            final ArduinoMsgFilter pFilter) {
        /* Create builder  */
        final StringBuilder myBuilder = new StringBuilder();

        /* Determine the parts to format */
        final boolean parseMsg = pFilter.isParsed(pMessage.getId());
        final boolean buildMsg = pFilter.isBuilt(pMessage.getId());

        /* Update standard message details */
        String myBase = ArduinoTemplate.BDYCLASS.replace(ArduinoVar.PARSEBDY + ArduinoChar.LF, parseMsg ? ArduinoTemplate.BDYPARSECLASS : "");
        myBase = myBase.replace(ArduinoVar.BUILDBDY, buildMsg ? ArduinoTemplate.BDYBUILDCLASS : "");
        myBuilder.append(formatClassBody(myBase, pMessage, ArduinoSignal.MULTI_NONE, pMessage.getSignals()));

        /* If we are a multiplex message */
        if (pMessage.hasMultiplex()) {
            /* Loop through the multiplexes */
            for (Entry<Long, List<ArduinoSignal>> myEntry : pMessage.getMultiplexMap().entrySet()) {
                /* Format the multiplex class */
                myBase = ArduinoTemplate.BDYMULTICLASS.replace(ArduinoVar.PARSEBDY + ArduinoChar.LF, parseMsg ? ArduinoTemplate.BDYPARSEMULTICLASS : "");
                myBase = myBase.replace(ArduinoVar.BUILDBDY, buildMsg ? ArduinoTemplate.BDYBUILDMULTICLASS : "");
                myBuilder.append(formatClassBody(myBase, pMessage, myEntry.getKey(), myEntry.getValue()));
            }
        }

        /* Return the result */
        return myBuilder.toString();
    }

    /**
     * Format class header.
     * @param pTemplate the template
     * @param pFilter the filter
     * @param pMessage the message
     * @param pMultiplex the multiplex value
     * @param pSignals the signals
     * @return the formatted class
     */
    private static String formatClassHeader(final String pTemplate,
                                            final ArduinoMsgFilter pFilter,
                                            final ArduinoMessage pMessage,
                                            final Long pMultiplex,
                                            final List<ArduinoSignal> pSignals) {
        /* determine whether we have public fields */
        final boolean pubFields = pFilter.publicFields();

        /* Update signal details */
        String myResult = pTemplate.replace(ArduinoVar.HDRSETTERS, pubFields ? "" : formatSignals(ArduinoTemplate.HDRSETTER, pSignals));
        myResult = myResult.replace(ArduinoVar.HDRGETTERS, pubFields ? "" : formatSignals(ArduinoTemplate.HDRGETTER, pSignals));
        myResult = myResult.replace(ArduinoVar.PUBFIELDS, pubFields ? "private:\n  " : "");
        myResult = myResult.replace(ArduinoVar.PRIVFIELDS, pubFields ? "\n" : "\n  private:\n");
        myResult = myResult.replace(ArduinoVar.HDRFIELDS, formatSignals(ArduinoTemplate.HDRFIELD, pSignals));
        myResult = myResult.replace(ArduinoVar.HDRFIELDDEFS, formatDefSignals(ArduinoTemplate.HDRFIELDDEF, pSignals));

        /* Determine the class name */
        String myClass = pMessage.getName();
        if (!ArduinoSignal.MULTI_NONE.equals(pMultiplex)) {
            /* Adjust class name */
            myClass += "_m" + pMultiplex;

            /* Override the multiplex setter */
            myResult = myResult.replace(ArduinoVar.MUXSETTER, pubFields ? "" : updateTemplate(ArduinoTemplate.MUXSETTER, pMessage.getMultiplexSignal()));
            myResult = myResult.replace(ArduinoVar.MUXSET, updateTemplate(pMessage.getMultiplexSignal(), pFilter, pMultiplex));
        }

        /* Update the message/class */
        myResult = updateTemplate(myResult, pMessage);
        myResult = myResult.replace(ArduinoVar.CLASSNAME, myClass);

        /* Return the result */
        return myResult;
    }

    /**
     * Format class body.
     * @param pTemplate the template
     * @param pMessage the message
     * @param pMultiplex the multiplex value
     * @param pSignals the signals
     * @return the formatted class
     */
    private static String formatClassBody(final String pTemplate,
                                          final ArduinoMessage pMessage,
                                          final Long pMultiplex,
                                          final List<ArduinoSignal> pSignals) {
        /* Update signal details */
        String myResult = pTemplate.replace(ArduinoVar.BDYREADERS, formatSignals(ArduinoTemplate.BDYPARSER, pSignals));
        myResult = myResult.replace(ArduinoVar.BDYWRITERS, formatSignals(ArduinoTemplate.BDYWRITER, pSignals));
        myResult = myResult.replace(ArduinoVar.BDYFIELDDEFS, formatDefSignals(ArduinoTemplate.BDYFIELDDEF, pSignals));

        /* Determine the class name */
        String myClass = pMessage.getName();
        if (!ArduinoSignal.MULTI_NONE.equals(pMultiplex)) {
            /* Adjust class name */
            myClass += "_m" + pMultiplex;
        }

        /* Update message/class */
        myResult = updateTemplate(myResult, pMessage);
        myResult = myResult.replace(ArduinoVar.CLASSNAME, myClass);

        /* Return the result */
        return myResult;
    }

    /**
     * Format signals.
     * @param pTemplate the template
     * @param pSignals the signals
     * @return the formatted signals
     */
    private static String formatSignals(final String pTemplate,
                                        final List<ArduinoSignal> pSignals) {
        /* Create builder  */
        final StringBuilder myBuilder = new StringBuilder();

        /* Loop through the signals */
        for (ArduinoSignal mySignal : pSignals) {
            myBuilder.append(updateTemplate(pTemplate, mySignal));
        }

        /* Return the signals */
        return myBuilder.toString();
    }

    /**
     * Format definitions for signals.
     * @param pTemplate the template
     * @param pSignals the signals
     * @return the formatted signals
     */
    private static String formatDefSignals(final String pTemplate,
                                           final List<ArduinoSignal> pSignals) {
        /* Create builder  */
        final StringBuilder myBuilder = new StringBuilder();

        /* Loop through the signals */
        for (ArduinoSignal mySignal : pSignals) {
            myBuilder.append(updateDefTemplate(pTemplate, mySignal));
        }

        /* Return the signals */
        return myBuilder.toString();
    }

    /**
     * Update a template for a system.
     * @param pTemplate the template
     * @param pSystem the system
     * @return the updated template
     */
    private static String updateTemplate(final String pTemplate,
                                         final ArduinoSystem pSystem) {
        return pTemplate.replace(ArduinoVar.SYSNAME, pSystem.getName())
                .replace(ArduinoVar.SYSCNAME, pSystem.getCName());
    }

    /**
     * Update a template for a message.
     * @param pTemplate the template
     * @param pMessage the message
     * @return the updated template
     */
    private static String updateTemplate(final String pTemplate,
                                         final ArduinoMessage pMessage) {
        return pTemplate.replace(ArduinoVar.MSGNAME, pMessage.getName())
                .replace(ArduinoVar.MSGID, pMessage.getId())
                .replace(ArduinoVar.MSGLEN, Integer.toString(pMessage.getLength()));
    }

    /**
     * Update a template for a signal.
     * @param pTemplate the template
     * @param pSignal the signal
     * @return the updated template
     */
    private static String updateTemplate(final String pTemplate,
                                         final ArduinoSignal pSignal) {
        final boolean isFloat = pSignal.isFloat();
        final String myClass = isFloat ? "Double" : "Long";
        final boolean unBounded = pSignal.getRange().unBounded();
        final String myBounds = unBounded ? "" : "Bounded";
        return pTemplate.replace(ArduinoVar.FIELDNAME, pSignal.getName())
                .replace(ArduinoVar.FIELDCLASS, myClass)
                .replace(ArduinoVar.FIELDBOUND, myBounds)
                .replace(ArduinoVar.FIELDTYPE, getFieldType(pSignal));
    }

    /**
     * Update a template for a signal.
     * @param pSignal the signal
     * @param pFilter the filter
     * @param pMultiplex the multiplex value
     * @return the updated template
     */
    private static String updateTemplate(final ArduinoSignal pSignal,
                                         final ArduinoMsgFilter pFilter,
                                         final Long pMultiplex) {
        /* determine whether we have public fields */
        final boolean pubFields = pFilter.publicFields();

        final String myBase = updateTemplate(pubFields ? ArduinoTemplate.MUXSETPUB : ArduinoTemplate.MUXSETPRIV, pSignal);
        return myBase.replace(ArduinoVar.MUXVALUE, pMultiplex.toString());
    }

    /**
     * Update a definition template for a signal.
     * @param pTemplate the template
     * @param pSignal the signal
     * @return the updated template
     */
    private static String updateDefTemplate(final String pTemplate,
                                            final ArduinoSignal pSignal) {
        final boolean isFloat = pSignal.isFloat();
        final String myClass = isFloat ? "Double" : "Long";
        return pTemplate.replace(ArduinoVar.DEFSTART, Integer.toString(pSignal.getDefinition().getStartBit()))
                .replace(ArduinoVar.DEFLENGTH, Integer.toString(pSignal.getDefinition().getBitLength()))
                .replace(ArduinoVar.DEFENDIAN, Boolean.toString(pSignal.getDefinition().getDataType().isLittleEndian()))
                .replace(ArduinoVar.DEFSIGNED, Boolean.toString(pSignal.getDefinition().getDataType().isSigned()))
                .replace(ArduinoVar.DEFFACTOR, pSignal.getFactor().getFactor().toString())
                .replace(ArduinoVar.DEFOFFSET, pSignal.getFactor().getOffset().toString())
                .replace(ArduinoVar.FIELDNAME, pSignal.getName())
                .replace(ArduinoVar.FIELDCLASS, myClass)
                .replace(ArduinoVar.DEFMIN, pSignal.getRange().getMinimum().toString())
                .replace(ArduinoVar.DEFMAX, pSignal.getRange().getMaximum().toString());
    }

    /**
     * Build message cases for the parser.
     * @param pSystem the system
     * @param pFilter the filter
     * @return the cases.
     */
    private static String buildMsgCases(final ArduinoSystem pSystem,
                                        final ArduinoMsgFilter pFilter) {
        /* Create the builder */
        final StringBuilder myBuilder = new StringBuilder();

        /* Loop through the messages */
        for (ArduinoMessage myMessage : pSystem.getMessages()) {
            /* If the message is not parsed, skip it */
            if (!pFilter.isParsed(myMessage.getId())) {
                continue;
            }

            /* If this is a standard message */
            if (myMessage.hasMultiplex()) {
                myBuilder.append(buildMultiplexCases(myMessage, pFilter));
            } else {
                myBuilder.append(updateTemplate(ArduinoTemplate.PRSMSGCASE, myMessage));
            }
        }

        /* Return the parser */
        return myBuilder.toString();
    }

    /**
     * Build multiplex message cases for the parser.
     * @param pMessage the multiplex message
     * @param pFilter the filter
     * @return the case.
     */
    private static String buildMultiplexCases(final ArduinoMessage pMessage,
                                              final ArduinoMsgFilter pFilter) {
        /* Create the builder */
        final StringBuilder myBuilder = new StringBuilder();

        /* determine whether we have public fields */
        final boolean pubFields = pFilter.publicFields();

        /* Create the multiplex cases */
        for (Long myId : pMessage.getMultiplexMap().keySet()) {
            /* Add the case */
            myBuilder.append(ArduinoTemplate.PRSMULTIMSGCASE.replace(ArduinoVar.MUXVALUE, myId.toString()));
        }

        /* Fold in to the case */
        final String myCase = ArduinoTemplate.PRSMULTIMSG
                .replace(ArduinoVar.MUXSWITCH, pubFields ? ArduinoTemplate.MUXSWITCHPUB : ArduinoTemplate.MUXSWITCHPRIV)
                .replace(ArduinoVar.MUXCASES, myBuilder.toString());
        return updateTemplate(myCase, pMessage).replace(ArduinoVar.FIELDNAME, pMessage.getMultiplexSignal().getName());
    }

    /**
     * Obtain the fieldType.
     * @param pSignal the signal
     * @return the fieldType
     */
    private static String getFieldType(final ArduinoSignal pSignal) {
        /* Handle float */
        if (pSignal.isFloat()) {
            return "double";
        }

        /* Obtain bitLength of signal */
        final int myBitLen = pSignal.getDefinition().getBitLength();
        if (myBitLen == 1) {
            return "bool";
        }
        final int myByteLen = 1 + ((myBitLen - 1) / Byte.SIZE);

        /* Return the correct sign/length of int */
        final String mySign = pSignal.isSigned() ? "" : "unsigned ";
        if (myByteLen == Byte.BYTES  || myByteLen == Short.BYTES) {
            return mySign + "short";
        } else if (myByteLen <= Integer.BYTES) {
            return mySign + "long";
        }
        return mySign + "long long";
    }
}