/*
 * Copyright 2000-2015 JetBrains s.r.o.
 *
 * Licensed 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.
 */

/*
 * @author max
 */
package com.intellij.util.io;

import com.intellij.openapi.Forceable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.vfs.CharsetToolkit;
import org.jetbrains.annotations.NotNull;

import java.io.*;
import java.nio.ByteBuffer;

public class RandomAccessDataFile implements Forceable, Closeable {
  protected static final Logger LOG = Logger.getInstance("#com.intellij.util.io.RandomAccessDataFile");

  private static final OpenChannelsCache ourCache = new OpenChannelsCache(150, "rw");
  private static int ourFilesCount;

  private final int myCount = ourFilesCount++;
  private final File myFile;
  private final PagePool myPool;
  private long lastSeek = -1L;

  private final byte[] myTypedIOBuffer = new byte[8];

  private final FileWriter log;

  private volatile long mySize;
  private volatile boolean myIsDirty;
  private volatile boolean myIsDisposed;

  private static final boolean DEBUG = false;

  public RandomAccessDataFile(@NotNull File file) throws IOException {
    this(file, PagePool.SHARED);
  }

  public RandomAccessDataFile(@NotNull File file, @NotNull PagePool pool) throws IOException {
    myPool = pool;
    myFile = file;
    if (!file.exists()) {
      throw new FileNotFoundException(file.getPath() + " does not exist");
    }

    mySize = file.length();
    if (DEBUG) {
      log = new FileWriter(file.getPath() + ".log");
    }
    else {
      log = null;
    }
  }

  @NotNull
  public File getFile() {
    return myFile;
  }

  public void put(long addr, byte[] bytes, int off, int len) {
    assertNotDisposed();

    myIsDirty = true;
    mySize = Math.max(mySize, addr + len);

    while (len > 0) {
      final Page page = myPool.alloc(this, addr);
      int written = page.put(addr, bytes, off, len);
      len -= written;
      addr += written;
      off += written;
    }
  }

  public void get(long addr, byte[] bytes, int off, int len) {
    assertNotDisposed();

    while (len > 0) {
      final Page page = myPool.alloc(this, addr);
      int read = page.get(addr, bytes, off, len);
      len -= read;
      addr += read;
      off += read;
    }
  }

  private void releaseFile() {
    ourCache.releaseChannel(myFile);
  }

  private RandomAccessFile getRandomAccessFile() throws FileNotFoundException {
    return ourCache.getChannel(myFile);
  }

  public void putInt(long addr, int value) {
    Bits.putInt(myTypedIOBuffer, 0, value);
    put(addr, myTypedIOBuffer, 0, 4);
  }

  public int getInt(long addr) {
    get(addr, myTypedIOBuffer, 0, 4);
    return Bits.getInt(myTypedIOBuffer, 0);
  }

  public void putLong(long addr, long value) {
    Bits.putLong(myTypedIOBuffer, 0, value);
    put(addr, myTypedIOBuffer, 0, 8);
  }

  public void putByte(final long addr, final byte b) {
    myTypedIOBuffer[0] = b;
    put(addr, myTypedIOBuffer, 0, 1);
  }

  public byte getByte(long addr) {
    get(addr, myTypedIOBuffer, 0, 1);
    return myTypedIOBuffer[0];
  }

  public long getLong(long addr) {
    get(addr, myTypedIOBuffer, 0, 8);
    return Bits.getLong(myTypedIOBuffer, 0);
  }

  public String getUTF(long addr) {
    int len = getInt(addr);
    byte[] bytes = new byte[len];
    get(addr + 4, bytes, 0, len);
    return new String(bytes, CharsetToolkit.UTF8_CHARSET);
  }

  public void putUTF(long addr, String value) {
    final byte[] bytes = value.getBytes(CharsetToolkit.UTF8_CHARSET);
    putInt(addr, bytes.length);
    put(addr + 4, bytes, 0, bytes.length);
  }

