docs: private int generateEncodeDecode(
          final StringBuilder sb,
          final String typeName,
          final List<Token> tokens,
          final boolean isMessage,
          final boolean isExtensible)
      {
          final char varName = Character.toLowerCase(typeName.charAt(0));
          final StringBuilder encode = new StringBuilder();
          final StringBuilder decode = new StringBuilder();
          final StringBuilder init = new StringBuilder();
          final StringBuilder rangeCheck = new StringBuilder();
          final StringBuilder nested = new StringBuilder();
          int currentOffset = 0;
          int gap;
          boolean extensionStarted = false;
  
          // Open all our methods
          generateEncodeDecodeOpen(encode, decode, rangeCheck, init, varName, typeName, isMessage, isExtensible);
  
          for (int i = 0; i < tokens.size(); i++)
          {
              final Token signalToken = tokens.get(i);
              final String propertyName = formatPropertyName(signalToken.name());
  
              switch (signalToken.signal())
              {
                  case BEGIN_MESSAGE: // Check range *before* we encode setting the acting version to schema version
                      encode.append(String.format(
                          "\tif doRangeCheck {\n" +
                          "\t\tif err := %1$s.RangeCheck(%1$s.SbeSchemaVersion(), %1$s.SbeSchemaVersion());" +
                          " err != nil {\n" +
                          "\t\t\treturn err\n" +
                          "\t\t}\n" +
                          "\t}\n",
                          varName));
                      break;
  
                  case END_MESSAGE:
                      // Newer version extra fields check
                      if (isExtensible && !extensionStarted)
                      {
                          generateExtensionCheck(decode, varName);
                          extensionStarted = true;
                      }
  
                      // Check range *after* we decode using the acting
                      // version of the encoded message
                      decode.append(String.format(
                          "\tif doRangeCheck {\n" +
                          "\t\tif err := %1$s.RangeCheck(actingVersion, %1$s.SbeSchemaVersion()); err != nil {\n" +
                          "\t\t\treturn err\n" +
                          "\t\t}\n" +
                          "\t}\n",
                          varName));
                      break;
  
                  case BEGIN_ENUM:
                  case BEGIN_SET:
                      currentOffset += generatePropertyEncodeDecode(
                          signalToken, typeName, encode, decode, currentOffset);
                      i += signalToken.componentTokenCount() - 1;
                      break;
  
                  case BEGIN_COMPOSITE:
                      currentOffset += generatePropertyEncodeDecode(
                          signalToken, typeName, encode, decode, currentOffset);
                      i += signalToken.componentTokenCount() - 1;
  
                      rangeCheck.append(String.format(
                          "\tif err := %1$s.%2$s.RangeCheck(actingVersion, schemaVersion); err != nil {\n" +
                          "\t\treturn err\n" +
                          "\t}\n",
                          varName, propertyName));
                      break;
  
                  case BEGIN_FIELD:
                      if (tokens.size() >= i + 1)
                      {
                          currentOffset += generateFieldEncodeDecode(
                              tokens.subList(i, tokens.size() - 1),
                              varName, currentOffset, encode, decode, rangeCheck, init);
  
                          // Encodings just move past the encoding token
                          if (tokens.get(i + 1).signal() == Signal.ENCODING)
                          {
                              i += 1;
                          }
                          else
                          {
                              i += signalToken.componentTokenCount() - 1;
                          }
                      }
                      break;
  
                  case ENCODING:
                      gap = signalToken.offset() - currentOffset;
                      encode.append(generateEncodeOffset(gap, ""));
                      decode.append(generateDecodeOffset(gap, ""));
                      currentOffset += signalToken.encodedLength() + gap;
                      final String primitive = varName + "." + propertyName;
  
                      // Encode of a constant is a nullop and we want to
                      // initialize constant values.
                      if (signalToken.isConstantEncoding())
                      {
                          generateConstantInitPrimitive(init, primitive, signalToken);
                      }
                      else
                      {
                          generateEncodePrimitive(encode, varName, formatPropertyName(signalToken.name()), signalToken);
                      }
  
                      // Optional tokens also get initialized
                      if (signalToken.isOptionalEncoding())
                      {
                          generateOptionalInitPrimitive(init, primitive, signalToken);
                      }
  
                      generateDecodePrimitive(decode, primitive, signalToken);
                      generateRangeCheckPrimitive(rangeCheck, primitive, signalToken, signalToken.isOptionalEncoding());
                      break;
  
                  case BEGIN_GROUP:
                      // Newer version extra fields check
                      if (isExtensible && !extensionStarted)
                      {
                          generateExtensionCheck(decode, varName);
                          extensionStarted = true;
                      }
  
                      // Write the group, saving any extra offset we need to skip
                      currentOffset += generateGroupEncodeDecode(
                          tokens.subList(i, tokens.size() - 1),
                          typeName,
                          encode, decode, rangeCheck, currentOffset);
  
                      // Recurse
                      gap = Math.max(0,
                          signalToken.encodedLength() -
                              generateEncodeDecode(
                                  nested,
                                  typeName + toUpperFirstChar(signalToken.name()),
                                  tokens.subList(i + 5, tokens.size() - 1),
                                  false, true));
  
                      // Group gap block length handling
                      encode.append(generateEncodeOffset(gap, "\t")).append("\t}\n");
                      decode.append(generateDecodeOffset(gap, "\t")).append("\t}\n");
  
                      // And we can move over this group to the END_GROUP
                      i += signalToken.componentTokenCount() - 1;
                      break;
  
                  case END_GROUP:
                      // Newer version extra fields check
                      if (isExtensible && !extensionStarted)
                      {
                          generateExtensionCheck(decode, varName);
                      }
                      // Close out this group and unwind
                      generateEncodeDecodeClose(encode, decode, rangeCheck, init);
                      sb.append(encode).append(decode).append(rangeCheck).append(init).append(nested);
                      return currentOffset; // for gap calculations
  
                  case BEGIN_VAR_DATA:
                      // Newer version extra fields check
                      if (isExtensible && !extensionStarted)
                      {
                          generateExtensionCheck(decode, varName);
                          extensionStarted = true;
                      }
                      currentOffset += generateVarDataEncodeDecode(
                          tokens.subList(i, tokens.size() - 1),
                          typeName,
                          encode, decode, rangeCheck, currentOffset);
                      // And we can move over this group
                      i += signalToken.componentTokenCount() - 1;
                      break;
              }
          }
  
          // You can use blockLength on both messages and groups (handled above)
          // to leave some space (akin to an offset).
          final Token endToken = tokens.get(tokens.size() - 1);
          if (endToken.signal() == Signal.END_MESSAGE)
          {
              gap = endToken.encodedLength() - currentOffset;
              encode.append(generateEncodeOffset(gap, ""));
              decode.append(generateDecodeOffset(gap, ""));
          }
  
          // Close out the methods and append
          generateEncodeDecodeClose(encode, decode, rangeCheck, init);
          sb.append(encode).append(decode).append(rangeCheck).append(init).append(nested);
  
          return currentOffset;
      }