/*
 * Copyright 2002-2015 SCOOP Software GmbH
 *
 * 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.copperengine.core.wfrepo;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.copperengine.core.CopperRuntimeException;
import org.copperengine.core.Workflow;
import org.copperengine.core.WorkflowDescription;
import org.copperengine.core.WorkflowFactory;
import org.copperengine.core.WorkflowVersion;
import org.copperengine.core.common.WorkflowRepository;
import org.copperengine.core.instrument.ClassInfo;
import org.copperengine.core.instrument.ScottyClassAdapter;
import org.copperengine.core.instrument.Transformed;
import org.copperengine.core.instrument.TryCatchBlockHandler;
import org.copperengine.management.FileBasedWorkflowRepositoryMXBean;
import org.copperengine.management.model.WorkflowClassInfo;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.util.CheckClassAdapter;
import org.objectweb.asm.util.TraceClassVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class AbstractWorkflowRepository implements WorkflowRepository, FileBasedWorkflowRepositoryMXBean {

    private static final Logger logger = LoggerFactory.getLogger(AbstractWorkflowRepository.class);

    private static final int flags = ClassReader.EXPAND_FRAMES;

    protected static final class VolatileState {
        public final Map<String, Class<?>> wfClassMap;
        public final Map<String, Class<?>> wfMapLatest;
        public final Map<String, Class<?>> wfMapVersioned;
        public final Map<String, List<WorkflowVersion>> wfVersions;
        public final Map<String, String> javaSources;
        public final Map<String, ClassInfo> classInfoMap;
        public final Map<String, WorkflowClassInfo> workflowClassInfoMap;
        public final ClassLoader classLoader;
        public final long checksum;

        public VolatileState(Map<String, Class<?>> wfMap, Map<String, Class<?>> wfMapVersioned, Map<String, List<WorkflowVersion>> wfVersions, ClassLoader classLoader, long checksum, Map<String, Class<?>> wfClassMap, Map<String, String> javaSources, Map<String, ClassInfo> classInfoMap, Map<String, WorkflowClassInfo> workflowClassInfoMap) {
            this.wfMapLatest = wfMap;
            this.wfMapVersioned = wfMapVersioned;
            this.classLoader = classLoader;
            this.checksum = checksum;
            this.wfVersions = wfVersions;
            this.wfClassMap = wfClassMap;
            this.javaSources = javaSources;
            this.classInfoMap = classInfoMap;
            this.workflowClassInfoMap = workflowClassInfoMap;
        }
    }

    @Override
    public <E> WorkflowFactory<E> createWorkflowFactory(final String wfName) throws ClassNotFoundException {
        return createWorkflowFactory(wfName, null);
    }

    @Override
    public <E> WorkflowFactory<E> createWorkflowFactory(final String wfName, final WorkflowVersion version) throws ClassNotFoundException {
        if (wfName == null)
            throw new NullPointerException();

        final VolatileState volatileState = getVolatileState();
        if (version == null) {
            if (!volatileState.wfMapLatest.containsKey(wfName)) {
                throw new ClassNotFoundException("Workflow " + wfName + " not found");
            }
            return new WorkflowFactory<E>() {
                @SuppressWarnings("unchecked")
                @Override
                public Workflow<E> newInstance() throws InstantiationException, IllegalAccessException {
                    return (Workflow<E>) volatileState.wfMapLatest.get(wfName).newInstance();
                }
            };
        }

        final String alias = createAliasName(wfName, version);
        if (!volatileState.wfMapVersioned.containsKey(alias)) {
            throw new ClassNotFoundException("Workflow " + wfName + " with version " + version + " not found");
        }
        return new WorkflowFactory<E>() {
            @SuppressWarnings("unchecked")
            @Override
            public Workflow<E> newInstance() throws InstantiationException, IllegalAccessException {
                return (Workflow<E>) volatileState.wfMapVersioned.get(alias).newInstance();
            }
        };
    }

    @Override
    public java.lang.Class<?> resolveClass(String classname) throws java.io.IOException, ClassNotFoundException {
        final VolatileState volatileState = getVolatileState();
        return Class.forName(classname, false, volatileState.classLoader);
    }

    @Override
    public WorkflowVersion findLatestMajorVersion(String wfName, long majorVersion) {
        final VolatileState volatileState = getVolatileState();
        final List<WorkflowVersion> versionsList = volatileState.wfVersions.get(wfName);
        if (versionsList == null)
            return null;

        WorkflowVersion rv = null;
        for (WorkflowVersion v : versionsList) {
            if (v.getMajorVersion() > majorVersion) {
                break;
            }
            rv = v;
        }
        return rv;
    }

    @Override
    public WorkflowVersion findLatestMinorVersion(String wfName, long majorVersion, long minorVersion) {
        final VolatileState volatileState = getVolatileState();
        final List<WorkflowVersion> versionsList = volatileState.wfVersions.get(wfName);
        if (versionsList == null)
            return null;

        WorkflowVersion rv = null;
        for (WorkflowVersion v : versionsList) {
            if ((v.getMajorVersion() > majorVersion) || (v.getMajorVersion() == majorVersion && v.getMinorVersion() > minorVersion)) {
                break;
            }
            rv = v;
        }
        return rv;
    }

    @Override
    public ClassInfo getClassInfo(@SuppressWarnings("rawtypes") Class<? extends Workflow> wfClazz) {
        return getVolatileState().classInfoMap.get(wfClazz.getCanonicalName().replace(".", "/"));
    }

    @Override
    public List<WorkflowClassInfo> getWorkflows() {
        return new ArrayList<>(getVolatileState().workflowClassInfoMap.values());
    }

    @Override
    public WorkflowClassInfo[] queryWorkflowsSubset(int max, int offset) {
        WorkflowClassInfo wfInfo[] = getVolatileState().workflowClassInfoMap.values().toArray(new WorkflowClassInfo[0]);
        int available = wfInfo.length - offset;
        if (available > max && max > 0) {
            available = max;
        }
        WorkflowClassInfo subset[]= new WorkflowClassInfo[available];
        int counter = 0;
        for (int i = offset; i < (available + offset); i++) {
            subset[counter] = wfInfo[i];
            counter++;
        }
        return subset;
    }

    @Override
    public int getWorkflowRepoSize() {
        return getVolatileState().workflowClassInfoMap.values().size();
    }

    protected static Map<String, WorkflowClassInfo> createWorkflowClassInfoMap(final Map<String, Class<?>> wfClassMap, final Map<String, String> javaSources) {
        final Map<String, WorkflowClassInfo> map = new HashMap<>();
        for (Class<?> wfClass : wfClassMap.values()) {
            WorkflowClassInfo wfi = new WorkflowClassInfo();
            wfi.setClassname(wfClass.getName());
            wfi.setSourceCode(javaSources.get(wfClass.getName()));
            if (wfi.getSourceCode() == null)
                wfi.setSourceCode("NA");
            WorkflowDescription wfDesc = wfClass.getAnnotation(WorkflowDescription.class);
            if (wfDesc != null) {
                wfi.setAlias(wfDesc.alias());
                wfi.setMajorVersion(wfDesc.majorVersion());
                wfi.setMinorVersion(wfDesc.minorVersion());
                wfi.setPatchLevel(wfDesc.patchLevelVersion());
            }
            map.put(wfClass.getName(), wfi);
        }
        return map;
    }

    protected void instrumentWorkflows(File adaptedTargetDir, Map<String, Clazz> clazzMap, Map<String, ClassInfo> classInfos, ClassLoader tmpClassLoader) throws IOException {
        logger.info("Instrumenting classfiles");
        for (Clazz clazz : clazzMap.values()) {
            byte[] bytes;
            InputStream is = clazz.classfile.openStream();
            try {
                ClassReader cr2 = new ClassReader(is);
                ClassNode cn = new ClassNode();
                cr2.accept(cn, flags);
                traceClassNode(clazz.classname + " - original", cn);

                // Now content of ClassNode can be modified and then serialized back into bytecode:
                new TryCatchBlockHandler().instrument(cn);

                ClassWriter cw2 = new ClassWriter(0);
                cn.accept(cw2);
                bytes = cw2.toByteArray();
                traceBytes(clazz.classname + " - after TryCatchBlockHandler", bytes);

                ClassReader cr = new ClassReader(bytes);
                ClassWriter cw = new ClassWriter(0);

                ScottyClassAdapter cv = new ScottyClassAdapter(cw, clazz.aggregatedInterruptableMethods);
                cr.accept(cv, flags);
                classInfos.put(clazz.classname, cv.getClassInfo());
                bytes = cw.toByteArray();
                traceBytes(clazz.classname + " - after ScottyClassAdapter", bytes);

                // Recompute frames, etc.
                ClassReader cr3 = new ClassReader(bytes);
                ClassWriter cw3 = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
                cr3.accept(cw3, ClassReader.SKIP_FRAMES);
                bytes = cw3.toByteArray();
                traceBytes(clazz.classname + " - after COMPUTE_FRAMES", bytes);

                StringWriter sw = new StringWriter();
                PrintWriter pw = new PrintWriter(sw);
                CheckClassAdapter.verify(new ClassReader(cw.toByteArray()), tmpClassLoader, false, pw);
                if (sw.toString().length() != 0) {
                    logger.error("CheckClassAdapter.verify failed for class " + cn.name + ":\n" + sw.toString());
                } else {
                    logger.info("CheckClassAdapter.verify succeeded for class " + cn.name);
                }

            } finally {
                is.close();
            }

            File adaptedClassfileName = new File(adaptedTargetDir, clazz.classname + ".class");
            adaptedClassfileName.getParentFile().mkdirs();
            FileOutputStream fos = new FileOutputStream(adaptedClassfileName);
            try {
                fos.write(bytes);
            } finally {
                fos.close();
            }
        }
    }

    private static void traceClassNode(String message, ClassNode cn) {
        if (logger.isTraceEnabled()) {
            ClassWriter cw = new ClassWriter(0);
            cn.accept(cw);
            traceBytes(message, cw.toByteArray());
        }
    }

    private static void traceBytes(String message, byte[] bytes) {
        if (logger.isTraceEnabled()) {
            StringWriter sw = new StringWriter();
            new ClassReader(bytes).accept(new TraceClassVisitor(new PrintWriter(sw)), 0);
            logger.trace(message + ":\n{}", sw.toString());
        }
    }

    protected ClassLoader createClassLoader(Map<String, Class<?>> map, File adaptedTargetDir, File compileTargetDir, Map<String, Clazz> clazzMap) throws MalformedURLException, ClassNotFoundException {
        logger.info("Creating classes");
        final Map<String, Clazz> clazzMapCopy = new HashMap<String, Clazz>(clazzMap);
        URLClassLoader classLoader = new URLClassLoader(new URL[] { adaptedTargetDir.toURI().toURL(), compileTargetDir.toURI().toURL() }, Thread.currentThread().getContextClassLoader()) {
            @Override
            protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    try {
                        c = super.findClass(name);
                        if (clazzMapCopy.containsKey(name)) {
                            // Check that the workflow class is transformed
                            if (c.getAnnotation(Transformed.class) == null)
                                throw new ClassFormatError("Copper workflow " + name + " is not transformed!");
                        }
                        logger.info(c.getName() + " created");
                    } catch (Exception e) {
                        c = super.loadClass(name, false);
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        };

        for (Clazz clazz : clazzMap.values()) {
            String name = clazz.classname.replace('/', '.');
            Class<?> c = classLoader.loadClass(name);
            map.put(name, c);
        }

        return classLoader;
    }

    protected void checkConstraints(Map<String, Class<?>> workflowClasses) throws CopperRuntimeException {
        for (Class<?> c : workflowClasses.values()) {
            if (c.getName().length() > 512) {
                throw new CopperRuntimeException("Workflow class names are limited to 512 characters");
            }
        }
    }

    protected String createAliasName(final String alias, final WorkflowVersion version) {
        return alias + "#" + version.format();
    }

    protected abstract VolatileState getVolatileState();

    @Override
    public WorkflowClassInfo getWorkflowInfo(String classname) {
        return getVolatileState().workflowClassInfoMap.get(classname);
    }
}