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

import static com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentTextDataProto.DocumentTextData;
import static java.lang.String.format;

import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPositionDataProto.DocumentPositionData;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPositionDataProto.DocumentPositionData.Position;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.TextRange;
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.RectangleTransformations;

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)
public class AtomicTextBlock implements TextBlock {

    Long id;
    Integer numberOnPage;
    Page page;

    //string coordinates
    TextRange textRange;
    String searchText;
    @Builder.Default
    List<Integer> lineBreaks = new ArrayList<>();
    @Builder.Default
    List<TextRange> boldTextBoundaries = new ArrayList<>();
    @Builder.Default
    List<TextRange> italicTextBoundaries = new ArrayList<>();
    String orientation;
    int textDirection;

    //position coordinates
    @Builder.Default
    List<Integer> stringIdxToPositionIdx = new ArrayList<>();
    @Builder.Default
    List<Rectangle2D> positions = new ArrayList<>();

    @EqualsAndHashCode.Exclude
    SemanticNode parent;


    @Override
    public int numberOfLines() {

        return lineBreaks.size() + 1;
    }


    @Override
    public String subSequenceWithLineBreaks(TextRange stringTextRange) {

        if (stringTextRange.length() == 0 || !getTextRange().contains(stringTextRange)) {
            return "";
        }

        Set<Integer> lbInBoundary = lineBreaks.stream()
                .map(i -> i + this.textRange.start())
                .filter(stringTextRange::contains)
                .collect(Collectors.toSet());
        if (stringTextRange.end() == getTextRange().end()) {
            lbInBoundary.add(getTextRange().end());
        }
        StringBuilder sb = new StringBuilder();
        for (int i = stringTextRange.start(); i < stringTextRange.end(); i++) {
            char character = this.charAt(i);
            if (lbInBoundary.contains(i + 1)) {
                // always plus one, due to the linebreaks being an exclusive end index
                if (!Character.isWhitespace(character)) {
                    lbInBoundary.remove(i + 1);
                    lbInBoundary.add(i + 2);
                    sb.append(character);
                    continue;
                }
                sb.append("\n");
            } else {
                sb.append(character);
            }
        }
        return sb.toString();
    }


    public static AtomicTextBlock fromSearchTextWithTextPosition(String searchText,
                                                                 List<Integer> lineBreaks,
                                                                 List<TextRange> boldTextBoundaries,
                                                                 List<TextRange> italicTextBoundaries,
                                                                 List<Rectangle2D> positions,
                                                                 List<Integer> stringIdxToPositionIdx,
                                                                 long idx,
                                                                 SemanticNode parent,
                                                                 int numberOnPage,
                                                                 Page page,
                                                                 int offset,
                                                                 String orientation,
                                                                 int textDirection) {

        return AtomicTextBlock.builder()
                .id(idx)
                .parent(parent)
                .searchText(searchText)
                .numberOnPage(numberOnPage)
                .page(page)
                .lineBreaks(lineBreaks)
                .boldTextBoundaries(boldTextBoundaries)
                .italicTextBoundaries(italicTextBoundaries)
                .positions(positions)
                .stringIdxToPositionIdx(stringIdxToPositionIdx)
                .textRange(new TextRange(offset, offset + searchText.length()))
                .textDirection(textDirection)
                .orientation(orientation)
                .build();
    }


    public static AtomicTextBlock empty(Long textBlockIdx, int stringOffset, Page page, int numberOnPage, SemanticNode parent) {

        return AtomicTextBlock.builder()
                .id(textBlockIdx)
                .textRange(new TextRange(stringOffset, stringOffset))
                .searchText("")
                .page(page)
                .numberOnPage(numberOnPage)
                .parent(parent)
                .build();
    }


    public static AtomicTextBlock fromAtomicTextBlockData(DocumentTextData documentTextData, DocumentPositionData documentPositionData, SemanticNode parent, Page page) {

        return AtomicTextBlock.builder()
                .id(documentTextData.getId())
                .numberOnPage(documentTextData.getNumberOnPage())
                .page(page)
                .textRange(new TextRange(documentTextData.getStart(), documentTextData.getEnd()))
                .searchText(documentTextData.getSearchText())
                .lineBreaks(documentTextData.getLineBreaksList())
                .stringIdxToPositionIdx(documentPositionData.getStringIdxToPositionIdxList())
                .positions(toRectangle2DList(documentPositionData.getPositionsList()))
                .parent(parent)
                .build();
    }


