package com.customer.work.model;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.stereotype.Component;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;


@Component
public class FlowableExcelMapper {

    protected static ObjectMapper objectMapper = new ObjectMapper();
    protected static JavaTimeModule javaTimeModule =  new JavaTimeModule();
    protected static FlowableExcelParser flowableExcelParser = new FlowableExcelParser();

    public FlowableExcelMapper() {
        // Enable ObjectMapper for handling Instant as string
        objectMapper.registerModule(javaTimeModule);
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    }

    // excelPath defines the location of the Excel file
    // FlowableExcelParser is used to create an ArrayList from the Excel file
    // The 1st row of every sheet contains the path of the variables to be created in the JsonNode
    // Delimiter is "." for an ObjectNode and "*" for an ArrayNode
    // The 2nd row contains tht type of the variable. Supported types are:
    // - String
    // - Boolean
    // - Integer
    // - Double
    // - Date
    // All sheets are added to an ArrayNode
    // All rows are added to an ArrayNode in the sheets ArrayNode
    // All cell values are added to an ObjectNode in the rows ArrayNode
    public ArrayNode excelBookResourceToJsonNode(String resourcePath) {
        ArrayList<ArrayList<ArrayList<String>>> book = flowableExcelParser.parseExcelBookFromResource(resourcePath);
        return bookToJsonNode(book);
    }

    protected ArrayNode bookToJsonNode(ArrayList<ArrayList<ArrayList<String>>> book) {
        ArrayNode bookNode = objectMapper.createArrayNode();
        for (ArrayList<ArrayList<String>> sheet : book) {
            bookNode.add(sheetToJsonNode(sheet));
        }
        return bookNode;
    }

    protected ArrayNode sheetToJsonNode(ArrayList<ArrayList<String>> rows) {
        ArrayList<String> paths = new ArrayList<>();
        ArrayList<String> types = new ArrayList<>();
        ArrayNode arrayNode = objectMapper.createArrayNode();
        for (int rowNum = 0; rowNum < rows.size(); rowNum++) {
            if (rowNum == 0) {
                // The 1st row contains the paths
                paths.addAll(rows.get(rowNum));
            } else if (rowNum == 1) {
                // The 2nd row contains the types of the variables
                types.addAll(rows.get(rowNum));
            } else {
                // All other rows contain the values of the variables
                arrayNode.add(rowToJsonNode(types, paths, rows.get(rowNum), null));
            }
        }
        return arrayNode;
    }

