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

import static com.google.common.base.Preconditions.checkNotNull;
import static org.apache.rya.accumulo.AccumuloRdfConstants.ALL_AUTHORIZATIONS;
import static org.apache.rya.api.RdfCloudTripleStoreConstants.EMPTY_TEXT;
import static org.apache.rya.api.RdfCloudTripleStoreConstants.INFO_NAMESPACE_TXT;
import static org.apache.rya.api.RdfCloudTripleStoreConstants.MAX_MEMORY;
import static org.apache.rya.api.RdfCloudTripleStoreConstants.MAX_TIME;
import static org.apache.rya.api.RdfCloudTripleStoreConstants.NUM_THREADS;
import static org.apache.rya.api.RdfCloudTripleStoreConstants.RTS_SUBJECT_RYA;
import static org.apache.rya.api.RdfCloudTripleStoreConstants.RTS_VERSION_PREDICATE_RYA;
import static org.apache.rya.api.RdfCloudTripleStoreConstants.VERSION_RYA;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.accumulo.core.client.AccumuloException;
import org.apache.accumulo.core.client.AccumuloSecurityException;
import org.apache.accumulo.core.client.BatchDeleter;
import org.apache.accumulo.core.client.BatchWriter;
import org.apache.accumulo.core.client.BatchWriterConfig;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.client.MultiTableBatchWriter;
import org.apache.accumulo.core.client.MutationsRejectedException;
import org.apache.accumulo.core.client.Scanner;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.client.admin.TableOperations;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Mutation;
import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.io.Text;
import org.apache.rya.accumulo.experimental.AccumuloIndexer;
import org.apache.rya.accumulo.query.AccumuloRyaQueryEngine;
import org.apache.rya.api.RdfCloudTripleStoreConfiguration;
import org.apache.rya.api.RdfCloudTripleStoreConstants.TABLE_LAYOUT;
import org.apache.rya.api.domain.RyaStatement;
import org.apache.rya.api.domain.RyaURI;
import org.apache.rya.api.layout.TableLayoutStrategy;
import org.apache.rya.api.persist.RyaDAO;
import org.apache.rya.api.persist.RyaDAOException;
import org.apache.rya.api.persist.RyaNamespaceManager;
import org.apache.rya.api.resolver.RyaTripleContext;
import org.openrdf.model.Namespace;

import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;

import info.aduna.iteration.CloseableIteration;

public class AccumuloRyaDAO implements RyaDAO<AccumuloRdfConfiguration>, RyaNamespaceManager<AccumuloRdfConfiguration> {
    private static final Log logger = LogFactory.getLog(AccumuloRyaDAO.class);

    private final AtomicBoolean isInitialized = new AtomicBoolean();
    private final AtomicBoolean flushEachUpdate = new AtomicBoolean(true);
    private Connector connector;
    private BatchWriterConfig batchWriterConfig;

    private MultiTableBatchWriter mt_bw;

    // Do not flush these individually
    private BatchWriter bw_spo;
    private BatchWriter bw_po;
    private BatchWriter bw_osp;

    private BatchWriter bw_ns;

    private List<AccumuloIndexer> secondaryIndexers;

    private AccumuloRdfConfiguration conf = new AccumuloRdfConfiguration();
    private RyaTableMutationsFactory ryaTableMutationsFactory;
    private TableLayoutStrategy tableLayoutStrategy;
    private AccumuloRyaQueryEngine queryEngine;
    private RyaTripleContext ryaContext;

    @Override
    public boolean isInitialized() throws RyaDAOException {
        return isInitialized.get();
    }

