/*
    int main() {
        return 5;
    }
*/

interface Iregexp {
    regexp:RegExp;
    tokenname:string;
}

let regexplist:Iregexp[];

regexplist = [
    { regexp: /^\s/, tokenname: 'whitespace' },
    { regexp: /^\(/, tokenname:'openround' },
    { regexp: /^\)/, tokenname:'closeround' },
    { regexp: /^{/, tokenname:'opencurly' },
    { regexp: /^}/, tokenname:'closecurly' },
    { regexp: /^;/, tokenname:'semicolon' },
    { regexp: /^int/, tokenname:'keyword' },
    { regexp: /^return/, tokenname:'keyword' },
    { regexp: /^[0-9]+/, tokenname:'numberconst' },
    { regexp: /^[a-zA-Z_'][a-zA-Z_0-9']+/, tokenname:'identifier' }
];

type Identifier = string;
type EOF = '<eof>';
type Whitespace = ' ';
type Openround = '(';
type Closeround = ')';
type Opencurly = '{';
type Closecurly = '}';
type Semicolon = ';';
type Numberconst = number;
type Keyword =
    'int'
    | 'return';
type Token =
    Tkeyword
    | Twhitespace
    | Tidentifier
    | Topenround
    | Tcloseround
    | Topencurly
    | Tclosecurly
    | Tsemicolon
    | Tnumberconst
    | Teof;
    
interface Twhitespace {
    tag: string;
    contents?: Whitespace;
}
interface Tidentifier {
    tag: string;
    contents?: Identifier;
}
interface Tkeyword {
    tag: string;
    contents?: Keyword;
}
interface Topenround {
    tag: string;
    contents?: Openround;
}
interface Tcloseround {
    tag: string;
    contents?: Closeround;
}
interface Topencurly {
    tag: string;
    contents?: Opencurly;
}
interface Tclosecurly {
    tag: string;
    contents?: Closecurly;
}
interface Tsemicolon {
    tag: string;
    contents?: Semicolon;
}
interface Tnumberconst {
    tag: string;
    contents?: Numberconst;
}
interface Teof {
    tag: string;
    contents?: EOF;
}

let keyword:(arg?:string)=>Tkeyword;
let whitespace:()=>Twhitespace;
let identifier:(arg?:string)=>Tidentifier;
let openround:()=>Topenround;
let closeround:()=>Tcloseround;
let opencurly:()=>Topencurly;
let closecurly:()=>Tclosecurly;
let semicolon:()=>Tsemicolon;
let numberconst:(arg?:number)=>Tnumberconst;
let eof:()=>Teof;

let eq:(t1:Token, t2:Token)=>boolean;

keyword = (arg:string=''): Tkeyword => ({
    tag: 'keyword',
    contents: arg as Keyword
});
whitespace = (): Twhitespace => ({
    tag: 'whitespace',
    contents: undefined as unknown as Whitespace
});
identifier = (arg:string=''): Tidentifier => ({
    tag: 'identifier',
    contents: arg as Identifier
});
openround = (): Topenround => ({
    tag: 'openround',
    contents: undefined as unknown as Openround
});
closeround = (): Tcloseround => ({
    tag: 'closeround',
    contents: undefined as unknown as Closeround
});
opencurly = (): Topencurly => ({
    tag: 'opencurly',
    contents: undefined as unknown as Opencurly
});
closecurly = (): Tclosecurly => ({
    tag: 'closecurly',
    contents: undefined as unknown as Closecurly
});
semicolon = (): Tsemicolon => ({
    tag: 'semicolon',
    contents: undefined as unknown as Semicolon
});
numberconst = (arg:number=0): Tnumberconst => ({
    tag: 'numberconst',
    contents: arg as Numberconst
});
eof = (): Teof => ({
    tag: '<eof>',
    contents: undefined as unknown as EOF
});

eq = (t1:Token, t2:Token): boolean => (t1 && t2 && (t1.tag==t2.tag));

interface Ilexer {
    constructor: Function;
    lex:()=>void;
    input: string;
    token?: Token;
}

interface Ifilelexer {
    constructor: Function;
    lexfile:()=>boolean;
    lexfile_:(ts:Token[], str:string)=>boolean;
    input: string;
    tokens: Token[];
}

class Lexer implements Ilexer {
    public input:string;
    public token?:Token;

    constructor(str:string) {
        this.input = str;
        this.lex();

        return this;
    }

    public lex() {
        let regexp:RegExp;
        let tokenname:string;
        let entry:Iregexp|undefined;
        let input:string;
        let inputarr:string[];
        let n:number;
        let matcharr:RegExpMatchArray;
        let token:Token|null;
        let tmp:string;
        let numconst:number;

        input = this.input;
        if (!input.length) {
            this.token = eof();
            return void 0;
        }

        entry = regexplist.find((x:Iregexp): boolean =>
            (input.match(x.regexp)) ? true : false);

        if (!entry)
            throw "Parse error (lexer)";

        regexp = entry.regexp;
        tokenname = entry.tokenname;
        matcharr = input.match(regexp) as RegExpMatchArray;
            // console.log('matcharr: ', matcharr);
        n = matcharr[0].length;
        inputarr = input.split('');
        while (n--)
            inputarr.shift();
        input = inputarr.join('');

        token = null;
        switch(tokenname) {
            case 'whitespace':  token = whitespace(); break;
            case 'openround':   token = openround(); break;
            case 'closeround':  token = closeround(); break;
            case 'opencurly':   token = opencurly(); break;
            case 'closecurly':  token = closecurly(); break;
            case 'semicolon':   token = semicolon(); break;
            case 'identifier':
                tmp = matcharr[0];
                token = identifier(tmp);

                break;
            case 'keyword':
                tmp = matcharr[0];
                token = keyword(tmp);

                break;
            case 'numberconst':
                tmp = matcharr[0];
                numconst = Number.parseInt(tmp);
                token = numberconst(numconst);

                break;
            default:
                throw 'Parse error (lexer)';
        }

        if (!token)
            throw 'Parse error (lexer)';

        this.input = input;
        this.token = token;

        return void 0;
    }
}

class FileLexer implements Ifilelexer {
    public input:string;
    public tokens:Token[];

    constructor(str:string) {
        let ret:boolean;

        if (!str)
            throw "Empty input string";
        
        this.input = str;
        this.tokens = [];
        ret = this.lexfile();

        if (!ret)
            throw "Parse error (lexing)";
    }

    public lexfile(): boolean {
        let ret:boolean;

        ret = this.lexfile_([], this.input);
        this.tokens = this.tokens.filter((x:Token): boolean =>
            (!eq(x,whitespace())));

        return ret;
    }

    public lexfile_(ts:Token[], str:string): boolean {
        let tok:Token;
        let toks:Token[];
        let str_:string;
        let lxr:Lexer;

        try {
            lxr = new Lexer(str);
            if (lxr.token) {
                tok = lxr.token;
                str_ = lxr.input;
            }
            else
               throw "Parse error (lexer)"

            toks = ts;
            toks.push(tok);

            if (eq(tok, eof())) {
                this.tokens = toks;
                this.input = '';

                return true;
            }
            else
                return this.lexfile_(toks, str_);
        } catch(err:unknown) {
            if (err instanceof String)
                console.error(err as string);
            return false;
        }

    }
}

export { Lexer, FileLexer, eq };
export type { Token };
export { keyword, whitespace, identifier, openround,
    closeround, opencurly, closecurly, semicolon, numberconst,
    eof };
export type { Identifier, EOF, Whitespace, Openround,
    Closeround, Opencurly, Closecurly, Semicolon,
    Numberconst, Keyword };

