using J2N;
using Lucene.Net.Index;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using JCG = J2N.Collections.Generic;
using WritableArrayAttribute = Lucene.Net.Support.WritableArrayAttribute;

namespace Lucene.Net.Search.Spell
{
    /*
     * Licensed to the Apache Software Foundation (ASF) under one or more
     * contributor license agreements.  See the NOTICE file distributed with
     * this work for additional information regarding copyright ownership.
     * The ASF licenses this file to You under the Apache License, Version 2.0
     * (the "License"); you may not use this file except in compliance with
     * the License.  You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */

    /// <summary>
    /// <para>
    /// A spell checker whose sole function is to offer suggestions by combining
    /// multiple terms into one word and/or breaking terms into multiple words.
    /// </para>
    /// </summary>
    public class WordBreakSpellChecker
    {
        private int minSuggestionFrequency = 1;
        private int minBreakWordLength = 1;
        private int maxCombineWordLength = 20;
        private int maxChanges = 1;
        private int maxEvaluations = 1000;

        /// <summary>
        /// Term that can be used to prohibit adjacent terms from being combined </summary>
        public static readonly Term SEPARATOR_TERM = new Term("", "");

        /// <summary>
        /// Creates a new spellchecker with default configuration values </summary>
        /// <seealso cref="MaxChanges"/>
        /// <seealso cref="MaxCombineWordLength"/>
        /// <seealso cref="MaxEvaluations"/>
        /// <seealso cref="MinBreakWordLength"/>
        /// <seealso cref="MinSuggestionFrequency"/>
        public WordBreakSpellChecker()
        {
        }

        /// <summary>
        /// Determines the order to list word break suggestions
        /// </summary>
        public enum BreakSuggestionSortMethod
        {
            /// <summary>
            /// <para>
            /// Sort by Number of word breaks, then by the Sum of all the component
            /// term's frequencies
            /// </para>
            /// </summary>
            NUM_CHANGES_THEN_SUMMED_FREQUENCY,
            /// <summary>
            /// <para>
            /// Sort by Number of word breaks, then by the Maximum of all the component
            /// term's frequencies
            /// </para>
            /// </summary>
            NUM_CHANGES_THEN_MAX_FREQUENCY
        }

        /// <summary>
        /// Generate suggestions by breaking the passed-in term into multiple words.
        /// The scores returned are equal to the number of word breaks needed so a
        /// lower score is generally preferred over a higher score.
        /// </summary>
        /// <param name="suggestMode">
        ///          - default = <see cref="SuggestMode.SUGGEST_WHEN_NOT_IN_INDEX"/> </param>
        /// <param name="sortMethod">
        ///          - default = <see cref="BreakSuggestionSortMethod.NUM_CHANGES_THEN_MAX_FREQUENCY"/> </param>
        /// <returns> one or more arrays of words formed by breaking up the original term </returns>
        /// <exception cref="IOException"> If there is a low-level I/O error. </exception>
        public virtual SuggestWord[][] SuggestWordBreaks(Term term, int maxSuggestions, IndexReader ir, 
            SuggestMode suggestMode = SuggestMode.SUGGEST_WHEN_NOT_IN_INDEX, 
            BreakSuggestionSortMethod sortMethod = BreakSuggestionSortMethod.NUM_CHANGES_THEN_MAX_FREQUENCY)
        {
            if (maxSuggestions < 1)
            {
                return new SuggestWord[0][];
            }

            int queueInitialCapacity = maxSuggestions > 10 ? 10 : maxSuggestions;
            IComparer<SuggestWordArrayWrapper> queueComparer = sortMethod == BreakSuggestionSortMethod.NUM_CHANGES_THEN_MAX_FREQUENCY 
                ? (IComparer<SuggestWordArrayWrapper>)new LengthThenMaxFreqComparer(this) 
                : new LengthThenSumFreqComparer(this);
            JCG.PriorityQueue<SuggestWordArrayWrapper> suggestions = new JCG.PriorityQueue<SuggestWordArrayWrapper>(queueInitialCapacity, queueComparer);

            int origFreq = ir.DocFreq(term);
            if (origFreq > 0 && suggestMode == SuggestMode.SUGGEST_WHEN_NOT_IN_INDEX)
            {
                return new SuggestWord[0][];
            }

            int useMinSuggestionFrequency = minSuggestionFrequency;
            if (suggestMode == SuggestMode.SUGGEST_MORE_POPULAR)
            {
                useMinSuggestionFrequency = (origFreq == 0 ? 1 : origFreq);
            }

            GenerateBreakUpSuggestions(term, ir, 1, maxSuggestions, useMinSuggestionFrequency, new SuggestWord[0], suggestions, 0, sortMethod);

            SuggestWord[][] suggestionArray = new SuggestWord[suggestions.Count][];
            for (int i = suggestions.Count - 1; i >= 0; i--)
            {
                suggestionArray[i] = suggestions.Dequeue().SuggestWords;
            }

            return suggestionArray;
        }

