/*******************************************************************************
 * Copyright (c) 2008, 2009 Heiko Seeberger and others.
 *
 * This program and the accompanying materials 
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0.
 * 
 * Contributors:
 *     Heiko Seeberger - initial implementation
 *     Martin Lippert - asynchronous cache writing
 *     Martin Lippert - caching of generated classes
 *******************************************************************************/

package org.eclipse.equinox.weaving.internal.caching;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.Map;
import java.util.concurrent.BlockingQueue;

import org.eclipse.equinox.service.weaving.CacheEntry;
import org.eclipse.equinox.service.weaving.ICachingService;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;

/**
 * <p>
 * {@link ICachingService} instantiated by {@link CachingServiceFactory} for
 * each bundle.
 * </p>
 * <p>
 * 
 * @author Heiko Seeberger
 * @author Martin Lippert
 */
public class BundleCachingService implements ICachingService {

    private static final int READ_BUFFER_SIZE = 8 * 1024;

    private final Bundle bundle;

    private File cacheDirectory;

    private final String cacheKey;

    private final BlockingQueue<CacheItem> cacheWriterQueue;

    /**
     * @param bundleContext Must not be null!
     * @param bundle Must not be null!
     * @param key Must not be null!
     * @param cacheWriterQueue The queue for items to be written to the cache,
     *            must not be null
     * @throws IllegalArgumentException if given bundleContext or bundle is
     *             null.
     */
    public BundleCachingService(final BundleContext bundleContext,
            final Bundle bundle, final String key,
            final BlockingQueue<CacheItem> cacheWriterQueue) {

        if (bundleContext == null) {
            throw new IllegalArgumentException(
                    "Argument \"bundleContext\" must not be null!"); //$NON-NLS-1$
        }
        if (bundle == null) {
            throw new IllegalArgumentException(
                    "Argument \"bundle\" must not be null!"); //$NON-NLS-1$
        }
        if (key == null) {
            throw new IllegalArgumentException(
                    "Argument \"key\" must not be null!"); //$NON-NLS-1$
        }

        this.bundle = bundle;
        this.cacheKey = hashNamespace(key);
        this.cacheWriterQueue = cacheWriterQueue;

        final File dataFile = bundleContext.getDataFile(cacheKey);
        if (dataFile != null) {
            final String bundleCacheDir = bundle.getBundleId()
                    + "-" + bundle.getLastModified(); //$NON-NLS-1$
            cacheDirectory = new File(dataFile, bundleCacheDir);
        } else {
            Log.error("Cannot initialize cache!", null); //$NON-NLS-1$
        }
    }

    /**
     * @see org.eclipse.equinox.service.weaving.ICachingService#canCacheGeneratedClasses()
     */
    public boolean canCacheGeneratedClasses() {
        return true;
    }

    /**
     * @see org.eclipse.equinox.service.weaving.ICachingService#findStoredClass(java.lang.String,
     *      java.net.URL, java.lang.String)
     */
    public CacheEntry findStoredClass(final String namespace,
            final URL sourceFileURL, final String name) {

        if (name == null) {
            throw new IllegalArgumentException(
                    "Argument \"name\" must not be null!"); //$NON-NLS-1$
        }

        byte[] storedClass = null;
        boolean isCached = false;

        if (cacheDirectory != null) {
            final File cachedBytecodeFile = new File(cacheDirectory, name);
            storedClass = read(name, cachedBytecodeFile);
            isCached = storedClass != null;
        }

        if (Log.isDebugEnabled()) {
            Log.debug(MessageFormat.format("for [{0}]: {1} {2}", bundle //$NON-NLS-1$
                    .getSymbolicName(), ((storedClass != null) ? "Found" //$NON-NLS-1$
                    : "Found NOT"), name)); //$NON-NLS-1$
        }
        return new CacheEntry(isCached, storedClass);
    }

    /**
     * Writes the remaining cache to disk.
     */
    public void stop() {
    }

