import { Injectable } from "@angular/core";
import { GridTuple } from "./GridTuple";
import { PrivateDiagramGridLoaderServiceA } from "./PrivateDiagramGridLoaderServiceA";
import { Subject } from "rxjs";
import { Observable } from "rxjs";

import {
    extend,
    Payload,
    PayloadEnvelope,
    TupleOfflineStorageNameService,
    TupleSelector,
    TupleStorageFactoryService,
    TupleStorageServiceABC,
    VortexService,
    VortexStatusService,
    TupleStorageBatchSaveArguments,
} from "@synerty/vortexjs";
import { diagramFilt, gridCacheStorageName } from "../PluginNames";
import { GridUpdateDateTuple } from "./GridUpdateDateTuple";
import { PrivateDiagramTupleService } from "../services";
import { OfflineConfigTuple } from "../tuples";
import { PrivateDiagramGridLoaderStatusTuple } from "./PrivateDiagramGridLoaderStatusTuple";
import { EncodedGridTuple } from "./EncodedGridTuple";

// ----------------------------------------------------------------------------

let clientGridWatchUpdateFromDeviceFilt = extend(
    { key: "clientGridWatchUpdateFromDevice" },
    diagramFilt
);

// ----------------------------------------------------------------------------

const noSpaceMsg = "there was not enough remaining storage space";

const indexDbNotOpenMsg = "IndexedDB peek_plugin_diagram_grids is not open";

// ----------------------------------------------------------------------------

/** Grid Key Tuple Selector
 *
 * We're using or own storage database, seprate from the typical
 * offline tuple storage. And we're only going to store grids in id.
 *
 * Because of this, we'll extend tuple selector to only return the grid key
 * instead of it's normal ordered tuplename, {} selector.
 *
 * We only need to convert from this class to string, the revers will attemp
 * to convert it back to a real TupleSelector
 *
 * In summary, this is a hack to avoid a little unnesasary bulk.
 */
class GridKeyTupleSelector extends TupleSelector {
    constructor(gridKey: string) {
        super(gridKey, {});
    }

    /** To Ordered Json Str (Override)
     *
     * This method is used by the Tuple Storage to generate the DB Primary Key
     */
    toOrderedJsonStr(): string {
        return this.name;
    }
}

// ----------------------------------------------------------------------------
/** Grid Cache
 *
 * This class has the following responsibilities:
 *
 * 3) Poll for grids from the local storage (IndexedDB or WebSQL), and:
 * 3.1) Update the cache
 *
 * 4) Poll for grids from the server and:
 * 4.1) Store these back into the local storage
 * 4.2) Update the cache
 *
 */
@Injectable()
export class PrivateDiagramGridLoaderService extends PrivateDiagramGridLoaderServiceA {
    private UPDATE_CHUNK_FETCH_SIZE = 5;
    private SAVE_POINT_ITERATIONS = 1000 / 5; // Every 1000 grids
    private OFFLINE_CHECK_PERIOD_MS = 15 * 60 * 1000; // 15 minutes

    private isReadySubject = new Subject<boolean>();

    private updatesObservable = new Subject<GridTuple[]>();

    private storage: TupleStorageServiceABC;

    // The last set of keys requested from the GridObserver
    private lastWatchedGridKeys: string[] = [];

    // All cached grid dates
    private index: GridUpdateDateTuple = new GridUpdateDateTuple();

    // The queue of grids to cache
    private askServerChunks = [];

    // Saving the cache after each chunk is so expensive, we only do it every 20 or so
    private chunksSavedSinceLastIndexSave = 0;

    private _statusSubject = new Subject<PrivateDiagramGridLoaderStatusTuple>();
    private _status = new PrivateDiagramGridLoaderStatusTuple();

    private offlineConfig: OfflineConfigTuple = new OfflineConfigTuple();

    private readonly RETRIES = 5;

