proc.js

/**
 * 여러 분석기를 모아놓은 module입니다.
 * @module koalanlp/proc
 * @example
 * import { SentenceSplitter, Tagger, Parser, RoleLabeler, EntityRecognizer, Dictionary } from 'koalanlp/proc';
 **/

import {JVM} from './jvm';
import * as API from './API';
import {POS} from './types';
import {Sentence} from './data';
import {isDefined} from './common';

/**
 * POSFilter 함수
 * @callback POSFilter
 * @param {!POS} tag 검사할 품사 태그
 * @return {boolean} 해당하면 true.
 */

/**
 * 형태소 사전 항목. {'surface':형태소, 'tag':품사}
 * @typedef {Object} DicEntry
 * @property {!string} surface 형태소 표면형
 * @property {!POS} tag 형태소 품사
 */

/**
 * Build a Function Proxy object
 * @param {Object} thisObj
 * @param {!string} method
 * @returns {Object}
 * @private
 */
function assignProxy(thisObj, method) {
    if (method.endsWith('Sync'))
        return new Proxy(thisObj, {
            apply: function (target, thisArg, argArray) {
                return target[method](...argArray);
            }
        });
    else
        return new Proxy(thisObj, {
            apply: async function (target, thisArg, argArray) {
                return await target[method](...argArray);
            }
        });
}

/**
 * 문장분리기 Wrapper입니다.
 * @example
 * import { SentenceSplitter } from 'koalanlp/proc';
 * import { OKT } from 'koalanlp/API';
 *
 * let splitter = new SentenceSplitter(OKT);
 * splitter("문장을 분리해봅니다. 이렇게요.");
 */
export class SentenceSplitter extends Function {
    /**
     * Java API Object
     * @type {Object}
     * @private
     */
    _api = null;

    /**
     * 문장분리기를 생성합니다.
     *
     * @param {!API} api 문장분리기 API 패키지.
     * @param {Object} [options={}] 기타 설정
     * @param {boolean} [options.isAsyncDefault=true] 객체를 함수처럼 사용할 때, 즉 processor("문장")과 같이 사용할 때, 기본 호출을 async로 할 지 선택합니다. 기본값은 Asynchronous 호출입니다.
     */
    constructor(api, options = {}) {
        super();
        this._api = API.query(api, this.constructor.name)();
        options.isAsyncDefault = isDefined(options.isAsyncDefault) ? options.isAsyncDefault : true;

        return assignProxy(this, (options.isAsyncDefault) ? 'sentences' : 'sentencesSync');
    }

    /**
     * 문단을 문장으로 분리합니다. (Asynchronous)
     * @param {...!string} text 분석할 문단들 (가변인자)
     * @returns {string[]} 분리한 문장들.
     */
    async sentences(...text) {
        let result = [];
        for (let paragraph of text) {
            if (Array.isArray(paragraph)) {
                result.push(...await this.sentences(...paragraph));
            } else {
                if (paragraph.trim().length == 0)
                    continue;

                let promiseResult = await this._api.sentencesPromise(paragraph);
                result.push(...JVM.toJsArray(promiseResult));
            }
        }
        return result;
    }

    /**
     * 문단을 문장으로 분리합니다. (Synchronous)
     * @param {...!string} text 분석할 문단들 (가변인자)
     * @returns {string[]} 분리한 문장들.
     */
    sentencesSync(...text) {
        let result = [];
        for (let paragraph of text) {
            if (Array.isArray(paragraph)) {
                result.push(...this.sentencesSync(...paragraph));
            } else {
                if (paragraph.trim().length == 0)
                    continue;

                result.push(...JVM.toJsArray(this._api.sentences(paragraph)));
            }
        }
        return result;
    }

