/*
 * 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.activemq.artemis.core.io.mapped;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.ref.WeakReference;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.ArrayList;

import io.netty.util.internal.PlatformDependent;

final class MappedByteBufferCache implements AutoCloseable {

   public static final int PAGE_SIZE = Integer.parseInt(System.getProperty("os_page_size", "4096"));
   private static final Object FILE_LOCK = new Object();
   private final RandomAccessFile raf;
   private final FileChannel fileChannel;
   private final long chunkBytes;
   private final long overlapBytes;
   private final ArrayList<WeakReference<MappedByteBuffer>> byteBuffers;
   private final File file;
   private final long mappedSize;
   private boolean closed;

   private MappedByteBufferCache(File file, RandomAccessFile raf, long chunkBytes, long overlapBytes, long alignment) {
      this.byteBuffers = new ArrayList<>();
      this.file = file;
      this.raf = raf;
      this.fileChannel = raf.getChannel();
      this.chunkBytes = BytesUtils.align(chunkBytes, alignment);
      this.overlapBytes = BytesUtils.align(overlapBytes, alignment);
      this.closed = false;
      this.mappedSize = this.chunkBytes + this.overlapBytes;
   }

   public static MappedByteBufferCache of(File file, long chunkSize, long overlapSize) throws FileNotFoundException {
      final RandomAccessFile raf = new RandomAccessFile(file, "rw");
      return new MappedByteBufferCache(file, raf, chunkSize, overlapSize, PAGE_SIZE);
   }

   public static boolean inside(long position, long mappedPosition, long mappedLimit) {
      return mappedPosition <= position && position < mappedLimit;
   }

   public File file() {
      return file;
   }

   public long chunkBytes() {
      return chunkBytes;
   }

   public long overlapBytes() {
      return overlapBytes;
   }

   public int indexFor(long position) {
      final int chunk = (int) (position / chunkBytes);
      return chunk;
   }

   public long mappedPositionFor(int index) {
      return index * chunkBytes;
   }

   public long mappedLimitFor(long mappedPosition) {
      return mappedPosition + chunkBytes;
   }

   public MappedByteBuffer acquireMappedByteBuffer(final int index) throws IOException, IllegalArgumentException, IllegalStateException {
      if (closed)
         throw new IOException("Closed");
      if (index < 0)
         throw new IOException("Attempt to access a negative index: " + index);
      while (byteBuffers.size() <= index) {
         byteBuffers.add(null);
      }
      final WeakReference<MappedByteBuffer> mbbRef = byteBuffers.get(index);
      if (mbbRef != null) {
         final MappedByteBuffer mbb = mbbRef.get();
         if (mbb != null) {
            return mbb;
         }
      }
      return mapAndAcquire(index);
   }

   //METHOD BUILT TO SEPARATE THE SLOW PATH TO ENSURE INLINING OF THE MOST OCCURRING CASE
   private MappedByteBuffer mapAndAcquire(final int index) throws IOException {
      final long chunkStartPosition = mappedPositionFor(index);
      final long minSize = chunkStartPosition + mappedSize;
      if (fileChannel.size() < minSize) {
         try {
            synchronized (FILE_LOCK) {
               try (FileLock lock = fileChannel.lock()) {
                  final long size = fileChannel.size();
                  if (size < minSize) {
                     raf.setLength(minSize);
                  }
               }
            }
         } catch (IOException ioe) {
            throw new IOException("Failed to resize to " + minSize, ioe);
         }
      }

      final MappedByteBuffer mbb = fileChannel.map(FileChannel.MapMode.READ_WRITE, chunkStartPosition, mappedSize);
      mbb.order(ByteOrder.nativeOrder());
      byteBuffers.set(index, new WeakReference<>(mbb));
      return mbb;
   }

   public long fileSize() throws IOException {
      if (closed)
         throw new IllegalStateException("Closed");
      return fileChannel.size();
   }

   public void closeAndResize(long length) {
      if (!closed) {
         //TO_FIX: unmap in this way is not portable BUT required on Windows that can't resize a memmory mapped file!
         for (final WeakReference<MappedByteBuffer> mbbRef : this.byteBuffers) {
            if (mbbRef != null) {
               final MappedByteBuffer mbb = mbbRef.get();
               if (mbb != null) {
                  try {
                     PlatformDependent.freeDirectBuffer(mbb);
                  } catch (Throwable t) {
                     //TO_FIX: force releasing of the other buffers
                  }
               }
            }
         }
         this.byteBuffers.clear();
         try {
            if (fileChannel.size() != length) {
               try {
                  synchronized (FILE_LOCK) {
                     try (FileLock lock = fileChannel.lock()) {
                        final long size = fileChannel.size();
                        if (size != length) {
                           raf.setLength(length);
                        }
                     }
                  }
               } catch (IOException ioe) {
                  throw new IllegalStateException("Failed to resize to " + length, ioe);
               }
            }
         } catch (IOException ex) {
            throw new IllegalStateException("Failed to get size", ex);
         } finally {
            try {
               fileChannel.close();
            } catch (IOException e) {
               throw new IllegalStateException("Failed to close channel", e);
            } finally {
               try {
                  raf.close();
               } catch (IOException e) {
                  throw new IllegalStateException("Failed to close RandomAccessFile", e);
               }
            }
            closed = true;
         }
      }
   }

   public boolean isClosed() {
      return closed;
   }

   @Override
   public void close() {
      if (!closed) {
         //TO_FIX: unmap in this way is not portable BUT required on Windows that can't resize a memory mapped file!
         for (final WeakReference<MappedByteBuffer> mbbRef : this.byteBuffers) {
            if (mbbRef != null) {
               final MappedByteBuffer mbb = mbbRef.get();
               if (mbb != null) {
                  try {
                     PlatformDependent.freeDirectBuffer(mbb);
                  } catch (Throwable t) {
                     //TO_FIX: force releasing of the other buffers
                  }
               }
            }
         }
         this.byteBuffers.clear();
         try {
            fileChannel.close();
         } catch (IOException e) {
            throw new IllegalStateException("Failed to close channel", e);
         } finally {
            try {
               raf.close();
            } catch (IOException e) {
               throw new IllegalStateException("Failed to close RandomAccessFile", e);
            }
         }
         closed = true;
      }
   }
}
