/*
 * Copyright (c) 2006, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package nsk.share.jdi;

import java.lang.reflect.*;
import com.sun.jdi.*;
import com.sun.jdi.event.*;
import com.sun.jdi.request.*;
import nsk.share.TestBug;

/*
 * EventFilters class just contain all filter classes
 */
public class EventFilters
{
    /*
     * Class is intended for testing event filters.
     *
     * Since different request classes have identical methods for adding filters(e.g. addInstanceFilter(),addClassFilter())
     * but this classes don't have common superclass reflection is used for filter adding.
     * Subclasses should implement following methods:
     * - getMethodName(), provide filter adding method's name
     * - getParametersTypes(), provide filter adding method's parameters types
     * - getFilterParameters(), provide parameters to be passed in filter adding method
     *
     * Also to check is generated event was really filtered subclasses should implement method 'isObjectMatch(ObjectReference eventObject, ThreadReference eventThread)',
     * this method should check is event generated by given object in given thread accepted by filter.
     */
    public abstract static class DebugEventFilter
    {
        // eventObject - object which generate event
        // eventThread - thread where event was generated
        abstract public boolean isObjectMatch(ObjectReference eventObject, ThreadReference eventThread);

        // call corresponding request's method to set event filter
        public void addFilter(EventRequest request)
        throws Throwable
        {
            java.lang.reflect.Method method;

            try
            {
                method = request.getClass().getMethod(getMethodName(), getParametersTypes());
            }
            catch(Exception e)
            {
                throw new TestBug("Can't get method '" + getMethodName() + "'");
            }

            try
            {
                method.setAccessible(true);
                method.invoke(request, getFilterParameters());
            }
            catch(IllegalAccessException e)
            {
                TestBug testBug = new TestBug("Can't call method '" + getMethodName() + "'");
                testBug.initCause(e);
                throw testBug;
            }
            catch(InvocationTargetException e)
            {
                throw e.getCause();
            }
        }

        public boolean isSupported(VirtualMachine vm)
        {
            return true;
        }

        abstract protected String getMethodName();
        abstract protected Class[] getParametersTypes();
        abstract protected Object[] getFilterParameters();
    }

    /*
     * Restricts the events to those whose method is in a class whose name matches this
     * restricted regular expression. Only simple regular expressions that begin with '*' or end with '*' are supported
     */
    public static class ClassFilter
    extends DebugEventFilter
    {
        protected String classPattern;

        private String startsWithPattern;
        private String endsWithPattern;

        public ClassFilter(String classPattern)
        {
            this.classPattern = classPattern;

            if(classPattern.startsWith("*"))
                endsWithPattern = classPattern.substring(1);
            else
                if(classPattern.endsWith("*"))
                    startsWithPattern = classPattern.substring(0, classPattern.length() - 1);
        }

        public String toString()
        {
            return "ClassFilter: classes should match pattern: " + classPattern;
        }

        protected String getMethodName()
        {
            return "addClassFilter";
        }

        protected Class[] getParametersTypes()
        {
            return new Class[]{String.class};
        }

        protected Object[] getFilterParameters()
        {
            return new Object[]{classPattern};
        }

        public boolean isObjectMatch(ObjectReference eventObject, ThreadReference eventThread)
        {
            if(!isNameMatch(eventObject.referenceType().name()))
                return false;
            else
                return true;
        }

        protected boolean isNameMatch(String className)
        {
            if(startsWithPattern != null)
                return className.startsWith(startsWithPattern);
            else
                if(endsWithPattern != null)
                    return className.endsWith(endsWithPattern);
                else
                    return className.equals(classPattern);
        }
    }

    /*
     * Restricts the events to those whose method is in a class whose name doesn't matches
     * restricted regular expression. Only simple regular expressions that begin with '*' or end with '*' are supported
     */
    public static class ClassExclusionFilter
    extends ClassFilter
    {
        public ClassExclusionFilter(String classPattern)
        {
            super(classPattern);
        }

        public String toString()
        {
            return "ClassExclusionFilter: classes match follows pattern should be excluded: " + classPattern;
        }

        protected String getMethodName()
        {
            return "addClassExclusionFilter";
        }

        public boolean isObjectMatch(ObjectReference eventObject, ThreadReference eventThread)
        {
            if(isNameMatch(eventObject.referenceType().name()))
                return false;
            else
                return true;
        }
    }