    /**
     * KoalaNLP가 구현한 문장분리기를 사용하여, 문단을 문장으로 분리합니다. (Asynchronous)
     * @param {Word[]} paragraph 분석할 문단. (품사표기가 되어있어야 합니다)
     * @returns {Sentence} 분리된 문장
     */
    static async sentences(paragraph) {
        let sent = [];
        for (let word of paragraph) {
            sent.push(word.getReference());
        }

        let promiseResult = await JVM.koalaClassOf('proc', 'SentenceSplitter').INSTANCE.sentencesPromise(sent);
        return JVM.toJsArray(promiseResult, (x) => new Sentence(x));
    }

    /**
     * KoalaNLP가 구현한 문장분리기를 사용하여, 문단을 문장으로 분리합니다. (Synchronous)
     * @param {Word[]} paragraph 분석할 문단. (품사표기가 되어있어야 합니다)
     * @returns {Sentence} 분리된 문장
     */
    static sentencesSync(paragraph) {
        let sent = [];
        for (let word of paragraph) {
            sent.push(word.getReference());
        }

        return JVM.toJsArray(JVM.koalaClassOf('proc', 'SentenceSplitter').INSTANCE.sentences(sent), (x) => new Sentence(x));
    }
}

/**
 * 형태소 분석기
 *
 * ## 참고
 *
 * **형태소** 는 의미를 가지는 요소로서는 더 이상 분석할 수 없는 가장 작은 말의 단위로 정의됩니다.
 *
 * **형태소 분석** 은 문장을 형태소의 단위로 나누는 작업을 의미합니다.
 *
 * 예) '문장을 형태소로 나눠봅시다'의 경우,
 *
 * * 문장/일반명사, -을/조사,
 * * 형태소/일반명사, -로/조사,
 * * 나누-(다)/동사, -어-/어미, 보-(다)/동사, -ㅂ시다/어미
 *
 * 로 대략 나눌 수 있습니다.
 *
 * 아래를 참고해보세요.
 *
 * * {@link module:koalanlp/data.Morpheme|Morpheme} 형태소를 저장하는 형태.
 * * {@link module:koalanlp/types.POS|POS} 형태소의 분류를 담은 Enum class
 *
 * @example
 * import { Tagger } from 'koalanlp/proc';
 * import { KMR } from 'koalanlp/API';
 *
 * let tagger = new Tagger(KMR);
 * tagger("문장을 분석해봅니다. 이렇게요.");
 */
export class Tagger extends Function {
    /**
     * Java API Object
     * @type {Object}
     * @private
     */
    _api = null;

