/*
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed 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.drools.android;

import com.android.dex.DexFormat;
import com.android.dx.cf.direct.DirectClassFile;
import com.android.dx.cf.direct.StdAttributeFactory;
import com.android.dx.dex.DexOptions;
import com.android.dx.dex.cf.CfOptions;
import com.android.dx.dex.cf.CfTranslator;
import com.android.dx.dex.file.DexFile;
import dalvik.system.BaseDexClassLoader;
import dalvik.system.DexClassLoader;
import org.drools.core.util.ByteArrayClassLoader;
import org.mvel2.util.MVELClassLoader;
import org.robolectric.annotation.internal.Instrument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.security.ProtectionDomain;

import static org.drools.android.DroolsAndroidContext.*;

/**
 * Maintains separate dex files for each defined class.
 */
@Instrument
public class MultiDexClassLoader extends DexClassLoader implements ByteArrayClassLoader, MVELClassLoader {
    private static final Logger log = LoggerFactory.getLogger(MultiDexClassLoader.class);

    private String dexPath="";
    final DexOptions dex_options = new DexOptions();
    final CfOptions cf_options = new CfOptions();
    private static Field pathListField;
    private static Class dexPathListClazz;
    private static Constructor dexPathListConstructor;

    static {
        try {
            dexPathListClazz = Class.forName("dalvik.system.DexPathList");
            dexPathListConstructor = dexPathListClazz.getConstructor(ClassLoader.class, String.class, String.class, File.class);
            pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
            pathListField.setAccessible(true);
        } catch (Exception e) {
            log.error("Reflection error", e);
        }
    }

    public MultiDexClassLoader(ClassLoader parent) {
        super(new File(getDexDir(), "temp.dex").getAbsolutePath(),
                getOptimizedDir().getAbsolutePath(),
                getContext().getApplicationInfo().nativeLibraryDir,
                parent!=null ? parent : getContext().getClassLoader());
        dex_options.targetApiLevel = DexFormat.API_NO_EXTENDED_OPCODES;
        cf_options.optimize = true;
    }

    /**
     * Convert class to dex
     * @param name class name
     * @param bytes   classfile bytes
     * @return  the dex file name
     */
    private String writeClass(String name, byte[] bytes) throws IOException {
        name = name.replace('.','/');
        File dexFile = new File(String.format("%s/%s.dex", getDexDir(), name));
        if(dexFile.exists() && isReuseClassFiles()) {
            if (log.isTraceEnabled())
                log.trace(String.format("Reused class [%s] from cache: %s", name, dexFile.getAbsolutePath()));
            return dexFile.getAbsolutePath();
        }
        FileOutputStream fos = null;
        try {
            //convert .class ==> .dex
            DexFile file = new DexFile(dex_options);
            DirectClassFile cf =
                    new DirectClassFile(bytes, name + ".class", cf_options.strictNameCheck);

            cf.setAttributeFactory(StdAttributeFactory.THE_ONE);
            cf.getMagic();

            file.add(CfTranslator.translate(cf, bytes, cf_options, dex_options, file));

            //write dex file to cache dir
            dexFile.getParentFile().mkdirs();
            if (dexFile.exists()) dexFile.delete();
            fos = new FileOutputStream(dexFile);
            file.writeTo(fos, null, false);
            if (log.isTraceEnabled())
                log.trace(String.format("Wrote class [%s] to cache: %s", name, dexFile.getAbsolutePath()));
            return dexFile.getAbsolutePath();
        } finally {
            if(fos!=null) {
                try {
                    fos.close();
                } catch (IOException e) {}
            }
        }
    }

    public Class defineClass(String name, byte[] bytes) {
        try {
            String path = writeClass(name, bytes);
            dexPath += (dexPath.isEmpty() ? "" : ":") + path;
            log.trace("New Dexpath: " + dexPath);
            pathListField.set(this, dexPathListConstructor.newInstance(this,
                    path,
                    getContext().getApplicationInfo().nativeLibraryDir,
                    getOptimizedDir()));
            return findClass(name);
        } catch (Exception e) {
            log.error("Error", e);
            throw new RuntimeException(e);
        }
    }

    @Override
    public Class< ? > defineClass(final String name,
                                  final byte[] bytes,
                                  final ProtectionDomain domain) {
        return defineClass(name, bytes);
    }

    @Override
    public Class defineClassX(String className, byte[] b, int start, int len) {
        byte[] newBytes = new byte[len];
        System.arraycopy(b, start, newBytes, 0, len);
        return defineClass(className, newBytes);
    }

    @Override
    public String toString() {
        try {
            return getClass().getName() + "[" + pathListField.get(this) + "]";
        } catch (IllegalAccessException e) {
            return super.toString();
        }
    }
}
