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

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
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.models.data.components.commons.datafield.CheckBoxData;
import eu.eudat.models.data.components.commons.datafield.ComboBoxData;
import eu.eudat.models.data.components.commons.datafield.WordListData;
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.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTAbstractNum;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDecimalNumber;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTLvl;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STNumberFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.math.BigInteger;
import java.util.*;

public class WordBuilder {
    private static final Logger logger = LoggerFactory.getLogger(WordBuilder.class);

    private Map<ParagraphStyle, ApplierWithValue<XWPFDocument, String, XWPFParagraph>> options = new HashMap<>();
    private CTAbstractNum cTAbstractNum;
    private BigInteger numId;

    public WordBuilder() {
        this.cTAbstractNum = CTAbstractNum.Factory.newInstance();
        this.cTAbstractNum.setAbstractNumId(BigInteger.valueOf(1));
        this.buildOptions();
    }

    private void buildOptions() {
        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.TITLE, (mainDocumentPart, item) -> {
            XWPFParagraph paragraph = mainDocumentPart.createParagraph();
            paragraph.setStyle("Title");
            paragraph.setAlignment(ParagraphAlignment.CENTER);
            XWPFRun run = paragraph.createRun();
            run.setText(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(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(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(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;
        });
    }

    public XWPFDocument build(XWPFDocument document, PagedDatasetProfile pagedDatasetProfile, VisibilityRuleService visibilityRuleService) throws IOException {
        createPages(pagedDatasetProfile.getPages(), document, true, visibilityRuleService);
        XWPFAbstractNum abstractNum = new XWPFAbstractNum(cTAbstractNum);
        XWPFNumbering numbering = document.createNumbering();
        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.HEADER4, 0, createListing, visibilityRuleService);
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        });
    }

    private void createSections(List<Section> sections, XWPFDocument mainDocumentPart, ParagraphStyle style, Integer indent, Boolean createListing, VisibilityRuleService visibilityRuleService) {
        if (createListing) this.addListing(mainDocumentPart, indent, false, true);
        sections.forEach(section -> {
            if (visibilityRuleService.isElementVisible(section.getId())) {
                if (!createListing) {
                    XWPFParagraph paragraph = addParagraphContent(section.getNumbering() + " " + section.getTitle(), mainDocumentPart, style, numId);
                    CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl();
                    number.setVal(BigInteger.valueOf(indent));
                }
                createSections(section.getSections(), mainDocumentPart, ParagraphStyle.HEADER5, 1, createListing, visibilityRuleService);
                createCompositeFields(section.getCompositeFields(), mainDocumentPart, 2, createListing, visibilityRuleService);
            }
        });
    }

    private void createCompositeFields(List<FieldSet> compositeFields, XWPFDocument mainDocumentPart, Integer indent, Boolean createListing, VisibilityRuleService visibilityRuleService) {
        if (createListing) this.addListing(mainDocumentPart, indent, true, true);
        compositeFields.forEach(compositeField -> {
            if (visibilityRuleService.isElementVisible(compositeField.getId()) && hasVisibleFields(compositeField, visibilityRuleService)) {
                if (compositeField.getTitle() != null && !compositeField.getTitle().isEmpty() && !createListing) {
                    XWPFParagraph paragraph = addParagraphContent(compositeField.getNumbering() + " " + compositeField.getTitle(), mainDocumentPart, ParagraphStyle.HEADER6, numId);
                    CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl();
                    number.setVal(BigInteger.valueOf(indent));
                }
                createFields(compositeField.getFields(), mainDocumentPart, 3, createListing, visibilityRuleService);
                if (compositeField.getMultiplicityItems() != null && !compositeField.getMultiplicityItems().isEmpty()) {
                    for (FieldSet multiplicityFieldset : compositeField.getMultiplicityItems()) {
                        createFields(multiplicityFieldset.getFields(), mainDocumentPart, 3, createListing, visibilityRuleService);
                    }
                }
                if (compositeField.getHasCommentField() && compositeField.getCommentFieldValue() != null && !compositeField.getCommentFieldValue().isEmpty() && !createListing) {
                    XWPFParagraph paragraph = addParagraphContent("Comment: " + compositeField.getCommentFieldValue(), mainDocumentPart, ParagraphStyle.COMMENT, numId);
                    CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl();
                    number.setVal(BigInteger.valueOf(indent));
                }
            }
        });
    }

