using J2N.Text;
using Lucene.Net.Index;
using Lucene.Net.Store;
using Lucene.Net.Util;
using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Lucene.Net.Codecs.BlockTerms
{
    /*
     * 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.
     */

    // TODO: Currently we encode all terms between two indexed terms as a block
    // But we could decouple the two, ie allow several blocks in between two indexed terms

    /// <summary>
    /// Writes terms dict, block-encoding (column stride) each term's metadata 
    /// for each set of terms between two index terms.
    /// <para/>
    /// @lucene.experimental
    /// </summary>
    public class BlockTermsWriter : FieldsConsumer
    {
        internal readonly static string CODEC_NAME = "BLOCK_TERMS_DICT";

        // Initial format
        public readonly static int VERSION_START = 0;
        public readonly static int VERSION_APPEND_ONLY = 1;
        public readonly static int VERSION_META_ARRAY = 2;
        public readonly static int VERSION_CHECKSUM = 3;
        public readonly static int VERSION_CURRENT = VERSION_CHECKSUM;

        /// <summary>Extension of terms file</summary>
        public readonly static string TERMS_EXTENSION = "tib";

        protected IndexOutput m_output;
        private readonly PostingsWriterBase postingsWriter;
        private readonly FieldInfos fieldInfos;
        private FieldInfo currentField;
        private readonly TermsIndexWriterBase termsIndexWriter;

        private class FieldMetaData
        {
            public FieldInfo FieldInfo { get; private set; }
            public long NumTerms { get; private set; }
            public long TermsStartPointer { get; private set; }
            public long SumTotalTermFreq { get; private set; }
            public long SumDocFreq { get; private set; }
            public int DocCount { get; private set; }
            /// <summary>
            /// NOTE: This was longsSize (field) in Lucene.
            /// </summary>
            public int Int64sSize { get; private set; }

            public FieldMetaData(FieldInfo fieldInfo, long numTerms, long termsStartPointer, long sumTotalTermFreq,
                long sumDocFreq, int docCount, int int64sSize)
            {
                Debug.Assert(numTerms > 0);

                FieldInfo = fieldInfo;
                TermsStartPointer = termsStartPointer;
                NumTerms = numTerms;
                SumTotalTermFreq = sumTotalTermFreq;
                SumDocFreq = sumDocFreq;
                DocCount = docCount;
                Int64sSize = int64sSize;
            }
        }

        private readonly IList<FieldMetaData> fields = new List<FieldMetaData>();

        // private final String segment;

        public BlockTermsWriter(TermsIndexWriterBase termsIndexWriter,
            SegmentWriteState state, PostingsWriterBase postingsWriter)
        {
            string termsFileName = IndexFileNames.SegmentFileName(state.SegmentInfo.Name, state.SegmentSuffix, TERMS_EXTENSION);
            this.termsIndexWriter = termsIndexWriter;
            m_output = state.Directory.CreateOutput(termsFileName, state.Context);
            bool success = false;
            try
            {
                fieldInfos = state.FieldInfos;
                WriteHeader(m_output);
                currentField = null;
                this.postingsWriter = postingsWriter;
                // segment = state.segmentName;

                //System.out.println("BTW.init seg=" + state.segmentName);

                postingsWriter.Init(m_output); // have consumer write its format/header
                success = true;
            }
            finally
            {
                if (!success)
                {
                    IOUtils.DisposeWhileHandlingException(m_output);
                }
            }
        }

        private void WriteHeader(IndexOutput output)
        {
            CodecUtil.WriteHeader(output, CODEC_NAME, VERSION_CURRENT);
        }

        public override TermsConsumer AddField(FieldInfo field)
        {
            //System.out.println("\nBTW.addField seg=" + segment + " field=" + field.name);
            Debug.Assert(currentField == null || currentField.Name.CompareToOrdinal(field.Name) < 0);
            currentField = field;
            TermsIndexWriterBase.FieldWriter fieldIndexWriter = termsIndexWriter.AddField(field, m_output.GetFilePointer());
            return new TermsWriter(this, fieldIndexWriter, field, postingsWriter);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (m_output != null)
                {
                    try
                    {
                        long dirStart = m_output.GetFilePointer();

                        m_output.WriteVInt32(fields.Count);
                        foreach (FieldMetaData field in fields)
                        {
                            m_output.WriteVInt32(field.FieldInfo.Number);
                            m_output.WriteVInt64(field.NumTerms);
                            m_output.WriteVInt64(field.TermsStartPointer);
                            if (field.FieldInfo.IndexOptions != IndexOptions.DOCS_ONLY)
                            {
                                m_output.WriteVInt64(field.SumTotalTermFreq);
                            }
                            m_output.WriteVInt64(field.SumDocFreq);
                            m_output.WriteVInt32(field.DocCount);
                            if (VERSION_CURRENT >= VERSION_META_ARRAY)
                            {
                                m_output.WriteVInt32(field.Int64sSize);
                            }
                        }
                        WriteTrailer(dirStart);
                        CodecUtil.WriteFooter(m_output);
                    }
                    finally
                    {
                        IOUtils.Dispose(m_output, postingsWriter, termsIndexWriter);
                        m_output = null;
                    }
                }
            }
        }

        private void WriteTrailer(long dirStart)
        {
            m_output.WriteInt64(dirStart);
        }

        private class TermEntry
        {
            public BytesRef Term { get; private set; }
            public BlockTermState State { get; set; }

            public TermEntry()
            {
                Term = new BytesRef();
            }
        }

        internal class TermsWriter : TermsConsumer
        {
            private readonly BlockTermsWriter outerInstance;

            private readonly FieldInfo fieldInfo;
            private readonly PostingsWriterBase postingsWriter;
            private readonly long termsStartPointer;
            private long numTerms;
            private readonly TermsIndexWriterBase.FieldWriter fieldIndexWriter;
            long sumTotalTermFreq;
            long sumDocFreq;
            int docCount;
            int longsSize;

            private TermEntry[] pendingTerms;

            private int pendingCount;

            internal TermsWriter(
                BlockTermsWriter outerInstance,
                TermsIndexWriterBase.FieldWriter fieldIndexWriter,
                FieldInfo fieldInfo,
                PostingsWriterBase postingsWriter)
            {
                this.outerInstance = outerInstance;

                this.fieldInfo = fieldInfo;
                this.fieldIndexWriter = fieldIndexWriter;
                pendingTerms = new TermEntry[32];
                for (int i = 0; i < pendingTerms.Length; i++)
                {
                    pendingTerms[i] = new TermEntry();
                }
                termsStartPointer = outerInstance.m_output.GetFilePointer();
                this.postingsWriter = postingsWriter;
                this.longsSize = postingsWriter.SetField(fieldInfo);
            }

            public override IComparer<BytesRef> Comparer => BytesRef.UTF8SortedAsUnicodeComparer;

            public override PostingsConsumer StartTerm(BytesRef text)
            {
                //System.out.println("BTW: startTerm term=" + fieldInfo.name + ":" + text.utf8ToString() + " " + text + " seg=" + segment);
                postingsWriter.StartTerm();
                return postingsWriter;
            }

            private readonly BytesRef lastPrevTerm = new BytesRef();

            public override void FinishTerm(BytesRef text, TermStats stats)
            {
                Debug.Assert(stats.DocFreq > 0);
                //System.out.println("BTW: finishTerm term=" + fieldInfo.name + ":" + text.utf8ToString() + " " + text + " seg=" + segment + " df=" + stats.docFreq);

                bool isIndexTerm = fieldIndexWriter.CheckIndexTerm(text, stats);

                if (isIndexTerm)
                {
                    if (pendingCount > 0)
                    {
                        // Instead of writing each term, live, we gather terms
                        // in RAM in a pending buffer, and then write the
                        // entire block in between index terms:
                        FlushBlock();
                    }
                    fieldIndexWriter.Add(text, stats, outerInstance.m_output.GetFilePointer());
                    //System.out.println("  index term!");
                }

                if (pendingTerms.Length == pendingCount)
                {
                    TermEntry[] newArray = new TermEntry[ArrayUtil.Oversize(pendingCount + 1, RamUsageEstimator.NUM_BYTES_OBJECT_REF)];
                    System.Array.Copy(pendingTerms, 0, newArray, 0, pendingCount);
                    for (int i = pendingCount; i < newArray.Length; i++)
                    {
                        newArray[i] = new TermEntry();
                    }
                    pendingTerms = newArray;
                }
                TermEntry te = pendingTerms[pendingCount];
                te.Term.CopyBytes(text);
                te.State = postingsWriter.NewTermState();
                te.State.DocFreq = stats.DocFreq;
                te.State.TotalTermFreq = stats.TotalTermFreq;
                postingsWriter.FinishTerm(te.State);

                pendingCount++;
                numTerms++;
            }

            // Finishes all terms in this field
            public override void Finish(long sumTotalTermFreq, long sumDocFreq, int docCount)
            {
                if (pendingCount > 0)
                {
                    FlushBlock();
                }
                // EOF marker:
                outerInstance.m_output.WriteVInt32(0);

                this.sumTotalTermFreq = sumTotalTermFreq;
                this.sumDocFreq = sumDocFreq;
                this.docCount = docCount;
                fieldIndexWriter.Finish(outerInstance.m_output.GetFilePointer());
                if (numTerms > 0)
                {
                    outerInstance.fields.Add(new FieldMetaData(fieldInfo,
                                                 numTerms,
                                                 termsStartPointer,
                                                 sumTotalTermFreq,
                                                 sumDocFreq,
                                                 docCount,
                                                 longsSize));
                }
            }

            private int SharedPrefix(BytesRef term1, BytesRef term2)
            {
                Debug.Assert(term1.Offset == 0);
                Debug.Assert(term2.Offset == 0);
                int pos1 = 0;
                int pos1End = pos1 + Math.Min(term1.Length, term2.Length);
                int pos2 = 0;
                while (pos1 < pos1End)
                {
                    if (term1.Bytes[pos1] != term2.Bytes[pos2])
                    {
                        return pos1;
                    }
                    pos1++;
                    pos2++;
                }
                return pos1;
            }

            private readonly RAMOutputStream bytesWriter = new RAMOutputStream();
            private readonly RAMOutputStream bufferWriter = new RAMOutputStream();

            private void FlushBlock()
            {
                //System.out.println("BTW.flushBlock seg=" + segment + " pendingCount=" + pendingCount + " fp=" + out.getFilePointer());

                // First pass: compute common prefix for all terms
                // in the block, against term before first term in
                // this block:
                int commonPrefix = SharedPrefix(lastPrevTerm, pendingTerms[0].Term);
                for (int termCount = 1; termCount < pendingCount; termCount++)
                {
                    commonPrefix = Math.Min(commonPrefix,
                                            SharedPrefix(lastPrevTerm,
                                                         pendingTerms[termCount].Term));
                }

                outerInstance.m_output.WriteVInt32(pendingCount);
                outerInstance.m_output.WriteVInt32(commonPrefix);

                // 2nd pass: write suffixes, as separate byte[] blob
                for (int termCount = 0; termCount < pendingCount; termCount++)
                {
                    int suffix = pendingTerms[termCount].Term.Length - commonPrefix;
                    // TODO: cutover to better intblock codec, instead
                    // of interleaving here:
                    bytesWriter.WriteVInt32(suffix);
                    bytesWriter.WriteBytes(pendingTerms[termCount].Term.Bytes, commonPrefix, suffix);
                }
                outerInstance.m_output.WriteVInt32((int)bytesWriter.GetFilePointer());
                bytesWriter.WriteTo(outerInstance.m_output);
                bytesWriter.Reset();

                // 3rd pass: write the freqs as byte[] blob
                // TODO: cutover to better intblock codec.  simple64?
                // write prefix, suffix first:
                for (int termCount = 0; termCount < pendingCount; termCount++)
                {
                    BlockTermState state = pendingTerms[termCount].State;
                    Debug.Assert(state != null);
                    bytesWriter.WriteVInt32(state.DocFreq);
                    if (fieldInfo.IndexOptions != IndexOptions.DOCS_ONLY)
                    {
                        bytesWriter.WriteVInt64(state.TotalTermFreq - state.DocFreq);
                    }
                }
                outerInstance.m_output.WriteVInt32((int)bytesWriter.GetFilePointer());
                bytesWriter.WriteTo(outerInstance.m_output);
                bytesWriter.Reset();

                // 4th pass: write the metadata 
                long[] longs = new long[longsSize];
                bool absolute = true;
                for (int termCount = 0; termCount < pendingCount; termCount++)
                {
                    BlockTermState state = pendingTerms[termCount].State;
                    postingsWriter.EncodeTerm(longs, bufferWriter, fieldInfo, state, absolute);
                    for (int i = 0; i < longsSize; i++)
                    {
                        bytesWriter.WriteVInt64(longs[i]);
                    }
                    bufferWriter.WriteTo(bytesWriter);
                    bufferWriter.Reset();
                    absolute = false;
                }
                outerInstance.m_output.WriteVInt32((int)bytesWriter.GetFilePointer());
                bytesWriter.WriteTo(outerInstance.m_output);
                bytesWriter.Reset();

                lastPrevTerm.CopyBytes(pendingTerms[pendingCount - 1].Term);
                pendingCount = 0;
            }
        }
    }
}