/*
 * 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.benchmark.byTask.feeds;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Properties;
import org.apache.lucene.benchmark.byTask.tasks.WriteLineDocTask;
import org.apache.lucene.benchmark.byTask.utils.Config;
import org.apache.lucene.benchmark.byTask.utils.StreamUtils;
import org.apache.lucene.util.IOUtils;

/**
 * A {@link ContentSource} reading one line at a time as a {@link
 * org.apache.lucene.document.Document} from a single file. This saves IO cost (over
 * DirContentSource) of recursing through a directory and opening a new file for every document.<br>
 * The expected format of each line is (arguments are separated by &lt;TAB&gt;): <i>title, date,
 * body</i>. If a line is read in a different format, a {@link RuntimeException} will be thrown. In
 * general, you should use this content source for files that were created with {@link
 * WriteLineDocTask}.<br>
 * <br>
 * Config properties:
 *
 * <ul>
 *   <li>docs.file=&lt;path to the file&gt;
 *   <li>content.source.encoding - default to UTF-8.
 *   <li>line.parser - default to {@link HeaderLineParser} if a header line exists which differs
 *       from {@link WriteLineDocTask#DEFAULT_FIELDS} and to {@link SimpleLineParser} otherwise.
 * </ul>
 */
public class LineDocSource extends ContentSource {

  /** Reader of a single input line into {@link DocData}. */
  public abstract static class LineParser {
    protected final String[] header;
    /**
     * Construct with the header
     *
     * @param header header line found in the input file, or null if none
     */
    public LineParser(String[] header) {
      this.header = header;
    }
    /** parse an input line and fill doc data appropriately */
    public abstract void parseLine(DocData docData, String line);
  }

  /**
   * {@link LineParser} which ignores the header passed to its constructor and assumes simply that
   * field names and their order are the same as in {@link WriteLineDocTask#DEFAULT_FIELDS}
   */
  public static class SimpleLineParser extends LineParser {
    public SimpleLineParser(String[] header) {
      super(header);
    }

    @Override
    public void parseLine(DocData docData, String line) {
      int k1 = 0;
      int k2 = line.indexOf(WriteLineDocTask.SEP, k1);
      if (k2 < 0) {
        throw new RuntimeException(
            "line: [" + line + "] is in an invalid format (missing: separator title::date)!");
      }
      docData.setTitle(line.substring(k1, k2));
      k1 = k2 + 1;
      k2 = line.indexOf(WriteLineDocTask.SEP, k1);
      if (k2 < 0) {
        throw new RuntimeException(
            "line: [" + line + "] is in an invalid format (missing: separator date::body)!");
      }
      docData.setDate(line.substring(k1, k2));
      k1 = k2 + 1;
      k2 = line.indexOf(WriteLineDocTask.SEP, k1);
      if (k2 >= 0) {
        throw new RuntimeException(
            "line: [" + line + "] is in an invalid format (too many separators)!");
      }
      // last one
      docData.setBody(line.substring(k1));
    }
  }

  /**
   * {@link LineParser} which sets field names and order by the header - any header - of the lines
   * file. It is less efficient than {@link SimpleLineParser} but more powerful.
   */
  public static class HeaderLineParser extends LineParser {
    private enum FieldName {
      NAME,
      TITLE,
      DATE,
      BODY,
      PROP
    }

    private final FieldName[] posToF;

    public HeaderLineParser(String[] header) {
      super(header);
      posToF = new FieldName[header.length];
      for (int i = 0; i < header.length; i++) {
        String f = header[i];
        if (DocMaker.NAME_FIELD.equals(f)) {
          posToF[i] = FieldName.NAME;
        } else if (DocMaker.TITLE_FIELD.equals(f)) {
          posToF[i] = FieldName.TITLE;
        } else if (DocMaker.DATE_FIELD.equals(f)) {
          posToF[i] = FieldName.DATE;
        } else if (DocMaker.BODY_FIELD.equals(f)) {
          posToF[i] = FieldName.BODY;
        } else {
          posToF[i] = FieldName.PROP;
        }
      }
    }

