package eu.eudat.logic.utilities.documents.word;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import eu.eudat.data.entities.DMP;
import eu.eudat.data.entities.Dataset;
import eu.eudat.data.entities.Organisation;
import eu.eudat.data.entities.Researcher;
import eu.eudat.logic.services.forms.VisibilityRuleService;
import eu.eudat.logic.utilities.documents.types.ParagraphStyle;
import eu.eudat.logic.utilities.interfaces.ApplierWithValue;
import eu.eudat.logic.utilities.json.JavaToJson;
import eu.eudat.models.data.components.commons.datafield.*;
import eu.eudat.models.data.user.components.datasetprofile.Field;
import eu.eudat.models.data.user.components.datasetprofile.FieldSet;
import eu.eudat.models.data.user.components.datasetprofile.Section;
import eu.eudat.models.data.user.composite.DatasetProfilePage;
import eu.eudat.models.data.user.composite.PagedDatasetProfile;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlCursor;
import org.json.JSONArray;
import org.json.JSONException;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.NodeTraversor;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.apache.poi.xwpf.usermodel.Document.*;

public class WordBuilder {
    private static final Logger logger = LoggerFactory.getLogger(WordBuilder.class);
    private static final Map<String, Integer> IMAGE_TYPE_MAP = Stream.of(new Object[][] {
            {"image/jpeg", PICTURE_TYPE_JPEG},
            {"image/png", PICTURE_TYPE_PNG},
            {"image/gif", PICTURE_TYPE_GIF},
            {"image/tiff", PICTURE_TYPE_TIFF},
            {"image/bmp", PICTURE_TYPE_BMP},
            {"image/wmf", PICTURE_TYPE_WMF}
    }
    ).collect(Collectors.toMap(objects -> (String)objects[0], o -> (Integer)o[1]));

    private Map<ParagraphStyle, ApplierWithValue<XWPFDocument, Object, XWPFParagraph>> options = new HashMap<>();
    private CTAbstractNum cTAbstractNum;
    private BigInteger numId;
    private Integer indent;
    private final ObjectMapper mapper;
    private Integer imageCount;

    public WordBuilder(Environment environment) {
        this.cTAbstractNum = CTAbstractNum.Factory.newInstance();
        this.cTAbstractNum.setAbstractNumId(BigInteger.valueOf(1));
        this.indent = 0;
        this.imageCount = 0;
        this.mapper = new ObjectMapper();
        this.buildOptions(environment);
    }

