// なでしこの字句解析を行う
// 既に全角半角を揃えたコードに対して字句解析を行う
import { opPriority } from './nako_parser_const.mjs';
// 予約語句
// (memo)「回」「間」「繰返」「反復」「抜」「続」「戻」「代入」などは _replaceWord で word から変換
/** @types {Record<string, string>} */
import reservedWords from './nako_reserved_words.mjs';
import { isIndentChars } from './nako_indent_chars.mjs';
// 助詞の一覧
import { josiRE, removeJosiMap, tararebaMap } from './nako_josi_list.mjs';
// 字句解析ルールの一覧
import { rules, unitRE } from './nako_lex_rules.mjs';
import { NakoLexerError, InternalLexerError } from './nako_errors.mjs';
export class NakoLexer {
    /**
     * @param logger
     */
    constructor(logger) {
        this.logger = logger; // 字句解析した際,確認された関数の一覧
        this.funclist = {};
        this.modList = []; // 字句解析した際,取り込むモジュール一覧 --- nako3::lex で更新される
        this.result = [];
        this.modName = 'main.nako3'; // モジュール名
        this.moduleExport = {};
    }
    /** 関数一覧をセット */
    setFuncList(listObj) {
        this.funclist = listObj;
    }
    /** モジュール公開既定値一覧をセット */
    setModuleExport(exportObj) {
        this.moduleExport = exportObj;
    }
    /**
     * @param tokens
     * @param {boolean} isFirst
     * @param {string} filename
     */
    replaceTokens(tokens, isFirst, filename) {
        this.result = tokens;
        this.modName = NakoLexer.filenameToModName(filename);
        // 関数の定義があれば funclist を更新
        NakoLexer.preDefineFunc(tokens, this.logger, this.funclist, this.moduleExport);
        this._replaceWord(this.result);
        if (isFirst) {
            if (this.result.length > 0) {
                const eof = this.result[this.result.length - 1];
                this.result.push({
                    type: 'eol',
                    line: eof.line,
                    column: 0,
                    indent: -1,
                    file: eof.file,
                    josi: '',
                    value: '---',
                    startOffset: eof.startOffset,
                    endOffset: eof.endOffset,
                    rawJosi: ''
                }); // 改行
                this.result.push({
                    type: 'eof',
                    line: eof.line,
                    indent: -1,
                    column: 0,
                    file: eof.file,
                    josi: '',
                    value: '',
                    startOffset: eof.startOffset,
                    endOffset: eof.endOffset,
                    rawJosi: ''
                }); // ファイル末尾
            }
            else {
                this.result.push({
                    type: 'eol',
                    line: 0,
                    column: 0,
                    indent: -1,
                    file: '',
                    josi: '',
                    value: '---',
                    startOffset: 0,
                    endOffset: 0,
                    rawJosi: ''
                }); // 改行
                this.result.push({
                    type: 'eof',
                    line: 0,
                    column: 0,
                    indent: -1,
                    file: '',
                    josi: '',
                    value: '',
                    startOffset: 0,
                    endOffset: 0,
                    rawJosi: ''
                }); // ファイル末尾
            }
        }
        return this.result;
    }
    /**
     * ファイル内で定義されている関数名を列挙する。結果はfunclistに書き込む。その他のトークンの置換処理も行う。
     * シンタックスハイライトの処理から呼び出すためにstaticメソッドにしている。
     */
    static preDefineFunc(tokens, logger, funclist, moduleexport) {
        // 関数を先読みして定義
        let i = 0;
        let isFuncPointer = false;
        const readArgs = () => {
            const args = [];
            const keys = {};
            if (tokens[i].type !== '(') {
                return [];
            }
            i++;
            while (tokens[i]) {
                const t = tokens[i];
                i++;
                if (t.type === ')') {
                    break;
                }
                if (t.type === 'func') {
                    isFuncPointer = true;
                }
                else if (t.type !== '|' && t.type !== 'comma') {
                    if (isFuncPointer) {
                        t.funcPointer = true;
                        isFuncPointer = false;
                    }
                    args.push(t);
                    if (!keys[t.value]) {
                        keys[t.value] = [];
                    }
                    keys[t.value].push(t.josi);
                }
            }
            const varnames = [];
            const funcPointers = [];
            const result = [];
            const already = {};
            for (const arg of args) {
                if (!already[arg.value]) {
                    const josi = keys[arg.value];
                    result.push(josi);
                    varnames.push(arg.value);
                    if (arg.funcPointer) {
                        funcPointers.push(arg.value);
                    }
                    else {
                        funcPointers.push(null);
                    }
                    already[arg.value] = true;
                }
            }
            return [result, varnames, funcPointers];
        };
        // トークンを一つずつ確認
        while (i < tokens.length) {
            // タイプの置換
            const t = tokens[i];
            if (t.type === 'not' && tokens.length - i > 3) {
                let prevToken = { type: 'eol' };
                if (i >= 1) {
                    prevToken = tokens[i - 1];
                }
                if (prevToken.type === 'eol') {
                    let nextToken = tokens[i + 1];
                    if (nextToken.type === 'word' && nextToken.value === 'モジュール公開既定値') {
                        nextToken.type = 'モジュール公開既定値';
                        nextToken = tokens[i + 2];
                        if (nextToken.type === 'string' && nextToken.value === '非公開') {
                            const modName = NakoLexer.filenameToModName(t.file);
                            moduleexport[modName] = false;
                            i += 3;
                            continue;
                        }
                        else if (nextToken.type === 'string' && nextToken.value === '公開') {
                            const modName = NakoLexer.filenameToModName(t.file);
                            moduleexport[modName] = true;
                            i += 3;
                            continue;
                        }
                    }
                }
            }
            // 無名関数の定義：「xxには**」があった場合 ... 暗黙的な関数定義とする
            if ((t.type === 'word' && t.josi === 'には') || (t.type === 'word' && t.josi === 'は~')) {
                t.josi = 'には';
                tokens.splice(i + 1, 0, { type: 'def_func', value: '関数', indent: t.indent, line: t.line, column: t.column, file: t.file, josi: '', startOffset: t.endOffset, endOffset: t.endOffset, rawJosi: '', tag: '無名関数' });
                i++;
                continue;
            }
            // N回をN|回に置換
            if (t.type === 'word' && t.josi === '' && t.value.length >= 2) {
                if (t.value.match(/回$/)) {
                    t.value = t.value.substring(0, t.value.length - 1);
                    // N回を挿入
                    if (!t.endOffset) {
                        t.endOffset = 1;
                    }
                    const kai = { type: '回', value: '回', indent: t.indent, line: t.line, column: t.column, file: t.file, josi: '', startOffset: t.endOffset - 1, endOffset: t.endOffset, rawJosi: '' };
                    tokens.splice(i + 1, 0, kai);
                    t.endOffset--;
                    i++;
                }
            }
            // 予約語の置換
            if (t.type === 'word' && reservedWords[t.value]) {
                t.type = reservedWords[t.value];
                if (t.value === 'そう') {
                    t.value = 'それ';
                }
            }
            // 関数定義の確認
            if (t.type !== 'def_test' && t.type !== 'def_func') {
                i++;
                continue;
            }
            // 無名関数か普通関数定義かを判定する (1つ前が改行かどうかで判定)
            let isMumei = true;
            let prevToken = { type: 'eol' };
            if (i >= 1) {
                prevToken = tokens[i - 1];
            }
            if (prevToken.type === 'eol') {
                isMumei = false;
            }
            // 関数名や引数を得る
            const defToken = t;
            i++; // skip "●" or "関数"
            let josi = [];
            let varnames = [];
            let funcPointers = [];
            let funcName = '';
            let funcNameToken = null;
            let isExport = null;
            // 関数の属性指定
            if (tokens[i] && tokens[i].type === '{') {
                i++;
                const attr = tokens[i] && tokens[i].type === 'word' ? tokens[i].value : '';
                if (attr === '公開') {
                    isExport = true;
                }
                else if (attr === '非公開') {
                    isExport = false;
                }
                else if (attr === 'エクスポート') {
                    isExport = true;
                }
                else {
                    logger.warn(`不明な関数属性『${attr}』が指定されています。`);
                }
                i++;
                if (tokens[i] && tokens[i].type === '}') {
                    i++;
                }
            }
            // 関数名の前に引数定義
            if (tokens[i] && tokens[i].type === '(') {
                [josi, varnames, funcPointers] = readArgs();
            }
            // 関数名を得る
            if (!isMumei && tokens[i] && tokens[i].type === 'word') {
                funcNameToken = tokens[i++];
                funcName = funcNameToken.value;
            }
            // 関数名の後で引数定義
            if (josi.length === 0 && tokens[i] && tokens[i].type === '(') {
                [josi, varnames, funcPointers] = readArgs();
            }
            // 名前のある関数定義ならば関数テーブルに関数名を登録
            // 無名関数は登録しないように気をつける
            if (funcName !== '' && funcNameToken) {
                const modName = NakoLexer.filenameToModName(t.file);
                funcName = modName + '__' + funcName;
                if (funcName in funclist) { // 関数の二重定義を警告
                    // main__は省略 #1223
                    const dispName = funcName.replace(/^main__/, '');
                    logger.warn(`関数『${dispName}』は既に定義されています。`, defToken);
                }
                funcNameToken.value = funcName;
                funclist[funcName] = {
                    type: 'func',
                    josi,
                    fn: null,
                    asyncFn: false,
                    isExport,
                    varnames,
                    funcPointers
                };
            }
            // 無名関数のために
            defToken.meta = {
                type: 'func',
                josi,
                varnames,
                funcPointers
            };
        }
    }
    /** 文字列を{と}の部分で分割する。中括弧が対応していない場合nullを返す。 */
    splitStringEx(code) {
        /** @type {string[]} */
        const list = [];
        // "A{B}C{D}E" -> ["A", "B}C", "D}E"] -> ["A", "B", "C", "D", "E"]
        // "A{B}C}D{E}F" -> ["A", "B}C}D", "E}F"] -> ["A", "B", "C}D", "E", "F"]
        const arr = code.split(/[{｛]/);
        list.push(arr[0]);
        for (const s of arr.slice(1)) {
            const end = s.replace('｝', '}').indexOf('}');
            if (end === -1) {
                return null;
            }
            list.push(s.slice(0, end), s.slice(end + 1));
        }
        return list;
    }
    _replaceWord(tokens) {
        let comment = [];
        let i = 0;
        const namespaceStack = [];
        const getLastType = () => {
            if (i <= 0) {
                return 'eol';
            }
            return tokens[i - 1].type;
        };
        let modSelf = (tokens.length > 0) ? NakoLexer.filenameToModName(tokens[0].file) : 'main';
        while (i < tokens.length) {
            const t = tokens[i];
            // モジュール名の変更に対応
            if ((t.type === 'word' || t.type === 'func') && t.value === '名前空間設定') {
                namespaceStack.push(modSelf);
                modSelf = tokens[i - 1].value;
            }
            if ((t.type === 'word' || t.type === 'func') && t.value === '名前空間ポップ') {
                const space = namespaceStack.pop();
                if (space) {
                    modSelf = space;
                }
            }
            // 関数を強制的に置換( word => func )
            if (t.type === 'word' && t.value !== 'それ') {
                // 関数を変換
                const funcName = t.value;
                if (funcName.indexOf('__') < 0) {
                    // 自身のモジュール名を検索
                    const gname1 = `${modSelf}__${funcName}`;
                    const gfo1 = this.funclist[gname1];
                    if (gfo1 && gfo1.type === 'func') {
                        t.type = 'func';
                        t.meta = gfo1;
                        t.value = gname1;
                        continue;
                    }
                    // モジュール関数を置換
                    for (const mod of this.modList) {
                        const gname = `${mod}__${funcName}`;
                        const gfo = this.funclist[gname];
                        const exportDefault = this.moduleExport[mod];
                        if (gfo && gfo.type === 'func' && (gfo.isExport === true || (gfo.isExport !== false && exportDefault !== false))) {
                            t.type = 'func';
                            t.meta = gfo;
                            t.value = gname;
                            break;
                        }
                    }
                    if (t.type === 'func') {
                        continue;
                    }
                }
                const fo = this.funclist[funcName];
                if (fo && fo.type === 'func') {
                    t.type = 'func';
                    t.meta = fo;
                }
            }
            // 数字につくマイナス記号を判定
            // (ng) 5 - 3 || word - 3
            // (ok) (行頭)-3 || 1 * -3 || Aに -3を 足す
            if (t.type === '-' && tokens[i + 1]) {
                const tokenType = tokens[i + 1].type;
                if (tokenType === 'number' || tokenType === 'bigint') {
                    // 一つ前の語句が、(行頭|演算子|助詞付きの語句)なら 負数である
                    const ltype = getLastType();
                    if (ltype === 'eol' || opPriority[ltype] || tokens[i - 1].josi !== '') {
                        tokens.splice(i, 1); // remove '-'
                        if (tokenType === 'number') {
                            tokens[i].value *= -1;
                        }
                        else {
                            tokens[i].value = '-' + tokens[i].value;
                        }
                    }
                }
            }
            // 助詞の「は」を = に展開
            if (t.josi === undefined) {
                t.josi = '';
            }
            if (t.josi === 'は') {
                if (!t.rawJosi) {
                    t.rawJosi = t.josi;
                }
                const startOffset = (t.endOffset === undefined) ? undefined : t.endOffset - t.rawJosi.length;
                tokens.splice(i + 1, 0, {
                    type: 'eq',
                    indent: t.indent,
                    line: t.line,
                    column: t.column,
                    file: t.file,
                    startOffset,
                    endOffset: t.endOffset,
                    josi: '',
                    rawJosi: '',
                    value: undefined
                });
                i += 2;
                t.josi = t.rawJosi = '';
                t.endOffset = startOffset;
                continue;
            }
            // 「とは」を一つの単語にする
            if (t.josi === 'とは') {
                if (!t.rawJosi) {
                    t.rawJosi = t.josi;
                }
                const startOffset = t.endOffset === undefined ? undefined : t.endOffset - t.rawJosi.length;
                tokens.splice(i + 1, 0, {
                    type: t.josi,
                    indent: t.indent,
                    line: t.line,
                    column: t.column,
                    file: t.file,
                    startOffset,
                    endOffset: t.endOffset,
                    josi: '',
                    rawJosi: '',
                    value: undefined
                });
                t.josi = t.rawJosi = '';
                t.endOffset = startOffset;
                i += 2;
                continue;
            }
            // 助詞のならばをトークンとする
            if (tararebaMap[t.josi]) {
                const josi = (t.josi === 'でなければ' || t.josi === 'なければ') ? 'でなければ' : 'ならば';
                if (!t.rawJosi) {
                    t.rawJosi = josi;
                }
                const startOffset = t.endOffset === undefined ? undefined : t.endOffset - t.rawJosi.length;
                tokens.splice(i + 1, 0, {
                    type: 'ならば',
                    value: josi,
                    indent: t.indent,
                    line: t.line,
                    column: t.column,
                    file: t.file,
                    startOffset,
                    endOffset: t.endOffset,
                    josi: '',
                    rawJosi: ''
                });
                t.josi = t.rawJosi = '';
                t.endOffset = startOffset;
                i += 2;
                continue;
            }
            // '_' + 改行 を飛ばす (演算子直後に改行を入れたい場合に使う)
            if (t.type === '_eol') {
                tokens.splice(i, 1);
                continue;
            }
            // コメントを飛ばす
            if (t.type === 'line_comment' || t.type === 'range_comment') {
                comment.push(t.value);
                tokens.splice(i, 1);
                continue;
            }
            // 改行にコメントを埋め込む
            if (t.type === 'eol') {
                t.value = comment.join('/');
                comment = [];
            }
            i++;
        }
    }
    /**
     * インデントの個数を数える
     * @returns 戻り値として[インデント数, 読み飛ばすべき文字数]を返す
     */
    countIndent(src) {
        let indent = 0;
        for (let i = 0; i < src.length; i++) {
            const c = src.charAt(i);
            const n = isIndentChars(c);
            if (n === 0) {
                return [indent, i];
            }
            indent += n;
        }
        return [indent, src.length];
    }
    /**
     * ソースコードをトークンに分割する
     * @param src なでしこのソースコード
     * @param line 先頭行の行番号
     * @param filename ファイル名
     */
    tokenize(src, line, filename) {
        const srcLength = src.length;
        const result = [];
        let columnCurrent;
        let lineCurrent;
        let column = 1;
        let isDefTest = false;
        let indent = 0;
        // 最初にインデントを数える
        const ia = this.countIndent(src);
        indent = ia[0]; // インデント数
        src = src.substring(ia[1]); // 読み飛ばす文字数
        column += ia[1];
        while (src !== '') {
            let ok = false;
            // 各ルールについて
            for (const rule of rules) {
                // 正規表現でマッチ
                const m = rule.pattern.exec(src);
                if (!m) {
                    continue;
                }
                ok = true;
                // 空白ならスキップ
                if (rule.name === 'space') {
                    column += m[0].length;
                    src = src.substring(m[0].length);
                    continue;
                }
                // マッチしたルールがコールバックを持つなら
                if (rule.cbParser) {
                    // コールバックを呼ぶ
                    let rp;
                    if (isDefTest && rule.name === 'word') {
                        rp = rule.cbParser(src, false);
                    }
                    else {
                        try {
                            rp = rule.cbParser(src);
                        }
                        catch (e) {
                            throw new NakoLexerError(e.message, srcLength - src.length, srcLength - src.length + 1, line, filename);
                        }
                    }
                    if (rule.name === 'string_ex') {
                        // 展開あり文字列 → aaa{x}bbb{x}cccc
                        const list = this.splitStringEx(rp.res);
                        if (list === null) {
                            throw new InternalLexerError('展開あり文字列で値の埋め込み{...}が対応していません。', srcLength - src.length, srcLength - rp.src.length, line, filename);
                        }
                        let offset = 0;
                        for (let i = 0; i < list.length; i++) {
                            const josi = (i === list.length - 1) ? rp.josi : '';
                            if (i % 2 === 0) {
                                result.push({
                                    type: 'string',
                                    value: list[i],
                                    file: filename,
                                    josi,
                                    indent,
                                    line,
                                    column,
                                    preprocessedCodeOffset: srcLength - src.length + offset,
                                    preprocessedCodeLength: list[i].length + 2 + josi.length
                                });
                                // 先頭なら'"...{'、それ以外なら'}...{'、最後は何でも良い
                                offset += list[i].length + 2;
                            }
                            else {
                                result.push({ type: '&', value: '&', josi: '', indent, file: filename, line, column, preprocessedCodeOffset: srcLength - src.length + offset, preprocessedCodeLength: 0 });
                                result.push({ type: '(', value: '(', josi: '', indent, file: filename, line, column, preprocessedCodeOffset: srcLength - src.length + offset, preprocessedCodeLength: 0 });
                                result.push({ type: 'code', value: list[i], josi: '', indent, file: filename, line, column, preprocessedCodeOffset: srcLength - src.length + offset, preprocessedCodeLength: list[i].length });
                                result.push({ type: ')', value: ')', josi: '', indent, file: filename, line, column, preprocessedCodeOffset: srcLength - src.length + offset + list[i].length, preprocessedCodeLength: 0 });
                                result.push({ type: '&', value: '&', josi: '', indent, file: filename, line, column, preprocessedCodeOffset: srcLength - src.length + offset + list[i].length, preprocessedCodeLength: 0 });
                                offset += list[i].length;
                            }
                        }
                        line += rp.numEOL;
                        column += src.length - rp.src.length;
                        src = rp.src;
                        if (rp.numEOL > 0) {
                            column = 1;
                        }
                        break;
                    }
                    columnCurrent = column;
                    column += src.length - rp.src.length;
                    result.push({ type: rule.name, value: rp.res, josi: rp.josi, indent, line, column: columnCurrent, file: filename, preprocessedCodeOffset: srcLength - src.length, preprocessedCodeLength: src.length - rp.src.length });
                    src = rp.src;
                    line += rp.numEOL;
                    if (rp.numEOL > 0) {
                        column = 1;
                    }
                    break;
                }
                // ソースを進める前に位置を計算
                const srcOffset = srcLength - src.length;
                // 値を変換する必要があるか？
                let value = m[0];
                if (rule.cb) {
                    value = rule.cb(value);
                }
                // ソースを進める
                columnCurrent = column;
                lineCurrent = line;
                column += m[0].length;
                src = src.substring(m[0].length);
                // 改行の時の処理
                if ((rule.name === 'eol' && value === '\n') || rule.name === '_eol') {
                    value = line++;
                    column = 1;
                }
                // 数値なら単位を持つか？ --- #994
                if (rule.name === 'number') {
                    // 単位があれば読み飛ばす
                    const um = unitRE.exec(src);
                    if (um) {
                        src = src.substring(um[0].length);
                        column += m[0].length;
                    }
                }
                let josi = '';
                if (rule.readJosi) {
                    // 正規表現で助詞があるか読み取る
                    const j = josiRE.exec(src);
                    if (j) {
                        column += j[0].length;
                        josi = j[0].replace(/^\s+/, '');
                        src = src.substring(j[0].length);
                        // 助詞の直後にあるカンマを無視 #877
                        if (src.charAt(0) === ',') {
                            src = src.substring(1);
                        }
                        // 「＊＊である」なら削除 #939 #974
                        if (removeJosiMap[josi]) {
                            josi = '';
                        }
                        // 「もの」構文 (#1614)
                        if (josi.substring(0, 2) === 'もの') {
                            josi = josi.substring(2);
                        }
                    }
                }
                switch (rule.name) {
                    case 'def_test': {
                        isDefTest = true;
                        break;
                    }
                    case 'eol': { // eolの処理はほかに↑と↓にある
                        isDefTest = false;
                        break;
                    }
                    default: {
                        break;
                    }
                }
                // ここまで‰(#682) を処理
                if (rule.name === 'dec_lineno') {
                    line--;
                    continue;
                }
                result.push({
                    type: rule.name,
                    value,
                    indent,
                    line: lineCurrent,
                    column: columnCurrent,
                    file: filename,
                    josi,
                    preprocessedCodeOffset: srcOffset,
                    preprocessedCodeLength: (srcLength - src.length) - srcOffset
                });
                // 改行のとき次の行のインデントを調べる。なお、改行の後は必ずcolumnが1になる。インデント構文のため、一行に2つ以上の文を含むときを考慮する。(core #66)
                if (rule.name === 'eol' && column === 1) {
                    const ia = this.countIndent(src);
                    indent = ia[0];
                    column += ia[1];
                    src = src.substring(ia[1]); // インデントを飛ばす
                }
                break;
            }
            if (!ok) {
                throw new InternalLexerError('未知の語句: ' + src.substring(0, 3) + '...', srcLength - src.length, srcLength - srcLength + 3, line, filename);
            }
        }
        return result;
    }
    // トークン配列をtype文字列に変換
    static tokensToTypeStr(tokens, sep) {
        const a = tokens.map((v) => {
            return v.type;
        });
        return a.join(sep);
    }
    /**
     * ファイル名からモジュール名へ変換
     * @param {string} filename
     * @returns {string}
     */
    static filenameToModName(filename) {
        if (!filename) {
            return 'main';
        }
        // パスがあればパスを削除
        filename = filename.replace(/[\\:]/g, '/'); // Windowsのpath記号を/に置換
        if (filename.indexOf('/') >= 0) {
            const a = filename.split('/');
            filename = a[a.length - 1];
        }
        filename = filename.replace(/\.nako3?$/, '');
        return filename;
    }
}
