/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.codehaus.groovy.classgen.asm

import groovy.test.GroovyTestCase
import org.apache.groovy.io.StringBuilderWriter
import org.codehaus.groovy.control.CompilationUnit
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.Phases
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.FieldVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.util.TraceClassVisitor

import java.security.CodeSource

/**
 * Abstract test case to extend to check the instructions we generate in the bytecode of groovy programs.
 */
abstract class AbstractBytecodeTestCase extends GroovyTestCase {

    Map extractionOptions
    InstructionSequence sequence
    Class clazz

    @Override
    protected void setUp() {
        super.setUp()
        extractionOptions = [method: 'run']
    }


    protected void assertScript(final String script) throws Exception {
        GroovyShell shell = new GroovyShell()
        def unit
        shell.loader = new GroovyClassLoader() {
            @Override
            protected CompilationUnit createCompilationUnit(final CompilerConfiguration config, final CodeSource source) {
                unit = super.createCompilationUnit(config, source)

                unit
            }
        }
        try {
            shell.evaluate(script, getTestClassName());
        } finally {
            if (unit) {
                try {
                    sequence = extractSequence(unit.classes[0].bytes, extractionOptions)
                    if (extractionOptions.print) println sequence
                } catch (e) {
                    // probably an error in the script
                }
            }
        }
    }

    /**
     * Compiles a script into bytecode and returns the decompiled string equivalent using ASM.
     *
     * @param scriptText the script to compile
     * @return the decompiled <code>InstructionSequence</code>
     */
    InstructionSequence compile(Map options = [:], String scriptText) {
        options = [method: "run", classNamePattern: '.*script', *: options]
        sequence = null
        clazz = null
        def cu = new CompilationUnit()
        def su = cu.addSource("script", scriptText)
        cu.compile(Phases.CONVERSION)
        if (options.conversionAction != null) {
            options.conversionAction(su)
        }
        cu.compile(Phases.CLASS_GENERATION)

        cu.classes.each {
            if (it.name ==~ (options.classNamePattern)) {
                sequence = extractSequence(it.bytes, options)
            }
        }
        if (sequence == null && cu.classes.size() > 0) {
            sequence = extractSequence(cu.classes[0].bytes, options)
        }
        cu.classes.each {
            try {
                def dep = cu.classLoader.defineClass(it.name, it.bytes)
                if (Script.class.isAssignableFrom(dep)) {
                    clazz = dep
                }
            } catch (Throwable e) {
                System.err.println(sequence)
                e.printStackTrace()
            }
        }
        return sequence
    }

    InstructionSequence extractSequence(byte[] bytes, Map options = [method: "run"]) {
        InstructionSequence sequence
        def output = new StringBuilderWriter()
        def tcf;
        tcf = new TraceClassVisitor(new ClassVisitor(CompilerConfiguration.ASM_API_VERSION) {
            MethodVisitor visitMethod(int access, String name, String desc, String signature, String... exceptions) {
                if (options.method == name) {
                    tcf.p.text << '--BEGIN--'
                    def res = super.visitMethod(access, name, desc, signature, exceptions)
                    tcf.p.text << '--END--'
                    res
                } else {
                    null
                }
            }

            FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
                if (options.field == name) {
                    tcf.p.text << '--BEGIN--'
                    def res = super.visitField(access, name, desc, signature, value)
                    tcf.p.text << '--END--'
                    res
                } else {
                    null
                }
            }

        }, new PrintWriter(output))
        def cr = new ClassReader(bytes)
        cr.accept(tcf, 0)

        def code = output.toString()
        sequence = new InstructionSequence(instructions: code.split('\n')*.trim())
        return sequence
    }
}

/**
 * A sequence of instruction with matching and strict matching capabilities
 * to find subsequences of bytecode instructions.
 */
class InstructionSequence {
    List<String> instructions

    /**
     * Find a sub-sequence of instructions of the list of instructions.
     *
     * @param pattern the list of instructions to find in the bytecode
     * @param offset at which to find the sub-sequence or remaining sub-sequence (start at offset 0)
     * @param strict whether the search should be strict with contiguous instructions (false by default)
     * @return true if a match is found
     */
    boolean hasSequence(List<String> pattern, int offset = 0, boolean strict = false) {
        if (pattern.size() == 0) return true
        def idx = offset
        while (true) {
            idx = indexOf(pattern[0], idx)
            if (idx == -1) break
            // not the first call with offset 0 and check that the next instruction match
            // is the exact following instruction in the pattern and in the bytecode instructions
            if (strict && offset > 0 && idx != offset) return false
            if (hasSequence(pattern.tail(), idx + 1, strict)) return true
            idx++
        }
        return false
    }

    /**
     * Find a strict sub-sequence of instructions of the list of instructions.
     *
     * @param pattern the list of instructions to find in the bytecode
     * @param offset at which to find the sub-sequence or remaining sub-sequence (start at offset 0)
     * @param strict whether the search should be strict with contiguous instructions (true by default)
     * @return true if a match is found
     */
    boolean hasStrictSequence(List<String> pattern, int offset = 0, boolean strict = true) {
        hasSequence(pattern, offset, strict)
    }

    /**
     * Finds the index of a single instruction in a list of instructions
     * @param singleInst single instruction to find
     * @param offset the offset from which to start the search
     * @return the index of that single instruction if found, -1 otherwise
     */
    private int indexOf(String singleInst, int offset = 0) {
        for (i in offset..<instructions.size()) {
            if (instructions[i].startsWith(singleInst))
                return i
        }
        return -1
    }

    String toString() {
        instructions.join('\n')
    }

    String toSequence() {
        def sb = new StringBuilder()
        instructions*.trim().each {
            sb << "'${it}'," << '\n'
        }
        sb.toString()
    }
}
