using Lucene.Net.Index;
using Lucene.Net.Store;
using Lucene.Net.Util;
using System.Diagnostics;

namespace Lucene.Net.Codecs.Sep
{
    /*
     * 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: -- should we switch "hasProx" higher up?  and
    // create two separate docs readers, one that also reads
    // prox and one that doesn't?

    /// <summary>
    /// Concrete class that reads the current doc/freq/skip
    /// postings format.    
    /// <para/>
    /// @lucene.experimental
    /// </summary>
    public class SepPostingsReader : PostingsReaderBase
    {
        private readonly Int32IndexInput freqIn;
        private readonly Int32IndexInput docIn;
        private readonly Int32IndexInput posIn;
        private readonly IndexInput payloadIn;
        private readonly IndexInput skipIn;

        private int skipInterval;
        private int maxSkipLevels;
        private int skipMinimum;

        public SepPostingsReader(Directory dir, FieldInfos fieldInfos, SegmentInfo segmentInfo, IOContext context,
            Int32StreamFactory intFactory, string segmentSuffix)
        {
            bool success = false;
            try
            {
                string docFileName = IndexFileNames.SegmentFileName(segmentInfo.Name, segmentSuffix, SepPostingsWriter.DOC_EXTENSION);
                docIn = intFactory.OpenInput(dir, docFileName, context);

                skipIn = dir.OpenInput(IndexFileNames.SegmentFileName(segmentInfo.Name, segmentSuffix, SepPostingsWriter.SKIP_EXTENSION), context);

                if (fieldInfos.HasFreq)
                {
                    freqIn = intFactory.OpenInput(dir, IndexFileNames.SegmentFileName(segmentInfo.Name, segmentSuffix, SepPostingsWriter.FREQ_EXTENSION), context);
                }
                else
                {
                    freqIn = null;
                }
                if (fieldInfos.HasProx)
                {
                    posIn = intFactory.OpenInput(dir, IndexFileNames.SegmentFileName(segmentInfo.Name, segmentSuffix, SepPostingsWriter.POS_EXTENSION), context);
                    payloadIn = dir.OpenInput(IndexFileNames.SegmentFileName(segmentInfo.Name, segmentSuffix, SepPostingsWriter.PAYLOAD_EXTENSION), context);
                }
                else
                {
                    posIn = null;
                    payloadIn = null;
                }
                success = true;
            }
            finally
            {
                if (!success)
                {
                    Dispose();
                }
            }
        }

        public override void Init(IndexInput termsIn)
        {
            // Make sure we are talking to the matching past writer
            CodecUtil.CheckHeader(termsIn, SepPostingsWriter.CODEC,
              SepPostingsWriter.VERSION_START, SepPostingsWriter.VERSION_START);
            skipInterval = termsIn.ReadInt32();
            maxSkipLevels = termsIn.ReadInt32();
            skipMinimum = termsIn.ReadInt32();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                IOUtils.Dispose(freqIn, docIn, skipIn, posIn, payloadIn);
            }
        }

        internal sealed class SepTermState : BlockTermState
        {
            // We store only the seek point to the docs file because
            // the rest of the info (freqIndex, posIndex, etc.) is
            // stored in the docs file:
            internal Int32IndexInput.Index docIndex;
            internal Int32IndexInput.Index posIndex;
            internal Int32IndexInput.Index freqIndex;
            internal long payloadFP;
            internal long skipFP;

            public override object Clone()
            {
                var other = new SepTermState();
                other.CopyFrom(this);
                return other;
            }

            public override void CopyFrom(TermState other)
            {
                base.CopyFrom(other);
                SepTermState other_ = (SepTermState)other;
                if (docIndex == null)
                {
                    docIndex = (Int32IndexInput.Index)other_.docIndex.Clone();
                }
                else
                {
                    docIndex.CopyFrom(other_.docIndex);
                }
                if (other_.freqIndex != null)
                {
                    if (freqIndex == null)
                    {
                        freqIndex = (Int32IndexInput.Index)other_.freqIndex.Clone();
                    }
                    else
                    {
                        freqIndex.CopyFrom(other_.freqIndex);
                    }
                }
                else
                {
                    freqIndex = null;
                }
                if (other_.posIndex != null)
                {
                    if (posIndex == null)
                    {
                        posIndex = (Int32IndexInput.Index)other_.posIndex.Clone();
                    }
                    else
                    {
                        posIndex.CopyFrom(other_.posIndex);
                    }
                }
                else
                {
                    posIndex = null;
                }
                payloadFP = other_.payloadFP;
                skipFP = other_.skipFP;
            }

            public override string ToString()
            {
                return base.ToString() + " docIndex=" + docIndex + " freqIndex=" + freqIndex + " posIndex=" + posIndex +
                       " payloadFP=" + payloadFP + " skipFP=" + skipFP;
            }
        }

        public override BlockTermState NewTermState()
        {
            SepTermState state = new SepTermState();
            state.docIndex = docIn.GetIndex();
            if (freqIn != null)
            {
                state.freqIndex = freqIn.GetIndex();
            }
            if (posIn != null)
            {
                state.posIndex = posIn.GetIndex();
            }
            return state;
        }

        public override void DecodeTerm(long[] empty, DataInput input, FieldInfo fieldInfo, BlockTermState termState,
            bool absolute)
        {
            SepTermState termState_ = (SepTermState)termState;
            termState_.docIndex.Read(input, absolute);
            if (fieldInfo.IndexOptions != IndexOptions.DOCS_ONLY)
            {
                termState_.freqIndex.Read(input, absolute);
                if (fieldInfo.IndexOptions == IndexOptions.DOCS_AND_FREQS_AND_POSITIONS)
                {
                    //System.out.println("  freqIndex=" + termState.freqIndex);
                    termState_.posIndex.Read(input, absolute);
                    //System.out.println("  posIndex=" + termState.posIndex);
                    if (fieldInfo.HasPayloads)
                    {
                        if (absolute)
                        {
                            termState_.payloadFP = input.ReadVInt64();
                        }
                        else
                        {
                            termState_.payloadFP += input.ReadVInt64();
                        }
                        //System.out.println("  payloadFP=" + termState.payloadFP);
                    }
                }
            }

            if (termState_.DocFreq >= skipMinimum)
            {
                //System.out.println("   readSkip @ " + in.getPosition());
                if (absolute)
                {
                    termState_.skipFP = input.ReadVInt64();
                }
                else
                {
                    termState_.skipFP += input.ReadVInt64();
                }
                //System.out.println("  skipFP=" + termState.skipFP);
            }
            else if (absolute)
            {
                termState_.skipFP = 0;
            }
        }

        public override DocsEnum Docs(FieldInfo fieldInfo, BlockTermState termState, IBits liveDocs, DocsEnum reuse,
            DocsFlags flags)
        {
            SepTermState termState_ = (SepTermState)termState;
            SepDocsEnum docsEnum;
            if (reuse == null || !(reuse is SepDocsEnum))
            {
                docsEnum = new SepDocsEnum(this);
            }
            else
            {
                docsEnum = (SepDocsEnum)reuse;
                if (docsEnum.startDocIn != docIn)
                {
                    // If you are using ParellelReader, and pass in a
                    // reused DocsAndPositionsEnum, it could have come
                    // from another reader also using sep codec
                    docsEnum = new SepDocsEnum(this);
                }
            }

            return docsEnum.Init(fieldInfo, termState_, liveDocs);
        }

        public override DocsAndPositionsEnum DocsAndPositions(FieldInfo fieldInfo, BlockTermState termState,
            IBits liveDocs, DocsAndPositionsEnum reuse, DocsAndPositionsFlags flags)
        {
            Debug.Assert(fieldInfo.IndexOptions == IndexOptions.DOCS_AND_FREQS_AND_POSITIONS);
            SepTermState termState_ = (SepTermState)termState;
            SepDocsAndPositionsEnum postingsEnum;
            if (reuse == null || !(reuse is SepDocsAndPositionsEnum))
            {
                postingsEnum = new SepDocsAndPositionsEnum(this);
            }
            else
            {
                postingsEnum = (SepDocsAndPositionsEnum)reuse;
                if (postingsEnum.startDocIn != docIn)
                {
                    // If you are using ParellelReader, and pass in a
                    // reused DocsAndPositionsEnum, it could have come
                    // from another reader also using sep codec
                    postingsEnum = new SepDocsAndPositionsEnum(this);
                }
            }

            return postingsEnum.Init(fieldInfo, termState_, liveDocs);
        }

        internal class SepDocsEnum : DocsEnum
        {
            private readonly SepPostingsReader outerInstance;

            private int docFreq;
            private int doc = -1;
            private int accum;
            private int count;
            private int freq;
            //private long freqStart; // LUCENENET: Not used

            // TODO: -- should we do omitTF with 2 different enum classes?
            private bool omitTF;
            private IndexOptions indexOptions;
            private bool storePayloads;
            private IBits liveDocs;
            private readonly Int32IndexInput.Reader docReader;
            private readonly Int32IndexInput.Reader freqReader;
            private long skipFP;

            private readonly Int32IndexInput.Index docIndex;
            private readonly Int32IndexInput.Index freqIndex;
            private readonly Int32IndexInput.Index posIndex;
            internal readonly Int32IndexInput startDocIn;

            // TODO: -- should we do hasProx with 2 different enum classes?

            private bool skipped;
            private SepSkipListReader skipper;

            internal SepDocsEnum(SepPostingsReader outerInstance)
            {
                this.outerInstance = outerInstance;

                startDocIn = outerInstance.docIn;
                docReader = outerInstance.docIn.GetReader();
                docIndex = outerInstance.docIn.GetIndex();
                if (outerInstance.freqIn != null)
                {
                    freqReader = outerInstance.freqIn.GetReader();
                    freqIndex = outerInstance.freqIn.GetIndex();
                }
                else
                {
                    freqReader = null;
                    freqIndex = null;
                }
                if (outerInstance.posIn != null)
                {
                    posIndex = outerInstance.posIn.GetIndex();                 // only init this so skipper can read it
                }
                else
                {
                    posIndex = null;
                }
            }

            internal virtual SepDocsEnum Init(FieldInfo fieldInfo, SepTermState termState, IBits liveDocs)
            {
                this.liveDocs = liveDocs;
                this.indexOptions = fieldInfo.IndexOptions;
                omitTF = indexOptions == IndexOptions.DOCS_ONLY;
                storePayloads = fieldInfo.HasPayloads;

                // TODO: can't we only do this if consumer
                // skipped consuming the previous docs?
                docIndex.CopyFrom(termState.docIndex);
                docIndex.Seek(docReader);

                if (!omitTF)
                {
                    freqIndex.CopyFrom(termState.freqIndex);
                    freqIndex.Seek(freqReader);
                }

                docFreq = termState.DocFreq;
                // NOTE: unused if docFreq < skipMinimum:
                skipFP = termState.skipFP;
                count = 0;
                doc = -1;
                accum = 0;
                freq = 1;
                skipped = false;

                return this;
            }

            public override int NextDoc()
            {
                while (true)
                {
                    if (count == docFreq)
                    {
                        return doc = NO_MORE_DOCS;
                    }

                    count++;

                    // Decode next doc
                    //System.out.println("decode docDelta:");
                    accum += docReader.Next();

                    if (!omitTF)
                    {
                        //System.out.println("decode freq:");
                        freq = freqReader.Next();
                    }

                    if (liveDocs == null || liveDocs.Get(accum))
                    {
                        break;
                    }
                }
                return (doc = accum);
            }

            public override int Freq => freq;

            public override int DocID => doc;

            public override int Advance(int target)
            {
                if ((target - outerInstance.skipInterval) >= doc && docFreq >= outerInstance.skipMinimum)
                {

                    // There are enough docs in the posting to have
                    // skip data, and its not too close

                    if (skipper == null)
                    {
                        // This DocsEnum has never done any skipping
                        skipper = new SepSkipListReader((IndexInput)outerInstance.skipIn.Clone(),
                                                        outerInstance.freqIn,
                                                        outerInstance.docIn,
                                                        outerInstance.posIn,
                                                        outerInstance.maxSkipLevels, outerInstance.skipInterval);

                    }

                    if (!skipped)
                    {
                        // We haven't yet skipped for this posting
                        skipper.Init(skipFP,
                                     docIndex,
                                     freqIndex,
                                     posIndex,
                                     0,
                                     docFreq,
                                     storePayloads);
                        skipper.SetIndexOptions(indexOptions);

                        skipped = true;
                    }

                    int newCount = skipper.SkipTo(target);

                    if (newCount > count)
                    {

                        // Skipper did move
                        if (!omitTF)
                        {
                            skipper.FreqIndex.Seek(freqReader);
                        }
                        skipper.DocIndex.Seek(docReader);
                        count = newCount;
                        doc = accum = skipper.Doc;
                    }
                }

                // Now, linear scan for the rest:
                do
                {
                    if (NextDoc() == NO_MORE_DOCS)
                    {
                        return NO_MORE_DOCS;
                    }
                } while (target > doc);

                return doc;
            }

            public override long GetCost()
            {
                return docFreq;
            }
        }

        internal class SepDocsAndPositionsEnum : DocsAndPositionsEnum
        {
            private readonly SepPostingsReader outerInstance;

            private int docFreq;
            private int doc = -1;
            private int accum;
            private int count;
            private int freq;
            //private long freqStart; // LUCENENET: Not used

            private bool storePayloads;
            private IBits liveDocs;
            private readonly Int32IndexInput.Reader docReader;
            private readonly Int32IndexInput.Reader freqReader;
            private readonly Int32IndexInput.Reader posReader;
            private readonly IndexInput payloadIn;
            private long skipFP;

            private readonly Int32IndexInput.Index docIndex;
            private readonly Int32IndexInput.Index freqIndex;
            private readonly Int32IndexInput.Index posIndex;
            internal readonly Int32IndexInput startDocIn;

            private long payloadFP;

            private int pendingPosCount;
            private int position;
            private int payloadLength;
            private long pendingPayloadBytes;

            private bool skipped;
            private SepSkipListReader skipper;
            private bool payloadPending;
            private bool posSeekPending;

            internal SepDocsAndPositionsEnum(SepPostingsReader outerInstance)
            {
                this.outerInstance = outerInstance;

                startDocIn = outerInstance.docIn;
                docReader = outerInstance.docIn.GetReader();
                docIndex = outerInstance.docIn.GetIndex();
                freqReader = outerInstance.freqIn.GetReader();
                freqIndex = outerInstance.freqIn.GetIndex();
                posReader = outerInstance.posIn.GetReader();
                posIndex = outerInstance.posIn.GetIndex();
                payloadIn = (IndexInput)outerInstance.payloadIn.Clone();
            }

            internal virtual SepDocsAndPositionsEnum Init(FieldInfo fieldInfo, SepTermState termState, IBits liveDocs)
            {
                this.liveDocs = liveDocs;
                storePayloads = fieldInfo.HasPayloads;
                //System.out.println("Sep D&P init");

                // TODO: can't we only do this if consumer
                // skipped consuming the previous docs?
                docIndex.CopyFrom(termState.docIndex);
                docIndex.Seek(docReader);
                //System.out.println("  docIndex=" + docIndex);

                freqIndex.CopyFrom(termState.freqIndex);
                freqIndex.Seek(freqReader);
                //System.out.println("  freqIndex=" + freqIndex);

                posIndex.CopyFrom(termState.posIndex);
                //System.out.println("  posIndex=" + posIndex);
                posSeekPending = true;
                payloadPending = false;

                payloadFP = termState.payloadFP;
                skipFP = termState.skipFP;
                //System.out.println("  skipFP=" + skipFP);

                docFreq = termState.DocFreq;
                count = 0;
                doc = -1;
                accum = 0;
                pendingPosCount = 0;
                pendingPayloadBytes = 0;
                skipped = false;

                return this;
            }

            public override int NextDoc()
            {
                while (true)
                {
                    if (count == docFreq)
                    {
                        return doc = NO_MORE_DOCS;
                    }

                    count++;

                    // TODO: maybe we should do the 1-bit trick for encoding
                    // freq=1 case?

                    // Decode next doc
                    //System.out.println("  sep d&p read doc");
                    accum += docReader.Next();

                    //System.out.println("  sep d&p read freq");
                    freq = freqReader.Next();

                    pendingPosCount += freq;

                    if (liveDocs == null || liveDocs.Get(accum))
                    {
                        break;
                    }
                }

                position = 0;
                return (doc = accum);
            }

            public override int Freq => freq;

            public override int DocID => doc;

            public override int Advance(int target)
            {
                //System.out.println("SepD&P advance target=" + target + " vs current=" + doc + " this=" + this);

                if ((target - outerInstance.skipInterval) >= doc && docFreq >= outerInstance.skipMinimum)
                {

                    // There are enough docs in the posting to have
                    // skip data, and its not too close

                    if (skipper == null)
                    {
                        //System.out.println("  create skipper");
                        // This DocsEnum has never done any skipping
                        skipper = new SepSkipListReader((IndexInput)outerInstance.skipIn.Clone(),
                                                        outerInstance.freqIn,
                                                        outerInstance.docIn,
                                                        outerInstance.posIn,
                                                        outerInstance.maxSkipLevels, outerInstance.skipInterval);
                    }

                    if (!skipped)
                    {
                        //System.out.println("  init skip data skipFP=" + skipFP);
                        // We haven't yet skipped for this posting
                        skipper.Init(skipFP,
                                     docIndex,
                                     freqIndex,
                                     posIndex,
                                     payloadFP,
                                     docFreq,
                                     storePayloads);
                        skipper.SetIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS);
                        skipped = true;
                    }
                    int newCount = skipper.SkipTo(target);
                    //System.out.println("  skip newCount=" + newCount + " vs " + count);

                    if (newCount > count)
                    {

                        // Skipper did move
                        skipper.FreqIndex.Seek(freqReader);
                        skipper.DocIndex.Seek(docReader);
                        //System.out.println("  doc seek'd to " + skipper.getDocIndex());
                        // NOTE: don't seek pos here; do it lazily
                        // instead.  Eg a PhraseQuery may skip to many
                        // docs before finally asking for positions...
                        posIndex.CopyFrom(skipper.PosIndex);
                        posSeekPending = true;
                        count = newCount;
                        doc = accum = skipper.Doc;
                        //System.out.println("    moved to doc=" + doc);
                        //payloadIn.seek(skipper.getPayloadPointer());
                        payloadFP = skipper.PayloadPointer;
                        pendingPosCount = 0;
                        pendingPayloadBytes = 0;
                        payloadPending = false;
                        payloadLength = skipper.PayloadLength;
                        //System.out.println("    move payloadLen=" + payloadLength);
                    }
                }

                // Now, linear scan for the rest:
                do
                {
                    if (NextDoc() == NO_MORE_DOCS)
                    {
                        //System.out.println("  advance nextDoc=END");
                        return NO_MORE_DOCS;
                    }
                    //System.out.println("  advance nextDoc=" + doc);
                } while (target > doc);

                //System.out.println("  return doc=" + doc);
                return doc;
            }

            public override int NextPosition()
            {
                if (posSeekPending)
                {
                    posIndex.Seek(posReader);
                    payloadIn.Seek(payloadFP);
                    posSeekPending = false;
                }

                // scan over any docs that were iterated without their
                // positions
                while (pendingPosCount > freq)
                {
                    int code2 = posReader.Next();
                    if (storePayloads && (code2 & 1) != 0)
                    {
                        // Payload length has changed
                        payloadLength = posReader.Next();
                        Debug.Assert(payloadLength >= 0);
                    }
                    pendingPosCount--;
                    position = 0;
                    pendingPayloadBytes += payloadLength;
                }

                int code = posReader.Next();

                if (storePayloads)
                {
                    if ((code & 1) != 0)
                    {
                        // Payload length has changed
                        payloadLength = posReader.Next();
                        Debug.Assert(payloadLength >= 0);
                    }
                    position += (int)(((uint)code) >> 1);
                    pendingPayloadBytes += payloadLength;
                    payloadPending = payloadLength > 0;
                }
                else
                {
                    position += code;
                }

                pendingPosCount--;
                Debug.Assert(pendingPosCount >= 0);
                return position;
            }

            public override int StartOffset => -1;

            public override int EndOffset => -1;

            private BytesRef payload;

            public override BytesRef GetPayload()
            {
                if (!payloadPending)
                {
                    return null;
                }

                if (pendingPayloadBytes == 0)
                {
                    return payload;
                }

                Debug.Assert(pendingPayloadBytes >= payloadLength);

                if (pendingPayloadBytes > payloadLength)
                {
                    payloadIn.Seek(payloadIn.GetFilePointer() + (pendingPayloadBytes - payloadLength));
                }

                if (payload == null)
                {
                    payload = new BytesRef();
                    payload.Bytes = new byte[payloadLength];
                }
                else if (payload.Bytes.Length < payloadLength)
                {
                    payload.Grow(payloadLength);
                }

                payloadIn.ReadBytes(payload.Bytes, 0, payloadLength);
                payload.Length = payloadLength;
                pendingPayloadBytes = 0;
                return payload;
            }

            public override long GetCost()
            {
                return docFreq;
            }
        }

        public override long RamBytesUsed()
        {
            return 0;
        }

        public override void CheckIntegrity()
        {
            // TODO: remove sep layout, its fallen behind on features...
        }
    }
}