/*
 This file is part of Airsonic.

 Airsonic is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 Airsonic is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with Airsonic.  If not, see <http://www.gnu.org/licenses/>.

 Copyright 2016 (C) Airsonic Authors
 Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
 */

package org.airsonic.player.service.search;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.cjk.CJKWidthFilterFactory;
import org.apache.lucene.analysis.core.KeywordTokenizerFactory;
import org.apache.lucene.analysis.core.LowerCaseFilterFactory;
import org.apache.lucene.analysis.core.StopFilterFactory;
import org.apache.lucene.analysis.custom.CustomAnalyzer;
import org.apache.lucene.analysis.custom.CustomAnalyzer.Builder;
import org.apache.lucene.analysis.en.EnglishPossessiveFilterFactory;
import org.apache.lucene.analysis.miscellaneous.ASCIIFoldingFilterFactory;
import org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper;
import org.apache.lucene.analysis.pattern.PatternReplaceFilterFactory;
import org.apache.lucene.analysis.standard.StandardTokenizerFactory;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import static org.springframework.util.ObjectUtils.isEmpty;

/**
 * Analyzer provider.
 * This class is a division of what was once part of SearchService and added functionality.
 * This class provides Analyzer which is used at index generation
 * and QueryAnalyzer which analyzes the specified query at search time.
 * Analyzer can be closed but is a reuse premise.
 * It is held in this class.
 */
@Component
public final class AnalyzerFactory {

    private static final String STOP_WORDS = "org/airsonic/player/service/search/analysis/stopwords.txt";

    private static final String STOP_WORDS_ARTIST = "org/airsonic/player/service/search/analysis/stopwords_artist.txt";

    private Analyzer analyzer;

    private Analyzer queryAnalyzer;

    /*
     * XXX 3.x -> 8.x : Convert UAX#29 Underscore Analysis to Legacy Analysis
     *
     * Because changes in underscores before and after words
     * have a major effect on user's forward match search.
     *
     * @see AnalyzerFactoryTestCase
     */
    private void addTokenFilterForUnderscoreRemovalAroundToken(Builder builder) throws IOException {
        builder
            .addTokenFilter(PatternReplaceFilterFactory.class,
                    "pattern", "^\\_", "replacement", "", "replace", "all")
            .addTokenFilter(PatternReplaceFilterFactory.class,
                    "pattern", "\\_$", "replacement", "", "replace", "all");
    }

    /*
     * XXX 3.x -> 8.x : Handle brackets correctly
     *
     * Process the input value of Genre search for search of domain value.
     *
     * The tag parser performs special character conversion
     * when converting input values ​​from a file.
     * Therefore, the domain value may be different from the original value.
     * This filter allows searching by user readable value (file tag value).
     *
     * @see org.jaudiotagger.tag.id3.framebody.FrameBodyTCON#convertID3v23GenreToGeneric
     * (TCON stands for Genre with ID3 v2.3-v2.4)
     * Such processing exists because brackets in the Gener string have a special meaning.
     */
    private void addTokenFilterForTokenToDomainValue(Builder builder) throws IOException {
        builder
            .addTokenFilter(PatternReplaceFilterFactory.class,
                    "pattern", "\\(", "replacement", "", "replace", "all")
            .addTokenFilter(PatternReplaceFilterFactory.class,
                    "pattern", "\\)$", "replacement", "", "replace", "all")
            .addTokenFilter(PatternReplaceFilterFactory.class,
                    "pattern", "\\)", "replacement", " ", "replace", "all")
            .addTokenFilter(PatternReplaceFilterFactory.class,
                    "pattern", "\\{\\}", "replacement", "\\{ \\}", "replace", "all")
            .addTokenFilter(PatternReplaceFilterFactory.class,
                    "pattern", "\\[\\]", "replacement", "\\[ \\]", "replace", "all");
    }