    /**
     * 품사분석기를 초기화합니다.
     *
     * @param {!API} api 사용할 품사분석기의 유형.
     * @param {Object} [options={}]
     * @param {string} [options.apiKey=''] ETRI 분석기의 경우, ETRI에서 발급받은 API Key (2.2.0에서 삭제 예정)
     * @param {string} [options.etriKey=''] ETRI 분석기의 경우, ETRI에서 발급받은 API Key
     * @param {boolean} [options.useLightTagger=false] 코모란(KMR) 분석기의 경우, 경량 분석기를 사용할 것인지의 여부. (2.2.0에서 삭제 예정)
     * @param {boolean} [options.kmrLight=false] 코모란(KMR) 분석기의 경우, 경량 분석기를 사용할 것인지의 여부.
     * @param {string} [options.khaResource=''] Khaiii 분석기의 경우, 리소스 파일이 위치한 폴더.
     * @param {string} [options.khaPreanal=true] Khaiii 분석기의 경우, 기분석 사전을 사용할지의 여부.
     * @param {string} [options.khaErrorpatch=true] Khaiii 분석기의 경우, 오분석 사전 사용 여부
     * @param {string} [options.khaRestore=true] Khaiii 분석기의 경우, 형태소 재구성 여부
     * @param {boolean} [options.isAsyncDefault=true] 객체를 함수처럼 사용할 때, 즉 processor("문장")과 같이 사용할 때, 기본 호출을 async로 할 지 선택합니다. 기본값은 Asynchronous 호출입니다.
     */
    constructor(api, options = {}) {
        super();
        if (api === API.ETRI) {
            if (options.apiKey) {
                console.warn(`2.2.0부터 ${this.constructor.name}의 키워드 인자 "apiKey"가 삭제될 예정입니다. 2.1.0부터 추가된 인자인 "etriKey"를 사용해주세요.`);
                options.etriKey = options.apiKey
            }
            let etriKey = options.etriKey || '';
            if (!options.etriKey){
                console.error(`${this.constructor.name}(API.ETRI)는 키워드 인자 "etriKey"가 필요합니다. ETRI OpenAI hub에서 키를 발급받으세요.`);
                throw Error('etriKey 값이 비어있음');
            }
            this._api = API.query(api, this.constructor.name)(etriKey)
        } else if (api === API.KMR) {
            if (options.useLightTagger) {
                console.warn(`2.2.0부터 ${this.constructor.name}의 키워드 인자 "useLightTagger"가 삭제될 예정입니다. 2.1.0부터 추가된 인자인 "kmrLight"를 사용해주세요.`);
                options.kmrLight = options.useLightTagger
            }
            let useLightTagger = options.kmrLight || false;
            this._api = API.query(api, this.constructor.name)(useLightTagger)
        } else if (api === API.KHAIII) {
            let config = JVM.koalaClassOf('khaiii', 'KhaiiiConfig')(
                options.khaPreanal || true,
                options.khaErrorpatch || true,
                options.khaRestore || true
            );
            let rsc = options.khaResource || '';
            if (!options.khaResource){
                console.error(`${this.constructor.name}(API.KHAIII)는 키워드 인자 "khaResource"가 필요합니다. 리소스 파일 위치를 지정해주세요.`);
                throw Error('khaResource 값이 비어있음');
            }

            this._api = API.query(api, this.constructor.name)(rsc, config)
        } else {
            this._api = API.query(api, this.constructor.name)()
        }

        options.isAsyncDefault = isDefined(options.isAsyncDefault) ? options.isAsyncDefault : true;
        return assignProxy(this, (options.isAsyncDefault) ? 'tag' : 'tagSync');
    }

    /**
     * 문단(들)을 품사분석합니다. (Asynchronous)
     * @param {...(string|string[])} text 분석할 문단들. 텍스트와 string 리스트 혼용 가능. (가변인자)
     * @returns {Sentence[]} 분석된 결과 (Flattened list)
     */
    async tag(...text) {
        let result = [];
        for (let paragraph of text) {
            let promiseResult;
            if (Array.isArray(paragraph)) {
                promiseResult = await this.tag(...paragraph);
                result.push(...promiseResult);
            } else {
                if (paragraph.trim().length == 0)
                    continue;

                promiseResult = await this._api.tagPromise(paragraph);
                result.push(...JVM.toJsArray(promiseResult, (x) => new Sentence(x)));
            }
        }
        return result;
    }

    /**
     * 문단(들)을 품사분석합니다. (Synchronous)
     * @param {...(string|string[])} text 분석할 문단들. 텍스트와 string 리스트 혼용 가능. (가변인자)
     * @returns {Sentence[]} 분석된 결과 (Flattened list)
     */
    tagSync(...text) {
        let result = [];
        for (let paragraph of text) {
            if (Array.isArray(paragraph)) {
                result.push(...this.tagSync(...paragraph));
            } else {
                if (paragraph.trim().length == 0)
                    continue;

                result.push(...JVM.toJsArray(this._api.tag(paragraph), (x) => new Sentence(x)));
            }
        }
        return result;
    }