        /// <summary>
        /// <para>
        /// Generate suggestions by combining one or more of the passed-in terms into
        /// single words. The returned <see cref="CombineSuggestion"/> contains both a
        /// <see cref="SuggestWord"/> and also an array detailing which passed-in terms were
        /// involved in creating this combination. The scores returned are equal to the
        /// number of word combinations needed, also one less than the length of the
        /// array <see cref="CombineSuggestion.OriginalTermIndexes"/>. Generally, a
        /// suggestion with a lower score is preferred over a higher score.
        /// </para>
        /// <para>
        /// To prevent two adjacent terms from being combined (for instance, if one is
        /// mandatory and the other is prohibited), separate the two terms with
        /// <see cref="WordBreakSpellChecker.SEPARATOR_TERM"/>
        /// </para>
        /// <para>
        /// When suggestMode equals <see cref="SuggestMode.SUGGEST_WHEN_NOT_IN_INDEX"/>, each
        /// suggestion will include at least one term not in the index.
        /// </para>
        /// <para>
        /// When suggestMode equals <see cref="SuggestMode.SUGGEST_MORE_POPULAR"/>, each
        /// suggestion will have the same, or better frequency than the most-popular
        /// included term.
        /// </para>
        /// </summary>
        /// <returns> an array of words generated by combining original terms </returns>
        /// <exception cref="IOException"> If there is a low-level I/O error. </exception>
        public virtual CombineSuggestion[] SuggestWordCombinations(Term[] terms, int maxSuggestions, 
            IndexReader ir, SuggestMode suggestMode = SuggestMode.SUGGEST_WHEN_NOT_IN_INDEX)
        {
            if (maxSuggestions < 1)
            {
                return new CombineSuggestion[0];
            }

            int[] origFreqs = null;
            if (suggestMode != SuggestMode.SUGGEST_ALWAYS)
            {
                origFreqs = new int[terms.Length];
                for (int i = 0; i < terms.Length; i++)
                {
                    origFreqs[i] = ir.DocFreq(terms[i]);
                }
            }

            int queueInitialCapacity = maxSuggestions > 10 ? 10 : maxSuggestions;
            IComparer<CombineSuggestionWrapper> queueComparer = new CombinationsThenFreqComparer(this);
            JCG.PriorityQueue<CombineSuggestionWrapper> suggestions = new JCG.PriorityQueue<CombineSuggestionWrapper>(queueInitialCapacity, queueComparer);

            int thisTimeEvaluations = 0;
            for (int i = 0; i < terms.Length - 1; i++)
            {
                if (terms[i].Equals(SEPARATOR_TERM))
                {
                    continue;
                }
                string leftTermText = terms[i].Text();
                int leftTermLength = leftTermText.CodePointCount(0, leftTermText.Length);
                if (leftTermLength > maxCombineWordLength)
                {
                    continue;
                }
                int maxFreq = 0;
                int minFreq = int.MaxValue;
                if (origFreqs != null)
                {
                    maxFreq = origFreqs[i];
                    minFreq = origFreqs[i];
                }
                string combinedTermText = leftTermText;
                int combinedLength = leftTermLength;
                for (int j = i + 1; j < terms.Length && j - i <= maxChanges; j++)
                {
                    if (terms[j].Equals(SEPARATOR_TERM))
                    {
                        break;
                    }
                    string rightTermText = terms[j].Text();
                    int rightTermLength = rightTermText.CodePointCount(0, rightTermText.Length);
                    combinedTermText += rightTermText;
                    combinedLength += rightTermLength;
                    if (combinedLength > maxCombineWordLength)
                    {
                        break;
                    }

                    if (origFreqs != null)
                    {
                        maxFreq = Math.Max(maxFreq, origFreqs[j]);
                        minFreq = Math.Min(minFreq, origFreqs[j]);
                    }

                    Term combinedTerm = new Term(terms[0].Field, combinedTermText);
                    int combinedTermFreq = ir.DocFreq(combinedTerm);

                    if (suggestMode != SuggestMode.SUGGEST_MORE_POPULAR || combinedTermFreq >= maxFreq)
                    {
                        if (suggestMode != SuggestMode.SUGGEST_WHEN_NOT_IN_INDEX || minFreq == 0)
                        {
                            if (combinedTermFreq >= minSuggestionFrequency)
                            {
                                int[] origIndexes = new int[j - i + 1];
                                origIndexes[0] = i;
                                for (int k = 1; k < origIndexes.Length; k++)
                                {
                                    origIndexes[k] = i + k;
                                }
                                SuggestWord word = new SuggestWord();
                                word.Freq = combinedTermFreq;
                                word.Score = origIndexes.Length - 1;
                                word.String = combinedTerm.Text();
                                CombineSuggestionWrapper suggestion = new CombineSuggestionWrapper(this, new CombineSuggestion(word, origIndexes), (origIndexes.Length - 1));
                                suggestions.Enqueue(suggestion);
                                if (suggestions.Count > maxSuggestions)
                                {
                                    suggestions.TryDequeue(out CombineSuggestionWrapper _);
                                }
                            }
                        }
                    }
                    thisTimeEvaluations++;
                    if (thisTimeEvaluations == maxEvaluations)
                    {
                        break;
                    }
                }
            }
            CombineSuggestion[] combineSuggestions = new CombineSuggestion[suggestions.Count];
            for (int i = suggestions.Count - 1; i >= 0; i--)
            {
                combineSuggestions[i] = suggestions.Dequeue().CombineSuggestion;
            }
            return combineSuggestions;
        }

