/**
 * 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.hdfs.server.datanode;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import org.apache.hadoop.hdfs.protocol.Block;
import org.apache.hadoop.hdfs.server.datanode.FSDataset.FSVolume;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.fs.HardLink;
import org.apache.hadoop.io.IOUtils;

/**
 * This class is used by the datanode to maintain the map from a block 
 * to its metadata.
 */
public class DatanodeBlockInfo {
  public static long UNFINALIZED = -1;

  private FSVolume volume;       // volume where the block belongs
  private File     file;         // block file
  private boolean detached;      // copy-on-write done for block
  private long finalizedSize;             // finalized size of the block
  

  DatanodeBlockInfo(FSVolume vol, File file, long finalizedSize) {
    this.volume = vol;
    this.file = file;
    this.finalizedSize = finalizedSize;
    detached = false;
  }
  
  DatanodeBlockInfo(FSVolume vol) {
    this.volume = vol;
    this.file = null;
    this.finalizedSize = UNFINALIZED;
    detached = false;
  }

  FSVolume getVolume() {
    return volume;
  }

  File getFile() {
    return file;
  }
  
  public boolean isFinalized() {
    return finalizedSize != UNFINALIZED;
  }

  public long getFinalizedSize() throws IOException {
    if (finalizedSize == UNFINALIZED) {
      throw new IOException("Try to get finalized size for unfinalized block");
    }
    return finalizedSize;
  }

  /**
   * THIS METHOD IS ONLY CALLED BY UNIT TESTS to synchronize
   * in memory size after directly calling truncateBlock()
   * 
   * @throws IOException
   */
  public void syncInMemorySize() throws IOException {
    if (finalizedSize == UNFINALIZED) {
      throw new IOException("Block is not finalized");
    }
    this.finalizedSize = file.length();
  }

  public void verifyFinalizedSize() throws IOException {
    if (this.file == null) {
      throw new IOException("No file for block.");
    }
    if (!this.file.exists()) {
      throw new IOException("File " + this.file + " doesn't exist on disk.");      
    }
    if (this.finalizedSize == UNFINALIZED) {
      return;
    }
    long onDiskSize = this.file.length();
    if (onDiskSize != this.finalizedSize) {
      throw new IOException("finalized size of file " + this.file
          + " doesn't match size on disk. On disk size: " + onDiskSize
          + " size in memory: " + this.finalizedSize);
    }
  }
  
  /**
   * Is this block already detached?
   */
  boolean isDetached() {
    return detached;
  }

  /**
   *  Block has been successfully detached
   */
  void setDetached() {
    detached = true;
  }

  /**
   * Copy specified file into a temporary file. Then rename the
   * temporary file to the original name. This will cause any
   * hardlinks to the original file to be removed. The temporary
   * files are created in the detachDir. The temporary files will
   * be recovered (especially on Windows) on datanode restart.
   */
  private void detachFile(int namespaceId, File file, Block b) throws IOException {
    File tmpFile = volume.createDetachFile(namespaceId, b, file.getName());
    try {
      IOUtils.copyBytes(new FileInputStream(file),
                        new FileOutputStream(tmpFile),
                        16*1024, true);
      if (file.length() != tmpFile.length()) {
        throw new IOException("Copy of file " + file + " size " + file.length()+
                              " into file " + tmpFile +
                              " resulted in a size of " + tmpFile.length());
      }
      FileUtil.replaceFile(tmpFile, file);
    } catch (IOException e) {
      boolean done = tmpFile.delete();
      if (!done) {
        DataNode.LOG.info("detachFile failed to delete temporary file " +
                          tmpFile);
      }
      throw e;
    }
  }

  /**
   * Returns true if this block was copied, otherwise returns false.
   */
  boolean detachBlock(int namespaceId, Block block, int numLinks) throws IOException {
    if (isDetached()) {
      return false;
    }
    if (file == null || volume == null) {
      throw new IOException("detachBlock:Block not found. " + block);
    }
    File meta = FSDataset.getMetaFile(file, block);
    if (meta == null) {
      throw new IOException("Meta file not found for block " + block);
    }

    if (HardLink.getLinkCount(file) > numLinks) {
      DataNode.LOG.info("CopyOnWrite for block " + block);
      detachFile(namespaceId, file, block);
    }
    if (HardLink.getLinkCount(meta) > numLinks) {
      detachFile(namespaceId, meta, block);
    }
    setDetached();
    return true;
  }
  
  public String toString() {
    return getClass().getSimpleName() + "(volume=" + volume
        + ", file=" + file + ", detached=" + detached + ")";
  }
}