    /**
     * 문장을 품사분석합니다. 각 인자 하나를 하나의 문장으로 간주합니다. (Asynchronous)
     *
     * @param {...!string} text 분석할 문장(들). (가변인자)
     * @returns {Sentence[]} 분석된 결과.
     */
    async tagSentence(...text) {
        let result = [];
        for (let sentence of text) {
            let promiseResult;
            if (Array.isArray(sentence)) {
                promiseResult = await this.tagSentence(...sentence);
                result.push(...promiseResult);
            } else {
                if (sentence.trim().length == 0)
                    continue;

                promiseResult = await this._api.tagSentencePromise(sentence);
                result.push(new Sentence(promiseResult));
            }
        }
        return result;
    }

    /**
     * 문장을 품사분석합니다. 각 인자 하나를 하나의 문장으로 간주합니다. (Synchronous)
     *
     * @param {...!string} text 분석할 문장(들). (가변인자)
     * @returns {Sentence[]} 분석된 결과.
     */
    tagSentenceSync(...text) {
        let result = [];
        for (let sentence of text) {
            if (Array.isArray(sentence)) {
                result.push(...this.tagSentenceSync(...sentence));
            } else {
                if (sentence.trim().length == 0)
                    continue;

                result.push(new Sentence(this._api.tagSentence(sentence)));
            }
        }
        return result;
    }
}


/**
 * 문장 속성 부착기 Wrapper
 * @private
 */
class CanAnalyzeProperty extends Function {
    /**
     * Java API Object
     * @type {Object}
     * @private
     */
    _api = null;

    /**
     * 특성 부착형 분석기를 초기화합니다.
     *
     * @param {!API} api 사용할 분석기의 유형.
     * @param {!string} cls 사용할 클래스 유형.
     * @param {Object=} options
     * @param {string} options.apiKey ETRI 분석기의 경우, ETRI에서 발급받은 API Key (2.2.0에서 삭제 예정)
     * @param {string} options.etriKey ETRI 분석기의 경우, ETRI에서 발급받은 API Key
     * @param {boolean} [options.isAsyncDefault=true] 객체를 함수처럼 사용할 때, 즉 processor("문장")과 같이 사용할 때, 기본 호출을 async로 할 지 선택합니다. 기본값은 Asynchronous 호출입니다.
     */
    constructor(api, cls, options = {}) {
        super();

        if (api === API.ETRI) {
            if (options.apiKey) {
                console.warn(`2.2.0부터 ${this.constructor.name}의 키워드 인자 "apiKey"가 삭제될 예정입니다. 2.1.0부터 추가된 인자인 "etriKey"를 사용해주세요.`);
                options.etriKey = options.apiKey
            }
            let etriKey = options.etriKey || '';
            if (!options.etriKey){
                console.error(`${this.constructor.name}(API.ETRI)는 키워드 인자 "etriKey"가 필요합니다. ETRI OpenAI hub에서 키를 발급받으세요.`);
                throw Error('etriKey 값이 비어있음');
            }
            this._api = API.query(api, cls)(etriKey);
        } else {
            this._api = API.query(api, cls)();
        }

        options.isAsyncDefault = isDefined(options.isAsyncDefault) ? options.isAsyncDefault : true;
        return assignProxy(this, (options.isAsyncDefault) ? 'analyze' : 'analyzeSync');
    }

    /**
     * 문단(들)을 분석합니다. (Asynchronous)
     *
     * @param {...(string|Sentence|string[]|Sentence[])} text 분석할 문단(들).
     * 각 인자는 텍스트(str), 문장 객체(Sentence), 텍스트의 리스트, 문장 객체의 리스트 혼용 가능 (가변인자)
     * @returns {Sentence[]} 분석된 결과 (Flattened list)
     */
    async analyze(...text) {
        let result = [];
        for (let paragraph of text) {
            let promiseResult;
            if (paragraph instanceof Sentence) {
                promiseResult = await this._api.analyzePromise(paragraph.reference);
                result.push(new Sentence(promiseResult));
            } else if (Array.isArray(paragraph)) {
                promiseResult = await this.analyze(...paragraph);
                result.push(...promiseResult);
            } else {
                if (paragraph.trim().length == 0)
                    continue;

                promiseResult = await this._api.analyzePromise(paragraph);
                result.push(...JVM.toJsArray(promiseResult, (x) => new Sentence(x)));
            }
        }

        return result;
    }

