/*
 * Copyright 2016 Palantir Technologies, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* eslint-disable max-classes-per-file */

class GridEntry<T> {
    public static key(i: number, j: number) {
        return `${i}_${j}`;
    }

    public constructor(public i: number, public j: number, public value: T) {}

    // there are two things here called `key` but they're certainly not overloaded (one being static)
    // TSLint bug report: https://github.com/palantir/tslint/issues/2139
    // eslint-disable-line @typescript-eslint/adjacent-overload-signatures
    public get key() {
        return GridEntry.key(this.i, this.j);
    }
}

export class SparseGridMutableStore<T> {
    private list: Array<GridEntry<T>>;

    private map: { [key: string]: GridEntry<T> };

    public constructor() {
        this.clear();
    }

    public clear() {
        this.list = [] as Array<GridEntry<T>>;
        this.map = {};
    }

    public set(i: number, j: number, value: T) {
        const entry = this.map[GridEntry.key(i, j)];
        if (entry != null) {
            entry.value = value;
        } else {
            this.add(i, j, value);
        }
    }

    public unset(i: number, j: number) {
        const entryKey = GridEntry.key(i, j);
        const entry = this.map[entryKey];
        if (entry != null) {
            const index = this.list.indexOf(entry);
            if (index > -1) {
                this.list.splice(index, 1);
            }
            delete this.map[entryKey];
        }
    }

    public get(i: number, j: number): T {
        const entry = this.map[GridEntry.key(i, j)];
        return entry == null ? undefined : entry.value;
    }

    public insertI(i: number, count: number) {
        this.shift(i, count, "i");
    }

    public insertJ(j: number, count: number) {
        this.shift(j, count, "j");
    }

    public deleteI(i: number, count: number) {
        this.remove(i, count, "i");
        this.shift(i + count, -count, "i");
    }

    public deleteJ(j: number, count: number) {
        this.remove(j, count, "j");
        this.shift(j + count, -count, "j");
    }

    private add(i: number, j: number, value: T) {
        const entry = new GridEntry<T>(i, j, value);
        this.list.push(entry);
        this.map[entry.key] = entry;
    }

    private shift(index: number, count: number, coordinate: "i" | "j") {
        const shifted = [] as Array<GridEntry<T>>;

        // remove entries that need to be shifted from map
        for (const entry of this.list) {
            if ((entry as any)[coordinate] >= index) {
                shifted.push(entry);
                delete this.map[entry.key];
            }
        }

        // adjust coordinates
        for (const entry of shifted) {
            (entry as any)[coordinate] += count;
        }

        // add shifted entries back to map
        for (const entry of shifted) {
            this.map[entry.key] = entry;
        }
    }

    private remove(index: number, count: number, coordinate: "i" | "j") {
        const maintained = [] as Array<GridEntry<T>>;

        // remove entries map as we go, rebuild list from maintained entries
        for (const entry of this.list) {
            if ((entry as any)[coordinate] >= index && (entry as any)[coordinate] < index + count) {
                delete this.map[entry.key];
            } else {
                maintained.push(entry);
            }
        }

        this.list = maintained;
    }
}