    constructor(
        private vortexService: VortexService,
        private vortexStatusService: VortexStatusService,
        private tupleService: PrivateDiagramTupleService,
        storageFactory: TupleStorageFactoryService
    ) {
        super();

        this.tupleService.offlineObserver
            .subscribeToTupleSelector(
                new TupleSelector(OfflineConfigTuple.tupleName, {}),
                false,
                false,
                true
            )
            .takeUntil(this.onDestroyEvent)
            .filter((v) => v.length != 0)
            .subscribe((tuples: OfflineConfigTuple[]) => {
                this.offlineConfig = tuples[0];
                this.askServerForUpdates();
                this._notifyStatus();
            });

        this.storage = storageFactory.create(
            new TupleOfflineStorageNameService(gridCacheStorageName)
        );
        this.storage
            .open()
            .then(() => this.loadGridCacheIndex())
            .then(() => this.isReadySubject.next(true))
            .catch((e) => console.log(`Failed to open grid cache db ${e}`));

        this.setupVortexSubscriptions();
        this._notifyStatus();

        // Check for updates every so often
        Observable.interval(this.OFFLINE_CHECK_PERIOD_MS)
            .takeUntil(this.onDestroyEvent)
            .subscribe(() => this.askServerForUpdates());
    }

    get observable(): Observable<GridTuple[]> {
        return this.updatesObservable;
    }

    isReady(): Promise<boolean> {
        return this.storage.isOpen();
    }

    isReadyObservable(): Observable<boolean> {
        return this.isReadySubject;
    }

    statusObservable(): Observable<PrivateDiagramGridLoaderStatusTuple> {
        return this._statusSubject;
    }

    status(): PrivateDiagramGridLoaderStatusTuple {
        return this._status;
    }

    /** Update Watched Grids
     *
     * Change the list of grids that the GridObserver is interested in.
     */
    watchGrids(gridKeys: string[]): void {
        this.lastWatchedGridKeys = gridKeys;
    }

    /** Update Watched Grids
     *
     * Change the list of grids that the GridObserver is interested in.
     */
    async loadGrids(
        currentGridUpdateTimes: { [gridKey: string]: string },
        gridKeys: string[]
    ): Promise<void> {
        for (let i = 0; i < this.RETRIES; i++) {
            try {
                // Query the local storage for the grids we don't have in the cache
                let gridTuples: GridTuple[] = await this.queryStorageGrids(
                    gridKeys
                );

                // Now that we have the results from the local storage,
                // we can send to the server.
                for (let gridTuple of gridTuples)
                    currentGridUpdateTimes[gridTuple.gridKey] =
                        gridTuple.lastUpdate;

                this.sendWatchedGridsToServer(currentGridUpdateTimes);
                return;
            } catch (err) {
                console.log(`GridCache.storeGridTuples: ${err}`);
                if (!this.retry(err.message)) return;
            }
        }
    }

    private _notifyStatus(): void {
        this._status.cacheForOfflineEnabled =
            this.offlineConfig.cacheChunksForOffline;
        this._status.initialLoadComplete = this.index.initialLoadComplete;

        this._status.loadProgress = Object.keys(
            this.index.updateDateByChunkKey
        ).length;
        for (let chunk of this.askServerChunks)
            this._status.loadProgress -= Object.keys(chunk).length;

        this._statusSubject.next(this._status);
    }

    private setupVortexSubscriptions(): void {
        // Services don't have destructors, I'm not sure how to unsubscribe.
        this.vortexService
            .createEndpointObservable(this, clientGridWatchUpdateFromDeviceFilt)
            .subscribe((payloadEnvelope: PayloadEnvelope) =>
                this.processGridsFromServer(payloadEnvelope)
            );

        // If the vortex service comes back online, update the watch grids.
        this.vortexStatusService.isOnline
            .filter((isOnline) => isOnline == true)
            .takeUntil(this.onDestroyEvent)
            .subscribe(() => {
                this.loadGrids({}, this.lastWatchedGridKeys);
                this.askServerForUpdates();
            });
    }

    private areWeTalkingToTheServer(): boolean {
        return (
            this.offlineConfig.cacheChunksForOffline &&
            this.vortexStatusService.snapshot.isOnline
        );
    }

