import { LRUMap } from "lru_map";
import { EventStore } from "../../spi/event/EventStore";
import {
    CommandIncoming,
    EventIncoming,
} from "../transport/RequestProcessor";
import {
    guid,
    hideString,
} from "../util/string";

/**
 * Simple {EventStore} implementation that stores events in memory.
 */
export class InMemoryEventStore implements EventStore {

    private readonly eventCache: LRUMap<CacheKey, EventIncoming>;
    private readonly commandCache: LRUMap<CacheKey, CommandIncoming>;
    private readonly messageCache: LRUMap<CacheKey, any>;

    // 5 mins for 3 hours
    private readonly eventSer = new RRD(60 * 5, 12 * 3);
    private readonly commandSer = new RRD(60 * 5, 12 * 3);

    constructor() {
        this.eventCache = new LRUMap<CacheKey, EventIncoming>(100);
        this.commandCache = new LRUMap<CacheKey, CommandIncoming>(100);
        this.messageCache = new LRUMap<CacheKey, any>(100);
    }

    public recordEvent(event: EventIncoming) {
        const id = event.extensions.correlation_id ? event.extensions.correlation_id : guid();
        this.eventCache.set({ guid: id, correlationId: id, ts: new Date().getTime() }, event);
        this.eventSer.update(1);
        return id;
    }

    public recordCommand(command: CommandIncoming) {
        const id = command.correlation_id ? command.correlation_id : guid();
        this.commandCache.set({ guid: id, correlationId: id, ts: new Date().getTime() }, command);
        this.commandSer.update(1);
        return id;
    }

    public recordMessage(id: string, correlationId: string, message: any) {
        this.messageCache.set({ guid: id, correlationId, ts: new Date().getTime() }, message);
        return id;
    }

    public events(from: number = -1): any[] {
        const entries: any[] = [];
        this.eventCache.forEach((v, k) => k.ts > from ? entries.push({ key: k, value: hideSecrets(v) }) : null);
        return entries;
    }

    public eventSeries(): [number[], number[]] {
        const buckets = this.eventSer.fetch().filter(b => b.ts);
        return [buckets.map(b => b.value), buckets.map(b => b.ts)];
    }

    public commands(from: number = -1): any[] {
        const entries: any[] = [];
        this.commandCache.forEach((v, k) => k.ts > from ? entries.push({ key: k, value: hideSecrets(v) }) : null);
        return entries;
    }

    public commandSeries(): [number[], number[]] {
        const buckets = this.commandSer.fetch().filter(b => b.ts);
        return [buckets.map(b => b.value), buckets.map(b => b.ts)];
    }

    public messages(from: number = -1): any[] {
        const entries: any[] = [];
        this.messageCache.forEach((v, k) => k.ts > from ? entries.push({ key: k, value: v }) : null);
        return entries;
    }
}

function hideSecrets(event: EventIncoming | CommandIncoming) {
    event.secrets = event.secrets
        ? event.secrets.map(s => ({ uri: s.uri, value: hideString(s.value) })) : undefined;
    return event;
}

interface CacheKey {
    guid: string;
    correlationId: string;
    ts: number;
}

class Count {

    private value: number = 0;

    public update(data: number): number {
        this.value++;
        return this.value;
    }

    public result() {
        const value = this.value;
        this.value = 0;
        return value;
    }
}

class RRD {

    private readonly buckets: any[];
    private readonly interval: any;
    private index: number;
    private readonly iid: any;
    private readonly dataFunc = new Count();

    constructor(interval, count) {
        this.buckets = new Array(count).fill(0);
        this.buckets[0] = { ts: Math.floor(Date.now() / 1000), value: 0 };
        this.index = 1;
        this.interval = interval * 1000;
        this.iid = setInterval(this.increment.bind(this), this.interval);
    }

    public increment() {
        if (this.index < this.buckets.length) {
            this.buckets[this.index] = { ts: Math.floor(Date.now() / 1000), value: this.dataFunc.result() };
            this.index += 1;
        } else {
            this.buckets.push({ ts: Math.floor(Date.now() / 1000), value: this.dataFunc.result() });
            this.buckets.shift();
        }
    }

    public update(data: any) {
        this.buckets[this.index] = { ts: Math.floor(Date.now() / 1000), value: this.dataFunc.update(data) };
    }

    public fetch(): any[] {
        return this.buckets;
    }
}
