/*
 * 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.clerezza.rdf.storage.externalizer;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.apache.clerezza.rdf.core.Graph;
import org.apache.clerezza.rdf.core.MGraph;
import org.apache.clerezza.rdf.core.TripleCollection;
import org.apache.clerezza.rdf.core.UriRef;
import org.apache.clerezza.rdf.core.access.EntityAlreadyExistsException;
import org.apache.clerezza.rdf.core.access.EntityUndeletableException;
import org.apache.clerezza.rdf.core.access.NoSuchEntityException;
import org.apache.clerezza.rdf.core.access.TcManager;
import org.apache.clerezza.rdf.core.access.TcProviderMultiplexer;
import org.apache.clerezza.rdf.core.access.WeightedTcProvider;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A {@link org.apache.clerezza.rdf.core.access.WeightedTcProvider} storing
 * large literals to the file system and the triples (with references
 * instead of the literals) to another TcProvider.
 * <br/>
 * As this provider is not currently included in any clerezza launcher, the way
 * to install it is as follows:<br/>
 *
 * - start clerezza<br/>
 * - install the bundle and set its startlevel to 3<br/>
 * - shutdown clerezza<br/>
 * - delete the MGraphs that shall use the externalizer from the felix cache<br/>
 * - start clerezza again<br/>
 *
 * @author reto
 *
 */
@Component
@Service(WeightedTcProvider.class)
@Property(name = "weight", intValue = 500)
@Reference(name="weightedTcProvider", policy=ReferencePolicy.DYNAMIC,
        referenceInterface=WeightedTcProvider.class,
        cardinality=ReferenceCardinality.MANDATORY_MULTIPLE)
public class ExternalizingProvider implements WeightedTcProvider {

    private static final String EXTERNALIZEDLITERALS_SUFFIX = "-externalizedliterals";

    //@Reference(policy=ReferencePolicy.DYNAMIC, cardinality=ReferenceCardinality.OPTIONAL_UNARY)
    private TcProviderMultiplexer tcProvider = new TcProviderMultiplexer();
    /**
     *    directory where all graphs are stored
     */
    private static final String RELATIVE_DATA_PATH_NAME = "externalized-literals/";
    private File dataPath;
    private static final Logger log = LoggerFactory.getLogger(ExternalizingProvider.class);
    private int weight = 500;

    public ExternalizingProvider() {
    }

    ExternalizingProvider(File directory) {
        dataPath = directory;
        tcProvider = TcManager.getInstance();
    }

    protected void activate(ComponentContext cCtx) {
        log.info("Activating literal externalizing provider");
        if (cCtx != null) {
            weight = (Integer) cCtx.getProperties().get("weight");
            dataPath = cCtx.getBundleContext().getDataFile(RELATIVE_DATA_PATH_NAME);
        }
    }

    protected void deactivate(ComponentContext cCtx) {
        dataPath = null;
    }

    @Override
    public int getWeight() {
        return weight;
    }

    /**
     * we don't do graphs
     * 
     * @param name
     * @return
     * @throws NoSuchEntityException
     */
    @Override
    public Graph getGraph(UriRef name) throws NoSuchEntityException {
        throw new NoSuchEntityException(name);
    }

    @Override
    public synchronized MGraph getMGraph(UriRef name) throws NoSuchEntityException {
        if (name.getUnicodeString().endsWith(EXTERNALIZEDLITERALS_SUFFIX)) {
            throw new IllegalArgumentException();
        }
        try {
            final UriRef baseGraphName = new UriRef(name.getUnicodeString() + EXTERNALIZEDLITERALS_SUFFIX);
            if (tcProvider == null) {
                throw new RuntimeException("MGraph retrieval currently not possible: tcManager unavailable");
            }
            final MGraph baseGraph = AccessController.doPrivileged(new PrivilegedExceptionAction<MGraph>() {

                @Override
                public MGraph run() {
                    return tcProvider.getMGraph(baseGraphName);
                }
            });
            return new ExternalizingMGraph(baseGraph, getHashStoreDir(name));
        } catch (PrivilegedActionException ex) {
            Throwable cause = ex.getCause();
            if (cause instanceof UnsupportedOperationException) {
                throw (UnsupportedOperationException)cause;
            }
            if (cause instanceof EntityAlreadyExistsException) {
                throw (EntityAlreadyExistsException)cause;
            }
            if (cause instanceof RuntimeException) {
                throw (RuntimeException)cause;
            }
            throw new RuntimeException(cause);
        }
    }

    @Override
    public TripleCollection getTriples(UriRef name) throws NoSuchEntityException {
        return getMGraph(name);

    }

