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

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.jute.BinaryInputArchive;
import org.apache.jute.InputArchive;
import org.apache.jute.Record;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.zookeeper.ZooDefs.OpCode;
import org.apache.zookeeper.data.StatPersisted;
import org.apache.zookeeper.data.StatPersistedV1;
import org.apache.zookeeper.server.DataNode;
import org.apache.zookeeper.server.DataTree;
import org.apache.zookeeper.server.Request;
import org.apache.zookeeper.server.ZooTrace;
import org.apache.zookeeper.server.persistence.FileTxnLog;
import org.apache.zookeeper.server.persistence.Util;
import org.apache.zookeeper.server.util.SerializeUtils;
import org.apache.zookeeper.txn.CreateSessionTxn;
import org.apache.zookeeper.txn.TxnHeader;

/**
 * This class gets the old snapshot 
 * and the old dataDir and creates 
 * an brand new snapshot that is 
 * then converted to the new snapshot
 * for upgrading.           
 */
public class UpgradeSnapShotV1 implements UpgradeSnapShot {
    private static final Logger LOG = LoggerFactory.getLogger(UpgradeSnapShotV1.class);
    
    ConcurrentHashMap<Long, Integer> sessionsWithTimeouts = 
        new ConcurrentHashMap<Long, Integer>();
    File dataDir;
    File snapShotDir;
    DataTreeV1 oldDataTree;
   
    /**
     * upgrade from version 1 to version 2
     * @param dataDir
     * @param snapShotDir
     */
    public UpgradeSnapShotV1(File dataDir, File snapShotDir) {
        this.dataDir = dataDir;
        this.snapShotDir = snapShotDir;
        oldDataTree = new DataTreeV1();
    }
    
    /**
     * deseriluize from an inputarchive
     * @param oldTree the tree to be created
     * @param ia the input archive to be read from
     * @param sessions the sessions to be created
     * @throws IOException 
     */
    private void deserializeSnapshot(DataTreeV1 oldTree, InputArchive ia,
            Map<Long, Integer> sessions) throws IOException {
        int count = ia.readInt("count");
        while (count > 0) {
            long id = ia.readLong("id");
            int to = ia.readInt("timeout");
            sessions.put(id, to);
            if (LOG.isTraceEnabled()) {
                ZooTrace.logTraceMessage(LOG, ZooTrace.SESSION_TRACE_MASK,
                        "loadData --- session in archive: " + id
                        + " with timeout: " + to);
            }
            count--;
        }
        oldTree.deserialize(ia, "tree");
    }
    
    /**
     * play the log from this logstream into the datatree
     * @param logStream
     * @return
     * @throws IOException
     */
    public long playLog(InputArchive logStream) throws IOException {
        long highestZxid = 0;
        try {
            while (true) {
                byte[] bytes = logStream.readBuffer("txnEntry");
                if (bytes.length == 0) {
                    // Since we preallocate, we define EOF to be an
                    // empty transaction
                    throw new EOFException();
                }
                TxnHeader hdr = new TxnHeader();
                Record txn = SerializeUtils.deserializeTxn(bytes, hdr);
                if (logStream.readByte("EOR") != 'B') {
                    LOG.warn("Last transaction was partial.");
                    throw new EOFException("Last transaction was partial.");
                }
                if (hdr.getZxid() <= highestZxid && highestZxid != 0) {
                    LOG.error(highestZxid + "(higestZxid) >= "
                            + hdr.getZxid() + "(next log) for type "
                            + hdr.getType());
                } else {
                    highestZxid = hdr.getZxid();
                }
                switch (hdr.getType()) {
                case OpCode.createSession:
                    sessionsWithTimeouts.put(hdr.getClientId(),
                            ((CreateSessionTxn) txn).getTimeOut());
                    if (LOG.isTraceEnabled()) {
                        ZooTrace.logTraceMessage(LOG,
                                                 ZooTrace.SESSION_TRACE_MASK,
                                "playLog --- create session in log: 0x"
                                        + Long.toHexString(hdr.getClientId())
                                        + " with timeout: "
                                        + ((CreateSessionTxn) txn).getTimeOut());
                    }
                    // give dataTree a chance to sync its lastProcessedZxid
                    oldDataTree.processTxn(hdr, txn);
                    break;
                case OpCode.closeSession:
                    sessionsWithTimeouts.remove(hdr.getClientId());
                    if (LOG.isTraceEnabled()) {
                        ZooTrace.logTraceMessage(LOG,
                                ZooTrace.SESSION_TRACE_MASK,
                                "playLog --- close session in log: 0x"
                                        + Long.toHexString(hdr.getClientId()));
                    }
                    oldDataTree.processTxn(hdr, txn);
                    break;
                default:
                    oldDataTree.processTxn(hdr, txn);
                }
                Request r = new Request(null, 0, hdr.getCxid(), hdr.getType(),
                        null, null);
                r.txn = txn;
                r.hdr = hdr;
                r.zxid = hdr.getZxid();
            }
        } catch (EOFException e) {
            // expected in some cases - see comments in try block
        }
        return highestZxid;
    }

   
    