    /**
     * 문단(들)을 분석합니다. (Synchronous)
     *
     * @param {...(string|Sentence|string[]|Sentence[])} text 분석할 문단(들).
     * 각 인자는 텍스트(str), 문장 객체(Sentence), 텍스트의 리스트, 문장 객체의 리스트 혼용 가능 (가변인자)
     * @returns {Sentence[]} 분석된 결과 (Flattened list)
     */
    analyzeSync(...text) {
        let result = [];
        for (let paragraph of text) {
            if (paragraph instanceof Sentence) {
                result.push(new Sentence(this._api.analyze(paragraph.reference)));
            } else if (Array.isArray(paragraph)) {
                result.push(...this.analyzeSync(...paragraph));
            } else {
                if (paragraph.trim().length == 0)
                    continue;

                result.push(...JVM.toJsArray(this._api.analyze(paragraph), (x) => new Sentence(x)));
            }
        }

        return result;
    }
}

/**
 * 구문구조/의존구조 분석기 Wrapper
 *
 * ## 참고
 *
 * **구문구조 분석** 은 문장의 구성요소들(어절, 구, 절)이 이루는 문법적 구조를 분석하는 방법입니다.
 *
 * 예) '나는 밥을 먹었고, 영희는 짐을 쌌다'라는 문장에는 2개의 절이 있습니다
 *
 * * 나는 밥을 먹었고
 * * 영희는 짐을 쌌다
 *
 * 각 절은 3개의 구를 포함합니다
 *
 * * 나는, 밥을, 영희는, 짐을: 체언구
 * * 먹었고, 쌌다: 용언구
 *
 * **의존구조 분석** 은 문장의 구성 어절들이 의존 또는 기능하는 관계를 분석하는 방법입니다.
 *
 * 예) '나는 밥을 먹었고, 영희는 짐을 쌌다'라는 문장에는
 *
 * 가장 마지막 단어인 '쌌다'가 핵심 어구가 되며,
 *
 * * '먹었고'가 '쌌다'와 대등하게 연결되고
 * * '나는'은 '먹었고'의 주어로 기능하며
 * * '밥을'은 '먹었고'의 목적어로 기능합니다.
 * * '영희는'은 '쌌다'의 주어로 기능하고,
 * * '짐을'은 '쌌다'의 목적어로 기능합니다.
 *
 * 아래를 참고해보세요.
 *
 * * {@link module:koalanlp/data.Word#phrase|Word#phrase} 어절이 속한 직속 상위 구구조(Phrase)를 돌려주는 API.
 * * {@link module:koalanlp/data.Word#governorEdge|Word#governorEdge} 어절이 지배당하는 상위 의존구조 [DepEdge]를 가져오는 API
 * * {@link module:koalanlp/data.Word#dependentEdges|Word#dependentEdges} 어절이 직접 지배하는 하위 의존구조 [DepEdge]의 목록를 가져오는 API
 * * {@link module:koalanlp/data.Sentence#syntaxTree|Sentence#syntaxTree} 전체 문장을 분석한 [SyntaxTree]를 가져오는 API
 * * {@link module:koalanlp/data.Sentence#dependencies|Sentence#dependencies} 전체 문장을 분석한 의존구조 [DepEdge]의 목록을 가져오는 API
 * * {@link module:koalanlp/data.SyntaxTree|SyntaxTree} 구문구조를 저장하는 형태
 * * {@link module:koalanlp/data.DepEdge|DepEdge} 의존구문구조의 저장형태
 * * {@link module:koalanlp/types.PhraseTag|PhraseTag} 의존구조의 형태 분류를 갖는 Enum 값 (구구조 분류와 같음)
 * * {@link module:koalanlp/types.DependencyTag|DependencyTag} 의존구조의 기능 분류를 갖는 Enum 값
 *
 * @inheritDoc
 * @example
 * import { Parser } from 'koalanlp/proc';
 * import { HNN } from 'koalanlp/API';
 *
 * let parser = new Parser(HNN);
 * parser("문장을 분석해봅니다. 이렇게요.");
 */