    @Override
    public void init() throws RyaDAOException {
        if (isInitialized.get()) {
            return;
        }
        try {
            checkNotNull(conf);
            checkNotNull(connector);

            if(batchWriterConfig == null){
                batchWriterConfig = new BatchWriterConfig();
                batchWriterConfig.setMaxMemory(MAX_MEMORY);
                batchWriterConfig.setTimeout(MAX_TIME, TimeUnit.MILLISECONDS);
                batchWriterConfig.setMaxWriteThreads(NUM_THREADS);
            }

            tableLayoutStrategy = conf.getTableLayoutStrategy();
            ryaContext = RyaTripleContext.getInstance(conf);
            ryaTableMutationsFactory = new RyaTableMutationsFactory(ryaContext);

            secondaryIndexers = conf.getAdditionalIndexers();

            flushEachUpdate.set(conf.flushEachUpdate());

            final TableOperations tableOperations = connector.tableOperations();
            AccumuloRdfUtils.createTableIfNotExist(tableOperations, tableLayoutStrategy.getSpo());
            AccumuloRdfUtils.createTableIfNotExist(tableOperations, tableLayoutStrategy.getPo());
            AccumuloRdfUtils.createTableIfNotExist(tableOperations, tableLayoutStrategy.getOsp());
            AccumuloRdfUtils.createTableIfNotExist(tableOperations, tableLayoutStrategy.getNs());

            for (final AccumuloIndexer index : secondaryIndexers) {
                index.setConf(conf);
            }

            mt_bw = connector.createMultiTableBatchWriter(batchWriterConfig);

            //get the batch writers for tables
            bw_spo = mt_bw.getBatchWriter(tableLayoutStrategy.getSpo());
            bw_po = mt_bw.getBatchWriter(tableLayoutStrategy.getPo());
            bw_osp = mt_bw.getBatchWriter(tableLayoutStrategy.getOsp());

            bw_ns = mt_bw.getBatchWriter(tableLayoutStrategy.getNs());

            for (final AccumuloIndexer index : secondaryIndexers) {
               index.setConnector(connector);
               index.setMultiTableBatchWriter(mt_bw);
               index.init();
            }

            queryEngine = new AccumuloRyaQueryEngine(connector, conf);

            checkVersion();

            isInitialized.set(true);
        } catch (final Exception e) {
            throw new RyaDAOException(e);
        }
    }

    @Override
    public String getVersion() throws RyaDAOException {
        String version = null;
        final CloseableIteration<RyaStatement, RyaDAOException> versIter = queryEngine.query(new RyaStatement(RTS_SUBJECT_RYA, RTS_VERSION_PREDICATE_RYA, null), conf);
        if (versIter.hasNext()) {
            version = versIter.next().getObject().getData();
        }
        versIter.close();

        return version;
    }

    @Override
    public void add(final RyaStatement statement) throws RyaDAOException {
        commit(Iterators.singletonIterator(statement));
    }

    @Override
    public void add(final Iterator<RyaStatement> iter) throws RyaDAOException {
        commit(iter);
    }

    @Override
    public void delete(final RyaStatement stmt, final AccumuloRdfConfiguration aconf) throws RyaDAOException {
        this.delete(Iterators.singletonIterator(stmt), aconf);
    }

    @Override
    public void delete(final Iterator<RyaStatement> statements, final AccumuloRdfConfiguration conf) throws RyaDAOException {
        try {
            while (statements.hasNext()) {
                final RyaStatement stmt = statements.next();
                //query first
                final CloseableIteration<RyaStatement, RyaDAOException> query = this.queryEngine.query(stmt, conf);
                while (query.hasNext()) {
                    deleteSingleRyaStatement(query.next());
                }

                for (final AccumuloIndexer index : secondaryIndexers) {
                    index.deleteStatement(stmt);
                }
            }
            if (flushEachUpdate.get()) {
                mt_bw.flush();
            }
        } catch (final Exception e) {
            throw new RyaDAOException(e);
        }
    }

