/*
 * 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.hadoop.fs.swift.util;

import org.apache.commons.logging.Log;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;

import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * Various utility classes for SwiftFS support
 */
public final class SwiftUtils {

  public static final String READ = "read(buffer, offset, length)";

  /**
   * Join two (non null) paths, inserting a forward slash between them
   * if needed
   *
   * @param path1 first path
   * @param path2 second path
   * @return the combined path
   */
  public static String joinPaths(String path1, String path2) {
    StringBuilder result =
            new StringBuilder(path1.length() + path2.length() + 1);
    result.append(path1);
    boolean insertSlash = true;
    if (path1.endsWith("/")) {
      insertSlash = false;
    } else if (path2.startsWith("/")) {
      insertSlash = false;
    }
    if (insertSlash) {
      result.append("/");
    }
    result.append(path2);
    return result.toString();
  }

  /**
   * This test contains the is-directory logic for Swift, so if
   * changed there is only one place for it.
   *
   * @param fileStatus status to examine
   * @return true if we consider this status to be representative of a
   *         directory.
   */
  public static boolean isDirectory(FileStatus fileStatus) {
    return fileStatus.isDir() || isFilePretendingToBeDirectory(fileStatus);
  }

  /**
   * Test for the entry being a file that is treated as if it is a
   * directory
   *
   * @param fileStatus status
   * @return true if it meets the rules for being a directory
   */
  public static boolean isFilePretendingToBeDirectory(FileStatus fileStatus) {
    return fileStatus.getLen() == 0;
  }

  /**
   * Predicate: Is a swift object referring to the root direcory?
   * @param swiftObject object to probe
   * @return true iff the object refers to the root
   */
  public static boolean isRootDir(SwiftObjectPath swiftObject) {
    return swiftObject.objectMatches("") || swiftObject.objectMatches("/");
  }

  /**
   * Sprintf() to the log iff the log is at debug level. If the log
   * is not at debug level, the printf operation is skipped, so
   * no time is spent generating the string.
   * @param log log to use
   * @param text text message
   * @param args args arguments to the print statement
   */
  public static void debug(Log log, String text, Object... args) {
    if (log.isDebugEnabled()) {
      log.debug(String.format(text, args));
    }
  }

  /**
   * Log an exception (in text and trace) iff the log is at debug
   * @param log Log to use
   * @param text text message
   * @param ex exception
   */
  public static void debugEx(Log log, String text, Exception ex) {
    if (log.isDebugEnabled()) {
      log.debug(text + ex, ex);
    }
  }

  /**
   * Sprintf() to the log iff the log is at trace level. If the log
   * is not at trace level, the printf operation is skipped, so
   * no time is spent generating the string.
   * @param log log to use
   * @param text text message
   * @param args args arguments to the print statement
   */
  public static void trace(Log log, String text, Object... args) {
    if (log.isTraceEnabled()) {
      log.trace(String.format(text, args));
    }
  }

  /**
   * Given a partition number, calculate the partition value.
   * This is used in the SwiftNativeOutputStream, and is placed
   * here for tests to be able to calculate the filename of
   * a partition.
   * @param partNumber part number
   * @return a string to use as the filename
   */
  public static String partitionFilenameFromNumber(int partNumber) {
    return String.format("%06d", partNumber);
  }

  /**
   * List a a path to string
   * @param fileSystem filesystem
   * @param path directory
   * @return a listing of the filestatuses of elements in the directory, one
   * to a line, precedeed by the full path of the directory
   * @throws IOException connectivity problems
   */
  public static String ls(FileSystem fileSystem, Path path) throws
                                                            IOException {
    if (path == null) {
      //surfaces when someone calls getParent() on something at the top of the path
      return "/";
    }
    FileStatus[] stats;
    String pathtext = "ls " + path;
    try {
      stats = fileSystem.listStatus(path);
    } catch (FileNotFoundException e) {
      return pathtext + " -file not found";
    } catch (IOException e) {
      return pathtext + " -failed: " + e;
    }
    return pathtext + fileStatsToString(stats, "\n");
  }

  /**
   * Take an array of filestats and convert to a string (prefixed w/ a [01] counter
   * @param stats array of stats
   * @param separator separator after every entry
   * @return a stringified set
   */
  public static String fileStatsToString(FileStatus[] stats, String separator) {
    StringBuilder buf = new StringBuilder(stats.length * 128);
    for (int i = 0; i < stats.length; i++) {
      buf.append(String.format("[%02d] %s", i, stats[i])).append(separator);
    }
    return buf.toString();
  }

  /**
   * Verify that the basic args to a read operation are valid;
   * throws an exception if not -with meaningful text includeing
   * @param buffer destination buffer
   * @param off offset
   * @param len number of bytes to read
   * @throws NullPointerException null buffer
   * @throws IndexOutOfBoundsException on any invalid range.
   */
  public static void validateReadArgs(byte[] buffer, int off, int len) {
    if (buffer == null) {
      throw new NullPointerException("Null byte array in"+ READ);
    }
    if (off < 0 ) {
      throw new IndexOutOfBoundsException("Negative buffer offset "
                                          + off
                                          + " in " + READ);
    }
    if (len < 0 ) {
      throw new IndexOutOfBoundsException("Negative read length "
                                          + len
                                          + " in " + READ);
    }
    if (off > buffer.length) {
      throw new IndexOutOfBoundsException("Buffer offset of "
                                          + off
                                          + "beyond buffer size of "
                                          + buffer.length
                                          + " in " + READ);
    }
  } 
}
