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

import static java.lang.String.format;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.IntStream;

import lombok.EqualsAndHashCode;
import lombok.Setter;

@Setter
@EqualsAndHashCode
@SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName")
public class TextRange implements Comparable<TextRange> {

    private int start;
    private int end;


    public TextRange(int start, int end) {

        if (start > end) {
            throw new IllegalArgumentException(format("start: %d > end: %d", start, end));
        }
        this.start = start;
        this.end = end;
    }


    public int length() {

        return end - start;
    }


    public int start() {

        return start;
    }


    public int end() {

        return end;
    }


    public boolean contains(TextRange textRange) {

        return start <= textRange.start() && textRange.end() <= end;
    }


    public boolean containedBy(TextRange textRange) {

        return textRange.contains(this);
    }


    public boolean contains(int start, int end) {

        if (start > end) {
            throw new IllegalArgumentException(format("start: %d > end: %d", start, end));
        }
        return this.start <= start && end <= this.end;
    }


    public boolean containedBy(int start, int end) {

        if (start > end) {
            throw new IllegalArgumentException(format("start: %d > end: %d", start, end));
        }
        return start <= this.start && this.end <= end;
    }


    public boolean contains(int index) {

        return start <= index && index <= end;
    }


    public boolean containsExclusive(int index) {

        return start <= index && index < end;
    }


    public boolean intersects(TextRange textRange) {

        return textRange.start() < this.end && this.start < textRange.end();
    }


    public List<TextRange> split(List<Integer> splitIndices) {

        if (splitIndices.stream()
                .anyMatch(idx -> !this.containsExclusive(idx))) {
            throw new IndexOutOfBoundsException(format("%s splitting indices are out of range for %s",
                                                       splitIndices.stream()
                                                               .filter(idx -> !this.containsExclusive(idx))
                                                               .toList(),
                                                       this));
        }
        List<TextRange> splitBoundaries = new LinkedList<>();
        int previousIndex = start;
        for (int splitIndex : splitIndices) {

            // skip split if it would produce a boundary of length 0
            if (splitIndex == previousIndex) {
                continue;
            }
            splitBoundaries.add(new TextRange(previousIndex, splitIndex));
            previousIndex = splitIndex;
        }
        splitBoundaries.add(new TextRange(previousIndex, end));
        return splitBoundaries;
    }


    public IntStream intStream() {

        return IntStream.range(start, end);
    }


    public static TextRange merge(Collection<TextRange> boundaries) {

        int minStart = boundaries.stream()
                .mapToInt(TextRange::start)
                .min()
                .orElseThrow(IllegalArgumentException::new);
        int maxEnd = boundaries.stream()
                .mapToInt(TextRange::end)
                .max()
                .orElseThrow(IllegalArgumentException::new);
        return new TextRange(minStart, maxEnd);
    }


    @Override
    public String toString() {

        return format("Boundary [%d|%d)", start, end);
    }


    @Override
    public int compareTo(TextRange textRange) {

        if (end < textRange.end() && start < textRange.start()) {
            return -1;
        }
        if (start > textRange.start() && end > textRange.end()) {
            return 1;
        }

        return 0;
    }

}
