/**
 * 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.zookeeper.server.persistence;

import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.Serializable;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Properties;

import org.apache.jute.BinaryOutputArchive;
import org.apache.jute.InputArchive;
import org.apache.jute.OutputArchive;
import org.apache.jute.Record;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.zookeeper.txn.TxnHeader;

/**
 * A collection of utility methods for dealing with file name parsing, 
 * low level I/O file operations and marshalling/unmarshalling.
 */
public class Util {
    private static final Logger LOG = LoggerFactory.getLogger(Util.class);
    private static final String SNAP_DIR="snapDir";
    private static final String LOG_DIR="logDir";
    private static final String DB_FORMAT_CONV="dbFormatConversion";
    private static final ByteBuffer fill = ByteBuffer.allocateDirect(1);
    
    public static String makeURIString(String dataDir, String dataLogDir, 
            String convPolicy){
        String uri="file:"+SNAP_DIR+"="+dataDir+";"+LOG_DIR+"="+dataLogDir;
        if(convPolicy!=null)
            uri+=";"+DB_FORMAT_CONV+"="+convPolicy;
        return uri.replace('\\', '/');
    }
    /**
     * Given two directory files the method returns a well-formed 
     * logfile provider URI. This method is for backward compatibility with the
     * existing code that only supports logfile persistence and expects these two
     * parameters passed either on the command-line or in the configuration file.
     * 
     * @param dataDir snapshot directory
     * @param dataLogDir transaction log directory
     * @return logfile provider URI
     */
    public static URI makeFileLoggerURL(File dataDir, File dataLogDir){
        return URI.create(makeURIString(dataDir.getPath(),dataLogDir.getPath(),null));
    }
    
    public static URI makeFileLoggerURL(File dataDir, File dataLogDir,String convPolicy){
        return URI.create(makeURIString(dataDir.getPath(),dataLogDir.getPath(),convPolicy));
    }

    /**
     * Creates a valid transaction log file name. 
     * 
     * @param zxid used as a file name suffix (extention)
     * @return file name
     */
    public static String makeLogName(long zxid) {
        return "log." + Long.toHexString(zxid);
    }

    /**
     * Creates a snapshot file name.
     * 
     * @param zxid used as a suffix
     * @return file name
     */
    public static String makeSnapshotName(long zxid) {
        return "snapshot." + Long.toHexString(zxid);
    }
    
    /**
     * Extracts snapshot directory property value from the container.
     * 
     * @param props properties container
     * @return file representing the snapshot directory
     */
    public static File getSnapDir(Properties props){
        return new File(props.getProperty(SNAP_DIR));
    }

    /**
     * Extracts transaction log directory property value from the container.
     * 
     * @param props properties container
     * @return file representing the txn log directory
     */
    public static File getLogDir(Properties props){
        return new File(props.getProperty(LOG_DIR));
    }
    
    /**
     * Extracts the value of the dbFormatConversion attribute.
     * 
     * @param props properties container
     * @return value of the dbFormatConversion attribute
     */
    public static String getFormatConversionPolicy(Properties props){
        return props.getProperty(DB_FORMAT_CONV);
    }
   
    /**
     * Extracts zxid from the file name. The file name should have been created
     * using one of the {@link makeLogName} or {@link makeSnapshotName}.
     * 
     * @param name the file name to parse
     * @param prefix the file name prefix (snapshot or log)
     * @return zxid
     */
    public static long getZxidFromName(String name, String prefix) {
        long zxid = -1;
        String nameParts[] = name.split("\\.");
        if (nameParts.length == 2 && nameParts[0].equals(prefix)) {
            try {
                zxid = Long.parseLong(nameParts[1], 16);
            } catch (NumberFormatException e) {
            }
        }
        return zxid;
    }

