import { cmp } from '@silane/datetime';


export class TimeTableError extends Error {
}


function setEquals(a, b) {
    if(a.size !== b.size) return false;
    for(let x of a)
        if(!b.has(x)) return false;
    return true;
}


function setIntersection(a, b) {
    const ret = new Set();
    for(let x of a) {
        if(b.has(x))
            ret.add(x);
    }
    return ret;
}


/**
 * @template T
 */
export class TimeTable {
    //             ---|-----|---|-----
    //  boundaryIdx:  0     1   2
    //  segmentIdx: 0    1    2    3
    constructor() {
        this.clear();
    }

    clear() {
        this._boundaries = /** @type {import('@silane/datetime').DateTime[]} */([]);
        this._segments = [/** @type {Set<T>} */(new Set())];
    }

    get boundaries() { return this._boundaries; }
    get segments() { return this._segments; }

    /**
     * @param {import('@silane/datetime').DateTime} startTime
     * @param {import('@silane/datetime').DateTime} endTime
     * @param {T} item
     */
    add(startTime, endTime, item) {
        this.update(startTime, endTime, [item], []);
    }

    /**
     * @param {import('@silane/datetime').DateTime} startTime
     * @param {import('@silane/datetime').DateTime} endTime
     * @param {T} item
     */
    remove(startTime, endTime, item) {
        this.update(startTime, endTime, [], [item]);
    }

    /**
     * @param {import('@silane/datetime').DateTime} startTime
     * @param {import('@silane/datetime').DateTime} endTime
     * @param {Iterable<T>} itemsToAdd
     * @param {Iterable<T>} itemsToRemove
     */
    update(startTime, endTime, itemsToAdd, itemsToRemove) {
        let startSegmentIdx = this._segments.length - 1;
        for(let [idx, boundary] of this._boundaries.entries()) {
            let c = cmp(startTime, boundary);
            if(c === 0)
                startSegmentIdx = -(idx + 1);
            else if(c < 0)
                startSegmentIdx = idx;
            else
                continue;
            break;
        }
        let startBoundaryIdx;
        if(startSegmentIdx >= 0) {
            this._segments.splice(
                startSegmentIdx, 0, new Set(this._segments[startSegmentIdx]));
            this._boundaries.splice(startSegmentIdx, 0, startTime);
            startBoundaryIdx = startSegmentIdx;
        } else {
            startBoundaryIdx = -startSegmentIdx - 1;
        }

        let endSegmentIdx = this._segments.length - 1;
        for(let [idx, boundary] of this._boundaries.slice(
                startBoundaryIdx).entries()) {
            idx += startBoundaryIdx;
            let c = cmp(endTime, boundary);
            if(c === 0)
                endSegmentIdx = -(idx + 1);
            else if(c < 0)
                endSegmentIdx = idx;
            else
                continue;
            break;
        }
        if(endSegmentIdx === -(startBoundaryIdx + 1))
            throw new TimeTableError('startTimeとendTimeが同じ時刻です');
        if(endSegmentIdx === startBoundaryIdx)
            throw new TimeTableError('startTimeがendTimeより後です');
        let endBoundaryIdx;
        if(endSegmentIdx >= 0) {
            this._segments.splice(
                endSegmentIdx, 0, new Set(this._segments[endSegmentIdx]));
            this._boundaries.splice(endSegmentIdx, 0, endTime);
            endBoundaryIdx = endSegmentIdx;
        } else {
            endBoundaryIdx = -endSegmentIdx - 1;
        }

        for(let idx = startBoundaryIdx; idx <= endBoundaryIdx; ++idx) {
            const prevSeg = this._segments[idx];
            const seg = this._segments[idx + 1];
            if(idx !== endBoundaryIdx) {
                for(let value of itemsToAdd)
                    seg.add(value);
                for(let value of itemsToRemove)
                    seg.delete(value);
            }
            if(setEquals(seg , prevSeg)) {
                this._boundaries.splice(idx, 1);
                this._segments.splice(idx, 1);
                --idx, --endBoundaryIdx;
            }
        }
    }

    /**
     * @param {TimeTable<T>} other
     * @returns {TimeTable<T>}
     */
    intersection(other) {
        const [a, b] = [this, other];
        const newBoundaries = [];
        const newSegments = [setIntersection(a.segments[0], b.segments[0])]
        let [aIdx, bIdx] = [0, 0];
        while(aIdx < a.boundaries.length || bIdx < b.boundaries.length) {
            let c;
            if(aIdx < a.boundaries.length && bIdx < b.boundaries.length)
                c = cmp(a.boundaries[aIdx], b.boundaries[bIdx]);
            else
                c = aIdx < a.boundaries.length ? +1 : -1;
            if(c < 0) {
                newBoundaries.push(a.boundaries[aIdx]);
                ++aIdx;
            } else if(c > 0) {
                newBoundaries.push(b.boundaries[bIdx]);
                ++bIdx;
            } else {
                newBoundaries.push(a.boundaries[aIdx]);
                ++aIdx, ++bIdx;
            }
            newSegments.push(
                setIntersection(a.segments[aIdx], b.segments[bIdx]));
        }
        const ret = new TimeTable();
        ret._boundaries = newBoundaries;
        ret._segments = newSegments;
        return ret;
    }

    /**
     * @param {import('@silane/datetime').DateTime | null} startTime
     * @param {import('@silane/datetime').DateTime | null} endTime
     * @returns {{
     *   start: import('@silane/datetime').DateTime,
     *   end: import('@silane/datetime').DateTime,
     *   items: Set<T>,
     * }[]}
     */
    get(startTime=null, endTime=null) {
        let startSegmentIdx = 1;
        if(startTime) {
            startSegmentIdx = this._boundaries.findIndex(
                x => cmp(x, startTime) > 0
            );
            if(startSegmentIdx === -1) {
                startSegmentIdx = this._boundaries.length;
            }
        }

        let endSegmentIdx = this._segments.length - 1;
        if(endTime) {
            endSegmentIdx = this._boundaries.findIndex(
                x => cmp(x, endTime) >= 0
            );
            if(endSegmentIdx === -1) {
                endSegmentIdx = this._boundaries.length;
            }
            ++endSegmentIdx;
        }

        const ret = [];
        for(let [idx, segment] of [
            ...this._segments.entries()
        ].slice(startSegmentIdx, endSegmentIdx)) {
            const entry = {
                start: this._boundaries[idx - 1], end: this._boundaries[idx],
                items: segment,
            };
            if(startTime && idx === startSegmentIdx) {
                entry[0] = startTime;
            }
            if(endTime && idx === endSegmentIdx - 1) {
                entry[1] = endTime;
            }
            ret.push(entry);
        }
        return ret;
    }

    allValues() {
        const ret = new Set();
        for(let segment of this._segments)
            for(let value of segment)
                ret.add(value);
        return ret;
    }

    /**
     * @returns {TimeTable<T>}
     */
    copy() {
        const ret = new TimeTable();
        ret._boundaries = this._boundaries.slice();
        ret._segments = this._segments.map(x => new Set(x));
        return ret;
    }
}
