/*
 * 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.
 */
package org.apache.lucene.tests.codecs.asserting;

import java.io.IOException;
import java.util.Iterator;
import org.apache.lucene.codecs.FieldsConsumer;
import org.apache.lucene.codecs.FieldsProducer;
import org.apache.lucene.codecs.NormsProducer;
import org.apache.lucene.codecs.PostingsFormat;
import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.index.Fields;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.PostingsEnum;
import org.apache.lucene.index.SegmentReadState;
import org.apache.lucene.index.SegmentWriteState;
import org.apache.lucene.index.Terms;
import org.apache.lucene.index.TermsEnum;
import org.apache.lucene.tests.index.AssertingLeafReader;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.BytesRefBuilder;

/** Just like the default postings format but with additional asserts. */
public final class AssertingPostingsFormat extends PostingsFormat {
  private final PostingsFormat in = TestUtil.getDefaultPostingsFormat();

  public AssertingPostingsFormat() {
    super("Asserting");
  }

  @Override
  public FieldsConsumer fieldsConsumer(SegmentWriteState state) throws IOException {
    return new AssertingFieldsConsumer(state, in.fieldsConsumer(state));
  }

  @Override
  public FieldsProducer fieldsProducer(SegmentReadState state) throws IOException {
    return new AssertingFieldsProducer(in.fieldsProducer(state));
  }

  static class AssertingFieldsProducer extends FieldsProducer {
    private final FieldsProducer in;

    AssertingFieldsProducer(FieldsProducer in) {
      this.in = in;
      // do a few simple checks on init
      assert toString() != null;
    }

    @Override
    public void close() throws IOException {
      in.close();
      in.close(); // close again
    }

    @Override
    public Iterator<String> iterator() {
      Iterator<String> iterator = in.iterator();
      assert iterator != null;
      return iterator;
    }

    @Override
    public Terms terms(String field) throws IOException {
      Terms terms = in.terms(field);
      return terms == null ? null : new AssertingLeafReader.AssertingTerms(terms);
    }

    @Override
    public int size() {
      return in.size();
    }

    @Override
    public void checkIntegrity() throws IOException {
      in.checkIntegrity();
    }

    @Override
    public FieldsProducer getMergeInstance() {
      return new AssertingFieldsProducer(in.getMergeInstance());
    }

    @Override
    public String toString() {
      return getClass().getSimpleName() + "(" + in.toString() + ")";
    }
  }

  static class AssertingFieldsConsumer extends FieldsConsumer {
    private final FieldsConsumer in;
    private final SegmentWriteState writeState;

    AssertingFieldsConsumer(SegmentWriteState writeState, FieldsConsumer in) {
      this.writeState = writeState;
      this.in = in;
    }

    @Override
    public void write(Fields fields, NormsProducer norms) throws IOException {
      in.write(fields, norms);

      // TODO: more asserts?  can we somehow run a
      // "limited" CheckIndex here???  Or ... can we improve
      // AssertingFieldsProducer and us it also to wrap the
      // incoming Fields here?

      String lastField = null;

      for (String field : fields) {

        FieldInfo fieldInfo = writeState.fieldInfos.fieldInfo(field);
        assert fieldInfo != null;
        assert lastField == null || lastField.compareTo(field) < 0;
        lastField = field;

        Terms terms = fields.terms(field);
        if (terms == null) {
          continue;
        }
        assert terms != null;

        TermsEnum termsEnum = terms.iterator();
        BytesRefBuilder lastTerm = null;
        PostingsEnum postingsEnum = null;

        boolean hasFreqs = fieldInfo.getIndexOptions().compareTo(IndexOptions.DOCS_AND_FREQS) >= 0;
        boolean hasPositions =
            fieldInfo.getIndexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS) >= 0;
        boolean hasOffsets =
            fieldInfo
                    .getIndexOptions()
                    .compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS)
                >= 0;
        boolean hasPayloads = terms.hasPayloads();

        assert hasPositions == terms.hasPositions();
        assert hasOffsets == terms.hasOffsets();

        while (true) {
          BytesRef term = termsEnum.next();
          if (term == null) {
            break;
          }
          assert lastTerm == null || lastTerm.get().compareTo(term) < 0;
          if (lastTerm == null) {
            lastTerm = new BytesRefBuilder();
            lastTerm.append(term);
          } else {
            lastTerm.copyBytes(term);
          }

          int flags = 0;
          if (hasPositions == false) {
            if (hasFreqs) {
              flags = flags | PostingsEnum.FREQS;
            }
            postingsEnum = termsEnum.postings(postingsEnum, flags);
          } else {
            flags = PostingsEnum.POSITIONS;
            if (hasPayloads) {
              flags |= PostingsEnum.PAYLOADS;
            }
            if (hasOffsets) {
              flags = flags | PostingsEnum.OFFSETS;
            }
            postingsEnum = termsEnum.postings(postingsEnum, flags);
          }

          assert postingsEnum != null : "termsEnum=" + termsEnum + " hasPositions=" + hasPositions;

          int lastDocID = -1;

          while (true) {
            int docID = postingsEnum.nextDoc();
            if (docID == PostingsEnum.NO_MORE_DOCS) {
              break;
            }
            assert docID > lastDocID;
            lastDocID = docID;
            if (hasFreqs) {
              int freq = postingsEnum.freq();
              assert freq > 0;

              if (hasPositions) {
                int lastPos = -1;
                int lastStartOffset = -1;
                for (int i = 0; i < freq; i++) {
                  int pos = postingsEnum.nextPosition();
                  assert pos >= lastPos
                      : "pos=" + pos + " vs lastPos=" + lastPos + " i=" + i + " freq=" + freq;
                  assert pos <= IndexWriter.MAX_POSITION
                      : "pos=" + pos + " is > IndexWriter.MAX_POSITION=" + IndexWriter.MAX_POSITION;
                  lastPos = pos;

                  if (hasOffsets) {
                    int startOffset = postingsEnum.startOffset();
                    int endOffset = postingsEnum.endOffset();
                    assert endOffset >= startOffset;
                    assert startOffset >= lastStartOffset;
                    lastStartOffset = startOffset;
                  }
                }
              }
            }
          }
        }
      }
    }

    @Override
    public void close() throws IOException {
      in.close();
      in.close(); // close again
    }
  }
}