    private void createFields(List<Field> fields, XWPFDocument mainDocumentPart, Integer indent, Boolean createListing, VisibilityRuleService visibilityRuleService) {
        if (createListing) this.addListing(mainDocumentPart, indent, false, false);
        fields.forEach(field -> {
            if (visibilityRuleService.isElementVisible(field.getId())) {
                if (!createListing) {
                    try {
                        XWPFParagraph paragraph = addParagraphContent(this.formatter(field), mainDocumentPart, ParagraphStyle.TEXT, numId);
                        CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl();
                        number.setVal(BigInteger.valueOf(indent));
                    } catch (IOException e) {
                        logger.error(e.getMessage(), e);
                    }
                }
            }
        });
    }

    public XWPFParagraph addParagraphContent(String text, XWPFDocument mainDocumentPart, ParagraphStyle style, BigInteger numId) {
        XWPFParagraph paragraph = this.options.get(style).apply(mainDocumentPart, text);
        if (numId != null) {
            paragraph.setNumID(numId);
        }
        return paragraph;
    }

    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.setIlvl(BigInteger.valueOf(indent));
        } else if (!question && hasIndication) {
            cTLvl.addNewNumFmt().setVal(STNumberFormat.DECIMAL);
            cTLvl.setIlvl(BigInteger.valueOf(indent));
        }
        if (!question && !hasIndication) {
            cTLvl.addNewNumFmt().setVal(STNumberFormat.NONE);
            cTLvl.setIlvl(BigInteger.valueOf(indent));
        }
    }

    private String formatter(Field field) throws IOException {
        switch (field.getViewStyle().getRenderStyle()) {
            case "combobox": {
                String comboboxType = ((ComboBoxData) field.getData()).getType();
                if (comboboxType.equals("autocomplete")) {
                    ObjectMapper mapper = new ObjectMapper();
                    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);
                            Map <String, Object> map = new HashMap<>();
                            map.put("label", field.getValue().toString());
                            mapList.add(map);
                        }
                        /*try {
                            if (field.getValue().toString().startsWith("[")) {
                                JSONArray jsonarray = new JSONArray(field.getValue().toString());
                                for (int i = 0; i < jsonarray.length(); i++) {
                                    JSONObject jsonObject = jsonarray.getJSONObject(i);
                                    String id = jsonObject.get("id").toString();
                                    String label = jsonObject.getString("label");
                                    if (id != null && label != null) {
                                        map.put(id, label);
                                    }
                                }
                            } else if (field.getValue().toString().startsWith("{")) {
                                JSONObject jsonObject = new JSONObject(field.getValue().toString());
                                String id = jsonObject.get("id").toString();
                                String label = jsonObject.getString("label");
                                if (id != null && label != null) {
                                    map.put(id, label);
                                }
                            }
                        } catch (Exception e) {
                            Map<String, String> exMap = mapper.readValue(field.getValue().toString(), new TypeReference<Map<String, String>>() {
                            });
                            return exMap.get("label");
                        }*/
                    }
                    StringBuilder sb = new StringBuilder();
                    int index = 0;
                    for (Map<String, Object> map: mapList) {
                        if (!map.containsKey("label") && !map.containsKey("description")) {
                            logger.error("Value is missing the \"label\" and the \"description\" attributes");
                            map.put("label", "unknown Name");
                        }
                        for (Map.Entry<String, Object> entry : map.entrySet()) {
                            if (entry.getValue() != null && (entry.getKey().equals("label") || entry.getKey().equals("description"))) {
                                sb.append(entry.getValue().toString());
                            }
                        }
                        if (index != mapList.size() - 1) sb.append(", ");
                        index++;
                    }
                    return sb.toString();
                } else if (comboboxType.equals("wordlist")) {
                    WordListData wordListData = (WordListData) field.getData();
                    if (wordListData.getOptions().isEmpty() && field.getValue() != null) {
                        logger.warn("World List has no values but the field has");
                        return field.getValue().toString();
                    } else if (field.getValue() != null){
                        ComboBoxData<WordListData>.Option selectedOption = null;
                        for (ComboBoxData<WordListData>.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";
                else return "No";
            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 "freetext":
            case "datepicker":
            case "textarea":
                return field.getValue() != null ? field.getValue().toString(): "";
        }
        return null;
    }

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