        private int GenerateBreakUpSuggestions(Term term, IndexReader ir, 
            int numberBreaks, int maxSuggestions, int useMinSuggestionFrequency, 
            SuggestWord[] prefix, JCG.PriorityQueue<SuggestWordArrayWrapper> suggestions, 
            int totalEvaluations, BreakSuggestionSortMethod sortMethod)
        {
            string termText = term.Text();
            int termLength = termText.CodePointCount(0, termText.Length);
            int useMinBreakWordLength = minBreakWordLength;
            if (useMinBreakWordLength < 1)
            {
                useMinBreakWordLength = 1;
            }
            if (termLength < (useMinBreakWordLength * 2))
            {
                return 0;
            }

            int thisTimeEvaluations = 0;
            for (int i = useMinBreakWordLength; i <= (termLength - useMinBreakWordLength); i++)
            {
                int end = termText.OffsetByCodePoints(0, i);
                string leftText = termText.Substring(0, end);
                string rightText = termText.Substring(end);
                SuggestWord leftWord = GenerateSuggestWord(ir, term.Field, leftText);

                if (leftWord.Freq >= useMinSuggestionFrequency)
                {
                    SuggestWord rightWord = GenerateSuggestWord(ir, term.Field, rightText);
                    if (rightWord.Freq >= useMinSuggestionFrequency)
                    {
                        SuggestWordArrayWrapper suggestion = new SuggestWordArrayWrapper(this, NewSuggestion(prefix, leftWord, rightWord));
                        suggestions.Enqueue(suggestion);
                        if (suggestions.Count > maxSuggestions)
                        {
                            suggestions.Dequeue();
                        }
                    }
                    int newNumberBreaks = numberBreaks + 1;
                    if (newNumberBreaks <= maxChanges)
                    {
                        int evaluations = GenerateBreakUpSuggestions(new Term(term.Field, rightWord.String), 
                            ir, newNumberBreaks, maxSuggestions, 
                            useMinSuggestionFrequency, NewPrefix(prefix, leftWord), 
                            suggestions, totalEvaluations, sortMethod);
                        totalEvaluations += evaluations;
                    }
                }

                thisTimeEvaluations++;
                totalEvaluations++;
                if (totalEvaluations >= maxEvaluations)
                {
                    break;
                }
            }
            return thisTimeEvaluations;
        }