    @Override
    public void dropGraph(final AccumuloRdfConfiguration conf, final RyaURI... graphs) throws RyaDAOException {
        BatchDeleter bd_spo = null;
        BatchDeleter bd_po = null;
        BatchDeleter bd_osp = null;

        try {
            bd_spo = createBatchDeleter(tableLayoutStrategy.getSpo(), conf.getAuthorizations());
            bd_po = createBatchDeleter(tableLayoutStrategy.getPo(), conf.getAuthorizations());
            bd_osp = createBatchDeleter(tableLayoutStrategy.getOsp(), conf.getAuthorizations());

            bd_spo.setRanges(Collections.singleton(new Range()));
            bd_po.setRanges(Collections.singleton(new Range()));
            bd_osp.setRanges(Collections.singleton(new Range()));

            for (final RyaURI graph : graphs){
                bd_spo.fetchColumnFamily(new Text(graph.getData()));
                bd_po.fetchColumnFamily(new Text(graph.getData()));
                bd_osp.fetchColumnFamily(new Text(graph.getData()));
            }

            bd_spo.delete();
            bd_po.delete();
            bd_osp.delete();

            //TODO indexers do not support delete-UnsupportedOperation Exception will be thrown
//            for (AccumuloIndex index : secondaryIndexers) {
//                index.dropGraph(graphs);
//            }

        } catch (final Exception e) {
            throw new RyaDAOException(e);
        } finally {
            if (bd_spo != null) {
                bd_spo.close();
            }
            if (bd_po != null) {
                bd_po.close();
            }
            if (bd_osp != null) {
                bd_osp.close();
            }
        }

    }

    protected void deleteSingleRyaStatement(final RyaStatement stmt) throws IOException, MutationsRejectedException {
        final Map<TABLE_LAYOUT, Collection<Mutation>> map = ryaTableMutationsFactory.serializeDelete(stmt);
        bw_spo.addMutations(map.get(TABLE_LAYOUT.SPO));
        bw_po.addMutations(map.get(TABLE_LAYOUT.PO));
        bw_osp.addMutations(map.get(TABLE_LAYOUT.OSP));
    }

    protected void commit(final Iterator<RyaStatement> commitStatements) throws RyaDAOException {
        try {
            //TODO: Should have a lock here in case we are adding and committing at the same time
            while (commitStatements.hasNext()) {
                final RyaStatement stmt = commitStatements.next();

                final Map<TABLE_LAYOUT, Collection<Mutation>> mutationMap = ryaTableMutationsFactory.serialize(stmt);
                final Collection<Mutation> spo = mutationMap.get(TABLE_LAYOUT.SPO);
                final Collection<Mutation> po = mutationMap.get(TABLE_LAYOUT.PO);
                final Collection<Mutation> osp = mutationMap.get(TABLE_LAYOUT.OSP);
                bw_spo.addMutations(spo);
                bw_po.addMutations(po);
                bw_osp.addMutations(osp);

                for (final AccumuloIndexer index : secondaryIndexers) {
                    index.storeStatement(stmt);
                }
            }

            if (flushEachUpdate.get()) {
                mt_bw.flush();
            }
        } catch (final Exception e) {
            throw new RyaDAOException(e);
        }
    }

    @Override
    public void destroy() throws RyaDAOException {
        if (!isInitialized.get()) {
            return;
        }
        //TODO: write lock
        try {
            isInitialized.set(false);
            mt_bw.flush();

            mt_bw.close();
        } catch (final Exception e) {
            throw new RyaDAOException(e);
        }
        for(final AccumuloIndexer indexer : this.secondaryIndexers) {
            try {
                indexer.destroy();
            } catch(final Exception e) {
                logger.warn("Failed to destroy indexer", e);
            }
        }
    }

    @Override
    public void addNamespace(final String pfx, final String namespace) throws RyaDAOException {
        try {
            final Mutation m = new Mutation(new Text(pfx));
            m.put(INFO_NAMESPACE_TXT, EMPTY_TEXT, new Value(namespace.getBytes(StandardCharsets.UTF_8)));
            bw_ns.addMutation(m);
            if (flushEachUpdate.get()) {
                mt_bw.flush();
            }
        } catch (final Exception e) {
            throw new RyaDAOException(e);
        }
    }

