package lc.mecha.math;

import lc.mecha.log.MechaLogger;
import lc.mecha.log.MechaLoggerFactory;
import lc.mecha.util.StringAccumulatorV2;

import java.io.Serializable;
import java.util.*;

/**
 * A set of integer ranges.
 *
 * @author Alex Leigh
 * @since mk14 mod10 (CRYSTAL)
 */
public class IntIntervals implements Serializable {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(IntIntervals.class);
    private final TreeSet<IntInterval> ranges = new TreeSet<>();
    private boolean isCompacted = false;

    public IntIntervals(String str) {
        String s[] = str.split(",");
        for (String x : s) {
            add(new IntInterval(x));
        }
    }

    public IntIntervals() {
        // Intentionally Blank
    }

    public boolean isCompacted() {
        return isCompacted;
    }

    /**
     * Return a {@link Set} of each distinct integer stored in this object. The integers are returned in no
     * particular order.
     */
    public Set<Integer> getIntegers() {
        HashSet<Integer> ints = new HashSet<>();
        for (IntInterval iv : ranges) {
            for (int i = iv.getStartIndex(); i <= iv.getStopIndex(); i++) {
                ints.add(i);
            }
        }
        return ints;
    }

    /**
     * Return the size of the intervals in terms of the number of integers represented.
     * <p>
     * Note: If the object has not been compacted, this will double-count any overlapping intervals. Depending on
     * your outlook this could be a bug or a feature.
     *
     * @return The number of integers represented.
     */
    public int size() {
        int x = 0;
        for (IntInterval range : ranges) {
            x += range.size();
        }
        return x;
    }

    public Set<IntInterval> getIntervals() {
        return ranges;
    }

    /**
     * Returns <code>true</code> if only one integer is selected.
     */
    public boolean isSingle() {
        compact();
        logger.info("IS SINGLE: {}", ranges);
        if (ranges.size() > 1) return false;
        IntInterval first = ranges.first();
        if (first != null && first.getStartIndex() == first.getStopIndex()) return true;
        return false;
    }

    public int getFirst() {
        if (!isSingle()) throw new IllegalStateException("Multiple selected.");
        IntInterval first = ranges.first();
        return first.getStartIndex();
    }

    public Integer getMax() {
        Integer x = null;

        for (IntInterval iv : ranges) {
            if (x == null) {
                x = iv.getStopIndex();
            } else {
                int y = iv.getStopIndex();
                if (y > x) x = y;
            }
        }

        return x;
    }

    public IntIntervals add(IntInterval range) {
        isCompacted = false;

        ranges.add(range);
        return this;
    }

    /**
     * Add the given range to the set of ranges.
     */
    public IntIntervals add(int startIdx, int stopIdx) {
        isCompacted = false;

        ranges.add(new IntInterval(startIdx, stopIdx));
        return this;
    }

    /**
     * Add all the ranges in the provided ranges object to this ranges object.
     */
    public IntIntervals addAll(IntIntervals ranges) {
        isCompacted = false;

        for (IntInterval r : ranges.getIntervals()) {
            add(r.getStartIndex(), r.getStopIndex());
        }
        return this;
    }

    public void remove(int startIdx, int stopIdx) {
        isCompacted = false;

        for (int i = startIdx; i <= stopIdx; i++) {
            ArrayList<IntInterval> remove = new ArrayList<>();
            ArrayList<IntInterval> add = new ArrayList<>();

            // This supports multiple ranges which happen to match
            for (IntInterval range : ranges) {
                // TODO exit early if we are "past" this range, since ranges is sorted
                if (!range.contains(i)) continue;

                remove.add(range);
                add.addAll(range.remove(i));

            }
            ranges.removeAll(remove);
            ranges.addAll(add);
        }
    }

    public boolean contains(int idx) {
        for (IntInterval r : ranges) {
            if (r.contains(idx)) return true;
        }
        return false;
    }

    @Override
    public String toString() {
        return "Ranges{" +
                "ranges=" + ranges +
                '}';
    }

    /**
     * Compact the Ranges set so that overlapping ranges are replaced with contiguous ranges.
     */
    public void compact() {
        if (ranges.size() == 0) return;
        if (isCompacted) return;

        ArrayList<IntInterval> compact = new ArrayList<>();

        IntInterval last = null;

        for (IntInterval r : ranges) {
            if (last == null) {
                last = r;
            } else {
                if (last.getStopIndex() >= r.getStartIndex() - 1) {
                    // Contiguous
                    last = new IntInterval(last.getStartIndex(), Math.max(last.getStopIndex(), r.getStopIndex()));
                } else {
                    // Not contiguous
                    compact.add(last);
                    last = r;
                }
            }
        }

        compact.add(last);

        this.ranges.clear();
        this.ranges.addAll(compact);
    }

    public void flip(int startIdx, int stopIdx) {
        isCompacted = false;

        for (int i = startIdx; i <= stopIdx; i++) {
            if (contains(i))
                remove(i, i);
            else
                add(i, i);
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        IntIntervals that = (IntIntervals) o;
        return Objects.equals(ranges, that.ranges);
    }

    @Override
    public int hashCode() {
        return Objects.hash(ranges);
    }

    public void clear() {
        ranges.clear();
    }

    /**
     * Convert the ranges into a string form.
     */
    public String asString() {
        StringAccumulatorV2 sa = new StringAccumulatorV2(",");
        for (IntInterval r : ranges) {
            sa.add(r.asString());
        }
        return sa.asString();
    }


    public static boolean equals(IntIntervals o1, IntIntervals o2) {
        if (o1 != null) o1.compact();
        if (o2 != null) o2.compact();

        return Objects.equals(o1, o2);
    }
}