export class Parser extends CanAnalyzeProperty {
    /**
     * 구문구조/의존구조분석기를 초기화합니다.
     *
     * @param {!API} api 사용할 분석기의 유형.
     * @param {Object=} options
     * @param {string} options.apiKey ETRI 분석기의 경우, ETRI에서 발급받은 API Key (2.2.0에서 삭제 예정)
     * @param {string} options.etriKey ETRI 분석기의 경우, ETRI에서 발급받은 API Key
     * @param {boolean} [options.isAsyncDefault=true] 객체를 함수처럼 사용할 때, 즉 processor("문장")과 같이 사용할 때, 기본 호출을 async로 할 지 선택합니다. 기본값은 Asynchronous 호출입니다.
     */
    constructor(api, options = {}) {
        super(api, 'Parser', options);
    }
}

/**
 * 개체명 인식기 Wrapper
 *
 * ## 참고
 *
 * **개체명 인식** 은 문장에서 인물, 장소, 기관, 대상 등을 인식하는 기술입니다.
 *
 * 예) '철저한 진상 조사를 촉구하는 국제사회의 목소리가 커지고 있는 가운데, 트럼프 미국 대통령은 되레 사우디를 감싸고 나섰습니다.'에서, 다음을 인식하는 기술입니다.
 *
 * * '트럼프': 인물
 * * '미국' : 국가
 * * '대통령' : 직위
 * * '사우디' : 국가
 *
 * 아래를 참고해보세요.
 *
 * * {@link module:koalanlp/data.Morpheme#entities|Morpheme#entities} 형태소를 포함하는 모든 [Entity]를 가져오는 API
 * * {@link module:koalanlp/data.Word#entities|Word#entities} 어절을 포함하는 모든 [Entity]를 가져오는 API
 * * {@link module:koalanlp/data.Sentence#entities|Sentence#entities} 문장에 포함된 모든 [Entity]를 가져오는 API
 * * {@link module:koalanlp/data.Entity|Entity} 개체명을 저장하는 형태
 * * {@link module:koalanlp/types.CoarseEntityType|CoarseEntityType} [Entity]의 대분류 개체명 분류구조 Enum 값
 *
 * @inheritDoc
 * @example
 * import { EntityRecognizer } from 'koalanlp/proc';
 * import { ETRI } from 'koalanlp/API';
 *
 * let parser = new EntityRecognizer(ETRI);
 * parser("문장을 분석해봅니다. 이렇게요.");
 */
export class EntityRecognizer extends CanAnalyzeProperty {
    /**
     * 개체명 인식기를 초기화합니다.
     *
     * @param {!API} api 사용할 분석기의 유형.
     * @param {Object=} options
     * @param {string} options.apiKey ETRI 분석기의 경우, ETRI에서 발급받은 API Key (2.2.0에서 삭제 예정)
     * @param {string} options.etriKey ETRI 분석기의 경우, ETRI에서 발급받은 API Key
     * @param {boolean} [options.isAsyncDefault=true] 객체를 함수처럼 사용할 때, 즉 processor("문장")과 같이 사용할 때, 기본 호출을 async로 할 지 선택합니다. 기본값은 Asynchronous 호출입니다.
     */
    constructor(api, options = {}) {
        super(api, 'EntityRecognizer', options);
        return assignProxy(this, 'analyze');
    }
}