    @Override
    public synchronized MGraph createMGraph(UriRef name)
            throws UnsupportedOperationException, EntityAlreadyExistsException {
        try {
            if (name.getUnicodeString().endsWith(EXTERNALIZEDLITERALS_SUFFIX)) {
                throw new IllegalArgumentException();
            }
            final UriRef baseGraphName = new UriRef(name.getUnicodeString() + EXTERNALIZEDLITERALS_SUFFIX);
            if (tcProvider == null) {
                throw new RuntimeException("MGraph creation currently not possible: tcManager unavailable");
            }
            final MGraph baseGraph = AccessController.doPrivileged(new PrivilegedExceptionAction<MGraph>() {

                @Override
                public MGraph run() {
                    return tcProvider.createMGraph(baseGraphName);
                }
            });
            File hashStoreDir = getHashStoreDir(name);
            hashStoreDir.mkdirs();
            return new ExternalizingMGraph(baseGraph, hashStoreDir);
        } catch (PrivilegedActionException ex) {
            Throwable cause = ex.getCause();
            if (cause instanceof UnsupportedOperationException) {
                throw (UnsupportedOperationException)cause;
            }
            if (cause instanceof EntityAlreadyExistsException) {
                throw (EntityAlreadyExistsException)cause;
            }
            if (cause instanceof RuntimeException) {
                throw (RuntimeException)cause;
            }
            throw new RuntimeException(cause);
        }
    }

    @Override
    public Graph createGraph(UriRef name, TripleCollection triples)
            throws UnsupportedOperationException, EntityAlreadyExistsException {
        throw new UnsupportedOperationException();
    }

    @Override
    public void deleteTripleCollection(UriRef name)
            throws UnsupportedOperationException, NoSuchEntityException,
            EntityUndeletableException {
        try {
            final UriRef baseGraphName = new UriRef(name.getUnicodeString() + EXTERNALIZEDLITERALS_SUFFIX);
            AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {

                @Override
                public Object run() {
                    tcProvider.deleteTripleCollection(baseGraphName);
                    return null;
                }
            });
            delete(getHashStoreDir(name));
        } catch (PrivilegedActionException ex) {
            Throwable cause = ex.getCause();
            if (cause instanceof UnsupportedOperationException) {
                throw (UnsupportedOperationException)cause;
            }
            if (cause instanceof EntityAlreadyExistsException) {
                throw (EntityAlreadyExistsException)cause;
            }
            if (cause instanceof RuntimeException) {
                throw (RuntimeException)cause;
            }
            throw new RuntimeException(cause);
        }
    }

    /**
     * Cleans the content of the specified directory recursively.
     * @param dir  Abstract path denoting the directory to clean.
     */
    private static void cleanDirectory(File dir) {
        File[] files = dir.listFiles();
        if (files != null && files.length > 0) {
            for (File file : files) {
                delete(file);
            }
        }
    }

    /**
     * Deletes the specified file or directory.
     * @param file  Abstract path denoting the file or directory to clean.
     */
    protected static void delete(File file) {
        if (file.isDirectory()) {
            cleanDirectory(file);
        }
        file.delete();
    }

    @Override
    public Set<UriRef> getNames(Graph graph) {
        //this could be done more efficiently with an index, could be done with
        //a MultiBidiMap (BidiMap allowing multiple keys for the same value)
        Set<UriRef> result = new HashSet<UriRef>();
        for (UriRef name : listGraphs()) {
            if (getGraph(name).equals(graph)) {
                result.add(name);
            }
        }
        return result;
    }

    @Override
    public Set<UriRef> listTripleCollections() {
        Set<UriRef> result = new HashSet<UriRef>();
        result.addAll(listGraphs());
        result.addAll(listMGraphs());
        return result;
    }

    @Override
    public Set<UriRef> listGraphs() {
        return new HashSet<UriRef>();
    }

    @Override
    public Set<UriRef> listMGraphs() {
        Set<UriRef> result = getHashStoreUris();
        for (Iterator<UriRef> it = result.iterator(); it.hasNext();) {
            UriRef graphName = it.next();
            final UriRef baseGraphName = new UriRef(graphName.getUnicodeString() + EXTERNALIZEDLITERALS_SUFFIX);
            try {
                tcProvider.getMGraph(baseGraphName);
            } catch (NoSuchEntityException e) {
                log.warn("Store for externalized literals but no base graph found for {}.", graphName);
                it.remove();
            }

        }
        return result;
    }

    private File getHashStoreDir(UriRef name) {
        try {
            String subDirName = URLEncoder.encode(name.getUnicodeString(), "utf-8");
            File result = new File(dataPath, subDirName);
            return result;
        } catch (UnsupportedEncodingException ex) {
            throw new RuntimeException("utf-8 not supported", ex);
        }
    }

    private Set<UriRef> getHashStoreUris() {
        Set<UriRef> result = new HashSet<UriRef>();
        if (dataPath.exists()) {
            for (String mGraphDirName : dataPath.list()) {
                try {
                    UriRef uri = new UriRef(URLDecoder.decode(mGraphDirName, "utf-8"));
                    result.add(uri);
                } catch (UnsupportedEncodingException ex) {
                    throw new RuntimeException("utf-8 not supported", ex);
                }
            }
        }
        return result;
    }

    /**
     * Register a provider
     *
     * @param provider
     *            the provider to be registered
     */
    protected void bindWeightedTcProvider(WeightedTcProvider provider) {
        tcProvider.addWeightedTcProvider(provider);
    }

    /**
     * Deregister a provider
     *
     * @param provider
     *            the provider to be deregistered
     */
    protected void unbindWeightedTcProvider(WeightedTcProvider provider) {
        tcProvider.removeWeightedTcProvider(provider);
    }
}