    private void buildOptions(Environment environment) {
        this.options.put(ParagraphStyle.TEXT, (mainDocumentPart, item) -> {
            XWPFParagraph paragraph = mainDocumentPart.createParagraph();
            XWPFRun run = paragraph.createRun();
            if (item != null)
                run.setText("" + item);
            run.setFontSize(11);
            return paragraph;
        });
        this.options.put(ParagraphStyle.HTML, (mainDocumentPart, item) -> {
            Document htmlDoc = Jsoup.parse(((String)item).replaceAll("\n", "<br>"));
//            HtmlToWorldBuilder htmlToWorldBuilder = HtmlToWorldBuilder.convert(mainDocumentPart, htmlDoc, indent > 0 ? (indent/2.0F) * 0.8F : 0.8F);
            HtmlToWorldBuilder htmlToWorldBuilder = HtmlToWorldBuilder.convert(mainDocumentPart, htmlDoc, this.indent);
            return htmlToWorldBuilder.getParagraph();
        });
        this.options.put(ParagraphStyle.TITLE, (mainDocumentPart, item) -> {
            XWPFParagraph paragraph = mainDocumentPart.createParagraph();
            paragraph.setStyle("Title");
            paragraph.setAlignment(ParagraphAlignment.CENTER);
            XWPFRun run = paragraph.createRun();
            run.setText((String)item);
            run.setBold(true);
            run.setFontSize(14);
            return paragraph;
        });
        this.options.put(ParagraphStyle.HEADER1, (mainDocumentPart, item) -> {
            XWPFParagraph paragraph = mainDocumentPart.createParagraph();
            paragraph.setStyle("Heading1");
            XWPFRun run = paragraph.createRun();
            run.setText((String)item);
//            run.setBold(true);
//            run.setFontSize(12);
//            run.setStyle("0");
            return paragraph;
        });
        this.options.put(ParagraphStyle.HEADER2, (mainDocumentPart, item) -> {
            XWPFParagraph paragraph = mainDocumentPart.createParagraph();
            paragraph.setStyle("Heading2");
            XWPFRun run = paragraph.createRun();
            run.setText("" + item);
//            run.setBold(true);
//            run.setFontSize(12);
            return paragraph;
        });
        this.options.put(ParagraphStyle.HEADER3, (mainDocumentPart, item) -> {
            XWPFParagraph paragraph = mainDocumentPart.createParagraph();
            paragraph.setStyle("Heading3");
            XWPFRun run = paragraph.createRun();
            run.setText("" + item);
//            run.setBold(true);
//            run.setFontSize(11);
            return paragraph;
        });
        this.options.put(ParagraphStyle.HEADER4, (mainDocumentPart, item) -> {
            XWPFParagraph paragraph = mainDocumentPart.createParagraph();
            paragraph.setStyle("Heading4");
            XWPFRun run = paragraph.createRun();
            run.setText((String)item);
            return paragraph;
        });
        this.options.put(ParagraphStyle.HEADER5, (mainDocumentPart, item) -> {
            XWPFParagraph paragraph = mainDocumentPart.createParagraph();
            paragraph.setStyle("Heading5");
            XWPFRun run = paragraph.createRun();
            run.setText("" + item);
            return paragraph;
        });
        this.options.put(ParagraphStyle.HEADER6, (mainDocumentPart, item) -> {
            XWPFParagraph paragraph = mainDocumentPart.createParagraph();
            paragraph.setStyle("Heading6");
            XWPFRun run = paragraph.createRun();
            run.setText("" + item);
            return paragraph;
        });
        this.options.put(ParagraphStyle.FOOTER, (mainDocumentPart, item) -> {
            XWPFParagraph paragraph = mainDocumentPart.createParagraph();
            XWPFRun run = paragraph.createRun();
            run.setText((String)item);
            return paragraph;
        });
        this.options.put(ParagraphStyle.COMMENT, (mainDocumentPart, item) -> {
            XWPFParagraph paragraph = mainDocumentPart.createParagraph();
            XWPFRun run = paragraph.createRun();
            run.setText("" + item);
            run.setItalic(true);
            return paragraph;
        });
        this.options.put(ParagraphStyle.IMAGE, (mainDocumentPart, item) -> {
            XWPFParagraph paragraph = mainDocumentPart.createParagraph();
            paragraph.setPageBreak(true);
            paragraph.setSpacingAfter(0);
            paragraph.setAlignment(ParagraphAlignment.CENTER); //GK: Center the image if it is too small
            XWPFRun run = paragraph.createRun();
            String imageId = ((Map<String, String>)item).get("id");
            String fileName = ((Map<String, String>)item).get("name");
            String fileType = ((Map<String, String>)item).get("type");
            int format;
            format = IMAGE_TYPE_MAP.getOrDefault(fileType, 0);
            try {
                FileInputStream image = new FileInputStream(environment.getProperty("file.storage") + imageId);
                ImageInputStream iis = ImageIO.createImageInputStream(new File(environment.getProperty("file.storage") + imageId));
                Iterator<ImageReader> readers = ImageIO.getImageReaders(iis);
                if (readers.hasNext()) {
                    ImageReader reader = readers.next();
                    reader.setInput(iis);

                    int initialImageWidth = reader.getWidth(0);
                    int initialImageHeight = reader.getHeight(0);

                    float ratio = initialImageHeight / (float)initialImageWidth;

                    int marginLeftInDXA = mainDocumentPart.getDocument().getBody().getSectPr().getPgMar().getLeft().intValue();
                    int marginRightInDXA = mainDocumentPart.getDocument().getBody().getSectPr().getPgMar().getRight().intValue();
                    int pageWidthInDXA = mainDocumentPart.getDocument().getBody().getSectPr().getPgSz().getW().intValue();
                    int pageWidth = Math.round((pageWidthInDXA - marginLeftInDXA - marginRightInDXA) / (float)20); // /20 converts dxa to points

                    int imageWidth = Math.round(initialImageWidth*(float)0.75);    // *0.75 converts pixels to points
                    int width = Math.min(imageWidth, pageWidth);

                    int marginTopInDXA = mainDocumentPart.getDocument().getBody().getSectPr().getPgMar().getTop().intValue();
                    int marginBottomInDXA = mainDocumentPart.getDocument().getBody().getSectPr().getPgMar().getBottom().intValue();
                    int pageHeightInDXA = mainDocumentPart.getDocument().getBody().getSectPr().getPgSz().getH().intValue();
                    int pageHeight = Math.round((pageHeightInDXA - marginTopInDXA - marginBottomInDXA) / (float)20);    // /20 converts dxa to points

                    int imageHeight = Math.round(initialImageHeight * ((float)0.75));  // *0.75 converts pixels to points

                    int height = Math.round(width*ratio);
                    if(height > pageHeight) {
                        // height calculated with ratio is too large. Image may have Portrait (vertical) orientation. Recalculate image dimensions.
                        height = Math.min(imageHeight, pageHeight);
                        width = Math.round(height/ratio);
                    }

                    XWPFPicture picture = run.addPicture(image, format, fileName, Units.toEMU(width), Units.toEMU(height));
                    paragraph.setPageBreak(false);
                    imageCount++;
                    XWPFParagraph captionParagraph = mainDocumentPart.createParagraph();
                    captionParagraph.setAlignment(ParagraphAlignment.CENTER);
                    captionParagraph.setSpacingBefore(0);
                    captionParagraph.setStyle("Caption");
                    XWPFRun captionRun = captionParagraph.createRun();
                    captionRun.setText("Image " + imageCount);

                }
            } catch (IOException | InvalidFormatException e){
                logger.error(e.getMessage(), e);
            }
            return paragraph;
        });
    }

