/*
 * 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.replicator.nrt;

import java.io.Closeable;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.NoSuchFileException;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.lucene.codecs.CodecUtil;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.ReferenceManager;
import org.apache.lucene.search.SearcherFactory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexInput;

/**
 * Common base class for {@link PrimaryNode} and {@link ReplicaNode}.
 *
 * @lucene.experimental
 */
public abstract class Node implements Closeable {

  public static boolean VERBOSE_FILES = true;
  public static boolean VERBOSE_CONNECTIONS = false;

  // Keys we store into IndexWriter's commit user data:

  /**
   * Key to store the primary gen in the commit data, which increments every time we promote a new
   * primary, so replicas can detect when the primary they were talking to is changed
   */
  public static String PRIMARY_GEN_KEY = "__primaryGen";

  /**
   * Key to store the version in the commit data, which increments every time we open a new NRT
   * reader
   */
  public static String VERSION_KEY = "__version";

  /** Compact ordinal for this node */
  protected final int id;

  protected final Directory dir;

  protected final SearcherFactory searcherFactory;

  // Tracks NRT readers, opened from IW (primary) or opened from replicated SegmentInfos pulled
  // across the wire (replica):
  protected ReferenceManager<IndexSearcher> mgr;

  /**
   * Startup time of original test, carefully propogated to all nodes to produce consistent "seconds
   * since start time" in messages
   */
  public static long globalStartNS;

  /** When this node was started */
  public static final long localStartNS = System.nanoTime();

  /** For debug logging */
  protected final PrintStream printStream;

  // public static final long globalStartNS;

  // For debugging:
  volatile String state = "idle";

  /** File metadata for last sync that succeeded; we use this as a cache */
  protected volatile Map<String, FileMetaData> lastFileMetaData;

  public Node(int id, Directory dir, SearcherFactory searcherFactory, PrintStream printStream) {
    this.id = id;
    this.dir = dir;
    this.searcherFactory = searcherFactory;
    this.printStream = printStream;
  }

  /** Returns the {@link ReferenceManager} to use for acquiring and releasing searchers */
  public ReferenceManager<IndexSearcher> getSearcherManager() {
    return mgr;
  }

  /** Returns the {@link Directory} this node is writing to */
  public Directory getDirectory() {
    return dir;
  }

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

  public abstract void commit() throws IOException;

  public static void nodeMessage(PrintStream printStream, String message) {
    if (printStream != null) {
      long now = System.nanoTime();
      printStream.println(
          String.format(
              Locale.ROOT,
              "%5.3fs %5.1fs:           [%11s] %s",
              (now - globalStartNS) / (double) TimeUnit.SECONDS.toNanos(1),
              (now - localStartNS) / (double) TimeUnit.SECONDS.toNanos(1),
              Thread.currentThread().getName(),
              message));
    }
  }

  public static void nodeMessage(PrintStream printStream, int id, String message) {
    if (printStream != null) {
      long now = System.nanoTime();
      printStream.println(
          String.format(
              Locale.ROOT,
              "%5.3fs %5.1fs:         N%d [%11s] %s",
              (now - globalStartNS) / (double) TimeUnit.SECONDS.toNanos(1),
              (now - localStartNS) / (double) TimeUnit.SECONDS.toNanos(1),
              id,
              Thread.currentThread().getName(),
              message));
    }
  }

  public void message(String message) {
    if (printStream != null) {
      long now = System.nanoTime();
      printStream.println(
          String.format(
              Locale.ROOT,
              "%5.3fs %5.1fs: %7s %2s [%11s] %s",
              (now - globalStartNS) / (double) TimeUnit.SECONDS.toNanos(1),
              (now - localStartNS) / (double) TimeUnit.SECONDS.toNanos(1),
              state,
              name(),
              Thread.currentThread().getName(),
              message));
    }
  }

  public String name() {
    char mode = this instanceof PrimaryNode ? 'P' : 'R';
    return mode + Integer.toString(id);
  }

  public abstract boolean isClosed();

  public long getCurrentSearchingVersion() throws IOException {
    IndexSearcher searcher = mgr.acquire();
    try {
      return ((DirectoryReader) searcher.getIndexReader()).getVersion();
    } finally {
      mgr.release(searcher);
    }
  }

  public static String bytesToString(long bytes) {
    if (bytes < 1024) {
      return bytes + " b";
    } else if (bytes < 1024 * 1024) {
      return String.format(Locale.ROOT, "%.1f KB", bytes / 1024.);
    } else if (bytes < 1024 * 1024 * 1024) {
      return String.format(Locale.ROOT, "%.1f MB", bytes / 1024. / 1024.);
    } else {
      return String.format(Locale.ROOT, "%.1f GB", bytes / 1024. / 1024. / 1024.);
    }
  }

  /**
   * Opens the specified file, reads its identifying information, including file length, full index
   * header (includes the unique segment ID) and the full footer (includes checksum), and returns
   * the resulting {@link FileMetaData}.
   *
   * <p>This returns null, logging a message, if there are any problems (the file does not exist, is
   * corrupt, truncated, etc.).
   */
  public FileMetaData readLocalFileMetaData(String fileName) throws IOException {

    Map<String, FileMetaData> cache = lastFileMetaData;
    FileMetaData result;
    if (cache != null) {
      // We may already have this file cached from the last NRT point:
      result = cache.get(fileName);
    } else {
      result = null;
    }

    if (result == null) {
      // Pull from the filesystem
      long checksum;
      long length;
      byte[] header;
      byte[] footer;
      try (IndexInput in = dir.openInput(fileName, IOContext.DEFAULT)) {
        try {
          length = in.length();
          header = CodecUtil.readIndexHeader(in);
          footer = CodecUtil.readFooter(in);
          checksum = CodecUtil.retrieveChecksum(in);
        } catch (@SuppressWarnings("unused") EOFException | CorruptIndexException cie) {
          // File exists but is busted: we must copy it.  This happens when node had crashed,
          // corrupting an un-fsync'd file.  On init we try
          // to delete such unreferenced files, but virus checker can block that, leaving this bad
          // file.
          if (VERBOSE_FILES) {
            message("file " + fileName + ": will copy [existing file is corrupt]");
          }
          return null;
        }
        if (VERBOSE_FILES) {
          message("file " + fileName + " has length=" + bytesToString(length));
        }
      } catch (@SuppressWarnings("unused") FileNotFoundException | NoSuchFileException e) {
        if (VERBOSE_FILES) {
          message("file " + fileName + ": will copy [file does not exist]");
        }
        return null;
      }

      // NOTE: checksum is redundant w/ footer, but we break it out separately because when the bits
      // cross the wire we need direct access to
      // checksum when copying to catch bit flips:
      result = new FileMetaData(header, footer, length, checksum);
    }

    return result;
  }
}
