package org.apache.bookkeeper.meta;

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

import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.bookkeeper.replication.ReplicationException;
import org.apache.bookkeeper.conf.AbstractConfiguration;
import org.apache.bookkeeper.util.ReflectionUtils;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;

public abstract class LedgerManagerFactory {

    static final Logger LOG = LoggerFactory.getLogger(LedgerManagerFactory.class);
    // v1 layout
    static final int V1 = 1;

    /**
     * Return current factory version.
     *
     * @return current version used by factory.
     */
    public abstract int getCurrentVersion();

    /**
     * Initialize a factory.
     *
     * @param conf
     *          Configuration object used to initialize factory
     * @param zk
     *          Available zookeeper handle for ledger manager to use.
     * @param factoryVersion
     *          What version used to initialize factory.
     * @return ledger manager factory instance
     * @throws IOException when fail to initialize the factory.
     */
    public abstract LedgerManagerFactory initialize(final AbstractConfiguration conf,
                                                    final ZooKeeper zk,
                                                    final int factoryVersion)
    throws IOException;

    /**
     * Uninitialize the factory.
     *
     * @throws IOException when fail to uninitialize the factory.
     */
    public abstract void uninitialize() throws IOException;

    /**
     * Return the ledger id generator, which is used for global unique ledger id
     * generation.
     *
     * @return ledger id generator.
     */
    public abstract LedgerIdGenerator newLedgerIdGenerator();

    /**
     * return ledger manager for client-side to manage ledger metadata.
     *
     * @return ledger manager
     * @see LedgerManager
     */
    public abstract LedgerManager newLedgerManager();

    /**
     * Return a ledger underreplication manager, which is used to
     * mark ledgers as unreplicated, and to retrieve a ledger which
     * is underreplicated so that it can be rereplicated.
     *
     * @return ledger underreplication manager
     * @see LedgerUnderreplicationManager
     */
    public abstract LedgerUnderreplicationManager newLedgerUnderreplicationManager()
            throws KeeperException, InterruptedException, ReplicationException.CompatibilityException;

    /**
     * Create new Ledger Manager Factory.
     *
     * @param conf
     *          Configuration Object.
     * @param zk
     *          ZooKeeper Client Handle, talk to zk to know which ledger manager is used.
     * @return new ledger manager factory
     * @throws IOException
     */
    public static LedgerManagerFactory newLedgerManagerFactory(
        final AbstractConfiguration conf, final ZooKeeper zk)
            throws IOException, KeeperException, InterruptedException {
        Class<? extends LedgerManagerFactory> factoryClass;
        try {
            factoryClass = conf.getLedgerManagerFactoryClass();
        } catch (Exception e) {
            throw new IOException("Failed to get ledger manager factory class from configuration : ", e);
        }
        String ledgerRootPath = conf.getZkLedgersRootPath();

        if (null == ledgerRootPath || ledgerRootPath.length() == 0) {
            throw new IOException("Empty Ledger Root Path.");
        }

        // if zk is null, return the default ledger manager
        if (zk == null) {
            return new FlatLedgerManagerFactory()
                   .initialize(conf, null, FlatLedgerManagerFactory.CUR_VERSION);
        }

        LedgerManagerFactory lmFactory;

        // check that the configured ledger manager is
        // compatible with the existing layout
        LedgerLayout layout = LedgerLayout.readLayout(zk, ledgerRootPath);
        if (layout == null) { // no existing layout
            lmFactory = createNewLMFactory(conf, zk, factoryClass);
            return lmFactory
                    .initialize(conf, zk, lmFactory.getCurrentVersion());
        }
        LOG.debug("read ledger layout {}", layout);

        // there is existing layout, we need to look into the layout.
        // handle pre V2 layout
        if (layout.getLayoutFormatVersion() <= V1) {
            // pre V2 layout we use type of ledger manager
            @SuppressWarnings("deprecation")
            String lmType = conf.getLedgerManagerType();
            if (lmType != null && !layout.getManagerFactoryClass().equals(lmType)) {
                throw new IOException("Configured layout " + lmType
                        + " does not match existing layout "  + layout.getManagerFactoryClass());
            }

            // create the ledger manager
            if (FlatLedgerManagerFactory.NAME.equals(layout.getManagerFactoryClass())) {
                lmFactory = new FlatLedgerManagerFactory();
            } else if (HierarchicalLedgerManagerFactory.NAME.equals(layout.getManagerFactoryClass())) {
                lmFactory = new HierarchicalLedgerManagerFactory();
            } else {
                throw new IOException("Unknown ledger manager type: " + lmType);
            }
            return lmFactory.initialize(conf, zk, layout.getManagerVersion());
        }

        // handle V2 layout case
        if (factoryClass != null &&
            !layout.getManagerFactoryClass().equals(factoryClass.getName())) {

            throw new IOException("Configured layout " + factoryClass.getName()
                                + " does not match existing layout "  + layout.getManagerFactoryClass());
        }
        if (factoryClass == null) {
            // no factory specified in configuration
            try {
                Class<?> theCls = Class.forName(layout.getManagerFactoryClass());
                if (!LedgerManagerFactory.class.isAssignableFrom(theCls)) {
                    throw new IOException("Wrong ledger manager factory " + layout.getManagerFactoryClass());
                }
                factoryClass = theCls.asSubclass(LedgerManagerFactory.class);
            } catch (ClassNotFoundException cnfe) {
                throw new IOException("Failed to instantiate ledger manager factory " + layout.getManagerFactoryClass());
            }
        }
        // instantiate a factory
        lmFactory = ReflectionUtils.newInstance(factoryClass);
        return lmFactory.initialize(conf, zk, layout.getManagerVersion());
    }

