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

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 com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPositionData;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentTextData;
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.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
    Boundary boundary;
    String searchText;
    @Builder.Default
    List<Integer> lineBreaks = new ArrayList<>();
    @Builder.Default
    List<Boundary> boldTextBoundaries = new ArrayList<>();
    @Builder.Default
    List<Boundary> 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;
    }


    public static AtomicTextBlock fromSearchTextWithTextPosition(String searchText,
                                                                 List<Integer> lineBreaks,
                                                                 List<Boundary> boldTextBoundaries,
                                                                 List<Boundary> 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)
                .boundary(new Boundary(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)
                .boundary(new Boundary(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)
                .boundary(new Boundary(documentTextData.getStart(), documentTextData.getEnd()))
                .searchText(documentTextData.getSearchText())
                .lineBreaks(Arrays.stream(documentTextData.getLineBreaks()).boxed().toList())
                .stringIdxToPositionIdx(Arrays.stream(documentPositionData.getStringIdxToPositionIdx()).boxed().toList())
                .positions(toRectangle2DList(documentPositionData.getPositions()))
                .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();
    }


    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) {
            return subSequence(boundary.start(), lineBreaks.get(0) + boundary.start());
        } else if (lineNumber == numberOfLines() - 1) {
            return subSequence(lineBreaks.get(lineBreaks.size() - 1) + boundary.start(), boundary.end());
        }
        return subSequence(lineBreaks.get(lineNumber - 1) + boundary.start(), lineBreaks.get(lineNumber) + boundary.start());
    }


    @Override
    public List<AtomicTextBlock> getAtomicTextBlocks() {

        return List.of(this);
    }


    @Override
    public int getNextLinebreak(int fromIndex) {

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


    @Override
    public int getPreviousLinebreak(int fromIndex) {

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


    @Override
    public Rectangle2D getPosition(int stringIdx) {

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


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

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

        int startPositionIdx = stringIdxToPositionIdx.get(stringBoundary.start() - this.boundary.start());

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

        return positions.subList(startPositionIdx, stringIdxToPositionIdx.get(stringBoundary.end() - this.boundary.start()));

    }


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

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


    private List<Integer> getAllLineBreaksInBoundary(Boundary boundary) {

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


    @Override
    public String toString() {

        return searchText;
    }

}
