import ceylon.time.base { Range, UnitOfDate, milliseconds, UnitOfTime, UnitOfYear, UnitOfMonth, UnitOfDay, UnitOfHour, UnitOfMinute, UnitOfSecond, UnitOfMillisecond }
import ceylon.time.internal { _gap = gap, _overlap = overlap }

"Implementation of [[Range]] and allows easy iteration between [[DateTime]] types.
 
 Provides all power of [[Iterable]] features and complements with:
 * Easy way to recover [[Period]]
 * Easy way to recover [[Duration]]
 * Recover the overlap between [[DateTimeRange]] types
 * Recover the gap between [[DateTimeRange]] types
 * Allows customized way to iterate as navigate between values by [[UnitOfDate]] or [[UnitOfTime]] cases
 "
shared class DateTimeRange( from, to, step = milliseconds ) satisfies Range<DateTime, UnitOfDate|UnitOfTime> {

    "The first Element returned by the iterator, if any.
     This should always produce the same value as
     `iterable.iterator().head`.
     It also represents the _caller_ that created the Range:
     
     Example: today().to(tomorrow) -> in this case today() is the caller/creator of the range."
    shared actual DateTime from;

    "The limit of the Range where. 

     Example:

     Given: today().to(tomorrow) then tomorrow is the _to_ element.
     
     Given: tomorrow.to(today()) then today() is the _to_ element."
    shared actual DateTime to;

    "Customized way to iterate over each element, it does not interfer in _from_
     and _to_ fields, but it does not guarantee that _to_ will be included in iterator."
    shared actual UnitOfDate|UnitOfTime step;

    "Returns the Period between _from_ and _to_ fields.

     Example: 
     
     Given: today().to(tomorrow).duration then duration is 1 day.
     
     Given: tomorrow().to(today).duration then duration is -1 day."
    shared actual Period period => from.periodTo(to);    

    "Returns the Duration between _from_ and _to_ fields.

     Example: 
     
     Given: today().to(tomorrow).duration then duration is 86400000 milliseconds.
     
     Given: tomorrow().to(today).duration then duration is -86400000 milliseconds."
    shared actual Duration duration  =>
        Duration(to.instant().millisecondsOfEpoch - from.instant().millisecondsOfEpoch);

    "Returns true if both: this and other are same type and have equal fields _from_ and _to_."
    shared actual Boolean equals( Object other ) => (super of Range<DateTime, UnitOfDate|UnitOfTime>).equals(other); 

    "This implementation respect the constraint that if `x==y` then `x.hash==y.hash`."
    shared actual Integer hash => (super of Range<DateTime, UnitOfDate|UnitOfTime>).hash;

    "Returns empty or a new Range:
     - Each Range is considered a _set_ then [A..B] is equivalent to [B..A] 
     - The precision is based on the lowest unit 
     - When the new Range exists it will follow these rules:\n
     Given: [A..B] overlap [C..D]\n 
     When: AB < CD\n
         [1..6] overlap [3..9] = [3,6]\n
         [1..6] overlap [9..3] = [3,6]\n
         [6..1] overlap [3..9] = [3,6]\n
         [6..1] overlap [9..3] = [3,6]\n\n

     Given: [A..B] overlap [C..D]\n 
     When: AB > CD\n
         [3..9] overlap [1..6] = [3,6]\n
         [3..9] overlap [6..1] = [3,6]\n
         [9..3] overlap [1..6] = [3,6]\n
         [9..3] overlap [6..1] = [3,6]"
    shared actual DateTimeRange|Empty overlap(Range<DateTime, UnitOfDate|UnitOfTime> other) {
        value response = _overlap([from,to], [other.from, other.to]);
        if ( is [DateTime,DateTime] response) {
            return DateTimeRange(*response);
        }
        assert( is Empty response);
        return response;
    }

    "Returns empty or a new Range:
     - Each Range is considered a _set_ then [A..B] is equivalent to [B..A] 
     - The precision is based on the lowest unit 
     - When the new Range exists it will follow these rules:\n
     Given: [A..B] gap [C..D]\n 
     When: AB < CD\n
         [1..2] gap [5..6] = (2,5)\n
         [1..2] gap [6..5] = (2,5)\n
         [2..1] gap [5..6] = (2,5)\n
         [2..1] gap [6..5] = (2,5)\n\n

     Given: [A..B] gap [C..D]\n 
     When: AB > CD\n
         [5..6] gap [1..2] = (2,5)\n
         [5..6] gap [2..1] = (2,5)\n
         [6..5] gap [1..2] = (2,5)\n
         [6..5] gap [2..1] = (2,5)"
    shared actual DateTimeRange|Empty gap( Range<DateTime, UnitOfDate|UnitOfTime> other ) {
        value response = _gap([from,to], [other.from, other.to]);
        switch( response )
        case( is [DateTime,DateTime] ) {
            return response[0].successor < response[1] 
                       then DateTimeRange(response[0].successor, response[1].predecessor)
                       else [];
        }
        case( is Empty ) {
            return response;
        }
    }

    "An iterator for the elements belonging to this 
     container. where each jump is based on actual step of this Range."
    shared actual Iterator<DateTime> iterator()  {
        object listIterator satisfies Iterator<DateTime> {
            variable Integer count = 0;
            shared actual DateTime|Finished next() {
                value date = from > to then previousByStep(count++) else nextByStep(count++);
                value continueLoop = from <= to then date <= to else date >= to;
                return continueLoop then date else finished;
            }
        }
        return listIterator;
    }

    "Define how this Range will get next or previous element while iterating."
    shared actual DateTimeRange stepBy( UnitOfDate|UnitOfTime step ) {
        return step == this.step then this 
               else DateTimeRange(from, to, step);
    }

    "The iteration for each element should always start from same point,
     this way is possible to not suffer with different number of days in months."
    DateTime nextByStep( Integer jump = 1 ) {
        if ( is UnitOfDate step ) {
            switch( step )
            case( is UnitOfYear )  { return from.plusYears(jump); }
            case( is UnitOfMonth ) { return from.plusMonths(jump); }
            case( is UnitOfDay )   { return from.plusDays(jump); }
        }
        if ( is UnitOfTime step ) {
            switch( step )
            case( is UnitOfHour )  { return from.plusHours(jump); }
            case( is UnitOfMinute ) { return from.plusMinutes(jump); }
            case( is UnitOfSecond )   { return from.plusSeconds(jump); }
            case( is UnitOfMillisecond )   { return from.plusMilliseconds(jump); }
        }
        throw;
    }

    "The iteration for each element should always start from same point,
     this way is possible to not suffer with different number of days in months."
    DateTime previousByStep( Integer jump = 1 ) {
        if ( is UnitOfDate step ) {
            switch( step )
            case( is UnitOfYear )  { return from.minusYears(jump); }
            case( is UnitOfMonth ) { return from.minusMonths(jump); }
            case( is UnitOfDay )   { return from.minusDays(jump); }
        }
        if ( is UnitOfTime step ) {
            switch( step )
            case( is UnitOfHour )  { return from.minusHours(jump); }
            case( is UnitOfMinute ) { return from.minusMinutes(jump); }
            case( is UnitOfSecond )   { return from.minusSeconds(jump); }
            case( is UnitOfMillisecond )   { return from.minusMilliseconds(jump); }
        }
        throw;
    }

}