/* eslint-disable no-underscore-dangle */
/* A SourceReader represents the current position in a source file.
 * It keeps track of line and column numbers.
 * Methods are non-destructive. For example:
 *
 *     let r = new SourceReader('foo.gbs', 'if\n(True)');
 *
 *     r.peek();                       // ~~> 'i'
 *     r = r.consumeCharacter();       // Note: returns a new file reader.
 *
 *     r.peek();                       // ~~> 'f'
 *     r = r.consumeCharacter();
 *
 *     r.peek();                       // ~~> '\n'
 *     r = r.consumeCharacter('\n');
 *
 *     r.line();                       // ~~> 2
 */
export class SourceReader {
    private _filename: string;
    private _string: string;
    private _index: number;
    private _line: number;
    private _column: number;
    private _regions: string[];

    public constructor(filename: string, string: string) {
        this._filename = filename; // Filename
        this._string = string; // Source of the current file
        this._index = 0; // Index in the current file
        this._line = 1; // Line in the current file
        this._column = 1; // Column in the current file
        this._regions = []; // Lexical (static) stack of regions
    }

    public _clone(): SourceReader {
        const r = new SourceReader(this._filename, this._string);
        r._index = this._index;
        r._line = this._line;
        r._column = this._column;
        r._regions = this._regions;
        return r;
    }

    public get filename(): string {
        return this._filename;
    }

    public get line(): number {
        return this._line;
    }

    public get column(): number {
        return this._column;
    }

    public get region(): string {
        if (this._regions.length > 0) {
            return this._regions[0];
        } else {
            return '';
        }
    }

    /* Consume one character */
    public consumeCharacter(): SourceReader {
        const r = this._clone();
        if (r.peek() === '\n') {
            r._line++;
            r._column = 1;
        } else {
            r._column++;
        }
        r._index++;
        return r;
    }

    /* Consume characters from the input, one per each character in the string
     * (the contents of the string are ignored). */
    public consumeString(string: string): SourceReader {
        let r = this._clone();
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        for (const _ of string) {
            r = r.consumeCharacter();
        }
        return r;
    }

    /* Returns the SourceReader after consuming an 'invisible' character.
     * Invisible characters affect the index but not the line or column.
     */
    public consumeInvisibleCharacter(): SourceReader {
        const r = this._clone();
        r._index++;
        return r;
    }

    /* Consume 'invisible' characters from the input, one per each character
     * in the string */
    public consumeInvisibleString(string: string): SourceReader {
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        let r: SourceReader = this;
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        for (const _ of string) {
            r = r.consumeInvisibleCharacter();
        }
        return r;
    }

    /* Return true if the substring occurs at the current point. */
    public startsWith(sub: string | any[]): boolean {
        const i = this._index;
        const j = this._index + sub.length;
        return j <= this._string.length && this._string.substring(i, j) === sub;
    }

    /* Return true if we have reached the end of the current file */
    public eof(): boolean {
        return this._index >= this._string.length;
    }

    /* Return the current character, assuming we have not reached EOF */
    public peek(): string {
        return this._string[this._index];
    }

    /* Push a region to the stack of regions (non-destructively) */
    public beginRegion(region: string): SourceReader {
        const r = this._clone();
        r._regions = [region].concat(r._regions);
        return r;
    }

    /* Pop a region from the stack of regions (non-destructively) */
    public endRegion(): SourceReader {
        const r = this._clone();
        if (r._regions.length > 0) {
            r._regions = r._regions.slice(1);
        }
        return r;
    }
}

/* Return a source reader that represents an unknown position */
export const UnknownPosition = new SourceReader('(?)', '');

export type Input = string | Record<string, string> | string[];

/* An instance of MultifileReader represents a scanner for reading
 * source code from a list of files.
 */
export class MultifileReader {
    private _filenames: string[];
    private _input: Input;
    private _index: number;
    /* The 'input' parameter should be either:
     * (1) a string. e.g.  'program {}', or
     * (2) a map from filenames to strings, e.g.
     *     {
     *       'foo.gbs': 'program { P() }',
     *       'bar.gbs': 'procedure P() {}',
     *     }
     */
    public constructor(input: Input) {
        if (typeof input === 'string') {
            input = { '(?)': input };
        }
        this._filenames = Object.keys(input);
        this._filenames.sort();
        this._input = input;
        this._index = 0;
    }

    /* Return true if there are more files */
    public moreFiles(): boolean {
        return this._index + 1 < this._filenames.length;
    }

    /* Advance to the next file */
    public nextFile(): void {
        this._index++;
    }

    /* Return a SourceReader for the current files */
    public readCurrentFile(): SourceReader {
        if (this._index < this._filenames.length) {
            const filename = this._filenames[this._index];
            return new SourceReader(filename, this._input[filename]);
        } else {
            return new SourceReader('(?)', '');
        }
    }
}