    /** Cache All Grids
     *
     * Cache all the grids from the server, into this device.
     */
    private askServerForUpdates(): void {
        this._notifyStatus();
        if (!this.areWeTalkingToTheServer()) return;

        // If we're still caching, then exit
        if (this.askServerChunks.length != 0) {
            this.askServerForNextUpdateChunk();
            return;
        }

        let keysNeedingUpdate: string[] = [];
        let total = 0;
        let start = 0;
        let chunkSize = 5000;

        let complete = () => {
            this._status.loadTotal = total;
            this.queueChunksToAskServer(keysNeedingUpdate);
        };

        // This is one big hoop to avoid memory issues on older iOS devices
        let queueNext = () => {
            let ts = new TupleSelector(GridUpdateDateTuple.tupleName, {
                start: start,
                count: start + chunkSize,
            });
            start += chunkSize;

            this.tupleService.observer
                .pollForTuples(ts)
                .then((tuples: any[]) => {
                    if (!tuples.length) {
                        complete();
                        return;
                    }

                    total += tuples.length;

                    for (let item of tuples) {
                        let chunkKey = item[0];
                        let lastUpdate = item[1];

                        if (
                            !this.index.updateDateByChunkKey.hasOwnProperty(
                                chunkKey
                            )
                        ) {
                            this.index.updateDateByChunkKey[chunkKey] = null;
                            keysNeedingUpdate.push(chunkKey);
                        } else if (
                            this.index.updateDateByChunkKey[chunkKey] !=
                            lastUpdate
                        ) {
                            keysNeedingUpdate.push(chunkKey);
                        }
                    }
                    setTimeout(() => queueNext(), 0);
                })
                .catch((e) => console.log(`ERROR in cacheAll : ${e}`));
        };
        queueNext();
    }

    /** Queue Chunks To Ask Server
     *
     */
    private queueChunksToAskServer(keysNeedingUpdate: string[]) {
        if (!this.areWeTalkingToTheServer()) return;

        this.askServerChunks = [];
        this.chunksSavedSinceLastIndexSave = 0;

        let count = 0;
        let indexChunk = {};

        for (let key of keysNeedingUpdate) {
            indexChunk[key] = this.index.updateDateByChunkKey[key];
            count++;

            if (count == this.UPDATE_CHUNK_FETCH_SIZE) {
                this.askServerChunks.push(indexChunk);
                count = 0;
                indexChunk = {};
            }
        }

        if (count) this.askServerChunks.push(indexChunk);

        this.askServerForNextUpdateChunk();

        this._status.lastCheck = new Date();
        this._notifyStatus();
    }

    /** Cache Request Next Chunk
     *
     * Request the next chunk of grids from the server
     */
    private askServerForNextUpdateChunk() {
        if (!this.areWeTalkingToTheServer()) return;

        if (this.askServerChunks.length == 0) return;

        let nextChunk = this.askServerChunks.pop();

        let payload = new Payload({ cacheAll: true }, [nextChunk]);
        extend(payload.filt, clientGridWatchUpdateFromDeviceFilt);
        this.vortexService.sendPayload(payload);

        this._status.lastCheck = new Date();
    }

    //
    private sendWatchedGridsToServer(updateTimeByGridKey: {
        [gridKey: string]: string;
    }) {
        // There is no point talking to the server if it's offline
        if (!this.vortexStatusService.snapshot.isOnline) return;

        let payload = new Payload(clientGridWatchUpdateFromDeviceFilt);
        payload.tuples = [updateTimeByGridKey];
        this.vortexService.sendPayload(payload);
    }

    /** Process Grids From Server
     *
     * Process the grids the server has sent us.
     */
    private processGridsFromServer(payloadEnvelope: PayloadEnvelope) {
        payloadEnvelope
            .decodePayload()
            .then((payload: Payload) => {
                let encodedGridTuples: EncodedGridTuple[] = <
                    EncodedGridTuple[]
                >payload.tuples;

                let isCacheAll = payload.filt["cacheAll"] === true;

                if (!isCacheAll) {
                    this.emitEncodedGridTuples(encodedGridTuples);
                }

                // We always cache the tuples
                let promise: any = this.storeGridTuples(encodedGridTuples).then(
                    () => {
                        if (!isCacheAll) return;

                        this.chunksSavedSinceLastIndexSave++;

                        if (this.askServerChunks.length == 0) {
                            this.saveGridCacheIndex(true);
                            this.index.initialLoadComplete = true;
                        } else {
                            this.saveGridCacheIndex();
                            this.askServerForNextUpdateChunk();
                        }
                        this._notifyStatus();
                    }
                );
                return promise;
            })
            .catch((e) => `ERROR GridLoader.processGridsFromServer: ${e}`);
    }