    public XWPFDocument build(XWPFDocument document, PagedDatasetProfile pagedDatasetProfile, VisibilityRuleService visibilityRuleService) throws IOException {
//        createPages(pagedDatasetProfile.getPages(), document, true, visibilityRuleService);
//        XWPFNumbering numbering = document.createNumbering();
//        BigInteger tempNumId = BigInteger.ONE;
//        boolean found = false;
//        while (!found) {
//            Object o = numbering.getAbstractNum(tempNumId);
//            found = (o == null);
//            if (!found) tempNumId = tempNumId.add(BigInteger.ONE);
//        }
//        cTAbstractNum.setAbstractNumId(tempNumId);
//        XWPFAbstractNum abstractNum = new XWPFAbstractNum(cTAbstractNum);
//        BigInteger abstractNumID = numbering.addAbstractNum(abstractNum);
//        this.numId = numbering.addNum(abstractNumID);

        createPages(pagedDatasetProfile.getPages(), document, false, visibilityRuleService);
        return document;
    }

    private void createPages(List<DatasetProfilePage> datasetProfilePages, XWPFDocument mainDocumentPart, Boolean createListing, VisibilityRuleService visibilityRuleService) {
        datasetProfilePages.forEach(item -> {
            try {
                createSections(item.getSections(), mainDocumentPart, ParagraphStyle.HEADER5, 0, createListing, visibilityRuleService, item.getOrdinal() + 1, null);
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        });
    }

    private void createSections(List<Section> sections, XWPFDocument mainDocumentPart, ParagraphStyle style, Integer indent, Boolean createListing, VisibilityRuleService visibilityRuleService, Integer page, String sectionString) {
        if (createListing) this.addListing(mainDocumentPart, indent, false, true);
        boolean hasValue = false;
        for (Section section: sections) {
            int paragraphPos = -1;
            String tempSectionString = sectionString != null ? sectionString + "." + (section.getOrdinal() + 1) : "" + (section.getOrdinal() + 1);
            if (visibilityRuleService.isElementVisible(section.getId())) {
                if (!createListing) {
                    XWPFParagraph paragraph = addParagraphContent(page + "." + tempSectionString + " " + section.getTitle(), mainDocumentPart, style, numId, indent);
//                    CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl();
//                    number.setVal(BigInteger.valueOf(indent));
                    paragraphPos = mainDocumentPart.getPosOfParagraph(paragraph);
                }
                createSections(section.getSections(), mainDocumentPart, ParagraphStyle.HEADER5, indent+1, createListing, visibilityRuleService, page, tempSectionString);
                hasValue = createCompositeFields(section.getCompositeFields(), mainDocumentPart, indent+1, createListing, visibilityRuleService, page, tempSectionString);

                if (!hasValue && paragraphPos > -1) {
                    mainDocumentPart.removeBodyElement(paragraphPos);
                }
            }
        }
    }

    private Boolean createCompositeFields(List<FieldSet> compositeFields, XWPFDocument mainDocumentPart, Integer indent, Boolean createListing, VisibilityRuleService visibilityRuleService, Integer page, String section) {
        if (createListing) this.addListing(mainDocumentPart, indent, true, true);
        boolean hasValue = false;
        boolean returnedValue = false;
        for (FieldSet compositeField: compositeFields) {
            if (visibilityRuleService.isElementVisible(compositeField.getId()) && hasVisibleFields(compositeField, visibilityRuleService)) {
                char c = 'a';
                int multiplicityItems = 0;
                boolean hasMultiplicityItems = false;
                int paragraphPos = -1;
                int paragraphPosInner = -1;
                if (compositeField.getTitle() != null && !compositeField.getTitle().isEmpty() && !createListing) {
                    XWPFParagraph paragraph = addParagraphContent(page + "." + section + "." + (compositeField.getOrdinal() +1) + " " + compositeField.getTitle(), mainDocumentPart, ParagraphStyle.HEADER6, numId, indent);
//                    CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl();
//                    number.setVal(BigInteger.valueOf(indent));
                    paragraphPos = mainDocumentPart.getPosOfParagraph(paragraph);
                    if(compositeField.getMultiplicityItems() != null && !compositeField.getMultiplicityItems().isEmpty()){
                        XWPFParagraph paragraphInner = addParagraphContent(c + ". ", mainDocumentPart, ParagraphStyle.TEXT, numId, indent);
                        paragraphPosInner = mainDocumentPart.getPosOfParagraph(paragraphInner);
                        hasMultiplicityItems = true;
                        multiplicityItems++;
                    }
                }
                hasValue = createFields(compositeField.getFields(), mainDocumentPart, indent, createListing, visibilityRuleService, hasMultiplicityItems);
                if(hasValue){
                    returnedValue = true;
                } else if(paragraphPosInner > -1){
                    mainDocumentPart.removeBodyElement(paragraphPosInner);
                    c--;
                    multiplicityItems--;
                }
                if (compositeField.getMultiplicityItems() != null && !compositeField.getMultiplicityItems().isEmpty()) {
                    List<FieldSet> list = compositeField.getMultiplicityItems().stream().sorted(Comparator.comparingInt(FieldSet::getOrdinal)).collect(Collectors.toList());
                    for (FieldSet multiplicityFieldset : list) {
                        paragraphPosInner = -1;
                        if(!createListing){
                            c++;
//                            addParagraphContent(c + ". ", mainDocumentPart, ParagraphStyle.HEADER6, numId);
                            XWPFParagraph paragraphInner = addParagraphContent(c + ". ", mainDocumentPart, ParagraphStyle.TEXT, numId, indent);
                            paragraphPosInner = mainDocumentPart.getPosOfParagraph(paragraphInner);
                            hasMultiplicityItems = true;
                            multiplicityItems++;
                        }
//                        hasValue = createFields(multiplicityFieldset.getFields(), mainDocumentPart, 3, createListing, visibilityRuleService, hasMultiplicityItems);
                        boolean hasValueInner = createFields(multiplicityFieldset.getFields(), mainDocumentPart, indent, createListing, visibilityRuleService, hasMultiplicityItems);
//                        if(hasValue){
                        if(hasValueInner){
                            hasValue = true;
                            returnedValue = true;
                        } else if(paragraphPosInner > -1){
                            mainDocumentPart.removeBodyElement(paragraphPosInner);
                            c--;
                            multiplicityItems--;
                        }
                    }
                    if(multiplicityItems == 1) {
                        String text = mainDocumentPart.getLastParagraph().getRuns().get(0).getText(0);
                        if(text.equals("a. ")) {
                            mainDocumentPart.getLastParagraph().removeRun(0);
                        }
                    }
                }
                if (hasValue && compositeField.getHasCommentField() && compositeField.getCommentFieldValue() != null && !compositeField.getCommentFieldValue().isEmpty() && !createListing) {
                    XWPFParagraph paragraph = addParagraphContent("<i>Comment:</i>\n"+compositeField.getCommentFieldValue(), mainDocumentPart, ParagraphStyle.HTML, numId, indent);
//                    CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl();
//                    number.setVal(BigInteger.valueOf(indent));
                }
                if (!hasValue && paragraphPos > -1) {
                    mainDocumentPart.removeBodyElement(paragraphPos);
                }
            }
        }
        return returnedValue;
    }

    private Boolean createFields(List<Field> fields, XWPFDocument mainDocumentPart, Integer indent, Boolean createListing, VisibilityRuleService visibilityRuleService, boolean hasMultiplicityItems) {
        if (createListing) this.addListing(mainDocumentPart, indent, false, false);
        boolean hasValue = false;
        List<Field> tempFields = fields.stream().sorted(Comparator.comparingInt(Field::getOrdinal)).collect(Collectors.toList());
        for (Field field: tempFields) {
            if (visibilityRuleService.isElementVisible(field.getId()) && field.getExport()) {
                if (!createListing) {
                    try {
                        if(field.getViewStyle().getRenderStyle().equals("upload")){
                            boolean isImage = false;
                            for(UploadData.Option type: ((UploadData)field.getData()).getTypes()){
                                String fileFormat = type.getValue();
                                if(IMAGE_TYPE_MAP.containsKey(fileFormat)){
                                    isImage = true;
                                    break;
                                }
                            }
                            if(isImage){
                                if (field.getValue() != null && !field.getValue().toString().isEmpty()) {
                                    XWPFParagraph paragraph = addParagraphContent(mapper.convertValue(field.getValue(), Map.class), mainDocumentPart, ParagraphStyle.IMAGE, numId, 0);
                                    if (paragraph != null) {
//                                        CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl();
//                                        number.setVal(BigInteger.valueOf(indent));
                                        hasValue = true;
                                    }
                                    if(hasMultiplicityItems){
                                        hasMultiplicityItems = false;
                                    }
                                }
                            }
                        }
                        else if (field.getValue() != null && !field.getValue().toString().isEmpty()) {
                            this.indent = indent;
                            String format = this.formatter(field);
                            if (field.getViewStyle().getRenderStyle().equals("tags")) {
                                format = getCommaSeparatedFormatsFromJson(format, "name");
                            } else if (field.getViewStyle().getRenderStyle().equals("combobox") && field.getData() instanceof AutoCompleteData) {
                                format = getCommaSeparatedFormatsFromJson(format, "label");
                            }
                            boolean isResearcher = field.getViewStyle().getRenderStyle().equals("researchers");
                            if(format != null && !format.isEmpty()){
                                Object hasMultiAutoComplete = mapper.convertValue(field.getData(), Map.class).get("multiAutoComplete");
                                boolean isMultiAutoComplete = hasMultiAutoComplete != null && (boolean)hasMultiAutoComplete;
                                boolean arrayStringFormat = format.charAt(0) == '[';
                                if(arrayStringFormat || isMultiAutoComplete){
                                    List<String> values = (arrayStringFormat) ? Arrays.asList(format.substring(1, format.length() - 1).split(",[ ]*")) : Arrays.asList(format.split(",[ ]*"));
                                    if(values.size() > 1) {
                                        boolean orcidResearcher;
                                        for (String val : values) {
                                            orcidResearcher = false;
                                            String orcId = null;
                                            if(isResearcher && val.contains("orcid:")){
                                                orcId = val.substring(val.indexOf(':') + 1, val.indexOf(')'));
                                                val = val.substring(0, val.indexOf(':') + 1) + " ";
                                                orcidResearcher = true;
                                            }
                                            format = "• " + val;
                                            if(hasMultiplicityItems){
                                                mainDocumentPart.getLastParagraph().createRun().setText(format);
                                                if(orcidResearcher){
                                                    XWPFHyperlinkRun run = mainDocumentPart.getLastParagraph().createHyperlinkRun("https://orcid.org/" + orcId);
                                                    run.setText(orcId);
                                                    run.setUnderline(UnderlinePatterns.SINGLE);
                                                    run.setColor("0000FF");
                                                    mainDocumentPart.getLastParagraph().createRun().setText(")");
                                                }
                                                hasMultiplicityItems = false;
                                            }
                                            else{
                                                XWPFParagraph paragraph = addParagraphContent(format, mainDocumentPart, field.getViewStyle().getRenderStyle().equals("richTextarea") ? ParagraphStyle.HTML : ParagraphStyle.TEXT, numId, indent);
                                                if(orcidResearcher){
                                                    XWPFHyperlinkRun run = paragraph.createHyperlinkRun("https://orcid.org/" + orcId);
                                                    run.setText(orcId);
                                                    run.setUnderline(UnderlinePatterns.SINGLE);
                                                    run.setColor("0000FF");
                                                    paragraph.createRun().setText(")");
                                                }
                                                if (paragraph != null) {
//                                                    CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl();
//                                                    number.setVal(BigInteger.valueOf(indent));
                                                    hasValue = true;
                                                }
                                            }
                                            format = null;
                                        }
                                    }
                                    else if(values.size() == 1){
                                        format = values.get(0);
                                    }
                                }
                            }
                            if(hasMultiplicityItems && format != null){
                                mainDocumentPart.getLastParagraph().createRun().setText(format);
                                hasMultiplicityItems = false;
                                hasValue = true;
                            }
                            else{
                                XWPFParagraph paragraph = addParagraphContent(format, mainDocumentPart, field.getViewStyle().getRenderStyle().equals("richTextarea") ? ParagraphStyle.HTML : ParagraphStyle.TEXT, numId, indent);
                                if (paragraph != null) {
//                                    CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl();
//                                    number.setVal(BigInteger.valueOf(indent));
                                    hasValue = true;
                                }
                            }
                        }
                    } catch (IOException e) {
                        logger.error(e.getMessage(), e);
                    }
                }
            }
        }
        return hasValue;
    }

    private String getCommaSeparatedFormatsFromJson(String format, String attribute){
        if((format == null || format.isEmpty()) || (attribute == null || attribute.isEmpty())){
            return null;
        }
        try {
            JSONArray array = new JSONArray(JavaToJson.objectStringToJson(format));
            StringBuilder multipleFormats = new StringBuilder();
            for (int i = 0; i < array.length(); i++) {
                multipleFormats.append(array.getJSONObject(i).getString(attribute)).append(", ");
            }
            if (multipleFormats.length() > 0) {
                multipleFormats.setLength(multipleFormats.length() - 2);
            }
            return multipleFormats.toString();
        } catch (JSONException e) {
            return format;
        }
    }

    public XWPFParagraph addParagraphContent(Object content, XWPFDocument mainDocumentPart, ParagraphStyle style, BigInteger numId, int indent) {
//        this.indent = 0;
        if (content != null) {
            if (content instanceof String && ((String)content).isEmpty()) {
                return null;
            }
            this.indent = indent;
            XWPFParagraph paragraph = this.options.get(style).apply(mainDocumentPart, content);
            if (paragraph != null) {
                paragraph.setIndentFromLeft(400*indent);
                if (numId != null) {
                    paragraph.setNumID(numId);
                }
                return paragraph;
            }
        }
        return null;
    }

    private void addListing(XWPFDocument document, int indent, Boolean question, Boolean hasIndication) {
        CTLvl cTLvl = this.cTAbstractNum.addNewLvl();

        String textLevel = "";
        for (int i = 0; i <= indent; i++) {
            textLevel += "%" + (i + 1) + ".";
        }

        if (question) {
            cTLvl.addNewNumFmt().setVal(STNumberFormat.DECIMAL);
//            cTLvl.addNewLvlText().setVal("");
            cTLvl.setIlvl(BigInteger.valueOf(indent));
        } else if (!question && hasIndication) {
            cTLvl.addNewNumFmt().setVal(STNumberFormat.DECIMAL);
//            cTLvl.addNewLvlText().setVal("");
            cTLvl.setIlvl(BigInteger.valueOf(indent));
        }
        if (!question && !hasIndication) {
            cTLvl.addNewNumFmt().setVal(STNumberFormat.NONE);
//            cTLvl.addNewLvlText().setVal("");
            cTLvl.setIlvl(BigInteger.valueOf(indent));
        }
    }

    private String formatter(Field field) throws IOException {
        String comboboxType = null;
        if (field.getValue() == null) {
            return null;
        }
        switch (field.getViewStyle().getRenderStyle()) {
            case "researchers":
            case "projects":
            case "organizations":
            case "externalDatasets":
            case "dataRepositories":
            case "pubRepositories":
            case "journalRepositories":
            case "taxonomies":
            case "licenses":
            case "publications":
            case "registries":
            case "services":
            case "tags":
            case "currency":
                comboboxType = "autocomplete";
            case "combobox": {
                if (comboboxType == null) {
                    comboboxType = ((ComboBoxData) field.getData()).getType();
                }
                if (comboboxType.equals("autocomplete")) {
                    mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
                    if (field.getValue() == null) return null;
                    List<Map<String, Object>> mapList = new ArrayList<>();
                    if (!field.getValue().equals("") && field.getValue().toString() != null) {
                        try {
                            mapList = Arrays.asList(mapper.readValue(field.getValue().toString(), HashMap[].class));
                        }catch (Exception e) {
                         //   logger.warn(e.getMessage(), e);
                         //   logger.info("Moving to fallback parsing");
                            Map <String, Object> map = new HashMap<>();
                            map.put("label", field.getValue().toString());
                            mapList.add(map);
                        }
                    }
                    StringBuilder sb = new StringBuilder();
                    int index = 0;
                    for (Map<String, Object> map: mapList) {
                        for (Map.Entry<String, Object> entry : map.entrySet()) {
                            if (entry.getValue() != null && (entry.getKey().equals("label") || entry.getKey().equals("description") || entry.getKey().equals("name"))) {
                                sb.append(entry.getValue());
                                break;
                            }
                        }
                        if (index != mapList.size() - 1) sb.append(", ");
                        index++;
                    }
                    return sb.toString();
                } else if (comboboxType.equals("wordlist")) {
                    WordListData wordListData = (WordListData) field.getData();
                    if (field.getValue() != null){
                        ComboBoxData.Option selectedOption = null;
                        if (!wordListData.getOptions().isEmpty()) {
                            for (ComboBoxData.Option option : wordListData.getOptions()) {
                                if (option.getValue().equals(field.getValue())) {
                                    selectedOption = option;
                                }
                            }
                        }
                        return selectedOption != null ? selectedOption.getLabel() : field.getValue().toString();
                    }
                    return "";
                }
            }
            case "booleanDecision":
                if (field.getValue() != null && field.getValue().equals("true")) return "Yes";
                if (field.getValue() != null && field.getValue().equals("false")) return "No";
                return null;
            case "radiobox":
                return field.getValue() != null ? field.getValue().toString() : null;
            case "checkBox":
                CheckBoxData data = (CheckBoxData) field.getData();
                if (field.getValue() == null || field.getValue().equals("false")) return null;
                return data.getLabel();
            case "datepicker":
            case "datePicker":{
                Instant instant;
                if (!((String)field.getValue()).isEmpty()) {
                    try {
                        instant = Instant.parse((String) field.getValue());
                    } catch (DateTimeParseException ex) {
                        instant = LocalDate.parse((String) field.getValue()).atStartOfDay().toInstant(ZoneOffset.UTC);
                    }
                    return field.getValue() != null ? DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.systemDefault()).format(instant) : "";
                }
                return (String) field.getValue();
            }
            case "freetext":
            case "textarea":
            case "richTextarea":
                return field.getValue() != null ? field.getValue().toString(): "";
            case "datasetIdentifier":
            case "validation":
                if (field.getValue() != null && !field.getValue().toString().isEmpty()) {
                    Map<String, String> identifierData;
                    try {
                        identifierData = mapper.readValue(field.getValue().toString(), HashMap.class);
                    } catch (Exception ex) {
                      //  logger.warn(ex.getLocalizedMessage(), ex);
                      //  logger.info("Reverting to custom parsing");
                        identifierData = customParse(field.getValue().toString());
                    }
                    return "id: " + identifierData.get("identifier") + ", Validation Type: " + identifierData.get("type");
                }
                return "";
        }
        return null;
    }