    /**
     * Verifies that the file is a valid snapshot. Snapshot may be invalid if 
     * it's incomplete as in a situation when the server dies while in the process
     * of storing a snapshot. Any file that is not a snapshot is also 
     * an invalid snapshot. 
     * 
     * @param f file to verify
     * @return true if the snapshot is valid
     * @throws IOException
     */
    public static boolean isValidSnapshot(File f) throws IOException {
        if (f==null || Util.getZxidFromName(f.getName(), "snapshot") == -1)
            return false;

        // Check for a valid snapshot
        RandomAccessFile raf = new RandomAccessFile(f, "r");
        try {
            // including the header and the last / bytes
            // the snapshot should be atleast 10 bytes
            if (raf.length() < 10) {
                return false;
            }
            raf.seek(raf.length() - 5);
            byte bytes[] = new byte[5];
            int readlen = 0;
            int l;
            while(readlen < 5 &&
                  (l = raf.read(bytes, readlen, bytes.length - readlen)) >= 0) {
                readlen += l;
            }
            if (readlen != bytes.length) {
                LOG.info("Invalid snapshot " + f
                        + " too short, len = " + readlen);
                return false;
            }
            ByteBuffer bb = ByteBuffer.wrap(bytes);
            int len = bb.getInt();
            byte b = bb.get();
            if (len != 1 || b != '/') {
                LOG.info("Invalid snapshot " + f + " len = " + len
                        + " byte = " + (b & 0xff));
                return false;
            }
        } finally {
            raf.close();
        }

        return true;
    }

    /**
     * Grows the file to the specified number of bytes. This only happenes if 
     * the current file position is sufficiently close (less than 4K) to end of 
     * file. 
     * 
     * @param f output stream to pad
     * @param currentSize application keeps track of the cuurent file size
     * @param preAllocSize how many bytes to pad
     * @return the new file size. It can be the same as currentSize if no
     * padding was done.
     * @throws IOException
     */
    public static long padLogFile(FileOutputStream f,long currentSize,
            long preAllocSize) throws IOException{
        long position = f.getChannel().position();
        if (position + 4096 >= currentSize) {
            currentSize = currentSize + preAllocSize;
            fill.position(0);
            f.getChannel().write(fill, currentSize-fill.remaining());
        }
        return currentSize;
    }

    /**
     * Reads a transaction entry from the input archive.
     * @param ia archive to read from
     * @return null if the entry is corrupted or EOF has been reached; a buffer
     * (possible empty) containing serialized transaction record.
     * @throws IOException
     */
    public static byte[] readTxnBytes(InputArchive ia) throws IOException {
        try{
            byte[] bytes = ia.readBuffer("txtEntry");
            // Since we preallocate, we define EOF to be an
            // empty transaction
            if (bytes.length == 0)
                return bytes;
            if (ia.readByte("EOF") != 'B') {
                LOG.error("Last transaction was partial.");
                return null;
            }
            return bytes;
        }catch(EOFException e){}
        return null;
    }
    

    /**
     * Serializes transaction header and transaction data into a byte buffer.
     *  
     * @param hdr transaction header
     * @param txn transaction data
     * @return serialized transaction record
     * @throws IOException
     */
    public static byte[] marshallTxnEntry(TxnHeader hdr, Record txn)
            throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        OutputArchive boa = BinaryOutputArchive.getArchive(baos);

        hdr.serialize(boa, "hdr");
        if (txn != null) {
            txn.serialize(boa, "txn");
        }
        return baos.toByteArray();
    }

    /**
     * Write the serialized transaction record to the output archive.
     *  
     * @param oa output archive
     * @param bytes serialized trasnaction record
     * @throws IOException
     */
    public static void writeTxnBytes(OutputArchive oa, byte[] bytes)
            throws IOException {
        oa.writeBuffer(bytes, "txnEntry");
        oa.writeByte((byte) 0x42, "EOR"); // 'B'
    }
    
    
    /**
     * Compare file file names of form "prefix.version". Sort order result
     * returned in order of version.
     */
    private static class DataDirFileComparator
        implements Comparator<File>, Serializable
    {
        private static final long serialVersionUID = -2648639884525140318L;

        private String prefix;
        private boolean ascending;
        public DataDirFileComparator(String prefix, boolean ascending) {
            this.prefix = prefix;
            this.ascending = ascending;
        }

        public int compare(File o1, File o2) {
            long z1 = Util.getZxidFromName(o1.getName(), prefix);
            long z2 = Util.getZxidFromName(o2.getName(), prefix);
            int result = z1 < z2 ? -1 : (z1 > z2 ? 1 : 0);
            return ascending ? result : -result;
        }
    }
    
    /**
     * Sort the list of files. Recency as determined by the version component
     * of the file name.
     *
     * @param files array of files
     * @param prefix files not matching this prefix are assumed to have a
     * version = -1)
     * @param ascending true sorted in ascending order, false results in
     * descending order
     * @return sorted input files
     */
    public static List<File> sortDataDir(File[] files, String prefix, boolean ascending)
    {
        if(files==null)
            return new ArrayList<File>(0);
        List<File> filelist = Arrays.asList(files);
        Collections.sort(filelist, new DataDirFileComparator(prefix, ascending));
        return filelist;
    }
    
}