    private static List<Rectangle2D> toRectangle2DList(float[][] positions) {

        return Arrays.stream(positions)
                .map(floatArr -> (Rectangle2D) new Rectangle2D.Float(floatArr[0], floatArr[1], floatArr[2], floatArr[3]))
                .toList();
    }


    private static List<Rectangle2D> toRectangle2DList(List<Position> positions) {

        return positions.stream()
                .map(pos -> (Rectangle2D) new Rectangle2D.Float(pos.getValue(0), pos.getValue(1), pos.getValue(2), pos.getValue(3)))
                .toList();
    }


    public CharSequence getLine(int lineNumber) {

        if (lineNumber >= numberOfLines() || lineNumber < 0) {
            throw new IndexOutOfBoundsException(format("line %d out of range for AtomicTextBlock with %d lines", lineNumber, numberOfLines()));
        }
        if (lineNumber == 0) {
            if (lineBreaks.isEmpty()) {
                return searchText;
            }
            return subSequence(textRange.start(), lineBreaks.get(0) + textRange.start());
        } else if (lineNumber == numberOfLines() - 1) {
            return subSequence(lineBreaks.get(lineBreaks.size() - 1) + textRange.start(), textRange.end());
        }
        return subSequence(lineBreaks.get(lineNumber - 1) + textRange.start(), lineBreaks.get(lineNumber) + textRange.start());
    }


    @Override
    public List<AtomicTextBlock> getAtomicTextBlocks() {

        return List.of(this);
    }


    @Override
    public int getNextLinebreak(int fromIndex) {

        return lineBreaks.stream()//
                       .filter(linebreak -> linebreak > fromIndex - textRange.start()) //
                       .findFirst() //
                       .orElse(searchText.length()) + textRange.start();
    }


    @Override
    public int getPreviousLinebreak(int fromIndex) {

        return lineBreaks.stream()//
                       .filter(linebreak -> linebreak <= fromIndex - textRange.start())//
                       .reduce((a, b) -> b)//
                       .orElse(0) + textRange.start();
    }


    @Override
    public Rectangle2D getPosition(int stringIdx) {

        return positions.get(stringIdxToPositionIdx.get(stringIdx - textRange.start()));
    }


    @Override
    public List<Rectangle2D> getPositions(TextRange stringTextRange) {

        if (!containsBoundary(stringTextRange)) {
            throw new IndexOutOfBoundsException(format("%s is out of bounds for %s", stringTextRange, this.textRange));
        }
        if (stringTextRange.length() == 0) {
            return Collections.emptyList();
        }

        int startPositionIdx = stringIdxToPositionIdx.get(stringTextRange.start() - this.textRange.start());

        if (stringTextRange.end() == this.textRange.end()) {
            return positions.subList(startPositionIdx, positions.size());
        }

        return positions.subList(startPositionIdx, stringIdxToPositionIdx.get(stringTextRange.end() - this.textRange.start()));

    }


    public Map<Page, List<Rectangle2D>> getPositionsPerPage(TextRange stringTextRange) {

        List<Rectangle2D> rectanglesPerLine = stringTextRange.split(getAllLineBreaksInBoundary(stringTextRange))
                .stream()
                .map(this::getPositions)
                .map(RectangleTransformations::rectangleBBoxWithGaps)
                .flatMap(Collection::stream)
                .toList();
        Map<Page, List<Rectangle2D>> rectanglePerLinePerPage = new HashMap<>();
        rectanglePerLinePerPage.put(page, rectanglesPerLine);
        return rectanglePerLinePerPage;
    }


    protected List<Integer> getAllLineBreaksInBoundary(TextRange textRange) {

        return getLineBreaks().stream()
                .map(linebreak -> linebreak + this.textRange.start())
                .filter(textRange::contains)
                .toList();
    }


    @Override
    public String toString() {

        return searchText;
    }

}
