/*
 * https://github.com/morethanwords/tweb
 * Copyright (C) 2019-2021 Eduard Kuzmenko
 * https://github.com/morethanwords/tweb/blob/master/LICENSE
 */

import {MOUNT_CLASS_TO} from '../config/debug';
import indexOfAndSplice from './array/indexOfAndSplice';
import compareValue from './compareValue';

/**
 * Descend sorted storage
 */

type ItemType = number | string;

export enum SliceEnd {
  None = 0,
  Top = 1,
  Bottom = 2,
  Both = SliceEnd.Top | SliceEnd.Bottom
};

export interface Slice<T extends ItemType> extends Array<T> {
  // slicedArray: SlicedArray;
  end: SliceEnd;

  isEnd: (side: SliceEnd) => boolean;
  setEnd: (side: SliceEnd) => void;
  unsetEnd: (side: SliceEnd) => void;
  getEnds: () => {top: boolean, bottom: boolean, both: boolean};

  slice: (from?: number, to?: number) => Slice<T>;
  splice: (start: number, deleteCount: number, ...items: ItemType[]) => Slice<T>;
}

export interface SliceConstructor<T extends ItemType> {
  // new(...items: T[]): Slice<T>;
  new(length: number): Slice<T>;
}

export type SliceSerialized<T extends ItemType> = {
  values: Slice<T>,
  isEnd: {
    top: boolean,
    bottom: boolean,
    both: boolean
  }
};

export type SlicedArraySerialized<T extends ItemType> = {
  slices: SliceSerialized<T>[]
};

export default class SlicedArray<T extends ItemType> {
  private slices: Slice<T>[]/*  = [[7,6,5],[4,3,2],[1,0,-1]] */;
  private sliceConstructor: SliceConstructor<T>;

  constructor() {
    // @ts-ignore
    this.sliceConstructor = SlicedArray.getSliceConstructor(this);

    const first = this.constructSlice();
    // first.setEnd(SliceEnd.Bottom);
    this.slices = [first];
  }

  private static getSliceConstructor(slicedArray: SlicedArray<ItemType>) {
    return class Slice<T> extends Array<ItemType> implements Slice<T> {
      // slicedArray: SlicedArray;
      end: SliceEnd = SliceEnd.None;

      /* constructor(...items: ItemType[]) {
        super(...items);
        //this.slicedArray = slicedArray;
      } */

      isEnd(side: SliceEnd): boolean {
        if((this.end & side) === side) {
          return true;
        }/*  else if(!this.slicedArray) {
          return false;
        } */

        let isEnd = false;
        if(side === SliceEnd.Top) {
          const slice = slicedArray.last;
          isEnd = slice.end & side ? this.includes(slice[slice.length - 1])/*  || !slice.length */ : false;
        } else if(side === SliceEnd.Bottom) {
          const slice = slicedArray.first;
          isEnd = slice.end & side ? this.includes(slice[0])/*  || !slice.length */ : false;
        } else if(side === SliceEnd.Both) {
          return this.isEnd(SliceEnd.Top) && this.isEnd(SliceEnd.Bottom);
        }

        if(isEnd) {
          this.setEnd(side);
        }

        return isEnd;
      }

      getEnds() {
        return {
          top: this.isEnd(SliceEnd.Top),
          bottom: this.isEnd(SliceEnd.Bottom),
          both: this.isEnd(SliceEnd.Both)
        };
      }

      setEnd(side: SliceEnd) {
        this.end |= side;
      }

      unsetEnd(side: SliceEnd) {
        this.end &= ~side;
      }

      splice(start: number, deleteCount: number, ...items: ItemType[]) {
        const ret = super.splice(start, deleteCount, ...items);

        if(!this.length) {
          const slices = slicedArray.slices as ItemType[][];
          const idx = slices.indexOf(this);
          if(idx !== -1) {
            if(slices.length === 1) { // left empty slice without ends
              this.unsetEnd(SliceEnd.Both);
            } else { // delete this slice
              slices.splice(idx, 1);
            }
          }
        }

        return ret;
      }
    }
  }

