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

import static java.lang.String.format;

import java.awt.geom.Rectangle2D;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;

import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;

@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class ConcatenatedTextBlock implements TextBlock {

    List<AtomicTextBlock> atomicTextBlocks;
    String searchText;
    Boundary boundary;


    public static ConcatenatedTextBlock empty() {

        return new ConcatenatedTextBlock(Collections.emptyList());
    }


    public ConcatenatedTextBlock(List<AtomicTextBlock> atomicTextBlocks) {

        this.atomicTextBlocks = new LinkedList<>();
        if (atomicTextBlocks.isEmpty()) {
            boundary = new Boundary(-1, -1);
            return;
        }
        var firstTextBlock = atomicTextBlocks.get(0);
        this.atomicTextBlocks.add(firstTextBlock);
        boundary = new Boundary(firstTextBlock.getBoundary().start(), firstTextBlock.getBoundary().end());

        atomicTextBlocks.subList(1, atomicTextBlocks.size()).forEach(this::concat);
    }


    public ConcatenatedTextBlock concat(TextBlock textBlock) {

        if (this.atomicTextBlocks.isEmpty()) {
            boundary.setStart(textBlock.getBoundary().start());
            boundary.setEnd(textBlock.getBoundary().end());
        } else if (boundary.end() != textBlock.getBoundary().start()) {
            throw new UnsupportedOperationException(format("Can only concat consecutive TextBlocks, trying to concat %s and %s", boundary, textBlock.getBoundary()));
        }
        this.atomicTextBlocks.addAll(textBlock.getAtomicTextBlocks());
        boundary.setEnd(textBlock.getBoundary().end());
        this.searchText = null;
        return this;
    }


    private AtomicTextBlock getAtomicTextBlockByStringIndex(int stringIdx) {

        return atomicTextBlocks.stream().filter(textBlock -> textBlock.getBoundary().contains(stringIdx)).findAny().orElseThrow(IndexOutOfBoundsException::new);
    }


    private List<AtomicTextBlock> getAllAtomicTextBlocksPartiallyInStringBoundary(Boundary boundary) {

        return atomicTextBlocks.stream().filter(tb -> tb.getBoundary().intersects(boundary)).toList();
    }


    @Override
    public String getSearchText() {

        if (searchText == null) {
            StringBuilder sb = new StringBuilder();
            getAtomicTextBlocks().forEach(atb -> sb.append(atb.getSearchText()));
            searchText = sb.toString();
        }
        return searchText;
    }


    @Override
    public int numberOfLines() {

        return atomicTextBlocks.stream().map(AtomicTextBlock::getLineBreaks).mapToInt(List::size).sum();
    }


    @Override
    public int getNextLinebreak(int fromIndex) {

        return getAtomicTextBlockByStringIndex(fromIndex).getNextLinebreak(fromIndex);
    }


    @Override
    public int getPreviousLinebreak(int fromIndex) {

        return getAtomicTextBlockByStringIndex(fromIndex).getPreviousLinebreak(fromIndex);
    }


    @Override
    public List<Integer> getLineBreaks() {

        return getAtomicTextBlocks().stream().flatMap(atomicTextBlock -> atomicTextBlock.getLineBreaks().stream()).toList();
    }


    @Override
    public Rectangle2D getPosition(int stringIdx) {

        return getAtomicTextBlockByStringIndex(stringIdx).getPosition(stringIdx);
    }


    @Override
    public List<Rectangle2D> getPositions(Boundary stringBoundary) {

        List<AtomicTextBlock> textBlocks = getAllAtomicTextBlocksPartiallyInStringBoundary(stringBoundary);

        if (textBlocks.size() == 1) {
            return textBlocks.get(0).getPositions(stringBoundary);
        }

        AtomicTextBlock firstTextBlock = textBlocks.get(0);
        List<Rectangle2D> positions = new LinkedList<>(firstTextBlock.getPositions(new Boundary(stringBoundary.start(), firstTextBlock.getBoundary().end())));

        for (AtomicTextBlock textBlock : textBlocks.subList(1, textBlocks.size() - 1)) {
            positions.addAll(textBlock.getPositions());
        }

        var lastTextBlock = textBlocks.get(textBlocks.size() - 1);
        positions.addAll(lastTextBlock.getPositions(new Boundary(lastTextBlock.getBoundary().start(), stringBoundary.end())));

        return positions;
    }


    @Override
    public Map<Page, List<Rectangle2D>> getPositionsPerPage(Boundary stringBoundary) {

        List<AtomicTextBlock> textBlocks = getAllAtomicTextBlocksPartiallyInStringBoundary(stringBoundary);

        if (textBlocks.size() == 1) {
            return textBlocks.get(0).getPositionsPerPage(stringBoundary);
        }

        AtomicTextBlock firstTextBlock = textBlocks.get(0);
        Map<Page, List<Rectangle2D>> rectanglesPerLinePerPage = firstTextBlock.getPositionsPerPage(new Boundary(stringBoundary.start(), firstTextBlock.getBoundary().end()));

        for (AtomicTextBlock textBlock : textBlocks.subList(1, textBlocks.size() - 1)) {
            rectanglesPerLinePerPage = mergeEntityPositionsWithSamePageNode(rectanglesPerLinePerPage, textBlock.getPositionsPerPage(textBlock.getBoundary()));
        }

        AtomicTextBlock lastTextBlock = textBlocks.get(textBlocks.size() - 1);
        rectanglesPerLinePerPage = mergeEntityPositionsWithSamePageNode(rectanglesPerLinePerPage,
                lastTextBlock.getPositionsPerPage(new Boundary(lastTextBlock.getBoundary().start(), stringBoundary.end())));

        return rectanglesPerLinePerPage;
    }


    private Map<Page, List<Rectangle2D>> mergeEntityPositionsWithSamePageNode(Map<Page, List<Rectangle2D>> map1, Map<Page, List<Rectangle2D>> map2) {

        Map<Page, List<Rectangle2D>> mergedMap = new HashMap<>(map1);
        map2.forEach((pageNode, rectangles) -> mergedMap.merge(pageNode, rectangles, (l1, l2) -> Stream.concat(l1.stream(), l2.stream()).toList()));
        return mergedMap;
    }


    @Override
    public String toString() {

        return getSearchText();
    }


    @Override
    public List<Boundary> getBoldTextBoundaries() {

        return getAtomicTextBlocks().stream().map(AtomicTextBlock::getBoldTextBoundaries).flatMap(Collection::stream).toList();
    }


    @Override
    public List<Boundary> getItalicTextBoundaries() {

        return getAtomicTextBlocks().stream().map(AtomicTextBlock::getItalicTextBoundaries).flatMap(Collection::stream).toList();
    }


    @Override
    public String getOrientation() {

        if (atomicTextBlocks.isEmpty()) {
            return "";
        }
        return atomicTextBlocks.get(0).getOrientation();
    }


    @Override
    public int getTextDirection() {

        if (atomicTextBlocks.isEmpty()) {
            return 0;
        }
        return atomicTextBlocks.get(0).getTextDirection();
    }

}