    private boolean hasVisibleFields(FieldSet compositeFields, VisibilityRuleService visibilityRuleService) {
        return compositeFields.getFields().stream().anyMatch(field -> visibilityRuleService.isElementVisible(field.getId()) && field.getExport());
    }

    private Map<String, String> customParse(String value) {
        Map<String, String> result = new LinkedHashMap<>();
        String parsedValue = value.replaceAll("[^a-zA-Z0-9\\s:=,]", "");
        StringTokenizer commaTokens = new StringTokenizer(parsedValue, ", ");
        String delimeter = parsedValue.contains("=") ? "=" : ":";
        while (commaTokens.hasMoreTokens()) {
            String token = commaTokens.nextToken();
            StringTokenizer delimiterTokens = new StringTokenizer(token, delimeter);
            result.put(delimiterTokens.nextToken(), delimiterTokens.nextToken());
        }
        return result;
    }

    public int findPosOfPoweredBy(XWPFDocument document) {
        for (XWPFParagraph p : document.getParagraphs()) {
            List<XWPFRun> runs = p.getRuns();
            if (runs != null) {
                for (XWPFRun r : runs) {
                    String text = r.getText(0);
                    if (text != null) {
                        if (text.equals("Powered by")) {
                            return document.getPosOfParagraph(p) - 1;
                        }
                    }
                }
            }
        }
        return -1;
    }

