package com.bbn.bue.common.strings.offsets;

import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.Ordering;
import com.google.common.collect.Range;
import com.google.common.primitives.Ints;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

public class OffsetRange<OffsetType extends Offset & Comparable<OffsetType>> {

  private final OffsetType startInclusive;
  private final OffsetType endInclusive;

  public static <OffsetType extends Offset & Comparable<OffsetType>> OffsetRange<OffsetType> fromInclusiveEndpoints(
      OffsetType startInclusive, OffsetType endInclusive) {
    checkArgument(startInclusive.asInt() <= endInclusive.asInt());
    return new OffsetRange<OffsetType>(startInclusive, endInclusive);
  }

  private OffsetRange(OffsetType startInclusive, OffsetType endInclusive) {
    this.startInclusive = checkNotNull(startInclusive);
    this.endInclusive = checkNotNull(endInclusive);
  }

  public OffsetType startInclusive() {
    return startInclusive;
  }

  public OffsetType endInclusive() {
    return endInclusive;
  }

  public Range<OffsetType> asRange() {
    return Range.closed(startInclusive, endInclusive);
  }

  public int length() {
    return endInclusive.asInt() - startInclusive().asInt() + 1;
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(startInclusive, endInclusive);
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null || getClass() != obj.getClass()) {
      return false;
    }
    final OffsetRange other = (OffsetRange) obj;
    return Objects.equal(this.startInclusive, other.startInclusive) && Objects
        .equal(this.endInclusive, other.endInclusive);
  }

  public static final <T extends Offset & Comparable<T>> Ordering<OffsetRange<T>> byLengthOrdering() {
    return new Ordering<OffsetRange<T>>() {
      @Override
      public int compare(final OffsetRange<T> left, final OffsetRange<T> right) {
        return Ints.compare(left.length(), right.length());
      }
    };
  }

  private static final <T extends Offset & Comparable<T>> Function<OffsetRange<T>, T> toStartInclusiveFunction() {
    return new Function<OffsetRange<T>, T>() {
      @Override
      public T apply(OffsetRange<T> x) {
        return x.startInclusive();
      }
    };
  }

  private static final <T extends Offset & Comparable<T>> Function<OffsetRange<T>, T> toEndInclusiveFunction() {
    return new Function<OffsetRange<T>, T>() {
      @Override
      public T apply(OffsetRange<T> x) {
        return x.endInclusive();
      }
    };
  }

  public static final <T extends Offset & Comparable<T>> Ordering<OffsetRange<T>> byStartOrdering() {
    return Ordering.<T>natural().onResultOf(OffsetRange.<T>toStartInclusiveFunction());
  }

  public static final <T extends Offset & Comparable<T>> Ordering<OffsetRange<T>> byEndOrdering() {
    return Ordering.<T>natural().onResultOf(OffsetRange.<T>toEndInclusiveFunction());
  }

  public static OffsetRange<CharOffset> charOffsetRange(int startInclusive, int endInclusive) {
    return fromInclusiveEndpoints(CharOffset.asCharOffset(startInclusive),
        CharOffset.asCharOffset(endInclusive));
  }

  public static OffsetRange<ByteOffset> byteOffsetRange(final int startInclusive,
      final int endInclusive) {
    return OffsetRange.fromInclusiveEndpoints(ByteOffset.asByteOffset(startInclusive),
        ByteOffset.asByteOffset(endInclusive));
  }


  /**
   * This returns optional because it is not possible to represent an empty offset span
   */
  public static Optional<OffsetRange<CharOffset>> charOffsetsOfWholeString(String s) {
    if (s.isEmpty()) {
      return Optional.absent();
    }
    return Optional.of(charOffsetRange(0, s.length() - 1));
  }

  public boolean overlaps(OffsetRange<OffsetType> other) {
    return asRange().isConnected(other.asRange());
  }

  public boolean contains(OffsetRange<OffsetType> other) {
    return asRange().encloses(other.asRange());
  }

  public static <OffsetType extends Offset & Comparable<OffsetType>>
  Predicate<OffsetRange<OffsetType>> containedInPredicate(final OffsetRange<OffsetType> container) {
    return new Predicate<OffsetRange<OffsetType>>() {
      @Override
      public boolean apply(OffsetRange<OffsetType> input) {
        return container.contains(input);
      }
    };
  }

  /**
   * Returns this range with its start and end positions clipped to fit within {@code bounds}. If
   * this does not intersect {@code bounds}, returns {@link com.google.common.base.Optional#absent()}
   * .
   */
  public Optional<OffsetRange<OffsetType>> clipToBounds(OffsetRange<OffsetType> bounds) {
    if (bounds.contains(this)) {
      return Optional.of(this);
    }
    if (!bounds.overlaps(this)) {
      return Optional.absent();
    }
    final OffsetType newLowerBound;
    final OffsetType newUpperBound;
    if (bounds.startInclusive().asInt() > startInclusive().asInt()) {
      newLowerBound = bounds.startInclusive();
    } else {
      newLowerBound = startInclusive();
    }
    if (bounds.endInclusive().asInt() < endInclusive().asInt()) {
      newUpperBound = bounds.endInclusive();
    } else {
      newUpperBound = endInclusive();
    }
    return Optional.of(OffsetRange.fromInclusiveEndpoints(newLowerBound, newUpperBound));
  }

  public String toString() {
    return "[" + startInclusive().toString() + "-" + endInclusive().toString() + "]";
  }

}

@@ -4082,32 +4082,544 @@
 sRange());%0A  %7D%0A%0A
+  public Optional%3COffsetRange%3COffsetType%3E%3E intersection(OffsetRange%3COffsetType%3E other) %7B%0A    final Range%3COffsetType%3E meAsRange = asRange();%0A    final Range%3COffsetType%3E otherAsRange = other.asRange();%0A%0A    if (meAsRange.isConnected(otherAsRange)) %7B%0A      final Range%3COffsetType%3E intersectionRange = meAsRange.intersection(otherAsRange);%0A      return Optional.of(fromInclusiveEndpoints(intersectionRange.lowerEndpoint(), intersectionRange.upperEndpoint()));%0A    %7D else %7B%0A      return Optional.absent();%0A    %7D%0A  %7D%0A%0A
   public boolean
@@ -6031,32 +6031,358 @@
 erBound));%0A  %7D%0A%0A
+  public static Function%3COffsetRange%3C?%3E, Integer%3E lengthFunction() %7B%0A    return LengthFunction.INSTANCE;%0A  %7D%0A%0A  private enum LengthFunction implements Function%3COffsetRange%3C?%3E, Integer%3E %7B%0A    INSTANCE %7B%0A      @Override%0A      public Integer apply(final OffsetRange%3C?%3E input) %7B%0A        return input.length();%0A      %7D%0A    %7D;%0A  %7D%0A%0A
   public String 

Add intersection, length function to OffsetRange