    protected JsonNode rowToJsonNode(ArrayList<String> types, ArrayList<String> paths, ArrayList<String> row, JsonNode jsonNode) {
        for (int cellNum = 0; cellNum < row.size(); cellNum++) {
            String cellValue = row.get(cellNum);
            // Empty cells are not added to node
            if (cellValue != null && !cellValue.isEmpty()) {
                String type = types.get(cellNum);
                Object content;
                switch (cellValue) {
                    case "__NULL": content = null; break;
                    case "__N_A":  content = "__N_A"; break;
                    default:
                        switch (type) {
                            case "String":
                                switch (cellValue) {
                                    case "__EMPTY": content = ""; break;
                                    case "__BLANK": content = " "; break;
                                    default:        content = cellValue;
                                }
                                break;
                            case "Boolean": content = Boolean.parseBoolean(cellValue); break;
                            case "Integer": content = Integer.parseInt(cellValue); break;
                            case "Double":  content = Double.parseDouble(cellValue); break;
                            case "Long":    content = Long.parseLong(cellValue); break;
                            case "Date":
                                // Excel considers dates as local dates, but we want them as UTC
                                Instant instant;
                                try {
                                    LocalDateTime localDateTime = LocalDateTime.parse(cellValue, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
                                    instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
                                } catch (DateTimeParseException e) {
                                    LocalDate localDate = LocalDate.parse(cellValue, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
                                    instant = localDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
                                }
                                content = instant.plusSeconds(ZonedDateTime.now().getOffset().getTotalSeconds());
                                break;
                            default:
                                throw new RuntimeException("Variable type not supported: " + type);
                        }
                }
                jsonNode = addNode(jsonNode, paths.get(cellNum), content);
            }
        }
        return jsonNode;
    }

    protected JsonNode addNode(JsonNode parent, String path, Object value) {
        if (parent == null) {
            parent = objectMapper.createObjectNode();
        }

        int delimiterPosition = indexOfFirstDelimiter(path, ".*");
        if (delimiterPosition <= 0) {
            if (parent instanceof ObjectNode) {
                // Key of a value in an ObjectNode
                ((ObjectNode) parent).set(path, objectMapper.valueToTree(value));
            } else {
                // Index of a value in an ArrayNode
                int index = Integer.parseInt(path);
                while (parent.size() <= index) {
                    // Create empty entries to parent ArrayNode
                    ((ArrayNode) parent).add(objectMapper.createObjectNode());
                }
                ((ArrayNode) parent).set(index, objectMapper.valueToTree(value));
            }
            return parent;
        }

        String key = path.substring(0, delimiterPosition);
        String newPath = path.substring(delimiterPosition + 1);
        String delimiter = path.substring(delimiterPosition, delimiterPosition + 1);
        JsonNode child = null;
        if (delimiter.equals(".")) {
            child = objectMapper.createObjectNode();
        } else if (delimiter.equals("*")) {
            child = objectMapper.createArrayNode();
        }

        JsonNode node;
        if (parent instanceof ObjectNode) {
            // Key of a JsonNode in an ObjectNode
            node = parent.get(key);
            if (node != null) {
                // Take the existing ObjectNode as child
                child = node;
            }
            ((ObjectNode) parent).set(key, addNode(child, newPath, value));
        } else {
            // Index of a JsonNode in an ArrayNode
            int index = Integer.parseInt(key);
            node = parent.get(index);
            if (node != null) {
                // Take the existing ArrayNode as child
                child = node;
            }
            while (parent.size() <= index) {
                // Create empty entries to parent ArrayNode
                ((ArrayNode) parent).add(objectMapper.createObjectNode());
            }
            ((ArrayNode) parent).set(index, addNode(child, newPath, value));
        }
        return parent;
    }

    protected int indexOfFirstDelimiter(String str, String delimiters) {
        for (int i = 0; i < str.length(); i++) {
            for (char c : delimiters.toCharArray()) {
                if (str.charAt(i) == c) {
                    return i;
                }
            }
        }
        return -1;
    }

    public ArrayList<ArrayList<Object>>  excelBookResourceToObj(String resourcePath) {
        ArrayList<ArrayList<ArrayList<String>>> book = flowableExcelParser.parseExcelBookFromResource(resourcePath);
        return bookToObj(book);
    }

    protected ArrayList<ArrayList<Object>> bookToObj(ArrayList<ArrayList<ArrayList<String>>> book) {
        ArrayList<ArrayList<Object>> bookNode = new ArrayList<>();
        for (ArrayList<ArrayList<String>> sheet : book) {
            bookNode.add(sheetToObj(sheet));
        }
        return bookNode;
    }

    protected ArrayList<Object> sheetToObj(ArrayList<ArrayList<String>> rows) {
        ArrayList<String> paths = new ArrayList<>();
        ArrayList<String> types = new ArrayList<>();
        ArrayList<Object> arrayNode = new ArrayList<>();
        for (int rowNum = 0; rowNum < rows.size(); rowNum++) {
            if (rowNum == 0) {
                // The 1st row contains the paths
                paths.addAll(rows.get(rowNum));
            } else if (rowNum == 1) {
                // The 2nd row contains the types of the variables
                types.addAll(rows.get(rowNum));
            } else {
                // All other rows contain the values of the variables
                arrayNode.add(rowToObj(types, paths, rows.get(rowNum), null));
            }
        }
        return arrayNode;
    }

    protected Object rowToObj(ArrayList<String> types, ArrayList<String> paths, ArrayList<String> row, Object node) {
        for (int cellNum = 0; cellNum < row.size(); cellNum++) {
            String cellValue = row.get(cellNum);
            // Empty cells are not added to node
            if (cellValue != null && !cellValue.isEmpty()) {
                String type = types.get(cellNum);
                Object content;
                switch (cellValue) {
                    case "__NULL": content = null; break;
                    case "__N_A":  content = "__N_A"; break;
                    default:
                        switch (type) {
                            case "String":
                                switch (cellValue) {
                                    case "__EMPTY": content = ""; break;
                                    case "__BLANK": content = " "; break;
                                    default:        content = cellValue;
                                }
                                break;
                            case "Boolean": content = Boolean.parseBoolean(cellValue); break;
                            case "Integer": content = Integer.parseInt(cellValue); break;
                            case "Double":  content = Double.parseDouble(cellValue); break;
                            case "Long":    content = Long.parseLong(cellValue); break;
                            case "Date":
                                // Excel considers dates as local dates, but we want them as UTC
                                Instant instant;
                                try {
                                    LocalDateTime localDateTime = LocalDateTime.parse(cellValue, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
                                    instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
                                } catch (DateTimeParseException e) {
                                    LocalDate localDate = LocalDate.parse(cellValue, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
                                    instant = localDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
                                }
                                content = instant.plusSeconds(ZonedDateTime.now().getOffset().getTotalSeconds());
                                break;
                            default:
                                throw new RuntimeException("Variable type not supported: " + type);
                        }
                }
                node = addObj(node, paths.get(cellNum), content);
            }
        }
        return node;
    }

    @SuppressWarnings("unchecked")
    protected Object addObj(Object parent, String path, Object value) {
        if (parent == null) {
            parent = new LinkedHashMap<String, Object>();
        }

        int delimiterPosition = indexOfFirstDelimiter(path, ".*");
        if (delimiterPosition <= 0) {
            if (parent instanceof LinkedHashMap) {
                // Key of a value in an ObjectNode
                ((Map<String, Object>) parent).put(path, value);
            } else if (parent instanceof ArrayList) {
                // Index of a value in an ArrayNode
                int index = Integer.parseInt(path);
                while (((ArrayList<Object>) parent).size() <= index) {
                    // Create empty entries to parent ArrayNode
                    ((ArrayList<Object>) parent).add(new LinkedHashMap<String, Object> ());
                }
                ((ArrayList<Object>) parent).set(index, value);
            } else {
                throw new RuntimeException("Parent type not supported: " + parent.getClass().getName());
            }
            return parent;
        }

        String key = path.substring(0, delimiterPosition);
        String newPath = path.substring(delimiterPosition + 1);
        String delimiter = path.substring(delimiterPosition, delimiterPosition + 1);
        Object child = null;
        if (delimiter.equals(".")) {
            child = new LinkedHashMap<String, Object>();
        } else if (delimiter.equals("*")) {
            child = new ArrayList<>();
        }

        Object node;
        if (parent instanceof LinkedHashMap) {
            // Key of a JsonNode in an ObjectNode
            node = ((Map<String, Object>) parent).get(key);
            if (node != null) {
                // Take the existing ObjectNode as child
                child = node;
            }
            ((Map<String, Object>) parent).put(key, addObj(child, newPath, value));
        } else if (parent instanceof ArrayList) {
            // Index of a JsonNode in an ArrayNode
            int index = Integer.parseInt(key);
            if (((ArrayList<Object>) parent).size() > index) {
                node = ((ArrayList<Object>) parent).get(index);
            } else {
                node = null;
            }
            if (node != null) {
                // Take the existing ArrayNode as child
                child = node;
            }
            while (((ArrayList<Object>) parent).size() <= index) {
                // Create empty entries to parent ArrayNode
                ((ArrayList<Object>) parent).add(new LinkedHashMap<String, Object> ());
            }
            ((ArrayList<Object>) parent).set(index, addObj(child, newPath, value));
        } else {
            throw new RuntimeException("Parent type not supported: " + parent.getClass().getName());
        }
        return parent;
    }

}