  public constructSlice(...items: T[]) {
    // const slice = new Slice(this, ...items);
    // can't pass items directly to constructor because first argument is length
    const slice = new this.sliceConstructor(items.length);
    for(let i = 0, length = items.length; i < length; ++i) {
      slice[i] = items[i];
    }
    return slice;

    // ! code below will slow execution in 15 times
    /* const self = this;
    const p: Slice = new Proxy(slice, {
      get: function(target, name: any) {
        if(name === 'constructor') {
          const p = new Proxy(Slice, {
            construct: (target, args) => {
              return self.constructSlice(...args);
            }
          });

          return p;
        }

        return target[name];
      }
    });

    return p; */

    /*
    var p = slicedArray.constructSlice();
    p.length = 100000;
    p.fill(255);

    var a = new Array(100000);
    a.fill(255);

    var b = 0;
    var perf = performance.now();
    for(var i = 0; i < p.length; ++i) {
        b += p[i];
    }

    console.log('perf 1', performance.now() - perf);

    b = 0;
    perf = performance.now();
    for(var i = 0; i < a.length; ++i) {
        b += a[i];
    }

    console.log('perf 2', performance.now() - perf);
    */
  }

  public insertSlice(slice: T[], flatten = true) {
    if(!slice.length) {
      return;
    }

    const first = this.slices[0];
    if(!first.length) {
      first.push(...slice);
      return first;
    }

    const lowerBound = slice[slice.length - 1];
    const upperBound = slice[0];

    let foundSlice: Slice<T>, lowerIndex = -1, upperIndex = -1, foundSliceIndex = 0;
    for(; foundSliceIndex < this.slices.length; ++foundSliceIndex) {
      foundSlice = this.slices[foundSliceIndex];
      lowerIndex = foundSlice.indexOf(lowerBound);
      upperIndex = foundSlice.indexOf(upperBound);

      if(upperIndex !== -1 && -1 !== lowerIndex) {
        break;
      } else if(upperIndex !== -1 || -1 !== lowerIndex) {
        break;
      }
    }

    if(upperIndex !== -1 && -1 !== lowerIndex) {

    } else if(upperIndex !== -1) {  // ([1, 2, 3] | [1, 2, 3, 4, 5]) -> [1, 2, 3, 4, 5]
      const sliced = slice.slice(foundSlice.length - upperIndex);
      foundSlice.push(...sliced);
    } else if(lowerIndex !== -1) {  // ([1, 2, 3] | [-1, 0, 1]) -> [-1, 0, 1, 2, 3]
      const sliced = slice.slice(0, slice.length - lowerIndex - 1);
      foundSlice.unshift(...sliced);
    } else {
      let insertIndex = 0;
      for(const length = this.slices.length; insertIndex < length; ++insertIndex) { // * maybe should iterate from the end, could be faster ?
        const s = this.slices[insertIndex];
        if(compareValue(slice[0], s[0]) === 1) {
          break;
        }
      }

      this.slices.splice(insertIndex, 0, this.constructSlice(...slice));
      foundSliceIndex = insertIndex;
    }

    if(flatten) {
      return this.flatten(foundSliceIndex);
    }
  }

  private flatten(foundSliceIndex: number) {
    if(this.slices.length >= 2) {
      for(let i = 0, length = this.slices.length; i < (length - 1); ++i) {
        const prevSlice = this.slices[i];
        const nextSlice = this.slices[i + 1];

        const upperIndex = prevSlice.indexOf(nextSlice[0]);
        if(upperIndex !== -1) {
          prevSlice.setEnd(nextSlice.end);
          this.slices.splice(i + 1, 1);

          if(i < foundSliceIndex) {
            --foundSliceIndex;
          }

          --length; // respect array bounds
          --i;      // repeat from the same place

          this.insertSlice(nextSlice, false);
        }
      }
    }

    return this.slices[foundSliceIndex];
  }

  // *

  get first() {
    return this.slices[0];
  }

  get last() {
    return this.slices[this.slices.length - 1];
  }

  get slice() {
    return this.first;
  }

  get length() {
    return this.slice.length;
  }

  public findSlice(item: T) {
    for(let i = 0, length = this.slices.length; i < length; ++i) {
      const slice = this.slices[i];
      const index = slice.indexOf(item);
      if(index !== -1) {
        return {slice, index};
      }
    }

    return undefined;
  }

  public findSliceOffset(maxId: T) {
    let slice: Slice<T>;
    for(let i = 0; i < this.slices.length; ++i) {
      let offset = 0;
      slice = this.slices[i];
      if(slice.length < 2) {
        continue;
      }

      for(; offset < slice.length; ++offset) {
        if(compareValue(maxId, slice[offset]) >= 0) {
          /* if(!offset) { // because can't find 3 in [[5,4], [2,1]]
            return undefined;
          } */

          return {
            slice,
            offset: maxId === slice[offset] ? offset : offset - 1
          };
        }
      }
    }

    if(slice && slice.isEnd(SliceEnd.Top)) {
      return {
        slice,
        offset: slice.length
      };
    }

    return undefined;
  }