    public void fillFirstPage(DMP dmpEntity, Dataset datasetEntity, XWPFDocument document) {
        int parPos = 0;
        int descrParPos = -1;
        XWPFParagraph descrPar = null;
        for(XWPFParagraph p: document.getParagraphs()){
            List<XWPFRun> runs = p.getRuns();
            if(runs != null){
                for(XWPFRun r : runs){
                    String text = r.getText(0);
                    if(text != null){
                        if(text.contains("{ARGOS.DMP.TITLE}")) {
                            text = text.replace("{ARGOS.DMP.TITLE}", dmpEntity.getLabel());
                            r.setText(text, 0);
                        } else if(text.contains("{ARGOS.DMP.VERSION}")) {
                            text = text.replace("{ARGOS.DMP.VERSION}", "Version " + dmpEntity.getVersion());
                            r.setText(text, 0);
                        } else if(datasetEntity != null && text.contains("{ARGOS.DATASET.TITLE}")) {
                            text = text.replace("{ARGOS.DATASET.TITLE}", datasetEntity.getLabel());
                            r.setText(text, 0);
                        } else if(datasetEntity != null && text.contains("{ARGOS.DATASET.DESCRIPTION}")) {
                            descrParPos = parPos;
                            descrPar = p;
                            text = text.replace("{ARGOS.DATASET.DESCRIPTION}", "");
                            r.setText(text, 0);
                        } else if(text.equals("{ARGOS.DMP.RESEARCHERS}")) {
                            String researchersNames = "";
                            Set<Researcher> researchers = dmpEntity.getResearchers();
                            int i = 0;
                            for(Researcher researcher : researchers){
                                i++;
                                researchersNames += researcher.getLabel() + (i < researchers.size() ? ", " : "");
                            }
                            text = text.replace("{ARGOS.DMP.RESEARCHERS}", researchersNames);
                            r.setText(text, 0);
                            r.setFontSize(17);
                        } else if(text.equals("{ARGOS.DMP.ORGANIZATIONS}")) {
                            String organisationsNames = "";
                            Set<Organisation> organisations = dmpEntity.getOrganisations();
                            int i = 0;
                            for(Organisation organisation : organisations){
                                i++;
                                organisationsNames += organisation.getLabel() + (i < organisations.size() ? ", " : "");
                            }
                            text = text.replace("{ARGOS.DMP.ORGANIZATIONS}", organisationsNames);
                            r.setText(text, 0);
                            r.setFontSize(17);
                        }
                    }
                }
            }
            parPos++;
        }
        if(descrParPos != -1 && datasetEntity.getDescription() != null) {
            XmlCursor cursor = descrPar.getCTP().newCursor();
            cursor.toNextSibling();
            Document htmlDoc = Jsoup.parse(((String)datasetEntity.getDescription()).replaceAll("\n", "<br>"));
            HtmlToWorldBuilder htmlToWorldBuilder = new HtmlToWorldBuilder(descrPar, 0, cursor);
            NodeTraversor.traverse(htmlToWorldBuilder, htmlDoc);
        }


            XWPFTable tbl = document.getTables().get(0);
            Iterator<XWPFTableRow> it = tbl.getRows().iterator();
            it.next(); // skip first row
            if(it.hasNext()){
                XWPFParagraph p = it.next().getCell(0).getParagraphs().get(0);
                XWPFRun run = p.createRun();
                run.setText(dmpEntity.getGrant().getFunder().getLabel());
                run.setFontSize(17);
                p.setAlignment(ParagraphAlignment.CENTER);
            }
            it = tbl.getRows().iterator();
            it.next();
            if(it.hasNext()){
                XWPFParagraph p = it.next().getCell(1).getParagraphs().get(0);
                XWPFRun run = p.createRun();
                String text = dmpEntity.getGrant().getLabel();
                String reference = dmpEntity.getGrant().getReference();
                if(reference != null) {
                    String[] parts = reference.split("::");
                    text += parts.length > 1 ? "/ No "+parts[parts.length - 1] : "";
                }
                run.setText(text);
                run.setFontSize(17);
                p.setAlignment(ParagraphAlignment.CENTER);
            }
    }

