package com.knecon.fforesight.service.layoutparser.processor.model.graph;

import static java.lang.String.format;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Stream;

import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.GenericSemanticNode;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Table;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.TableCell;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlockCollector;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.experimental.FieldDefaults;

@Data
@EqualsAndHashCode
public class DocumentTree {

    private final Entry root;


    public DocumentTree(Document document) {

        root = Entry.builder().treeId(Collections.emptyList()).children(new LinkedList<>()).node(document).build();
    }


    public TextBlock buildTextBlock() {

        return allEntriesInOrder().map(Entry::getNode).filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock).collect(new TextBlockCollector());
    }


    public List<Integer> createNewMainEntryAndReturnId(GenericSemanticNode node) {

        return createNewChildEntryAndReturnIdImpl(Collections.emptyList(), node);
    }


    public List<Integer> createNewChildEntryAndReturnId(GenericSemanticNode parentNode, GenericSemanticNode node) {

        return createNewChildEntryAndReturnIdImpl(parentNode.getTreeId(), node);
    }


    public List<Integer> createNewChildEntryAndReturnId(GenericSemanticNode parentNode, Table node) {

        return createNewChildEntryAndReturnIdImpl(parentNode.getTreeId(), node);
    }


    public List<Integer> createNewTableChildEntryAndReturnId(Table parentTable, TableCell tableCell) {

        return createNewChildEntryAndReturnIdImpl(parentTable.getTreeId(), tableCell);
    }


    @SuppressWarnings("PMD.UnusedPrivateMethod") // PMD actually flags this wrong
    private List<Integer> createNewChildEntryAndReturnIdImpl(List<Integer> parentId, SemanticNode node) {

        if (!entryExists(parentId)) {
            throw new IllegalArgumentException(format("parentId %s does not exist!", parentId));
        }

        Entry parent = getEntryById(parentId);
        List<Integer> newId = new LinkedList<>(parentId);
        newId.add(parent.children.size());
        parent.children.add(Entry.builder().treeId(newId).node(node).build());

        return newId;
    }


    private boolean entryExists(List<Integer> treeId) {

        if (treeId.isEmpty()) {
            return root != null;
        }
        Entry entry = root.children.get(treeId.get(0));
        for (int id : treeId.subList(1, treeId.size())) {
            if (id >= entry.children.size() || 0 > id) {
                return false;
            }
            entry = entry.children.get(id);
        }
        return true;
    }


    public Entry getParentEntryById(List<Integer> treeId) {

        return getEntryById(getParentId(treeId));
    }


    public boolean hasParentById(List<Integer> treeId) {

        return !treeId.isEmpty();
    }


    public Stream<SemanticNode> childNodes(List<Integer> treeId) {

        return getEntryById(treeId).children.stream().map(Entry::getNode);
    }


    public Stream<SemanticNode> childNodesOfType(List<Integer> treeId, NodeType nodeType) {

        return getEntryById(treeId).children.stream().filter(entry -> entry.node.getType().equals(nodeType)).map(Entry::getNode);
    }


    private static List<Integer> getParentId(List<Integer> treeId) {

        if (treeId.isEmpty()) {
            throw new UnsupportedOperationException("Root has no parent!");
        }
        if (treeId.size() < 2) {
            return Collections.emptyList();
        }
        return treeId.subList(0, treeId.size() - 1);
    }


    public Entry getEntryById(List<Integer> treeId) {

        if (treeId.isEmpty()) {
            return root;
        }
        Entry entry = root.children.get(treeId.get(0));
        for (int id : treeId.subList(1, treeId.size())) {
            entry = entry.children.get(id);
        }
        return entry;
    }


    public Stream<Entry> mainEntries() {

        return root.children.stream();
    }


    public Stream<Entry> allEntriesInOrder() {

        return Stream.of(root).flatMap(DocumentTree::flatten);
    }


    public Stream<Entry> allSubEntriesInOrder(List<Integer> parentId) {

        return getEntryById(parentId).children.stream().flatMap(DocumentTree::flatten);
    }


    @Override
    public String toString() {

        return String.join("\n", allEntriesInOrder().map(Entry::toString).toList());
    }


    private static Stream<Entry> flatten(Entry entry) {

        return Stream.concat(Stream.of(entry), entry.children.stream().flatMap(DocumentTree::flatten));
    }


    public SemanticNode getHighestParentById(List<Integer> treeId) {

        if (treeId.isEmpty()) {
            return root.node;
        }
        return root.children.get(treeId.get(0)).node;
    }


    @Builder
    @Getter
    @AllArgsConstructor
    @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
    public static class Entry {

        List<Integer> treeId;
        SemanticNode node;
        @Builder.Default
        List<Entry> children = new LinkedList<>();


        @Override
        public String toString() {

            return node.toString();
        }


        public NodeType getType() {

            return node.getType();
        }

    }

}