/**
 * 의미역 분석기 Wrapper
 *
 * ## 참고
 *
 * **의미역 결정** 은 문장의 구성 어절들의 역할/기능을 분석하는 방법입니다.
 *
 * 예) '나는 밥을 어제 집에서 먹었다'라는 문장에는
 *
 * 동사 '먹었다'를 중심으로
 *
 * * '나는'은 동작의 주체를,
 * * '밥을'은 동작의 대상을,
 * * '어제'는 동작의 시점을
 * * '집에서'는 동작의 장소를 나타냅니다.
 *
 * 아래를 참고해보세요.
 *
 * * {@link module:koalanlp/data.Word#predicateRoles|Word#predicateRoles} 어절이 논항인 [RoleEdge]의 술어를 가져오는 API
 * * {@link module:koalanlp/data.Word#argumentRoles|Word#argumentRoles} 어절이 술어인 [RoleEdge]의 논항들을 가져오는 API
 * * {@link module:koalanlp/data.Sentence#roles|Sentence#roles} 전체 문장을 분석한 의미역 구조 [RoleEdge]를 가져오는 API
 * * {@link module:koalanlp/data.RoleEdge|RoleEdge} 의미역 구조를 저장하는 형태
 * * {@link module:koalanlp/types.RoleType|RoleType} 의미역 분류를 갖는 Enum 값
 *
 * @inheritDoc
 * @example
 * import { RoleLabeler } from 'koalanlp/proc';
 * import { ETRI } from 'koalanlp/API';
 *
 * let parser = new RoleLabeler(ETRI);
 * parser("문장을 분석해봅니다. 이렇게요.");
 */
export class RoleLabeler extends CanAnalyzeProperty {
    /**
     * 의미역 분석기를 초기화합니다.
     *
     * @param {!API} api 사용할 분석기의 유형.
     * @param {Object=} options
     * @param {string} options.apiKey ETRI 분석기의 경우, ETRI에서 발급받은 API Key (2.2.0에서 삭제 예정)
     * @param {string} options.etriKey ETRI 분석기의 경우, ETRI에서 발급받은 API Key
     * @param {boolean} [options.isAsyncDefault=true] 객체를 함수처럼 사용할 때, 즉 processor("문장")과 같이 사용할 때, 기본 호출을 async로 할 지 선택합니다. 기본값은 Asynchronous 호출입니다.
     */
    constructor(api, options = {}) {
        super(api, 'RoleLabeler', options);
        return assignProxy(this, 'analyze');
    }
}

/**
 * Java 사전 항목을 JS 사전 항목으로 변환.
 * @param entry Java 사전 항목
 * @return {DicEntry} JS 사전 항목
 * @private
 */
function readDicEntry(entry) {
    return {
        'surface': entry.getFirst(),
        'tag': POS.withName(entry.getSecond().name())
    };
}

/**
 * 사전 Wrapper
 * @example
 * import { Dictionary } from 'koalanlp/proc';
 * import { KKMA } from 'koalanlp/API';
 *
 * let dict = Dictionary(KKMA);
 * dict.addUserDictionary({'surface': "하림"});
 */
export class Dictionary {
    /**
     * Java API Object
     * @type {Object}
     * @private
     */
    _api = null;

    /**
     * 사용자 정의 사전을 연결합니다.
     *
     * @param {!API} api 사용자 정의 사전을 연결할 API 패키지.
     */
    constructor(api) {
        this._api = API.query(api, 'Dictionary').INSTANCE;
    }

    /**
     * 사용자 사전에, 표면형과 그 품사를 추가.
     * @param {...DicEntry} pairs 추가할 형태소와 품사들. (가변인자)
     */
    addUserDictionary(...pairs) {
        let surfaceList = [];
        let tagList = [];

        for (let pair of pairs) {
            surfaceList.push(pair.surface);

            let tag = pair.tag ? pair.tag.reference : POS.NNP.reference;
            tagList.push(tag);
        }

        this._api.addUserDictionary(JVM.listOf(surfaceList), JVM.listOf(tagList));
    }