    /**
     * @see org.eclipse.equinox.service.weaving.ICachingService#storeClass(java.lang.String,
     *      java.net.URL, java.lang.Class, byte[])
     */
    public boolean storeClass(final String namespace, final URL sourceFileURL,
            final Class<?> clazz, final byte[] classbytes) {
        if (clazz == null) {
            throw new IllegalArgumentException(
                    "Argument \"clazz\" must not be null!"); //$NON-NLS-1$
        }
        if (classbytes == null) {
            throw new IllegalArgumentException(
                    "Argument \"classbytes\" must not be null!"); //$NON-NLS-1$
        }
        if (cacheDirectory == null) {
            return false;
        }

        final CacheItem item = new CacheItem(classbytes, cacheDirectory
                .getAbsolutePath(), clazz.getName());

        return this.cacheWriterQueue.offer(item);
    }

    /**
     * @see org.eclipse.equinox.service.weaving.ICachingService#storeClassAndGeneratedClasses(java.lang.String,
     *      java.net.URL, java.lang.Class, byte[], java.util.Map)
     */
    public boolean storeClassAndGeneratedClasses(final String namespace,
            final URL sourceFileUrl, final Class<?> clazz,
            final byte[] classbytes, final Map<String, byte[]> generatedClasses) {

        final CacheItem item = new CacheItem(classbytes, cacheDirectory
                .getAbsolutePath(), clazz.getName(), generatedClasses);

        return this.cacheWriterQueue.offer(item);
    }

    /**
     * Hash the shared class namespace using MD5
     * 
     * @param keyToHash
     * @return the MD5 version of the input string
     */
    private String hashNamespace(final String namespace) {
        MessageDigest md = null;
        try {
            md = MessageDigest.getInstance("MD5"); //$NON-NLS-1$
        } catch (final NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        final byte[] bytes = md.digest(namespace.getBytes());
        final StringBuffer result = new StringBuffer();
        for (final byte b : bytes) {
            int num;
            if (b < 0) {
                num = b + 256;
            } else {
                num = b;
            }
            String s = Integer.toHexString(num);
            while (s.length() < 2) {
                s = "0" + s; //$NON-NLS-1$
            }
            result.append(s);
        }
        return new String(result);
    }

    private byte[] read(final String name, final File file) {
        int length = (int) file.length();

        InputStream in = null;
        try {
            byte[] classbytes = new byte[length];
            int bytesread = 0;
            int readcount;

            in = new FileInputStream(file);

            if (length > 0) {
                classbytes = new byte[length];
                for (; bytesread < length; bytesread += readcount) {
                    readcount = in.read(classbytes, bytesread, length
                            - bytesread);
                    if (readcount <= 0) /* if we didn't read anything */
                    break; /* leave the loop */
                }
            } else /* BundleEntry does not know its own length! */{
                length = READ_BUFFER_SIZE;
                classbytes = new byte[length];
                readloop: while (true) {
                    for (; bytesread < length; bytesread += readcount) {
                        readcount = in.read(classbytes, bytesread, length
                                - bytesread);
                        if (readcount <= 0) /* if we didn't read anything */
                        break readloop; /* leave the loop */
                    }
                    final byte[] oldbytes = classbytes;
                    length += READ_BUFFER_SIZE;
                    classbytes = new byte[length];
                    System.arraycopy(oldbytes, 0, classbytes, 0, bytesread);
                }
            }
            if (classbytes.length > bytesread) {
                final byte[] oldbytes = classbytes;
                classbytes = new byte[bytesread];
                System.arraycopy(oldbytes, 0, classbytes, 0, bytesread);
            }
            return classbytes;
        } catch (final IOException e) {
            Log.debug(MessageFormat.format(
                    "for [{0}]: Cannot read [1] from cache!", bundle //$NON-NLS-1$
                            .getSymbolicName(), name));
            return null;
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (final IOException e) {
                    Log.error(MessageFormat.format(
                            "for [{0}]: Cannot close cache file for [1]!", //$NON-NLS-1$
                            bundle.getSymbolicName(), name), e);
                }
            }
        }
    }

}
