/*
 * 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.drill.exec.memory;

import java.lang.reflect.Field;
import java.util.Formatter;

import com.google.common.base.Preconditions;

import io.netty.buffer.AbstractByteBuf;
import io.netty.buffer.DrillBuf;
import io.netty.util.IllegalReferenceCountException;

import static org.apache.drill.exec.util.SystemPropertyUtil.getBoolean;

public class BoundsChecking {
  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(BoundsChecking.class);

  public static final String ENABLE_UNSAFE_BOUNDS_CHECK_PROPERTY = "drill.exec.memory.enable_unsafe_bounds_check";
  // for backward compatibility check "drill.enable_unsafe_memory_access" property and enable bounds checking when
  // unsafe memory access is explicitly disabled
  public static final String ENABLE_UNSAFE_MEMORY_ACCESS_PROPERTY = "drill.enable_unsafe_memory_access";
  public static final boolean BOUNDS_CHECKING_ENABLED =
      getBoolean(ENABLE_UNSAFE_BOUNDS_CHECK_PROPERTY, !getBoolean(ENABLE_UNSAFE_MEMORY_ACCESS_PROPERTY, true));
  private static final boolean checkAccessible = getStaticBooleanField(AbstractByteBuf.class, "checkAccessible", false);

  static {
    if (BOUNDS_CHECKING_ENABLED) {
      logger.warn("Drill is running with direct memory bounds checking enabled. If this is a production system, disable it.");
    } else if (logger.isDebugEnabled()) {
      logger.debug("Direct memory bounds checking is disabled.");
    }
  }

  private BoundsChecking() {
  }

  private static boolean getStaticBooleanField(Class cls, String name, boolean def) {
    try {
      Field field = cls.getDeclaredField(name);
      field.setAccessible(true);
      return field.getBoolean(null);
    } catch (ReflectiveOperationException e) {
      return def;
    }
  }

  private static void checkIndex(DrillBuf buf, int index, int fieldLength) {
    Preconditions.checkNotNull(buf);
    if (checkAccessible && buf.refCnt() == 0) {
      Formatter formatter = new Formatter().format("%s, refCnt: 0", buf);
      if (BaseAllocator.DEBUG) {
        formatter.format("%n%s", buf.toVerboseString());
      }
      throw new IllegalReferenceCountException(formatter.toString());
    }
    if (fieldLength < 0) {
      throw new IllegalArgumentException(String.format("length: %d (expected: >= 0)", fieldLength));
    }
    if (index < 0 || index > buf.capacity() - fieldLength) {
      Formatter formatter = new Formatter().format("%s, index: %d, length: %d (expected: range(0, %d))", buf, index, fieldLength, buf.capacity());
      if (BaseAllocator.DEBUG) {
        formatter.format("%n%s", buf.toVerboseString());
      }
      throw new IndexOutOfBoundsException(formatter.toString());
    }
  }

  public static void lengthCheck(DrillBuf buf, int start, int length) {
    if (BOUNDS_CHECKING_ENABLED) {
      checkIndex(buf, start, length);
    }
  }

  public static void rangeCheck(DrillBuf buf, int start, int end) {
    if (BOUNDS_CHECKING_ENABLED) {
      checkIndex(buf, start, end - start);
    }
  }

  public static void rangeCheck(DrillBuf buf1, int start1, int end1, DrillBuf buf2, int start2, int end2) {
    if (BOUNDS_CHECKING_ENABLED) {
      checkIndex(buf1, start1, end1 - start1);
      checkIndex(buf2, start2, end2 - start2);
    }
  }

  public static void ensureWritable(DrillBuf buf, int minWritableBytes) {
    if (BOUNDS_CHECKING_ENABLED) {
      buf.ensureWritable(minWritableBytes);
    }
  }
}
