package mindustry.android;

import android.annotation.*;
import android.os.*;
import android.os.Build.*;
import arc.*;
import arc.backend.android.*;
import com.android.dex.*;
import com.android.dx.cf.direct.*;
import com.android.dx.command.dexer.*;
import com.android.dx.dex.*;
import com.android.dx.dex.cf.*;
import com.android.dx.dex.file.DexFile;
import com.android.dx.merge.*;
import dalvik.system.*;
import rhino.*;

import java.io.*;
import java.nio.*;

/**
 * Helps to prepare a Rhino Context for usage on android.
 * @author F43nd1r
 * @since 11.01.2016
 */
public class AndroidRhinoContext{

    /**
     * call this instead of {@link Context#enter()}
     * @return a context prepared for android
     */
    public static Context enter(File cacheDirectory){
        if(!SecurityController.hasGlobal())
            SecurityController.initGlobal(new SecurityController(){
                @Override
                public GeneratedClassLoader createClassLoader(ClassLoader classLoader, Object o){
                    return Context.getCurrentContext().createClassLoader(classLoader);
                }

                @Override
                public Object getDynamicSecurityDomain(Object o){
                    return null;
                }
            });

        AndroidContextFactory factory;
        if(!ContextFactory.hasExplicitGlobal()){
            factory = new AndroidContextFactory(cacheDirectory);
            ContextFactory.getGlobalSetter().setContextFactoryGlobal(factory);
        }else if(!(ContextFactory.getGlobal() instanceof AndroidContextFactory)){
            throw new IllegalStateException("Cannot initialize factory for Android Rhino: There is already another factory");
        }else{
            factory = (AndroidContextFactory)ContextFactory.getGlobal();
        }

        return factory.enterContext();
    }

    /**
     * Ensures that the classLoader used is correct
     * @author F43nd1r
     * @since 11.01.2016
     */
    public static class AndroidContextFactory extends ContextFactory{
        private final File cacheDirectory;

        /**
         * Create a new factory. It will cache generated code in the given directory
         * @param cacheDirectory the cache directory
         */
        public AndroidContextFactory(File cacheDirectory){
            this.cacheDirectory = cacheDirectory;
            initApplicationClassLoader(createClassLoader(AndroidContextFactory.class.getClassLoader()));
        }

        /**
         * Create a ClassLoader which is able to deal with bytecode
         * @param parent the parent of the create classloader
         * @return a new ClassLoader
         */
        @Override
        public BaseAndroidClassLoader createClassLoader(ClassLoader parent){
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
                return new InMemoryAndroidClassLoader(parent);
            }
            return new FileAndroidClassLoader(parent, cacheDirectory);
        }

        @Override
        protected void onContextReleased(final Context cx){
            super.onContextReleased(cx);
            ((BaseAndroidClassLoader)cx.getApplicationClassLoader()).reset();
        }
    }

    /**
     * Compiles java bytecode to dex bytecode and loads it
     * @author F43nd1r
     * @since 11.01.2016
     */
    abstract static class BaseAndroidClassLoader extends ClassLoader implements GeneratedClassLoader{

        public BaseAndroidClassLoader(ClassLoader parent){
            super(parent);
        }

        @Override
        public Class<?> defineClass(String name, byte[] data){
            try{
                DexOptions dexOptions = new DexOptions();
                DexFile dexFile = new DexFile(dexOptions);
                DirectClassFile classFile = new DirectClassFile(data, name.replace('.', '/') + ".class", true);
                classFile.setAttributeFactory(StdAttributeFactory.THE_ONE);
                classFile.getMagic();
                DxContext context = new DxContext();
                dexFile.add(CfTranslator.translate(context, classFile, null, new CfOptions(), dexOptions, dexFile));
                Dex dex = new Dex(dexFile.toDex(null, false));
                Dex oldDex = getLastDex();
                if(oldDex != null){
                    dex = new DexMerger(new Dex[]{dex, oldDex}, CollisionPolicy.KEEP_FIRST, context).merge();
                }
                return loadClass(dex, name);
            }catch(IOException | ClassNotFoundException e){
                throw new FatalLoadingException(e);
            }
        }

        protected abstract Class<?> loadClass(Dex dex, String name) throws ClassNotFoundException;

        protected abstract Dex getLastDex();

        protected abstract void reset();

        @Override
        public void linkClass(Class<?> aClass){}

        @Override
        public Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException{
            Class<?> loadedClass = findLoadedClass(name);
            if(loadedClass == null){
                Dex dex = getLastDex();
                if(dex != null){
                    loadedClass = loadClass(dex, name);
                }
                if(loadedClass == null){
                    loadedClass = getParent().loadClass(name);
                }
            }
            return loadedClass;
        }
    }


    /** Might be thrown in any Rhino method that loads bytecode if the loading failed. */
    public static class FatalLoadingException extends RuntimeException{
        FatalLoadingException(Throwable t){
            super("Failed to define class", t);
        }
    }

    static class FileAndroidClassLoader extends BaseAndroidClassLoader{
        private static int instanceCounter = 0;
        private final File dexFile;

        public FileAndroidClassLoader(ClassLoader parent, File cacheDir){
            super(parent);
            int id = instanceCounter++;
            dexFile = new File(cacheDir, id + ".dex");
            cacheDir.mkdirs();
            reset();
        }

        @Override
        protected Class<?> loadClass(Dex dex, String name) throws ClassNotFoundException{
            try{
                dex.writeTo(dexFile);
            }catch(IOException e){
                e.printStackTrace();
            }
            android.content.Context context = ((AndroidApplication) Core.app).getContext();
            return new DexClassLoader(dexFile.getPath(), VERSION.SDK_INT >= 21 ? context.getCodeCacheDir().getPath() : context.getCacheDir().getAbsolutePath(), null, getParent()).loadClass(name);
        }

        @Override
        protected Dex getLastDex(){
            if(dexFile.exists()){
                try{
                    return new Dex(dexFile);
                }catch(IOException e){
                    e.printStackTrace();
                }
            }
            return null;
        }

        @Override
        protected void reset(){
            dexFile.delete();
        }
    }

    @TargetApi(Build.VERSION_CODES.O)
    static class InMemoryAndroidClassLoader extends BaseAndroidClassLoader{
        private Dex last;

        public InMemoryAndroidClassLoader(ClassLoader parent){
            super(parent);
        }

        @Override
        protected Class<?> loadClass(Dex dex, String name) throws ClassNotFoundException{
            last = dex;
            return new InMemoryDexClassLoader(ByteBuffer.wrap(dex.getBytes()), getParent()).loadClass(name);
        }

        @Override
        protected Dex getLastDex(){
            return last;
        }

        @Override
        protected void reset(){
            last = null;
        }
    }
}