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

import java.awt.geom.Rectangle2D;
import java.util.Collection;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Engine;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
import com.knecon.fforesight.service.layoutparser.processor.utils.IdBuilder;

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

@Data
@Builder
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class RedactionEntity {

    // initial values
    @EqualsAndHashCode.Include
    final Boundary boundary;
    @EqualsAndHashCode.Include
    final String type;
    @EqualsAndHashCode.Include
    final EntityType entityType;

    // empty defaults
    boolean redaction;
    boolean removed;
    boolean ignored;
    boolean resized;
    boolean skipRemoveEntitiesContainedInLarger;
    boolean dictionaryEntry;
    boolean dossierDictionaryEntry;
    Set<Engine> engines;
    Set<RedactionEntity> references;
    @Builder.Default
    Deque<Integer> matchedRules = new LinkedList<>();
    String redactionReason;
    String legalBasis;

    // inferred on graph insertion
    @EqualsAndHashCode.Include
    String value;
    String textBefore;
    String textAfter;
    @Builder.Default
    Set<Page> pages = new HashSet<>();
    List<RedactionPosition> redactionPositionsPerPage;
    @Builder.Default
    List<SemanticNode> intersectingNodes = new LinkedList<>();
    SemanticNode deepestFullyContainingNode;


    public static RedactionEntity initialEntityNode(Boundary boundary, String type, EntityType entityType) {

        return RedactionEntity.builder().type(type).entityType(entityType).boundary(boundary).engines(new HashSet<>()).references(new HashSet<>()).build();
    }


    public boolean occursInNodeOfType(Class<? extends SemanticNode> clazz) {

        return intersectingNodes.stream().anyMatch(clazz::isInstance);
    }


    public boolean occursInNode(SemanticNode semanticNode) {

        return intersectingNodes.stream().anyMatch(node -> node.equals(semanticNode));
    }


    public boolean isType(String type) {

        return this.type.equals(type);
    }


    public boolean isAnyType(List<String> types) {

        return types.contains(type);
    }


    public void addIntersectingNode(SemanticNode containingNode) {

        intersectingNodes.add(containingNode);
    }


    public void removeFromGraph() {

        intersectingNodes.forEach(node -> node.getEntities().remove(this));
        pages.forEach(page -> page.getEntities().remove(this));
        intersectingNodes = new LinkedList<>();
        deepestFullyContainingNode = null;
        pages = new HashSet<>();
        removed = true;
        ignored = true;
    }


    public void addMatchedRule(int ruleNumber) {

        matchedRules.add(ruleNumber);
    }


    public int getMatchedRule() {

        if (matchedRules.isEmpty()) {
            return 0;
        }
        return matchedRules.getLast();
    }


    public List<RedactionPosition> getRedactionPositionsPerPage() {

        if (redactionPositionsPerPage == null || redactionPositionsPerPage.isEmpty()) {
            Map<Page, List<Rectangle2D>> rectanglesPerLinePerPage = deepestFullyContainingNode.getTextBlock().getPositionsPerPage(boundary);

            Page firstPage = rectanglesPerLinePerPage.keySet()
                    .stream()
                    .min(Comparator.comparingInt(Page::getNumber))
                    .orElseThrow(() -> new RuntimeException("No Positions found on any page!"));
            String id = IdBuilder.buildId(pages, rectanglesPerLinePerPage.values().stream().flatMap(Collection::stream).toList());
            redactionPositionsPerPage = rectanglesPerLinePerPage.entrySet().stream().map(entry -> buildRedactionPosition(firstPage, id, entry)).toList();
        }
        return redactionPositionsPerPage;
    }


    private static RedactionPosition buildRedactionPosition(Page firstPage, String id, Map.Entry<Page, List<Rectangle2D>> entry) {

        if (entry.getKey().equals(firstPage)) {
            return new RedactionPosition(id, entry.getKey(), entry.getValue());
        } else {
            return new RedactionPosition(id + "-" + entry.getKey().getNumber(), entry.getKey(), entry.getValue());
        }
    }


    public boolean containedBy(RedactionEntity redactionEntity) {

        return this.boundary.containedBy(redactionEntity.getBoundary());
    }


    public boolean contains(RedactionEntity redactionEntity) {

        return this.boundary.contains(redactionEntity.getBoundary());
    }


    public boolean intersects(RedactionEntity redactionEntity) {

        return this.boundary.intersects(redactionEntity.getBoundary());
    }


    public void addEngine(Engine engine) {

        engines.add(engine);
    }


    public void addEngines(Set<Engine> engines) {

        this.engines.addAll(engines);
    }


    public void addReference(RedactionEntity reference) {

        references.add(reference);
    }


    public void addReferences(List<RedactionEntity> references) {

        this.references.addAll(references);
    }


    public boolean matchesAnnotationId(String manualRedactionId) {

        return getRedactionPositionsPerPage().stream().anyMatch(entityPosition -> entityPosition.getId().equals(manualRedactionId));
    }


    @Override
    public String toString() {

        StringBuilder sb = new StringBuilder();
        sb.append("Entity[\"");
        sb.append(value);
        sb.append("\", ");
        sb.append(boundary);
        sb.append(", pages[");
        pages.forEach(page -> {
            sb.append(page.getNumber());
            sb.append(", ");
        });
        sb.delete(sb.length() - 2, sb.length());
        sb.append("], type = \"");
        sb.append(type);
        sb.append("\", EntityType.");
        sb.append(entityType);
        sb.append("]");
        return sb.toString();
    }

}
