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

import org.apache.directory.mavibot.btree.BTree;
import org.apache.directory.mavibot.btree.BTreeFactory;
import org.apache.directory.mavibot.btree.BTreeTypeEnum;
import org.apache.directory.mavibot.btree.KeyCursor;
import org.apache.directory.mavibot.btree.PersistedBTreeConfiguration;
import org.apache.directory.mavibot.btree.RecordManager;
import org.apache.directory.mavibot.btree.Tuple;
import org.apache.directory.mavibot.btree.exception.KeyNotFoundException;
import org.apache.directory.mavibot.btree.serializer.StringSerializer;
import org.apache.kerby.kerberos.kerb.KrbException;
import org.apache.kerby.kerberos.kerb.identity.backend.AbstractIdentityBackend;
import org.apache.kerby.kerberos.kerb.request.KrbIdentity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * A backend based on Apache Mavibot(an MVCC BTree library).
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class MavibotBackend extends AbstractIdentityBackend {
    //Name of the database
    private static final String DATA_TREE = "kerby-data";
    // Name of the database file
    private static final String DATABASE_NAME = "kerby-data.db";
    private static final Logger LOG = LoggerFactory.getLogger(MavibotBackend.class);
    //The RecordManager of Mavibot
    private RecordManager rm;
    //The BTree holding all data
    private BTree<String, KrbIdentity> database;
    
    /**
     * Creates a new instance of MavibotBackend.
     *
     * @param location
     *            the File handle pointing to the database file or the directory
     *            where it is present
     * @throws Exception e
     */
    public MavibotBackend(File location) throws Exception {
        String dbPath = location.getAbsolutePath();

        LOG.info("Initializing the mavibot backend");
        
        if (!location.exists() && !location.mkdirs()) {
            throw new KrbException("Can't create location file");
        }

        if (location.isDirectory()) {
            dbPath += File.separator + DATABASE_NAME;
        }

        rm = new RecordManager(dbPath);

        if (rm.getManagedTrees().contains(DATA_TREE)) {
            database = rm.getManagedTree(DATA_TREE);
        } else {
            PersistedBTreeConfiguration<String, KrbIdentity> config =
                    new PersistedBTreeConfiguration<>();
            // _no_ duplicates
            config.setAllowDuplicates(false);
            config.setBtreeType(BTreeTypeEnum.PERSISTED);
            config.setFilePath(dbPath);
            config.setKeySerializer(StringSerializer.INSTANCE);
            config.setName(DATA_TREE);
            config.setValueSerializer(KrbIdentitySerializer.INSTANCE);

            database = BTreeFactory.createPersistedBTree(config);
            rm.manage(database);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected Iterable<String> doGetIdentities() throws KrbException {
        List<String> keys = new ArrayList<>();
        KeyCursor<String> cursor = null;

        try {
            cursor = database.browseKeys();
            while (cursor.hasNext()) {
                keys.add(cursor.next());
            }
        } catch (Exception e) {
            throw new KrbException("Errors occurred while fetching the principals", e);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        return keys;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected KrbIdentity doGetIdentity(String principalName) throws KrbException {
        try {
            return database.get(principalName);
        } catch (KeyNotFoundException e) {
            LOG.debug("Identity {} doesn't exist", principalName);
            return null;
        } catch (IOException e) {
            throw new KrbException("Failed to get the identity " + principalName);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected synchronized KrbIdentity doAddIdentity(KrbIdentity identity) throws KrbException {
        String p = identity.getPrincipalName();
        try {
            if (database.hasKey(p)) {
                throw new KrbException("Identity already exists " + p);
            }
            
            return database.insert(p, identity);
        } catch (KeyNotFoundException e) {
            throw new KrbException("No such identity exists " + p);
        } catch (IOException e) {
            throw new KrbException("Failed to add the identity " + p);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected synchronized KrbIdentity doUpdateIdentity(KrbIdentity identity) throws KrbException {
        String p = identity.getPrincipalName();
        try {
            if (!database.hasKey(p)) {
                throw new KrbException("No identity found with the principal " + p);
            }
            
            database.delete(p);
            
            return database.insert(p, identity);
        } catch (Exception e) {
            throw new KrbException("Failed to update the identity " + p);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void doDeleteIdentity(String principalName) throws KrbException {
        try {
            Tuple<String, KrbIdentity> t = database.delete(principalName);
            if (t == null) {
                throw new KrbException("Not existing, identity = " + principalName);
            }
        } catch (IOException e) {
            throw new KrbException("Failed to delete the identity " + principalName);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void doStop() throws KrbException {
        try {
            rm.close();
        } catch (IOException e) {
            throw new KrbException("Failed to close the database", e);
        }
    }
}