    /**
     * apply the log files to the datatree
     * @param oldTree the datatreee to apply the logs to
     * @param logFiles the logs to be applied
     * @throws IOException
     */
    private long processLogFiles(DataTreeV1 oldTree, 
            File[] logFiles) throws IOException {
        long zxid = 0;
        for (File f: logFiles) { 
            LOG.info("Processing log file: " + f);
            InputStream logIs = 
                new BufferedInputStream(new FileInputStream(f));
            zxid = playLog(BinaryInputArchive.getArchive(logIs));
            logIs.close();
        }
        return zxid;
    }
    
    /**
     * create the old snapshot database
     * apply logs to it and create the final
     * database
     * @throws IOException
     */
    private void loadThisSnapShot() throws IOException {  
        // pick the most recent snapshot
        File snapshot = findMostRecentSnapshot();
        if (snapshot == null) {
            throw new IOException("Invalid snapshots " +
            		"or not snapshots in " + snapShotDir);
        }
        InputStream inputstream = new BufferedInputStream(
                new FileInputStream(snapshot));
        InputArchive ia = BinaryInputArchive.getArchive(inputstream);
        deserializeSnapshot(oldDataTree, ia, sessionsWithTimeouts);
        //ok done with the snapshot 
        // now apply the logs
        long snapshotZxid = oldDataTree.lastProcessedZxid;
        File[] files = FileTxnLog.getLogFiles(
                dataDir.listFiles(), snapshotZxid);
        long zxid = processLogFiles(oldDataTree, files);
        //check for this zxid to be sane
        if (zxid != oldDataTree.lastProcessedZxid) {
            LOG.error("Zxids not equal " + " log zxid " +
                    zxid + " datatree processed " + oldDataTree.lastProcessedZxid);
        }
    }
    
    /**
     * find the most recent snapshot 
     * in the snapshot directory
     * @return
     * @throws IOException
     */
    private File findMostRecentSnapshot() throws IOException {
        List<File> files = Util.sortDataDir(snapShotDir.listFiles(),
                "snapshot", false);
        for (File f: files) {
            try {
                if (Util.isValidSnapshot(f))
                    return f;
            } catch(IOException e) {
                LOG.info("Invalid snapshot " + f, e);
            }
        }
        return null;
    }
    
    /**
     * convert the old stat to new stat
     * @param oldStat the old stat
     * @return the new stat
     */
    private StatPersisted convertStat(StatPersistedV1 oldStat) {
        StatPersisted stat = new StatPersisted();
        stat.setAversion(oldStat.getAversion());
        stat.setCtime(oldStat.getCtime());
        stat.setCversion(oldStat.getCversion());
        stat.setCzxid(oldStat.getCzxid());
        stat.setEphemeralOwner(oldStat.getEphemeralOwner());
        stat.setMtime(oldStat.getMtime());
        stat.setMzxid(oldStat.getMzxid());
        stat.setVersion(oldStat.getVersion());
        return stat;
    }
    
    /**
     * convert a given old datanode to new datanode
     * @param dt the new datatree
     * @param parent the parent of the datanode to be constructed
     * @param oldDataNode the old datanode 
     * @return the new datanode
     */
    private DataNode convertDataNode(DataTree dt, DataNode parent, 
            DataNodeV1 oldDataNode) {
        StatPersisted stat = convertStat(oldDataNode.stat);
        DataNode dataNode =  new DataNode(parent, oldDataNode.data,
                dt.getACL(oldDataNode), stat);
        dataNode.setChildren(oldDataNode.children);
        return dataNode;
    }
    
    /**
     * recurse through the old datatree and construct the 
     * new data tree
     * @param dataTree the new datatree to be constructed
     * @param path the path to start with
     */
    private void recurseThroughDataTree(DataTree dataTree, String path) {
        if (path == null)
            return;
        DataNodeV1 oldDataNode = oldDataTree.getNode(path);
        HashSet<String> children = oldDataNode.children;
        DataNode parent = null;
        if ("".equals(path)) {
            parent = null;
        }
        else {
            int lastSlash = path.lastIndexOf('/');
            String parentPath = path.substring(0, lastSlash);
            parent = dataTree.getNode(parentPath);
        }
        DataNode thisDatNode = convertDataNode(dataTree, parent,
                                    oldDataNode);
        dataTree.addDataNode(path, thisDatNode);
        if (children == null || children.size() == 0) {
            return;
        }
        else {
            for (String child: children) {
                recurseThroughDataTree(dataTree, path + "/" +child);
            }
        }
    }   
    
    private DataTree convertThisSnapShot() throws IOException {
        // create a datatree 
        DataTree dataTree = new DataTree();
        DataNodeV1 oldDataNode = oldDataTree.getNode("");
        if (oldDataNode == null) {
            //should never happen
            LOG.error("Upgrading from an empty snapshot.");
        }
        
        recurseThroughDataTree(dataTree, "");
        dataTree.lastProcessedZxid = oldDataTree.lastProcessedZxid;
        return dataTree;
    }
    
    public DataTree getNewDataTree() throws IOException {
        loadThisSnapShot();
        DataTree dt = convertThisSnapShot();
        return dt;
    }
    
    public ConcurrentHashMap<Long, Integer> getSessionWithTimeOuts() {
        return this.sessionsWithTimeouts;
    }
}
