/**
 * 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.raid;

import java.io.InputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;

import org.apache.hadoop.util.LineReader;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;

/**
 * Represents the contents of a HAR Index file. The HAR is assumed to be
 * comprising of RAID parity files only and no directories.
 */
public class HarIndex {
  static final String INDEX = "_index";
  static final String HAR = ".har";
  private List<IndexEntry> entries = new LinkedList<IndexEntry>();
  private Path harDirectory;

  /**
   * Represents information in a single line of the HAR index file.
   */
  public static class IndexEntry {
    String fileName; // Name of the file in the part file.
    long startOffset; // Start offset within the part file.
    long length; // Length of this file within the part file.
    long mtime; // Modification time of the file.
    String partFileName; // Name of the part file.

    IndexEntry(String fileName, long startOffset, long length,
                long mtime, String partFileName) {
      this.fileName = fileName;
      this.startOffset = startOffset;
      this.length = length;
      this.mtime = mtime;
      this.partFileName = partFileName;
    }

    public String toString() {
      return "fileName=" + fileName +
             ", startOffset=" + startOffset +
             ", length=" + length +
             ", mtime=" + mtime +
             ", partFileName=" + partFileName;
    }
  }

  /**
   * Creates a HarIndex object with the path to either the HAR
   * or a part file in the HAR.
   */
  public static HarIndex getHarIndex(FileSystem fs, Path initializer)
      throws IOException {
    if (!initializer.getName().endsWith(HAR)) {
      initializer = initializer.getParent();
    }
    InputStream in = null;
    try {
      Path indexFile = new Path(initializer, INDEX);
      FileStatus indexStat = fs.getFileStatus(indexFile);
      in = fs.open(indexFile);
      HarIndex harIndex = new HarIndex(in, indexStat.getLen());
      harIndex.harDirectory = initializer;
      return harIndex;
    } finally {
      if (in != null) {
        in.close();
      }
    }
  }


  /**
   * Constructor that reads the contents of the index file.
   * @param in An input stream to the index file.
   * @param max The size of the index file.
   * @throws IOException
   */
  public HarIndex(InputStream in, long max) throws IOException {
    LineReader lineReader = new LineReader(in);
    Text text = new Text();
    long nread = 0;
    while (nread < max) {
      int n = lineReader.readLine(text);
      nread += n;
      String line = text.toString();
      try {
        parseLine(line);
      } catch (UnsupportedEncodingException e) {
        throw new IOException("UnsupportedEncodingException after reading " +
                              nread + "bytes");
      }
    }
  }

  /**
   * Parses each line and extracts relevant information.
   * @param line
   * @throws UnsupportedEncodingException
   */
  void parseLine(String line) throws UnsupportedEncodingException {
    String[] splits = line.split(" ");

    boolean isDir = "dir".equals(splits[1]) ? true: false;
    if (!isDir && splits.length >= 6) {
      String name = URLDecoder.decode(splits[0], "UTF-8");
      String partName = URLDecoder.decode(splits[2], "UTF-8");
      long startIndex = Long.parseLong(splits[3]);
      long length = Long.parseLong(splits[4]);
      String[] newsplits = URLDecoder.decode(splits[5],"UTF-8").split(" ");
      if (newsplits != null && newsplits.length >= 5) {
        long mtime = Long.parseLong(newsplits[0]);
        IndexEntry entry = new IndexEntry(
          name, startIndex, length, mtime, partName);
        entries.add(entry);
      }
    }
  }

  /**
   * Finds the index entry corresponding to a HAR partFile at an offset.
   * @param partName The name of the part file (part-*).
   * @param partFileOffset The offset into the part file.
   * @return The entry corresponding to partName:partFileOffset.
   */
  public IndexEntry findEntry(String partName, long partFileOffset) {
    for (IndexEntry e: entries) {
      boolean nameMatch = partName.equals(e.partFileName);
      boolean inRange = (partFileOffset >= e.startOffset) &&
                        (partFileOffset < e.startOffset + e.length);
      if (nameMatch && inRange) {
        return e;
      }
    }
    return null;
  }

  /**
   * Finds the index entry corresponding to a file in the archive
   */
  public IndexEntry findEntryByFileName(String fileName) {
    for (IndexEntry e: entries) {
      if (fileName.equals(e.fileName)) {
        return e;
      }
    }
    return null;
  }

  public Iterator<IndexEntry> getEntries() {
    return entries.iterator();
  }

  public Path partFilePath(IndexEntry entry) {
    return new Path(harDirectory, entry.partFileName);
  }
}