    private Builder createDefaultAnalyzerBuilder() throws IOException {
        Builder builder = CustomAnalyzer.builder()
                .withTokenizer(StandardTokenizerFactory.class)
                .addTokenFilter(CJKWidthFilterFactory.class)
                .addTokenFilter(ASCIIFoldingFilterFactory.class, "preserveOriginal", "false")
                .addTokenFilter(LowerCaseFilterFactory.class)
                .addTokenFilter(StopFilterFactory.class, "words", STOP_WORDS)
                .addTokenFilter(EnglishPossessiveFilterFactory.class);
        addTokenFilterForUnderscoreRemovalAroundToken(builder);
        return builder;
    }

    private Builder createArtistAnalyzerBuilder() throws IOException {
        Builder builder = CustomAnalyzer.builder()
                .withTokenizer(StandardTokenizerFactory.class)
                .addTokenFilter(CJKWidthFilterFactory.class)
                .addTokenFilter(ASCIIFoldingFilterFactory.class, "preserveOriginal", "false")
                .addTokenFilter(LowerCaseFilterFactory.class)
                .addTokenFilter(StopFilterFactory.class, "words", STOP_WORDS_ARTIST)
                .addTokenFilter(EnglishPossessiveFilterFactory.class);
        addTokenFilterForUnderscoreRemovalAroundToken(builder);
        return builder;
    }

    private Builder createKeywordAnalyzerBuilder() throws IOException {
        return CustomAnalyzer.builder()
                .withTokenizer(KeywordTokenizerFactory.class);
    }

    private Builder createGenreAnalyzerBuilder() throws IOException {
        Builder builder = createKeywordAnalyzerBuilder();
        addTokenFilterForTokenToDomainValue(builder);
        return builder;
    }

    /**
     * Returns the Analyzer to use when generating the index.
     *
     * Whether this analyzer is applied to input values ​​depends on
     * the definition of the document's fields.
     *
     * @return analyzer for index
     * @see DocumentFactory
     */
    public Analyzer getAnalyzer() throws IOException {
        if (isEmpty(analyzer)) {
            try {

                Analyzer defaultAnalyzer = createDefaultAnalyzerBuilder().build();
                Analyzer artistAnalyzer = createArtistAnalyzerBuilder().build();

                Map<String, Analyzer> fieldAnalyzers = new HashMap<>();
                fieldAnalyzers.put(FieldNames.ARTIST, artistAnalyzer);

                analyzer = new PerFieldAnalyzerWrapper(defaultAnalyzer, fieldAnalyzers);

            } catch (IOException e) {
                throw new IOException("Error when initializing Analyzer.", e);
            }
        }
        return analyzer;
    }

    /**
     * Returns the analyzer to use when generating a query for index search.
     *
     * String processing handled by QueryFactory
     * is limited to Lucene's modifier.
     *
     * The processing of the operands is expressed
     * in the AnalyzerFactory implementation.
     * Rules for tokenizing/converting input values ​
     * should not be described in QueryFactory.
     *
     * @return analyzer for query
     * @see QueryFactory
     */
    public Analyzer getQueryAnalyzer() throws IOException {
        if (isEmpty(queryAnalyzer)) {
            try {

                Analyzer defaultAnalyzer = createDefaultAnalyzerBuilder().build();
                Analyzer artistAnalyzer = createArtistAnalyzerBuilder().build();
                Analyzer genreAnalyzer = createGenreAnalyzerBuilder().build();

                Map<String, Analyzer> fieldAnalyzers = new HashMap<>();
                fieldAnalyzers.put(FieldNames.ARTIST, artistAnalyzer);
                fieldAnalyzers.put(FieldNames.GENRE, genreAnalyzer);

                queryAnalyzer = new PerFieldAnalyzerWrapper(defaultAnalyzer, fieldAnalyzers);

            } catch (IOException e) {
                throw new IOException("Error when initializing Analyzer.", e);
            }
        }
        return queryAnalyzer;
    }

}