    public void fillFooter(DMP dmpEntity, Dataset datasetEntity, XWPFDocument document, boolean isDataset) {
        document.getFooterList().forEach(xwpfFooter -> {
            List<XWPFRun> runs = xwpfFooter.getParagraphs().get(0).getRuns();
            if(runs != null){
                for(XWPFRun r : runs){
                    String text = r.getText(0);
                    if(text != null){
                        if(text.contains("{ARGOS.DMP.TITLE}")){
                            text = text.replace("{ARGOS.DMP.TITLE}", dmpEntity.getLabel());
                            r.setText(text, 0);
                        }
                        if(text.contains("{ARGOS.DATASET.TITLE}") && datasetEntity != null){
                            text = text.replace("{ARGOS.DATASET.TITLE}", datasetEntity.getLabel());
                            r.setText(text, 0);
                        }
                        if(text.contains("{ARGOS.DMP.LICENSE}")){
                            try{
                                Map<String, String> license = ((Map<String, String>) mapper.readValue(dmpEntity.getExtraProperties(), Map.class).get("license"));
                                text = text.replace("{ARGOS.DMP.LICENSE}", license.get("pid"));
                            }
                            catch (JsonProcessingException | NullPointerException e){
                                text = text.replace("{ARGOS.DMP.LICENSE}", "License: -");
                            }
                            r.setText(text, 0);
                        }
                        if(text.contains("{ARGOS.DMP.DOI}")){
                            String doi = dmpEntity.getDoi();
                            if(doi != null)
                                text = text.replace("{ARGOS.DMP.DOI}", doi);
                            else
                                text = text.replace("{ARGOS.DMP.DOI}", "-");
                            r.setText(text, 0);
                        }
                        if(text.contains("{ARGOS.DMP.LAST_MODIFIED}")){
                            DateFormat formatter = new SimpleDateFormat("dd/MM/yyyy");
                            text = text.replace("{ARGOS.DMP.LAST_MODIFIED}", formatter.format(dmpEntity.getModified()));
                            r.setText(text, 0);
                        }
                    }
                }
            }
        });
    }
}