  // * https://core.telegram.org/api/offsets
  public sliceMe(offsetId: T, add_offset: number, limit: number) {
    let slice = this.slice;
    let offset = 0;
    let sliceOffset = 0;

    if(offsetId) {
      const pos = this.findSliceOffset(offsetId);
      if(!pos) {
        return undefined;
      }

      slice = pos.slice;
      offset = sliceOffset = pos.offset;

      if(slice.includes(offsetId)) {
        sliceOffset += 1;
      }

      /* if(slice.includes(offsetId) && add_offset < 0) {
        add_offset += 1;
      } */
    }

    const sliceStart = Math.max(sliceOffset + add_offset, 0);
    const sliceEnd = sliceOffset + add_offset + limit;
    // const fixHalfBackLimit = add_offset && !(limit / add_offset % 2) && (sliceEnd % 2) ? 1 : 0;
    // sliceEnd += fixHalfBackLimit;

    const sliced = slice.slice(sliceStart, sliceEnd) as Slice<T>;

    const topWasMeantToLoad = add_offset < 0 ? limit + add_offset : limit;
    const bottomWasMeantToLoad = Math.abs(add_offset);

    // can use 'slice' here to check because if it's end, then 'sliced' is out of 'slice'
    // useful when there is only 1 message in chat on its reopening
    const topFulfilled = (slice.length - sliceOffset) >= topWasMeantToLoad || (slice.isEnd(SliceEnd.Top) ? (sliced.setEnd(SliceEnd.Top), true) : false);
    const bottomFulfilled = (sliceOffset - bottomWasMeantToLoad) >= 0 || (slice.isEnd(SliceEnd.Bottom) ? (sliced.setEnd(SliceEnd.Bottom), true) : false);

    // if(topFulfilled) sliced.isEnd(SliceEnd.Top);
    // if(bottomFulfilled) sliced.isEnd(SliceEnd.Bottom);

    return {
      slice: sliced,
      offsetIdOffset: offset,
      fulfilled: SliceEnd.None | (topFulfilled && bottomFulfilled ? SliceEnd.Both : ((topFulfilled ? SliceEnd.Top : SliceEnd.None) | (bottomFulfilled ? SliceEnd.Bottom : SliceEnd.None)))
    };
  }

  public unshift(...items: T[]) {
    let slice = this.first;
    if(!slice.length) {
      slice.setEnd(SliceEnd.Bottom);
    } else if(!slice.isEnd(SliceEnd.Bottom)) {
      slice = this.constructSlice();
      slice.setEnd(SliceEnd.Bottom);
      this.slices.unshift(slice);
    }

    slice.unshift(...items);
  }

  public push(...items: T[]) {
    let slice = this.last;
    if(!slice.length) {
      slice.setEnd(SliceEnd.Top);
    } else if(!slice.isEnd(SliceEnd.Top)) {
      slice = this.constructSlice();
      slice.setEnd(SliceEnd.Top);
      this.slices.push(slice);
    }

    slice.push(...items);
  }

  public delete(item: T) {
    const found = this.findSlice(item);
    if(found) {
      found.slice.splice(found.index, 1);
      return true;
    }

    return false;
  }

  public deleteSlice(slice: Slice<T>) {
    indexOfAndSplice(this.slices, slice);
  }

  public toJSON() {
    const slices: SlicedArraySerialized<T>['slices'] = this.slices.map((slice) => {
      return {
        values: slice.slice(),
        isEnd: slice.getEnds()
      };
    });

    const serialized: SlicedArraySerialized<T> = {
      slices
    };

    return JSON.stringify(serialized);
  }

  public static fromJSON<T extends ItemType>(json: string) {
    const parsed: SlicedArraySerialized<T> = JSON.parse(json);
    const sliced = new SlicedArray<T>();
    parsed.slices.forEach((slice) => {
      const inserted = sliced.insertSlice(slice.values) || sliced.first;
      if(slice.isEnd.top) inserted.setEnd(SliceEnd.Top);
      if(slice.isEnd.bottom) inserted.setEnd(SliceEnd.Bottom);
    });

    return sliced;
  }
}

MOUNT_CLASS_TO && (MOUNT_CLASS_TO.SlicedArray = SlicedArray);
