/**
 * OpenAtlasForAndroid Project
 * The MIT License (MIT) Copyright (OpenAtlasForAndroid) 2015 Bunny Blue,achellies
 * <p>
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 * and associated documentation files (the "Software"), to deal in the Software
 * without restriction, including without limitation the rights to use, copy, modify,
 * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * <p>
 * The above copyright notice and this permission notice shall be included in all copies
 * or substantial portions of the Software.
 * <p>
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
 * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * @author BunnyBlue
 **/
package com.openatlas.framework;

import com.openatlas.framework.bundlestorage.Archive;
import com.openatlas.framework.bundlestorage.BundleArchive;
import com.openatlas.log.Logger;
import com.openatlas.log.LoggerFactory;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleException;
import org.osgi.framework.BundleListener;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
/***OSGI Bundle implementation **/
public final class BundleImpl implements Bundle {
    static final Logger log;
    Archive archive;
    final File bundleDir;
    BundleClassLoader classloader;
    private final BundleContextImpl context;
    int currentStartlevel;
    ProtectionDomain domain;
    Hashtable<String, String> headers = new Hashtable<String, String>();
    final String location;
    boolean persistently;
    List<BundleListener> registeredBundleListeners;
    List<FrameworkListener> registeredFrameworkListeners;
    List<ServiceListener> registeredServiceListeners;
    List<ServiceReference> registeredServices;
    Package[] staleExportedPackages;
    int state = 0;


    static {
        log = LoggerFactory.getInstance("BundleImpl");
    }