    @Override
    public String getNamespace(final String pfx) throws RyaDAOException {
        try {
            final Scanner scanner = connector.createScanner(tableLayoutStrategy.getNs(),
                    ALL_AUTHORIZATIONS);
            scanner.fetchColumn(INFO_NAMESPACE_TXT, EMPTY_TEXT);
            scanner.setRange(new Range(new Text(pfx)));
            final Iterator<Map.Entry<Key, Value>> iterator = scanner
                    .iterator();

            if (iterator.hasNext()) {
                return new String(iterator.next().getValue().get(), StandardCharsets.UTF_8);
            }
        } catch (final Exception e) {
            throw new RyaDAOException(e);
        }
        return null;
    }

    @Override
    public void removeNamespace(final String pfx) throws RyaDAOException {
        try {
            final Mutation del = new Mutation(new Text(pfx));
            del.putDelete(INFO_NAMESPACE_TXT, EMPTY_TEXT);
            bw_ns.addMutation(del);
            if (flushEachUpdate.get()) {
                mt_bw.flush();
            }
        } catch (final Exception e) {
            throw new RyaDAOException(e);
        }
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    @Override
    public CloseableIteration<Namespace, RyaDAOException> iterateNamespace() throws RyaDAOException {
        try {
            final Scanner scanner = connector.createScanner(tableLayoutStrategy.getNs(),
                    ALL_AUTHORIZATIONS);
            scanner.fetchColumnFamily(INFO_NAMESPACE_TXT);
            final Iterator<Map.Entry<Key, Value>> result = scanner.iterator();
            return new AccumuloNamespaceTableIterator(result);
        } catch (final Exception e) {
            throw new RyaDAOException(e);
        }
    }

    @Override
    public RyaNamespaceManager<AccumuloRdfConfiguration> getNamespaceManager() {
        return this;
    }

    @Override
    public void purge(final RdfCloudTripleStoreConfiguration configuration) {
        for (final String tableName : getTables()) {
            try {
                purge(tableName, configuration.getAuths());
                compact(tableName);
            } catch (final TableNotFoundException e) {
                logger.error(e.getMessage());
            } catch (final MutationsRejectedException e) {
                logger.error(e.getMessage());
            }
        }
        for(final AccumuloIndexer indexer : this.secondaryIndexers) {
            try {
                indexer.purge(configuration);
            } catch(final Exception e) {
                logger.error("Failed to purge indexer", e);
            }
        }
    }

    @Override
    public void dropAndDestroy() throws RyaDAOException {
        for (final String tableName : getTables()) {
            try {
                if (tableName != null) {
                    drop(tableName);
                }
            } catch (final AccumuloSecurityException e) {
                logger.error(e.getMessage());
                throw new RyaDAOException(e);
            } catch (final AccumuloException e) {
                logger.error(e.getMessage());
                throw new RyaDAOException(e);
            } catch (final TableNotFoundException e) {
                logger.warn(e.getMessage());
            }
        }
        destroy();
        for(final AccumuloIndexer indexer : this.secondaryIndexers) {
            try {
                indexer.dropAndDestroy();
            } catch(final Exception e) {
                logger.error("Failed to drop and destroy indexer", e);
            }
        }
    }

    public synchronized Connector getConnector() {
        return connector;
    }

    public synchronized void setConnector(final Connector connector) {
        this.connector = connector;
    }

    public BatchWriterConfig getBatchWriterConfig(){
        return batchWriterConfig;
    }

    public void setBatchWriterConfig(final BatchWriterConfig batchWriterConfig) {
        this.batchWriterConfig = batchWriterConfig;
    }

    protected MultiTableBatchWriter getMultiTableBatchWriter(){
        return mt_bw;
    }

    @Override
    public synchronized AccumuloRdfConfiguration getConf() {
        return conf;
    }

    @Override
    public synchronized void setConf(final AccumuloRdfConfiguration conf) {
        this.conf = conf;
    }

    public RyaTableMutationsFactory getRyaTableMutationsFactory() {
        return ryaTableMutationsFactory;
    }

    public void setRyaTableMutationsFactory(final RyaTableMutationsFactory ryaTableMutationsFactory) {
        this.ryaTableMutationsFactory = ryaTableMutationsFactory;
    }

    @Override
    public AccumuloRyaQueryEngine getQueryEngine() {
        return queryEngine;
    }

    public void setQueryEngine(final AccumuloRyaQueryEngine queryEngine) {
        this.queryEngine = queryEngine;
    }

    @Override
    public void flush() throws RyaDAOException {
        try {
            mt_bw.flush();
            flushIndexers();
        } catch (final MutationsRejectedException e) {
            throw new RyaDAOException(e);
        }
    }

    private void flushIndexers() throws RyaDAOException {
        for (final AccumuloIndexer indexer : secondaryIndexers) {
            try {
                indexer.flush();
            } catch (final IOException e) {
                logger.error("Error flushing data in indexer: " + indexer.getClass().getSimpleName(), e);
            }
        }
    }

    protected String[] getTables() {
        // core tables
        final List<String> tableNames = Lists.newArrayList(
                tableLayoutStrategy.getSpo(),
                tableLayoutStrategy.getPo(),
                tableLayoutStrategy.getOsp(),
                tableLayoutStrategy.getNs(),
                tableLayoutStrategy.getEval());

        // Additional Tables
        for (final AccumuloIndexer index : secondaryIndexers) {
            tableNames.add(index.getTableName());
        }

        return tableNames.toArray(new String[]{});
    }

    private void purge(final String tableName, final String[] auths) throws TableNotFoundException, MutationsRejectedException {
        if (tableExists(tableName)) {
            logger.info("Purging accumulo table: " + tableName);
            final BatchDeleter batchDeleter = createBatchDeleter(tableName, new Authorizations(auths));
            try {
                batchDeleter.setRanges(Collections.singleton(new Range()));
                batchDeleter.delete();
            } finally {
                batchDeleter.close();
            }
        }
    }

    private void compact(final String tableName) {
        logger.info("Requesting major compaction for table " + tableName);
        try {
            connector.tableOperations().compact(tableName, null, null, true, false);
        } catch (final Exception e) {
            logger.error(e.getMessage());
        }
    }

    private boolean tableExists(final String tableName) {
        return getConnector().tableOperations().exists(tableName);
    }

    private BatchDeleter createBatchDeleter(final String tableName, final Authorizations authorizations) throws TableNotFoundException {
        return connector.createBatchDeleter(tableName, authorizations, NUM_THREADS, MAX_MEMORY, MAX_TIME, NUM_THREADS);
    }

    private void checkVersion() throws RyaDAOException, IOException, MutationsRejectedException {
        final String version = getVersion();
        if (version == null) {
            //adding to core Rya tables but not Indexes
            final Map<TABLE_LAYOUT, Collection<Mutation>> mutationMap = ryaTableMutationsFactory.serialize(getVersionRyaStatement());
            final Collection<Mutation> spo = mutationMap.get(TABLE_LAYOUT.SPO);
            final Collection<Mutation> po = mutationMap.get(TABLE_LAYOUT.PO);
            final Collection<Mutation> osp = mutationMap.get(TABLE_LAYOUT.OSP);
            bw_spo.addMutations(spo);
            bw_po.addMutations(po);
            bw_osp.addMutations(osp);
        }
        //TODO: Do a version check here
    }

    protected RyaStatement getVersionRyaStatement() {
        return new RyaStatement(RTS_SUBJECT_RYA, RTS_VERSION_PREDICATE_RYA, VERSION_RYA);
    }

    private void drop(final String tableName) throws TableNotFoundException, AccumuloException, AccumuloSecurityException {
        logger.info("Dropping cloudbase table: " + tableName);
        connector.tableOperations().delete(tableName);
    }
}