    private emitEncodedGridTuples(encodedGridTuples: EncodedGridTuple[]): void {
        let promises: Promise<void>[] = [];
        let gridTuples: GridTuple[] = [];

        for (let encodedGridTuple of encodedGridTuples) {
            if (encodedGridTuple.encodedGridTuple == null) {
                // Add an empty grid
                const gridTuple = new GridTuple();
                gridTuple.gridKey = encodedGridTuple.gridKey;
                gridTuple.dispJsonStr = null;
                gridTuple.lastUpdate = null;
                gridTuples.push(gridTuple);
                promises.push(Promise.resolve());
            } else {
                let promise: any = Payload.fromEncodedPayload(
                    encodedGridTuple.encodedGridTuple
                )
                    .then((payload: Payload) => {
                        gridTuples.push(payload.tuples[0]);
                    })
                    .catch((err) => {
                        console.log(
                            `GridLoader.emitEncodedGridTuples decode error: ${err}`
                        );
                    });
                promises.push(promise);
            }
        }

        Promise.all(promises)
            .then(() => {
                this.updatesObservable.next(gridTuples);
            })
            .catch((err) => {
                console.log(
                    `GridLoader.emitEncodedGridTuples all error: ${err}`
                );
            });
    }

    /** Query Storage Grids
     *
     * Load grids from local storage if they exist in it.
     *
     */
    private queryStorageGrids(gridKeys: string[]): Promise<GridTuple[]> {
        let promises = [];
        //noinspection JSMismatchedCollectionQueryUpdate
        let gridTuples: GridTuple[] = [];

        for (let gridKey of gridKeys) {
            promises.push(
                this.storage
                    .loadTuples(new GridKeyTupleSelector(gridKey))
                    .then((grids: GridTuple[]) => {
                        // Length should be 0 or 1
                        if (!grids.length) return;
                        gridTuples.push(grids[0]);
                        this.updatesObservable.next(grids);
                    })
            );
        }

        return Promise.all(promises) //
            .then(() => {
                // Return the grid tuples.
                return gridTuples;
            });
    }

    /** Store Grid Tuples
     * This is called with grids from the server, store them for later.
     */
    private storeGridTuples(
        encodedGridTuples: EncodedGridTuple[],
        retries = 0
    ): Promise<void> {
        if (encodedGridTuples.length == 0) {
            return Promise.resolve();
        }

        let gridKeys = [];
        for (let encodedGridTuple of encodedGridTuples) {
            gridKeys.push(encodedGridTuple.gridKey);
        }
        console.log(`Caching grids ${gridKeys}`);

        let promises = [];

        for (let encodedGridTuple of encodedGridTuples) {
            if (encodedGridTuple.encodedGridTuple == null) {
                delete this.index.updateDateByChunkKey[
                    encodedGridTuple.gridKey
                ];
                promises.push(
                    this.storage.deleteTuples(
                        new GridKeyTupleSelector(encodedGridTuple.gridKey)
                    )
                );
            } else {
                this.index.updateDateByChunkKey[encodedGridTuple.gridKey] =
                    encodedGridTuple.lastUpdate;

                promises.push(
                    this.storage.saveTuplesEncoded(
                        new GridKeyTupleSelector(encodedGridTuple.gridKey),
                        encodedGridTuple.encodedGridTuple
                    )
                );
            }
        }

        Promise.all(promises)
            .then(() => this.saveGridCacheIndex(false))
            .catch((err) => {
                console.log(`GridCache.storeGridTuples: ${err}`);
                if (retries < this.RETRIES && this.retry(err.message))
                    return this.storeGridTuples(encodedGridTuples, retries++);
            });

        return Promise.resolve();
    }

    private retry(message: string): boolean {
        if (message.indexOf(noSpaceMsg) !== -1) return true;

        return message.indexOf(indexDbNotOpenMsg) !== -1;
    }

    /** Load Grid Cache Index
     *
     * Loads the running tab of the update dates of the cached grids
     *
     */
    private loadGridCacheIndex(): Promise<void> {
        let retPromise: any = this.storage
            .loadTuples(new TupleSelector(GridUpdateDateTuple.tupleName, {}))
            .then((tuples: GridUpdateDateTuple[]) => {
                // Length should be 0 or 1
                if (tuples.length) this.index = tuples[0];
            });
        return retPromise;
    }

    /** Store Grid Cache Index
     *
     * Updates our running tab of the update dates of the cached grids
     *
     */
    private saveGridCacheIndex(force = false): Promise<void> {
        if (
            this.chunksSavedSinceLastIndexSave <= this.SAVE_POINT_ITERATIONS &&
            !force
        )
            return Promise.resolve();

        let ts = new TupleSelector(GridUpdateDateTuple.tupleName, {});
        let tuples = [this.index];
        let errCb = (e) => console.log(`GridCache.storeGridCacheIndex: ${e}`);

        this.chunksSavedSinceLastIndexSave = 0;

        return this.storage.saveTuples(ts, tuples).catch(errCb);
    }
}