    /**
     * Creates the new layout and stores in zookeeper and returns the
     * LedgerManagerFactory instance.
     */
    private static LedgerManagerFactory createNewLMFactory(
            final AbstractConfiguration conf, final ZooKeeper zk,
            Class<? extends LedgerManagerFactory> factoryClass)
            throws IOException, KeeperException, InterruptedException {

        String ledgerRootPath = conf.getZkLedgersRootPath();
        LedgerManagerFactory lmFactory;
        LedgerLayout layout;
        // use default ledger manager factory if no one provided
        if (factoryClass == null) {
            // for backward compatibility, check manager type
            @SuppressWarnings("deprecation")
            String lmType = conf.getLedgerManagerType();
            if (lmType == null) {
                factoryClass = FlatLedgerManagerFactory.class;
            } else {
                if (FlatLedgerManagerFactory.NAME.equals(lmType)) {
                    factoryClass = FlatLedgerManagerFactory.class;
                } else if (HierarchicalLedgerManagerFactory.NAME.equals(lmType)) {
                    factoryClass = HierarchicalLedgerManagerFactory.class;
                } else {
                    throw new IOException("Unknown ledger manager type: "
                            + lmType);
                }
            }
        }

        lmFactory = ReflectionUtils.newInstance(factoryClass);

        layout = new LedgerLayout(factoryClass.getName(),
                lmFactory.getCurrentVersion());
        try {
            layout.store(zk, ledgerRootPath);
        } catch (KeeperException.NodeExistsException nee) {
            LedgerLayout layout2 = LedgerLayout.readLayout(zk, ledgerRootPath);
            if (!layout2.equals(layout)) {
                throw new IOException(
                        "Contention writing to layout to zookeeper, "
                                + " other layout " + layout2
                                + " is incompatible with our " + "layout "
                                + layout);
            }
        }
        return lmFactory;
    }

    /**
     * Format the ledger metadata for LedgerManager
     * 
     * @param conf
     *            Configuration instance
     * @param zk
     *            Zookeeper instance
     */
    public void format(final AbstractConfiguration conf, final ZooKeeper zk)
            throws InterruptedException, KeeperException, IOException {
        
        Class<? extends LedgerManagerFactory> factoryClass;
        try {
            factoryClass = conf.getLedgerManagerFactoryClass();
        } catch (ConfigurationException e) {
            throw new IOException("Failed to get ledger manager factory class from configuration : ", e);
        }
       
        LedgerLayout layout = LedgerLayout.readLayout(zk,
                conf.getZkLedgersRootPath());
        layout.delete(zk, conf.getZkLedgersRootPath());
        // Create new layout information again.        
        createNewLMFactory(conf, zk, factoryClass);
    }
}