    /*
     * Restricts the events to those whose method is in the given reference type or any of its subtypes
     */
    public static class ClassReferenceFilter
    extends DebugEventFilter
    {
        private Class<?> filterClass;
        private ReferenceType referenceType;

        public ClassReferenceFilter(ReferenceType referenceType)
        {
            this.referenceType = referenceType;
            filterClass = findClass(referenceType);
        }

        public String toString()
        {
            return "ClassReferenceFilter: expect only " + filterClass.getName() + " and its subclasses";
        }

        protected String getMethodName()
        {
            return "addClassFilter";
        }

        protected Class[] getParametersTypes()
        {
            return new Class[]{ReferenceType.class};
        }

        protected Object[] getFilterParameters()
        {
            return new Object[]{referenceType};
        }

        public boolean isObjectMatch(ObjectReference eventObject, ThreadReference eventThread)
        {
            Class<?> eventObjectClass = findClass(eventObject.referenceType());

            if(!filterClass.isAssignableFrom(eventObjectClass))
                return false;
            else
                return true;
        }

        // find class represented by given referenceType
        private Class<?> findClass(ReferenceType referenceType)
        {
            try
            {
                return Class.forName(referenceType.name());
            }
            catch(ClassNotFoundException e)
            {
                throw new TestBug("Can't find class: " + referenceType.name());
            }
        }
    }

    /*
     * Restricts the events to those in which the currently executing instance ("this") is the given object
     */
    public static class ObjectReferenceFilter
    extends DebugEventFilter
    {
        private ObjectReference objectReference;

        public ObjectReferenceFilter(ObjectReference objectReference)
        {
            this.objectReference = objectReference;
        }

        public String toString()
        {
            return "ObjectReferenceFilter: expect only object " + objectReference;
        }

        protected String getMethodName()
        {
            return "addInstanceFilter";
        }

        protected Class[] getParametersTypes()
        {
            return new Class[]{ObjectReference.class};
        }

        protected Object[] getFilterParameters()
        {
            return new Object[]{objectReference};
        }

        public boolean isObjectMatch(ObjectReference eventObject, ThreadReference eventThread)
        {
            return objectReference.equals(eventObject);
        }

        public boolean isSupported(VirtualMachine vm)
        {
            return vm.canUseInstanceFilters();
        }
    }

    /*
     * Restricts the events to those in the given thread
     */
    public static class ThreadFilter
    extends DebugEventFilter
    {
        private ThreadReference threadReference;

        public ThreadFilter(ThreadReference threadReference)
        {
            this.threadReference = threadReference;
        }

        public String toString()
        {
            return "ThreadReferenceFilter: expect only thread " + threadReference;
        }

        protected String getMethodName()
        {
            return "addThreadFilter";
        }

        protected Class[] getParametersTypes()
        {
            return new Class[]{ThreadReference.class};
        }

        protected Object[] getFilterParameters()
        {
            return new Object[]{threadReference};
        }

        public boolean isObjectMatch(ObjectReference eventObject, ThreadReference eventThread)
        {
            return threadReference.equals(eventThread);
        }
    }

    public static boolean filtered(Event event) {
        if (event.toString().contains("VM JFR Buffer Thread"))
            return true;

        if (event.toString().contains("JFR request timer"))
            return true;

        return false;
    }

    // Filters out events with location not matching the given type.
    public static boolean filtered(Event event, String typeName) {
        if (event instanceof Locatable) {
            Location location = ((Locatable) event).location();
            if (location != null) {
                ReferenceType declaringType = location.declaringType();
                if (declaringType != null && typeName.equals(declaringType.name())) {
                    return false;
                }
            }
        }
        return true;
    }
}