    @Override
    public void parseLine(DocData docData, String line) {
      int n = 0;
      int k1 = 0;
      int k2;
      while ((k2 = line.indexOf(WriteLineDocTask.SEP, k1)) >= 0) {
        if (n >= header.length) {
          throw new RuntimeException(
              "input line has invalid format: "
                  + (n + 1)
                  + " fields instead of "
                  + header.length
                  + " :: ["
                  + line
                  + "]");
        }
        setDocDataField(docData, n, line.substring(k1, k2));
        ++n;
        k1 = k2 + 1;
      }
      if (n != header.length - 1) {
        throw new RuntimeException(
            "input line has invalid format: "
                + (n + 1)
                + " fields instead of "
                + header.length
                + " :: ["
                + line
                + "]");
      }
      // last one
      setDocDataField(docData, n, line.substring(k1));
    }

    private void setDocDataField(DocData docData, int position, String text) {
      switch (posToF[position]) {
        case NAME:
          docData.setName(text);
          break;
        case TITLE:
          docData.setTitle(text);
          break;
        case DATE:
          docData.setDate(text);
          break;
        case BODY:
          docData.setBody(text);
          break;
        case PROP:
          Properties p = docData.getProps();
          if (p == null) {
            p = new Properties();
            docData.setProps(p);
          }
          p.setProperty(header[position], text);
          break;
      }
    }
  }

  private Path file;
  private BufferedReader reader;
  private int readCount;

  private LineParser docDataLineReader = null;
  private boolean skipHeaderLine = false;

  private synchronized void openFile() {
    try {
      IOUtils.close(reader);
      InputStream is = StreamUtils.inputStream(file);
      reader = new BufferedReader(new InputStreamReader(is, encoding), StreamUtils.BUFFER_SIZE);
      if (skipHeaderLine) {
        reader.readLine(); // skip one line - the header line - already handled that info
      }
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public void close() throws IOException {
    IOUtils.close(reader);
    reader = null;
  }

  @Override
  public DocData getNextDocData(DocData docData) throws NoMoreDataException, IOException {
    final String line;
    final int myID;

    synchronized (this) {
      line = reader.readLine();
      if (line == null) {
        if (!forever) {
          throw new NoMoreDataException();
        }
        // Reset the file
        openFile();
        return getNextDocData(docData);
      }
      if (docDataLineReader == null) { // first line ever, one time initialization,
        docDataLineReader = createDocDataLineReader(line);
        if (skipHeaderLine) {
          return getNextDocData(docData);
        }
      }
      // increment IDS only once...
      myID = readCount++;
    }

    // The date String was written in the format of DateTools.dateToString.
    docData.clear();
    docData.setID(myID);
    docDataLineReader.parseLine(docData, line);
    return docData;
  }

  private LineParser createDocDataLineReader(String line) {
    String[] header;
    String headIndicator = WriteLineDocTask.FIELDS_HEADER_INDICATOR + WriteLineDocTask.SEP;

    if (line.startsWith(headIndicator)) {
      header =
          line.substring(headIndicator.length()).split(Character.toString(WriteLineDocTask.SEP));
      skipHeaderLine = true; // mark to skip the header line when input file is reopened
    } else {
      header = WriteLineDocTask.DEFAULT_FIELDS;
    }

    // if a specific DocDataLineReader was configured, must respect it
    String docDataLineReaderClassName = getConfig().get("line.parser", null);
    if (docDataLineReaderClassName != null) {
      try {
        final Class<? extends LineParser> clazz =
            Class.forName(docDataLineReaderClassName).asSubclass(LineParser.class);
        Constructor<? extends LineParser> cnstr = clazz.getConstructor(String[].class);
        return cnstr.newInstance((Object) header);
      } catch (Exception e) {
        throw new RuntimeException("Failed to instantiate " + docDataLineReaderClassName, e);
      }
    }

    // if this the simple case,
    if (Arrays.deepEquals(header, WriteLineDocTask.DEFAULT_FIELDS)) {
      return new SimpleLineParser(header);
    }
    return new HeaderLineParser(header);
  }

  @Override
  public void resetInputs() throws IOException {
    super.resetInputs();
    openFile();
  }

  @Override
  public void setConfig(Config config) {
    super.setConfig(config);
    String fileName = config.get("docs.file", null);
    if (fileName == null) {
      throw new IllegalArgumentException("docs.file must be set");
    }
    file = Paths.get(fileName).toAbsolutePath();
    if (encoding == null) {
      encoding = IOUtils.UTF_8;
    }
  }
}