  public long length() {
    assertNotDisposed();
    return mySize;
  }

  public long physicalLength() {
    assertNotDisposed();

    long res;

    try {
      RandomAccessFile file = getRandomAccessFile();
      try {
        synchronized (file) {
          res = file.length();
        }
      }
      finally {
        releaseFile();
      }
    }
    catch (IOException e) {
      return 0;
    }
    return res;
  }

  public void dispose() {
    if (myIsDisposed) return;
    myPool.flushPages(this);
    ourCache.closeChannel(myFile);

    myIsDisposed = true;
  }

  @Override
  public void close() {
    dispose();
  }

  /**
   * Flushes dirty pages to underlying buffers
   */
  @Override
  public void force() {
    assertNotDisposed();
    if (isDirty()) {
      myPool.flushPages(this);
      myIsDirty = false;
    }
  }

  /**
   * Flushes dirty pages to buffers and saves them to disk
   */
  public void sync() {
    force();
    try {
      RandomAccessFile file = getRandomAccessFile();
      file.getChannel().force(true);
    }
    catch (IOException ignored) {

    }
    finally {
      releaseFile();
    }
  }

  public void flushSomePages(int maxPagesToFlush) {
    assertNotDisposed();
    if (isDirty()) {
      myIsDirty = !myPool.flushPages(this, maxPagesToFlush);
    }
  }

  @Override
  public boolean isDirty() {
    assertNotDisposed();
    return myIsDirty;
  }

  public boolean isDisposed() {
    return myIsDisposed;
  }

  private void assertNotDisposed() {
    if (myIsDisposed) {
      LOG.error("storage file is disposed: " + myFile);
    }
  }

  public static int totalReads;
  public static long totalReadBytes;

  public static int seekcount;
  public static int totalWrites;
  public static long totalWriteBytes;

  void loadPage(final Page page) {
    assertNotDisposed();
    try {
      final RandomAccessFile file = getRandomAccessFile();
      try {
        synchronized (file) {
          seek(file, page.getOffset());
          final ByteBuffer buf = page.getBuf();

          totalReads++;
          totalReadBytes += Page.PAGE_SIZE;

          if (DEBUG) {
            log.write("Read at: \t" + page.getOffset() + "\t len: " + Page.PAGE_SIZE + ", size: " + mySize + "\n");
          }
          file.read(buf.array(), 0, Page.PAGE_SIZE);
          lastSeek += Page.PAGE_SIZE;
        }
      }
      finally {
        releaseFile();
      }
    }
    catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  void flushPage(final Page page, int start, int end) {
    assertNotDisposed();
    try {
      flush(page.getBuf(), page.getOffset() + start, start, end - start);
    }
    catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  private void flush(final ByteBuffer buf, final long fileOffset, final int bufOffset, int length) throws IOException {
    if (fileOffset + length > mySize) {
      length = (int)(mySize - fileOffset);
    }

    final RandomAccessFile file = getRandomAccessFile();
    try {
      synchronized (file) {
        seek(file, fileOffset);

        totalWrites++;
        totalWriteBytes += length;

        if (DEBUG) {
          log.write("Write at: \t" + fileOffset + "\t len: " + length + ", size: " + mySize + ", filesize: " + file.length() + "\n");
        }
        file.write(buf.array(), bufOffset, length);
        lastSeek += length;
      }
    }
    finally {
      releaseFile();
    }
  }

  private void seek(final RandomAccessFile file, final long fileOffset) throws IOException {
    if (DEBUG) {
      if (lastSeek != -1L && fileOffset != lastSeek) {
        long delta = fileOffset - lastSeek;
        seekcount++;
        log.write("Seeking: " + delta + "\n");
      }
      lastSeek = fileOffset;
    }

    file.seek(fileOffset);
  }

  @Override
  public int hashCode() {
    return myCount;
  }

  @Override
  public synchronized String toString() {
    return "RandomAccessFile[" + myFile + ", dirty=" + myIsDirty + "]";
  }
}