    /**
     * 사전에 등재되어 있는지 확인합니다.
     * @param {string} word 확인할 형태소
     * @param {POS} posTags 세종품사들(기본값: NNP 고유명사, NNG 일반명사)
     * @returns {boolean} 사전에 포함된다면 True 아니면 False.
     */
    contains(word, ...posTags) {
        let tags = (posTags.length > 0) ? posTags : [POS.NNP, POS.NNG];
        if (tags.length === 1) {
            let tag = tags[0];
            return this._api.contains(JVM.pair(word, tag.reference));
        } else {
            let tagsRef = tags.map((tag) => tag.reference);
            return this._api.contains(word, JVM.setOf(tagsRef));
        }
    }

    /**
     * 다른 사전을 참조하여, 선택된 사전에 없는 단어를 사용자사전으로 추가합니다.
     *
     * @param {Dictionary} other 참조할 사전
     * @param {boolean} [fastAppend=false] 선택된 사전에 존재하는지를 검사하지 않고 빠르게 추가하고자 할 때.
     * @param {POSFilter} [filter=(x) => x.isNoun()] 가져올 품사나, 품사의 리스트, 또는 해당 품사인지 판단하는 함수.
     */
    async importFrom(other, fastAppend = false, filter = (x) => x.isNoun()) {
        let tags = [];
        if (filter instanceof Function) {
            for (let tag of POS.values()) {
                if (filter(tag)) tags.push(tag.tagname);
            }
        } else {
            for (let tag of filter) {
                tags.push(tag.tagname);
            }
        }

        await this._api.importFromPromise(other._api, fastAppend, JVM.posFilter(tags));
    }

    /**
     * 원본 사전에 등재된 항목 중에서, 지정된 형태소의 항목만을 가져옵니다. (복합 품사 결합 형태는 제외)
     *
     * @param {POSFilter} [filter=(x) => x.isNoun()] 가져올 품사나, 품사의 리스트, 또는 해당 품사인지 판단하는 함수.
     * @return {Iterator.<DicEntry>} {'surface':형태소, 'tag':품사}의 generator
     */
    async getBaseEntries(filter = (x) => x.isNoun()) {
        let tags = [];
        if (filter instanceof Function) {
            for (let tag of POS.values()) {
                if (filter(tag)) tags.push(tag.tagname);
            }
        } else {
            for (let tag of filter) {
                tags.push(tag.tagname);
            }
        }

        let entries = await this._api.getBaseEntriesPromise(JVM.posFilter(tags));
        return (function* () {
            while (entries.hasNext()) {
                yield readDicEntry(entries.next());
            }
        })();
    }

    /**
     * 사용자 사전에 등재된 모든 항목을 가져옵니다.
     * @return {DicEntry[]} {'surface':형태소, 'tag':품사}의 list
     */
    async getItems() {
        return JVM.toJsArray(await this._api.getItemsPromise(), readDicEntry, true);
    }

    /**
     * 사전에 등재되어 있는지 확인하고, 사전에 없는단어만 반환합니다.
     * @param {boolean} onlySystemDic 시스템 사전에서만 검색할지 결정합니다.
     * @param {DicEntry} word {'surface':형태소, 'tag':품사}들. (가변인자)
     * @return {DicEntry[]} 사전에 없는 단어들
     */
    async getNotExists(onlySystemDic, ...word) {
        let zipped = word.map((pair) => JVM.pair(pair.surface, pair.tag.reference));
        return JVM.toJsArray(await this._api.getNotExistsPromise(onlySystemDic, ...zipped), readDicEntry);
    }
}


/**
 * 울산대 UTagger 라이브러리 연결용 Static class
 */
export class UTagger {
    /**
     * UTagger의 라이브러리와 설정파일의 위치를 지정합니다.
     *
     * @param libPath 라이브러리 파일의 위치
     * @param confPath 설정 파일의 위치
     */
    static setPath(libPath, confPath) {
        JVM.koalaClassOf('utagger', 'UTagger').setPath(libPath, confPath);
    }
}