    BundleImpl(File bundleDir, String location, BundleContextImpl bundleContextImpl,
               InputStream archiveInputStream, File archiveFile, boolean isInstall)
            throws BundleException, IOException {
        this.persistently = false;
        this.domain = null;
        this.registeredServices = null;
        this.registeredFrameworkListeners = null;
        this.registeredBundleListeners = null;
        this.registeredServiceListeners = null;
        this.staleExportedPackages = null;
        long currentTimeMillis = System.currentTimeMillis();
        this.location = location;
        bundleContextImpl.bundle = this;
        this.context = bundleContextImpl;
        this.currentStartlevel = Framework.initStartlevel;
        this.bundleDir = bundleDir;
        if (archiveInputStream != null) {
            //  try {
            this.archive = new BundleArchive(location, bundleDir, archiveInputStream);
//            } catch (Throwable e) {
//                Framework.deleteDirectory(bundleDir);
//                throw new BundleException("Could not install bundle " + location, e);
//            }
        } else if (archiveFile != null) {
            try {
                this.archive = new BundleArchive(location, bundleDir, archiveFile);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        this.state = BundleEvent.STARTED;

        updateMetadata();
        if (isInstall) {
            Framework.bundles.put(location, this);
            resolveBundle(false);
            Framework.notifyBundleListeners(1, this);
        }

        if (Framework.DEBUG_BUNDLES && log.isInfoEnabled()) {
            log.info("Framework: Bundle " + toString() + " created. "
                    + (System.currentTimeMillis() - currentTimeMillis) + " ms");
        }
    }

    BundleImpl(File file, BundleContextImpl bundleContextImpl) throws Exception {
        long currentTimeMillis = System.currentTimeMillis();
        DataInputStream dataInputStream = new DataInputStream(new FileInputStream(new File(file, "meta")));
        this.location = dataInputStream.readUTF();
        this.currentStartlevel = dataInputStream.readInt();
        this.persistently = dataInputStream.readBoolean();
        dataInputStream.close();
        bundleContextImpl.bundle = this;
        this.context = bundleContextImpl;
        this.bundleDir = file;
        this.state = BundleEvent.STARTED;
        try {
            this.archive = new BundleArchive(this.location, file);
            resolveBundle(false);
            Framework.bundles.put(this.location, this);
            Framework.notifyBundleListeners(1, this);
            if (Framework.DEBUG_BUNDLES && log.isInfoEnabled()) {
                log.info("Framework: Bundle " + toString() + " loaded. " + (System.currentTimeMillis() - currentTimeMillis) + " ms");
            }
        } catch (Exception e) {
            throw new BundleException("Could not load bundle " + this.location, e.getCause());
        }
    }


    private synchronized void resolveBundle(boolean recursive) throws BundleException {
        if (this.state != 4) {
            if (this.classloader == null) {
                this.classloader = new BundleClassLoader(this);
            }
            if (recursive) {
                this.classloader.resolveBundle(true, new HashSet(0));
                this.state = 4;
            } else if (this.classloader.resolveBundle(false, null)) {
                this.state = 4;
            }
            Framework.notifyBundleListeners(0, this);
        }
    }

    @Override
    public long getBundleId() {
        return 0;
    }

    @Override
    public Dictionary<String, String> getHeaders() {
        return this.headers;
    }

    @Override
    public String getLocation() {
        return this.location;
    }

    public Archive getArchive() {
        return this.archive;
    }
/***
 * @return  BundleClassLoader BundleClassLoader
 * **/
    public ClassLoader getClassLoader() {
        return this.classloader;
    }

    @Override
    public ServiceReference[] getRegisteredServices() {
        if (this.state == BundleEvent.INSTALLED) {
            throw new IllegalStateException("Bundle " + toString()
                    + "has been unregistered.");
        } else if (this.registeredServices == null) {
            return null;
        } else {
            return this.registeredServices
                    .toArray(new ServiceReference[this.registeredServices
                            .size()]);
        }
    }

    @Override
    public URL getResource(String name) {
        if (this.state != BundleEvent.INSTALLED) {
            return this.classloader.getResource(name);
        }
        throw new IllegalStateException("Bundle " + toString()
                + " has been uninstalled");
    }

    @Override
    public ServiceReference[] getServicesInUse() {
        if (this.state == BundleEvent.INSTALLED) {
            throw new IllegalStateException("Bundle " + toString()
                    + "has been unregistered.");
        }
        ArrayList<ServiceReferenceImpl> arrayList = new ArrayList<ServiceReferenceImpl>();
        ServiceReferenceImpl[] serviceReferenceImplArr = Framework.services
                .toArray(new ServiceReferenceImpl[Framework.services.size()]);
        int i = 0;
        while (i < serviceReferenceImplArr.length) {
            synchronized (serviceReferenceImplArr[i].useCounters) {
                if (serviceReferenceImplArr[i].useCounters.get(this) != null) {
                    arrayList.add(serviceReferenceImplArr[i]);
                }
            }
            i++;
        }
        return arrayList
                .toArray(new ServiceReference[arrayList.size()]);
    }

    @Override
    public int getState() {
        return this.state;
    }

    @Override
    public boolean hasPermission(Object permission) {
        if (this.state != BundleEvent.INSTALLED) {
            return true;
        }
        throw new IllegalStateException("Bundle " + toString()
                + "has been unregistered.");
    }

    @Override
    public synchronized void start() throws BundleException {
        this.persistently = true;
        updateMetadata();
        if (this.currentStartlevel <= Framework.startlevel) {
            startBundle();
        }
    }

    public synchronized void startBundle() throws BundleException {
        if (this.state == BundleEvent.INSTALLED) {
            throw new IllegalStateException("Cannot start uninstalled bundle "
                    + toString());
        } else if (this.state != BundleEvent.RESOLVED) {
            if (this.state == BundleEvent.STARTED) {
                resolveBundle(true);
            }
            this.state = BundleEvent.UPDATED;
            try {

                this.context.isValid = true;
                // if (!(this.classloader.activatorClassName == null ||
                // StringUtils
                // .isBlank(this.classloader.activatorClassName))) {
                // Class<?> loadClass = this.classloader
                // .loadClass(this.classloader.activatorClassName);
                // if (loadClass == null) {
                // throw new ClassNotFoundException(
                // this.classloader.activatorClassName);
                // }
                // this.classloader.activator = (BundleActivator) loadClass
                // .newInstance();
                // this.classloader.activator.start(this.context);
                //
                // }
                this.state = BundleEvent.RESOLVED;
                Framework.notifyBundleListeners(BundleEvent.STARTED, this);
                if (Framework.DEBUG_BUNDLES && log.isInfoEnabled()) {
                    log.info("Framework: Bundle " + toString() + " started.");
                }
            } catch (Throwable th) {

                Framework.clearBundleTrace(this);
                this.state = BundleEvent.STOPPED;
                String msg = "Error starting bundle " + toString();

                BundleException bundleException = new BundleException(msg, th);
            }
        }
    }

    @Override
    public synchronized void stop() throws BundleException {
        this.persistently = false;
        updateMetadata();
        stopBundle();
    }

    public synchronized void stopBundle() throws BundleException {
        if (this.state == BundleEvent.INSTALLED) {
            throw new IllegalStateException("Cannot stop uninstalled bundle "
                    + toString());
        } else if (this.state == BundleEvent.RESOLVED) {
            this.state = BundleEvent.UNINSTALLED;
            try {
                // if (this.classloader.activator != null) {
                // this.classloader.activator.stop(this.context);
                // }
                if (Framework.DEBUG_BUNDLES && log.isInfoEnabled()) {
                    log.info("Framework: Bundle " + toString() + " stopped.");
                }
                // this.classloader.activator = null;
                Framework.clearBundleTrace(this);
                this.state = BundleEvent.STOPPED;
                Framework.notifyBundleListeners(BundleEvent.STOPPED, this);
                this.context.isValid = false;
            } catch (Throwable th) {
                // this.classloader.activator = null;
                Framework.clearBundleTrace(this);
                this.state = BundleEvent.STOPPED;
                Framework.notifyBundleListeners(BundleEvent.STOPPED, this);
                this.context.isValid = false;
            }
        }
    }

    @Override
    public synchronized void uninstall() throws BundleException {
        if (this.state == BundleEvent.INSTALLED) {
            throw new IllegalStateException("Bundle " + toString() + " is already uninstalled.");
        }
        if (this.state == BundleEvent.RESOLVED) {
            try {
                stopBundle();
            } catch (Throwable th) {
                Framework.notifyFrameworkListeners(BundleEvent.STARTED, this, th);
            }
        }
        this.state = BundleEvent.INSTALLED;
        new File(this.bundleDir, "meta").delete();
        if (this.classloader.originalExporter != null) {
            this.classloader.originalExporter.cleanup(true);
            this.classloader.originalExporter = null;
        }
        this.classloader.cleanup(true);
        this.classloader = null;
        Framework.bundles.remove(this);
        Framework.notifyBundleListeners(BundleEvent.UNINSTALLED, this);
        this.context.isValid = false;
        this.context.bundle = null;


    }

    @Override
    public synchronized void update() throws BundleException {
        String locationUpdate = this.headers.get(Constants.BUNDLE_UPDATELOCATION);
        try {

            if (locationUpdate == null) {
                locationUpdate = this.location;
            }
            update(new URL(locationUpdate).openConnection().getInputStream());
        } catch (Throwable e) {
            throw new BundleException("Could not update " + toString()
                    + " from " + locationUpdate, e);
        }
    }

    @Override
    public synchronized void update(InputStream inputStream)
            throws BundleException {
        if (this.state == BundleEvent.INSTALLED) {
            throw new IllegalStateException("Cannot update uninstalled bundle "
                    + toString());
        }
        try {
            this.archive
                    .newRevision(this.location, this.bundleDir, inputStream);
        } catch (Throwable e) {
            throw new BundleException("Could not update bundle " + toString(),
                    e);
        }
    }

    @Override
    public synchronized void update(File bundleFile) throws BundleException {
        if (this.state == BundleEvent.INSTALLED) {
            throw new IllegalStateException("Cannot update uninstalled bundle "
                    + toString());
        }
        try {
            this.archive.newRevision(this.location, this.bundleDir, bundleFile);
        } catch (Throwable e) {
            throw new BundleException("Could not update bundle " + toString(),
                    e);
        }
    }

    public synchronized void refresh() throws BundleException {
        if (this.state == BundleEvent.INSTALLED) {
            throw new IllegalStateException(
                    "Cannot refresh uninstalled bundle " + toString());
        }
       boolean isResolved=false;
        if (this.state == BundleEvent.RESOLVED) {
            stopBundle();
            isResolved=true;
        }
        try {
            this.archive = new BundleArchive(this.location, this.bundleDir);
            BundleClassLoader bundleClassLoader = new BundleClassLoader(this);
            String[] exports = this.classloader.exports;
            if (exports.length > 0) {
                int i = 0;
                boolean resetoriginalExporter=false;
                while (i < exports.length) {

                    Package packageR = Framework.exportedPackages
                            .get(new Package(exports[i], null, false));
                    if (packageR.importingBundles == null
                            || packageR.classloader != this.classloader) {

                    } else {
                        packageR.removalPending = true;

                        resetoriginalExporter=true;
                    }
                    i++;

                }
                if (resetoriginalExporter) {
                    if (this.classloader.originalExporter != null) {
                        bundleClassLoader.originalExporter = this.classloader.originalExporter;
                    } else {
                        bundleClassLoader.originalExporter = this.classloader;
                    }
                }
            }
            this.classloader.cleanup(true);
            this.classloader = bundleClassLoader;
            if (this.classloader.resolveBundle(false, null)) {
                this.state = BundleEvent.STOPPED;
            } else {
                this.state = BundleEvent.STARTED;
            }
            Framework.notifyBundleListeners(BundleEvent.UPDATED, this);
            if (isResolved) {
                startBundle();
            }
        } catch (BundleException e) {
            throw e;
        } catch (Throwable e) {
            throw new BundleException("Could not refresh bundle " + toString(), e);
        }
    }

    public synchronized void optDexFile() {
        getArchive().optDexFile();
    }

    public synchronized void purge() throws BundleException {
        try {
            getArchive().purge();
        } catch (Throwable e) {
            throw new BundleException("Could not purge bundle " + toString(), e);
        }
    }

    void updateMetadata() {
        File file = new File(this.bundleDir, "meta");
        DataOutputStream dataOutputStream = null;
        try {
            if (!file.getParentFile().exists()) {
                file.getParentFile().mkdirs();
            }
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            dataOutputStream = new DataOutputStream(fileOutputStream);
            dataOutputStream.writeUTF(this.location);
            dataOutputStream.writeInt(this.currentStartlevel);
            dataOutputStream.writeBoolean(this.persistently);
            dataOutputStream.flush();
            fileOutputStream.getFD().sync();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (dataOutputStream != null) {
                try {
                    dataOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    @Override
    public String toString() {
        return this.location;
    }


}
