content: package altinn.platform.pdf.services;
  
  import altinn.platform.pdf.models.*;
  import altinn.platform.pdf.utils.*;
  
  import com.microsoft.applicationinsights.core.dependencies.apachecommons.lang3.StringUtils;
  import org.apache.pdfbox.cos.COSDictionary;
  import org.apache.pdfbox.cos.COSName;
  import org.apache.pdfbox.pdmodel.*;
  import org.apache.pdfbox.pdmodel.common.PDNumberTreeNode;
  import org.apache.pdfbox.pdmodel.common.PDRectangle;
  import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.*;
  import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDMarkedContent;
  import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList;
  import org.apache.pdfbox.pdmodel.documentinterchange.taggedpdf.StandardStructureTypes;
  import org.apache.pdfbox.pdmodel.font.PDType0Font;
  import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination;
  import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageFitWidthDestination;
  import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
  import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
  import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
  import org.apache.pdfbox.pdmodel.interactive.viewerpreferences.PDViewerPreferences;
  import org.apache.tomcat.util.codec.binary.Base64;
  import org.w3c.dom.Document;
  
  import java.awt.*;
  import java.io.ByteArrayOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.nio.charset.StandardCharsets;
  import java.time.ZonedDateTime;
  import java.time.ZoneId;
  import java.time.format.DateTimeFormatter;
  import java.util.*;
  import java.util.List;
  import java.util.Map;
  import java.util.logging.Level;
  
  public class PDFGenerator {
    private PDDocument document;
    private PDAcroForm form;
    private float width;
    private float yPoint;
    private float xPoint;
    private float componentMargin = 25;
    private float textFieldMargin = 5;
    private float fontSize = 10;
    private float headerFontSize = 14;
    private float leading = 1.2f * fontSize;
    private float margin = 50;
    private PDType0Font font;
    private PDType0Font fontBold;
    private TextResources textResources;
    private String data;
    private Instance instance;
    private Document formData;
    private FormLayout originalFormLayout;
    private LayoutSettings layoutSettings;
    private Map<String, FormLayout> formLayouts;
    private Map<String, Map<String, String>> optionsDictionary;
    private List<FormLayoutElement> repeatingGroups;
    private Party party;
    private Party userParty;
    private UserProfile userProfile;
    private String language;
    private PDPage currentPage;
    private PDPageContentStream currentContent;
    private PDDocumentOutline outline;
    private PDOutlineItem pagesOutline;
    private ByteArrayOutputStream output;
    private COSDictionary currentMarkedContentDictionary;
    private int mcid = 1;
    private PDStructureElement currentPart;
    private PDStructureElement currentSection;
    private List<String> componentsIgnoredFromGeneration = Arrays.asList(
        "PrintButton",
        "Button",
        "Image",
        "NavigationBar",
        "NavigationButtons",
        "Summary");
  
    /**
     * Constructor for the AltinnPDFGenerator object
     */
    public PDFGenerator(PdfContext pdfContext) {
      this.document = new PDDocument();
      this.form = new PDAcroForm(this.document);
      this.outline = new PDDocumentOutline();
      this.pagesOutline = new PDOutlineItem();
      this.originalFormLayout = pdfContext.getFormLayout();
      this.formLayouts = pdfContext.getFormLayouts();
      this.optionsDictionary = pdfContext.getOptionsDictionary();
      this.data = pdfContext.getData();
      this.textResources = pdfContext.getTextResources();
      this.instance = pdfContext.getInstance();
      this.output = new ByteArrayOutputStream();
      this.party = pdfContext.getParty();
      this.userParty = pdfContext.getUserParty();
      this.language = pdfContext.getLanguage();
      this.userProfile = pdfContext.getUserProfile();
      this.layoutSettings = pdfContext.getLayoutSettings();
    }
  
    /**
     * Generates the pdf based on the pdf context
     *
     * @return a byte array output stream containing the generated pdf
     * @throws IOException
     */
    public ByteArrayOutputStream generatePDF() throws IOException {
      // General pdf setup
      PDDocumentInformation info = new PDDocumentInformation();
      info.setCreationDate(Calendar.getInstance());
      info.setTitle(InstanceUtils.getInstanceName(instance));
      document.setDocumentInformation(info);
      pagesOutline.setTitle(getLanguageString("all_pages"));
      outline.addLast(pagesOutline);
      PDDocumentCatalog catalog = document.getDocumentCatalog();
      catalog.setDocumentOutline((outline));
      PDResources resources = new PDResources();
  
      font = PDType0Font.load(document, getFileAsIOStream("font/inter/Inter-Medium.ttf"), true);
      fontBold = PDType0Font.load(document, getFileAsIOStream("font/inter/Inter-Bold.ttf"), true);
      COSName fontCOSName = resources.add(font);
  
      data = new String(Base64.decodeBase64(data), StandardCharsets.UTF_8);
      data = TextUtils.removeIllegalChars(data, font);
  
      try {
        formData = FormUtils.parseXml(data);
        textResources.setResources(parseAndCleanTextResources(textResources.getResources(), formData, font));
      } catch (Exception e) {
        BasicLogger.log(Level.SEVERE, e.toString());
      }
  
      form.setDefaultResources(resources);
      String defaultAppearance = "/" + fontCOSName.getName() + " 10 Tf 0 0 0 rg";
      form.setDefaultAppearance(defaultAppearance);
      catalog.setAcroForm(form);
      catalog.setLanguage(getLanguage());
      catalog.setMarkInfo(new PDMarkInfo());
      catalog.getMarkInfo().setMarked(true);
      catalog.setViewerPreferences(new PDViewerPreferences(new COSDictionary()));
      catalog.getViewerPreferences().setDisplayDocTitle(true);
      catalog.setStructureTreeRoot(new PDStructureTreeRoot());
      catalog.getStructureTreeRoot().setParentTree(new PDNumberTreeNode(PDParentTreeValue.class));
  
      // creates a new page
      createNewPage();
      float pageWidth = currentPage.getMediaBox().getWidth();
      float pageHeight = currentPage.getMediaBox().getHeight();
      this.width = pageWidth - 2 * margin;
  
      // sets background color
      currentContent.setNonStrokingColor(Color.decode("#FFFFFF"));
      currentContent.addRect(0, 0, pageWidth, pageHeight);
      currentContent.fill();
      currentContent.setNonStrokingColor(Color.black);
  
      // init starting drawing points
      float headerMargin = 25;
      yPoint = pageHeight - headerMargin;
      xPoint = margin;
      yPoint = pageHeight - margin;
  
      // draws header
      renderHeader();
  
      // draws submitted by
      renderSubmittedBy();
  
      boolean firstPage = true;
      // Loop through all pdfLayout elements and draws them
      if (originalFormLayout != null) {
        // Older versions of our PlatformService nuget package we supplied only one form
        // layout. Have to be backwards compatible here.
        this.repeatingGroups = FormUtils.setupRepeatingGroups(this.originalFormLayout.getData().getLayout(),
            this.formData);
        List<FormLayoutElement> filteredLayout = FormUtils
            .getFilteredLayout(this.originalFormLayout.getData().getLayout());
        renderFormLayout(filteredLayout);
      } else if (formLayouts != null) {
        // contains a map of form layouts. Render each page and separate by a new page
        if (layoutSettings != null && layoutSettings.getPages() != null && layoutSettings.getPages().getOrder() != null
            && !layoutSettings.getPages().getOrder().isEmpty()) {
          // The app developer has specified the order on a page => render pages in
          // accordance
          List<String> order = layoutSettings.getPages().getOrder();
          for (String layoutKey : order) {
            FormLayout layout = formLayouts.get(layoutKey);
            firstPage = checkLayoutAndRenderPage(firstPage, layoutKey, layout);
          }
        } else {
          for (Map.Entry<String, FormLayout> formLayoutKeyValuePair : formLayouts.entrySet()) {
            String layoutKey = formLayoutKeyValuePair.getKey();
            FormLayout layout = formLayoutKeyValuePair.getValue();
            firstPage = checkLayoutAndRenderPage(firstPage, layoutKey, layout);
          }
        }
      }
  
      // close document and save
      currentContent.close();
      document.getDocumentCatalog().getMarkInfo().setMarked(true);
      document.save(output);
      document.close();
      return output;
    }
  
    private InputStream getFileAsIOStream(final String fileName) {
      InputStream ioStream = this.getClass()
          .getClassLoader()
          .getResourceAsStream(fileName);
  
      if (ioStream == null) {
        throw new IllegalArgumentException(fileName + " is not found");
      }
      return ioStream;
    }
  
    private boolean checkLayoutAndRenderPage(boolean firstPage, String layoutKey, FormLayout layout) throws IOException {
      if (LayoutUtils.includePageInPdf(layoutKey, layoutSettings, layout.getData().getLayout())) {
        if (!firstPage) {
          createNewPage();
          yPoint = currentPage.getMediaBox().getHeight() - margin;
        }
  
        originalFormLayout = layout;
        this.repeatingGroups = FormUtils.setupRepeatingGroups(this.originalFormLayout.getData().getLayout(),
            this.formData);
        List<FormLayoutElement> filteredLayout = FormUtils.getFilteredLayout(layout.getData().getLayout());
        renderFormLayout(filteredLayout);
        firstPage = false;
      }
      return firstPage;
    }
  
    private void renderFormLayout(List<FormLayoutElement> formLayout) throws IOException {
      for (FormLayoutElement element : formLayout) {
        String componentType = element.getType();
        if (componentType.equalsIgnoreCase("group")) {
          if (LayoutUtils.includeComponentInPdf(element.getId(), layoutSettings)) {
            if (element.getDataModelBindings() == null || element.getDataModelBindings().get("group") == null) {
              renderGroup(element);
            } else {
              renderRepeatingGroup(element, false);
            }
          }
        } else {
          renderLayoutElement(element);
        }
      }
    }
  
    private void renderGroup(FormLayoutElement element) throws IOException {
      for (String childId : element.getChildren()) {
        FormLayoutElement childElement = originalFormLayout.getData().getLayout().stream()
            .filter(formLayoutElement -> formLayoutElement.getId().equals(childId)).findFirst().orElse(null);
  
        if (childElement == null) {
          continue;
        }
  
        if (childElement.getType().equalsIgnoreCase("group")) {
          if (childElement.getDataModelBindings() == null || childElement.getDataModelBindings().get("group") == null) {
            renderGroup(element);
          } else {
            renderRepeatingGroup(childElement, false);
          }
        } else {
          if (LayoutUtils.includeComponentInPdf(childElement.getId(), layoutSettings)) {
            renderLayoutElement(childElement);
          }
        }
      }
    }
  
    private void renderRepeatingGroup(FormLayoutElement element, boolean childGroup) throws IOException {
  
      String groupBinding = element.getDataModelBindings().get("group");
      int groupOccurrence = FormUtils.getGroupCount(groupBinding, this.formData);
  
      for (int groupIndex = 0; groupIndex < element.getCount(); groupIndex++) {
  
        if (groupIndex >= groupOccurrence) {
          continue;
        }
  
        for (String childId : element.getChildren()) {
  
          if (element.getEdit() != null && element.getEdit().isMultiPage() && childId.contains(":")) {
            childId = FormUtils.filterMultiPageId(childId);
          }
  
          String finalChildId = childId;
  
          FormLayoutElement childElement = originalFormLayout.getData().getLayout().stream()
              .filter(formLayoutElement -> formLayoutElement.getId().equals(finalChildId)).findFirst().orElse(null);
          HashMap<String, String> originalDataModelBindings = new HashMap<>();
          if (childElement != null && childElement.getDataModelBindings() != null) {
            childElement.getDataModelBindings().entrySet().forEach(stringStringEntry -> originalDataModelBindings
                .put(stringStringEntry.getKey(), stringStringEntry.getValue()));
          }
  
          if (childElement != null && childElement.getType().equalsIgnoreCase("group")) {
            childElement = repeatingGroups.stream()
                .filter(formLayoutElement -> formLayoutElement.getId().equals(finalChildId)).findFirst().orElse(null);
  
            if (childElement == null) {
              continue;
            }
  
            String currentDataModelGroupBinding = childElement.getDataModelBindings().get("group");
  
            // cloning the child to not make changes to the original datamodelBinding
            FormLayoutElement clonedChildElement = (FormLayoutElement) childElement.clone();
            clonedChildElement.getDataModelBindings().put("group",
                FormUtils.setGroupIndexForBinding(currentDataModelGroupBinding, groupBinding, groupIndex));
  
            renderRepeatingGroup(clonedChildElement, true);
  
            continue;
          }
  
          if (childElement == null) {
            continue;
          }
  
          if (childElement.getDataModelBindings() != null && !childElement.getType().equalsIgnoreCase("group")) {
            Map<String, String> dataBindings = childElement.getDataModelBindings();
            for (Map.Entry<String, String> dataBinding : dataBindings.entrySet()) {
              String currentBinding = dataBinding.getValue();
              if (childGroup) {
                int indexStart = groupBinding.indexOf("[");
                int indexEnd = groupBinding.indexOf("]");
                String nonIndexedGroupBinding = groupBinding.replace(groupBinding.substring(indexStart, indexEnd + 1),
                    "");
                currentBinding = currentBinding.replace(nonIndexedGroupBinding, groupBinding);
              }
              String replacedBinding = FormUtils.setGroupIndexForBinding(currentBinding, groupBinding, groupIndex);
              dataBinding.setValue(replacedBinding);
            }
          }
  
          if (!childElement.getType().equalsIgnoreCase("group")) {
            if (LayoutUtils.includeComponentInPdf(childElement.getId() + "-" + groupIndex, layoutSettings)) {
              renderLayoutElement(childElement);
            }
            childElement.setDataModelBindings(originalDataModelBindings);
          }
        }
      }
    }
  
    private void renderLayoutElement(FormLayoutElement element) throws IOException {
      String componentType = element.getType();
      String componentId = element.getId();
      if (componentsIgnoredFromGeneration.contains(componentType)
          || !LayoutUtils.includeComponentInPdf(componentId, layoutSettings)) {
        return;
      }
      // Render title
      float elementHeight = LayoutUtils.getElementHeight(element, font, fontSize, width, leading, textFieldMargin,
          textResources, formData, instance);
      if ((yPoint - elementHeight) < (0 + margin)) {
        // the element would fall outside the page, we create new page and start from
        // there
        createNewPage();
        yPoint = currentPage.getMediaBox().getHeight() - margin;
      }
      addPart();
  
      TextResourceBindings bindings = element.getTextResourceBindings();
      // render title and description
      if (bindings != null) {
        renderTextIfKeyExists(bindings.getTitle(), fontBold, StandardStructureTypes.H2);
        renderTextIfKeyExists(bindings.getDescription(), font, StandardStructureTypes.P);
      }
  
      String elementType = element.getType();
      // Render content
      if (elementType.equalsIgnoreCase("paragraph") || elementType.equalsIgnoreCase("header")) {
        // has no content, ignore
        return;
      }
  
      if (elementType.equalsIgnoreCase("fileupload")) {
        // different view for file upload
        renderFileUploadContent(element);
      } else if (elementType.equalsIgnoreCase("fileuploadwithtag")) {
        // different view for file upload with tags
        renderFileUploadWithTagsContent(element);
      } else if (elementType.equalsIgnoreCase("attachmentlist")) {
        // different view for attachment list
        renderAttachmentListContent(element);
      } else if (elementType.equalsIgnoreCase("AddressComponent")) {
        renderAddressComponent(element);
      } else if (elementType.equalsIgnoreCase("Panel")) {
        renderPanelComponent(element);
      } else {
        // all other components rendered equally
        renderLayoutElementContent(element);
      }
      yPoint -= componentMargin;
    }
  
    private void renderTextIfKeyExists(String key, PDType0Font font, String structureType)
        throws IOException {
      if (key != null && !key.isEmpty()) {
        String text = TextUtils.getTextResourceByKey(key, textResources);
        renderText(text, font, fontSize, structureType);
      }
    }
  
    private void renderPanelComponent(FormLayoutElement element) throws IOException {
      String bodyKey = element.getTextResourceBindings().getBody();
      if (bodyKey != null && !bodyKey.isEmpty()) {
        String description = TextUtils.getTextResourceByKey(bodyKey, textResources);
        renderText(description, font, fontSize, StandardStructureTypes.P);
      }
    }
  
    private void renderHeader() throws IOException {
      addPart();
      addSection(currentPart);
      beginMarkedContent(COSName.P);
      addContentToCurrentSection(COSName.P, StandardStructureTypes.H1);
      currentContent.endMarkedContent();
      currentContent.beginText();
      currentContent.newLineAtOffset(xPoint, yPoint);
      currentContent.setFont(fontBold, headerFontSize);
      String header = TextUtils.getAppOwnerName(instance.getOrg(), getLanguage(), textResources) + " - "
          + TextUtils.getAppName(textResources);
      List<String> lines = TextUtils.splitTextToLines(header, fontBold, headerFontSize, width);
      for (String line : lines) {
        currentContent.showText(line);
        currentContent.newLineAtOffset(0, -leading);
        yPoint -= leading;
      }
      currentContent.endText();
      yPoint -= textFieldMargin;
      addContentToCurrentSection(COSName.P, StandardStructureTypes.H1);
      currentContent.endMarkedContent();
    }
  
    private void renderSubmittedBy() throws IOException {
      if (party == null) {
        return;
      }
      beginMarkedContent(COSName.P);
      addSection(currentPart);
      currentContent.beginText();
      currentContent.newLineAtOffset(xPoint, yPoint);
      currentContent.setFont(font, fontSize);
      String submittedBy;
      if (party.equals(userParty) || userParty == null) {
        submittedBy = getLanguageString("delivered_by") + " " + party.getName();
      } else {
        submittedBy = getLanguageString("delivered_by") + " " + userParty.getName() + " "
            + getLanguageString("on_behalf_of") + " " + party.getName();
      }
      List<String> lines = TextUtils.splitTextToLines(submittedBy, font, fontSize, width);
      lines.add(getLanguageString("reference_number") + " " + TextUtils.getInstanceGuid(instance.getId()).split("-")[4]);
      lines.add(getLanguageString("date_generated") + " " + ZonedDateTime.now()
          .withZoneSameInstant(ZoneId.of("Europe/Oslo")).format(DateTimeFormatter.ofPattern("dd.MM.yyyy / HH:mm")));
      for (String line : lines) {
        currentContent.showText(line);
        currentContent.newLineAtOffset(0, -leading);
        yPoint -= leading;
      }
      currentContent.endText();
      addContentToCurrentSection(COSName.P, StandardStructureTypes.P);
      currentContent.endMarkedContent();
      yPoint -= componentMargin;
    }
  
    private void createNewPage() throws IOException {
      if (currentContent != null) {
        // if we have several pages, close the last
        currentContent.close();
      }
      currentPage = new PDPage(PDRectangle.A4);
      document.addPage((currentPage));
      // adds page to bookmarks
      PDPageDestination dest = new PDPageFitWidthDestination();
      dest.setPage(currentPage);
      PDOutlineItem bookmark = new PDOutlineItem();
      bookmark.setDestination(dest);
      bookmark.setTitle(getLanguageString("page") + " " + document.getPages().getCount());
      pagesOutline.addLast(bookmark);
      currentContent = new PDPageContentStream(document, currentPage);
    }
  
    private void renderText(String text, PDType0Font font, float fontSize, String type) throws IOException {
      addSection(currentPart);
      beginMarkedContent(COSName.P);
      addContentToCurrentSection(COSName.P, type);
      currentContent.beginText();
      currentContent.newLineAtOffset(xPoint, yPoint);
      currentContent.setFont(font, fontSize);
      List<String> lines = TextUtils.splitTextToLines(text, font, fontSize, width);
      for (String line : lines) {
        currentContent.showText(line);
        currentContent.newLineAtOffset(0, -leading);
        yPoint -= leading;
      }
      currentContent.endText();
      currentContent.endMarkedContent();
      yPoint -= textFieldMargin;
    }
  
    private void renderContent(String content) throws IOException {
      float rectHeight = TextUtils.getHeightNeededForTextBox(content, font, fontSize, width - 2 * textFieldMargin,
          leading);
      float fontHeight = TextUtils.getFontHeight(font, fontSize);
      renderBox(xPoint, yPoint + fontHeight + 2, width, rectHeight);
      renderText(content, font, fontSize, StandardStructureTypes.P);
      yPoint -= (rectHeight + fontHeight);
    }
  
    private void renderBox(float xStart, float yStart, float width, float height) throws IOException {
      renderLine(xStart, yStart, xStart + width, yStart); // top
      renderLine(xStart, yStart - height, xStart + width, yStart - height); // bottom
      renderLine(xStart, yStart, xStart, yStart - height); // left
      renderLine(xStart + width, yStart, xStart + width, yStart - height); // right
    }
  
    private void renderLine(float xStart, float yStart, float xEnd, float yEnd) throws IOException {
      currentContent.moveTo(xStart, yStart);
      currentContent.lineTo(xEnd, yEnd);
      currentContent.stroke();
    }
  
    private void renderLayoutElementContent(FormLayoutElement element) throws IOException {
  
      String value;
  
      if (element.getOptionsId() != null || element.getOptions() != null || element.getSource() != null) {
        value = getDisplayValueFromOptions(element);
      } else {
        value = FormUtils.getFormDataByKey(element.getDataModelBindings().get("simpleBinding"), formData);
      }
  
      if (element.getType().equalsIgnoreCase("Datepicker")) {
        renderContent(TextUtils.getDateFormat(value, getLanguage()));
      } else {
        renderContent(value);
      }
    }
  
    private String getDisplayValueFromOptions(FormLayoutElement element) {
      String value = FormUtils.getFormDataByKey(element.getDataModelBindings().get("simpleBinding"), formData);
      List<String> splitFormData;
      if (element.getType().equalsIgnoreCase("Checkboxes") || element.getType().equalsIgnoreCase("MultipleSelect")) {
        // checkboxes can have multiple values, need to fetch label for each one
        splitFormData = Arrays.asList(value.split((",")));
      } else {
        // all other option components only have one selected value
        splitFormData = new ArrayList<>();
        splitFormData.add(value);
      }
  
      List<String> returnValues = new ArrayList<>();
  
      if (element.getOptionsId() != null) {
        if (optionsDictionary == null) {
          return value;
        }
        splitFormData.forEach(formDataValue -> {
          String label = MapUtils.getLabelFromValue(optionsDictionary, element.getOptionsId(), formDataValue);
          returnValues.add(TextUtils.getTextResourceByKey(label, textResources));
        });
      } else if (element.getSource() != null) {
        FormLayoutElement group = this.repeatingGroups
            .stream()
            .filter((FormLayoutElement e) -> e.getDataModelBindings().get("group").equals(element.getSource().getGroup()))
            .findFirst()
            .orElseThrow();
        List<Option> optionList = OptionUtils.getOptionsFromOptionSource(element.getSource(), group, formData,
            textResources);
        splitFormData.forEach(formDataValue -> {
          var option = optionList.stream()
              .filter(o -> o.getValue().equals(formDataValue))
              .findFirst()
              .orElse(null);
          String label = (option != null) ? option.getLabel() : value;
          returnValues.add(TextUtils.getTextResourceByKey(label, textResources));
        });
      } else {
        List<Option> optionList = element.getOptions();
        splitFormData.forEach(formDataValue -> {
          var option = optionList.stream()
              .filter(o -> o.getValue().equals(formDataValue))
              .findFirst()
              .orElse(null);
          String label = (option != null) ? option.getLabel() : value;
          returnValues.add(TextUtils.getTextResourceByKey(label, textResources));
        });
  
      }
  
      return String.join(", ", returnValues);
    }
  
    private Map<String, List<String>> getFileTagDisplayValueFromOptions(FormLayoutElement element,
        Map<String, List<String>> files) {
      Map<String, List<String>> returnValues = new HashMap<String, List<String>>();
  
      if (element.getOptionsId() != null) {
        if (optionsDictionary == null) {
          return files;
        }
        files.forEach((name, tags) -> {
          List<String> tmpTags = new ArrayList<>();
          tags.forEach(tag -> {
            String label = MapUtils.getLabelFromValue(optionsDictionary, element.getOptionsId(), tag);
            tmpTags.add(TextUtils.getTextResourceByKey(label, textResources));
          });
          returnValues.put(name, tmpTags);
        });
      } else {
        List<Option> optionList = element.getOptions();
        files.forEach((name, tags) -> {
          List<String> tmpTags = new ArrayList<>();
          tags.forEach(tag -> {
            var option = optionList.stream()
                .filter(o -> o.getValue().equals(tag))
                .findFirst()
                .orElse(null);
            String label = (option != null) ? option.getLabel() : tag;
            tmpTags.add(TextUtils.getTextResourceByKey(label, textResources));
          });
          returnValues.put(name, tmpTags);
        });
      }
  
      return returnValues;
    }
  
    private void renderFileUploadContent(FormLayoutElement element) throws IOException {
      List<String> files = InstanceUtils.getAttachmentsByComponentId(element.getId(), this.instance);
      renderFileListContent(files);
    }
  
    private void renderFileUploadWithTagsContent(FormLayoutElement element) throws IOException {
      Map<String, List<String>> filesAndTags = InstanceUtils.getAttachmentsAndTagsByComponentId(element.getId(),
          this.instance);
      Map<String, List<String>> filesAndTagsDisplay = getFileTagDisplayValueFromOptions(element, filesAndTags);
      renderFileListWithTagsContent(filesAndTagsDisplay);
    }
  
    private void renderAttachmentListContent(FormLayoutElement element) throws IOException {
      List<String> files = new ArrayList<>();
      for (String id : element.getDataTypeIds()) {
        files.addAll(InstanceUtils.getAttachmentsByComponentId(id, this.instance));
      }
  
      renderFileListContent(files);
    }
  
    private void renderFileListContent(List<String> files) throws IOException {
      addSection(currentPart);
      beginMarkedContent(COSName.P);
      currentContent.setFont(font, fontSize);
      currentContent.beginText();
      float indent = 10;
      currentContent.newLineAtOffset(xPoint + indent, yPoint);
      for (String file : files) {
        currentContent.showText("- " + TextUtils.removeIllegalChars(file, font));
        currentContent.newLineAtOffset(0, -leading);
        yPoint -= leading;
      }
      currentContent.endText();
      addContentToCurrentSection(COSName.P, StandardStructureTypes.P);
      currentContent.endMarkedContent();
    }
  
    private void renderFileListWithTagsContent(Map<String, List<String>> files) throws IOException {
      addSection(currentPart);
      beginMarkedContent(COSName.P);
      currentContent.setFont(font, fontSize);
      currentContent.beginText();
      float indent = 10;
      currentContent.newLineAtOffset(xPoint + indent, yPoint);
      for (String name : files.keySet()) {
        List<String> tags = files.get(name);
  
        currentContent.showText("- " + TextUtils.removeIllegalChars(name, font) + " - ");
        for (String tag : tags) {
          if (tag != tags.get(tags.size() - 1))
            currentContent.showText(tag + ", ");
          else
            currentContent.showText(tag);
        }
        currentContent.newLineAtOffset(0, -leading);
        yPoint -= leading;
      }
      currentContent.endText();
      addContentToCurrentSection(COSName.P, StandardStructureTypes.P);
      currentContent.endMarkedContent();
    }
  
    private void renderAddressComponent(FormLayoutElement element) throws IOException {
      renderText(getLanguageString("address"), font, fontSize, StandardStructureTypes.P);
      renderContent(FormUtils.getFormDataByKey(element.getDataModelBindings().get("address"), this.formData));
      yPoint -= componentMargin;
  
      renderText(getLanguageString("zip_code"), font, fontSize, StandardStructureTypes.P);
      renderContent(FormUtils.getFormDataByKey(element.getDataModelBindings().get("zipCode"), this.formData));
      yPoint -= componentMargin;
  
      renderText(getLanguageString("post_place"), font, fontSize, StandardStructureTypes.P);
      renderContent(FormUtils.getFormDataByKey(element.getDataModelBindings().get("postPlace"), this.formData));
      yPoint -= componentMargin;
  
      if (!element.isSimplified()) {
        renderText(getLanguageString("care_of"), font, fontSize, StandardStructureTypes.P);
        renderText(getLanguageString("house_number_helper"), font, fontSize, StandardStructureTypes.P);
        renderContent(FormUtils.getFormDataByKey(element.getDataModelBindings().get("careOf"), this.formData));
        yPoint -= componentMargin;
  
        renderText(getLanguageString("house_number"), font, fontSize, StandardStructureTypes.P);
        renderContent(FormUtils.getFormDataByKey(element.getDataModelBindings().get("houseNumber"), this.formData));
        yPoint -= componentMargin;
      }
    }
  
    private void addPart() {
      PDStructureElement part = new PDStructureElement(StandardStructureTypes.PART,
          document.getDocumentCatalog().getStructureTreeRoot());
      document.getDocumentCatalog().getStructureTreeRoot().appendKid(part);
      currentPart = part;
    }
  
    private void addSection(PDStructureElement parent) {
      PDStructureElement sect = new PDStructureElement(StandardStructureTypes.SECT, parent);
      parent.appendKid(sect);
      currentSection = sect;
    }
  
    private void addContentToCurrentSection(COSName name, String type) {
      PDStructureElement structureElement = new PDStructureElement(type, currentSection);
      structureElement.setPage(currentPage);
      PDMarkedContent markedContent = new PDMarkedContent(name, currentMarkedContentDictionary);
      structureElement.appendKid(markedContent);
      currentSection.appendKid(structureElement);
    }
  
    private void beginMarkedContent(COSName name) throws IOException {
      currentMarkedContentDictionary = new COSDictionary();
      currentMarkedContentDictionary.setInt(COSName.MCID, mcid);
      mcid++;
      currentContent.beginMarkedContent(name, PDPropertyList.create(currentMarkedContentDictionary));
    }
  
    private List<TextResourceElement> parseAndCleanTextResources(List<TextResourceElement> resources, Document formData,
        PDType0Font font) {
      List<String> replaceValues = new ArrayList<>();
  
      for (TextResourceElement res : resources) {
        res.setValue(TextUtils.removeIllegalChars(res.getValue(), font));
        replaceValues.clear();
        if (res.getVariables() != null) {
          for (TextResourceVariableElement variable : res.getVariables()) {
            if (variable.getDataSource().startsWith("dataModel")) {
              replaceValues.add(FormUtils.getFormDataByKey(variable.getKey(), formData));
            }
          }
          res.setValue(TextUtils.replaceParameters(res.getValue(), replaceValues));
        }
      }
  
      return resources;
    }
  
    private String getLanguageString(String key) {
      return TextUtils.getLanguageStringByKey(key, getLanguage());
    }
  
    private String getLanguage() {
      if (StringUtils.isNotEmpty(this.language)) {
        return this.language;
      } else {
        return (this.userProfile != null) ? this.userProfile.getProfileSettingPreference().getLanguage() : "nb";
      }
    }
  }
  