        private static SuggestWord[] NewPrefix(SuggestWord[] oldPrefix, SuggestWord append)
        {
            SuggestWord[] newPrefix = new SuggestWord[oldPrefix.Length + 1];
            Array.Copy(oldPrefix, 0, newPrefix, 0, oldPrefix.Length);
            newPrefix[newPrefix.Length - 1] = append;
            return newPrefix;
        }

        private static SuggestWord[] NewSuggestion(SuggestWord[] prefix, SuggestWord append1, SuggestWord append2)
        {
            SuggestWord[] newSuggestion = new SuggestWord[prefix.Length + 2];
            int score = prefix.Length + 1;
            for (int i = 0; i < prefix.Length; i++)
            {
                SuggestWord word = new SuggestWord();
                word.String = prefix[i].String;
                word.Freq = prefix[i].Freq;
                word.Score = score;
                newSuggestion[i] = word;
            }
            append1.Score = score;
            append2.Score = score;
            newSuggestion[newSuggestion.Length - 2] = append1;
            newSuggestion[newSuggestion.Length - 1] = append2;
            return newSuggestion;
        }

        private SuggestWord GenerateSuggestWord(IndexReader ir, string fieldname, string text)
        {
            Term term = new Term(fieldname, text);
            int freq = ir.DocFreq(term);
            SuggestWord word = new SuggestWord();
            word.Freq = freq;
            word.Score = 1;
            word.String = text;
            return word;
        }

        /// <summary>
        /// Gets or sets the minimum frequency a term must have to be 
        /// included as part of a suggestion. Default=1 Not applicable when used with
        /// <see cref="SuggestMode.SUGGEST_MORE_POPULAR"/>
        /// </summary>
        public virtual int MinSuggestionFrequency
        {
            get => minSuggestionFrequency;
            set => this.minSuggestionFrequency = value;
        }

        /// <summary>
        /// Gets or sets the maximum length of a suggestion made 
        /// by combining 1 or more original terms. Default=20.
        /// </summary>
        public virtual int MaxCombineWordLength
        {
            get => maxCombineWordLength;
            set => this.maxCombineWordLength = value;
        }

        /// <summary>
        /// Gets or sets the minimum length to break words down to. Default=1.
        /// </summary>
        public virtual int MinBreakWordLength
        {
            get => minBreakWordLength;
            set => this.minBreakWordLength = value;
        }

        /// <summary>
        /// Gets or sets the maximum numbers of changes (word breaks or combinations) to make 
        /// on the original term(s). Default=1.
        /// </summary>
        public virtual int MaxChanges
        {
            get => maxChanges;
            set => this.maxChanges = value;
        }

        /// <summary>
        /// Gets or sets the maximum number of word combinations to evaluate. Default=1000. A higher
        /// value might improve result quality. A lower value might improve performance.
        /// </summary>
        public virtual int MaxEvaluations
        {
            get => maxEvaluations;
            set => this.maxEvaluations = value;
        }

        private sealed class LengthThenMaxFreqComparer : IComparer<SuggestWordArrayWrapper>
        {
            private readonly WordBreakSpellChecker outerInstance;

            public LengthThenMaxFreqComparer(WordBreakSpellChecker outerInstance)
            {
                this.outerInstance = outerInstance;
            }

            public int Compare(SuggestWordArrayWrapper o1, SuggestWordArrayWrapper o2)
            {
                if (o1.SuggestWords.Length != o2.SuggestWords.Length)
                {
                    return o2.SuggestWords.Length - o1.SuggestWords.Length;
                }
                if (o1.FreqMax != o2.FreqMax)
                {
                    return o1.FreqMax - o2.FreqMax;
                }
                return 0;
            }
        }

        private sealed class LengthThenSumFreqComparer : IComparer<SuggestWordArrayWrapper>
        {
            private readonly WordBreakSpellChecker outerInstance;

            public LengthThenSumFreqComparer(WordBreakSpellChecker outerInstance)
            {
                this.outerInstance = outerInstance;
            }

            public int Compare(SuggestWordArrayWrapper o1, SuggestWordArrayWrapper o2)
            {
                if (o1.SuggestWords.Length != o2.SuggestWords.Length)
                {
                    return o2.SuggestWords.Length - o1.SuggestWords.Length;
                }
                if (o1.FreqSum != o2.FreqSum)
                {
                    return o1.FreqSum - o2.FreqSum;
                }
                return 0;
            }
        }

        private sealed class CombinationsThenFreqComparer : IComparer<CombineSuggestionWrapper>
        {
            private readonly WordBreakSpellChecker outerInstance;

            public CombinationsThenFreqComparer(WordBreakSpellChecker outerInstance)
            {
                this.outerInstance = outerInstance;
            }

            public int Compare(CombineSuggestionWrapper o1, CombineSuggestionWrapper o2)
            {
                if (o1.NumCombinations != o2.NumCombinations)
                {
                    return o2.NumCombinations - o1.NumCombinations;
                }
                if (o1.CombineSuggestion.Suggestion.Freq != o2.CombineSuggestion.Suggestion.Freq)
                {
                    return o1.CombineSuggestion.Suggestion.Freq - o2.CombineSuggestion.Suggestion.Freq;
                }
                return 0;
            }
        }

        private class SuggestWordArrayWrapper : IComparable<SuggestWordArrayWrapper>
        {
            private readonly WordBreakSpellChecker outerInstance;

            private readonly SuggestWord[] suggestWords;
            private readonly int freqMax;
            private readonly int freqSum;

            internal SuggestWordArrayWrapper(WordBreakSpellChecker outerInstance, SuggestWord[] suggestWords)
            {
                this.outerInstance = outerInstance;
                this.suggestWords = suggestWords;
                int aFreqSum = 0;
                int aFreqMax = 0;
                foreach (SuggestWord sw in suggestWords)
                {
                    aFreqSum += sw.Freq;
                    aFreqMax = Math.Max(aFreqMax, sw.Freq);
                }
                this.freqSum = aFreqSum;
                this.freqMax = aFreqMax;
            }

            [WritableArray]
            [SuppressMessage("Microsoft.Performance", "CA1819", Justification = "Lucene's design requires some writable array properties")]
            public SuggestWord[] SuggestWords => suggestWords;

            public int FreqMax => freqMax;

            public int FreqSum => freqSum;

            // Required by the PriorityQueue's generic constraint, but we are using
            // IComparer<T> here rather than IComparable<T>
            public int CompareTo(SuggestWordArrayWrapper other)
            {
                throw new NotSupportedException();
            }
        }

        private class CombineSuggestionWrapper : IComparable<CombineSuggestionWrapper>
        {
            private readonly WordBreakSpellChecker outerInstance;

            private readonly CombineSuggestion combineSuggestion;
            private readonly int numCombinations;

            internal CombineSuggestionWrapper(WordBreakSpellChecker outerInstance, CombineSuggestion combineSuggestion, int numCombinations)
            {
                this.outerInstance = outerInstance;
                this.combineSuggestion = combineSuggestion;
                this.numCombinations = numCombinations;
            }

            public CombineSuggestion CombineSuggestion => combineSuggestion;

            public int NumCombinations => numCombinations;

            // Required by the PriorityQueue's generic constraint, but we are using
            // IComparer<T> here rather than IComparable<T>
            public int CompareTo(CombineSuggestionWrapper other)
            {
                throw new NotSupportedException();
            }
        }
    }
}