/*
 * 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.netbeans.spi.debugger;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.netbeans.debugger.registry.ContextAwareServiceHandler;
import org.netbeans.debugger.registry.ContextAwareServicePath;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.RequestProcessor;

/**
 * Represents implementation of one or more actions.
 *
 * @author   Jan Jancura
 */
public abstract class ActionsProvider {

    private static final RequestProcessor debuggerActionsRP = new RequestProcessor("Debugger Actions", 5);

    /**
     * Returns set of actions supported by this ActionsProvider.
     *
     * @return set of actions supported by this ActionsProvider
     */
    public abstract Set getActions ();

    /**
     * Called when the action is called (action button is pressed).
     *
     * @param action an action which has been called
     */
    public abstract void doAction (Object action);
    
    /**
     * Should return a state of given action.
     *
     * @param action action
     */
    public abstract boolean isEnabled (Object action);
    
    /**
     * Adds property change listener.
     *
     * @param l new listener.
     */
    public abstract void addActionsProviderListener (ActionsProviderListener l);
    

    /**
     * Removes property change listener.
     *
     * @param l removed listener.
     */
    public abstract void removeActionsProviderListener (ActionsProviderListener l);
    
    /**
     * Post the action and let it process asynchronously.
     * The default implementation just delegates to {@link #doAction}
     * in a separate thread and returns immediately.
     *
     * @param action The action to post
     * @param actionPerformedNotifier run this notifier after the action is
     *        done.
     * @since 1.5
     */
    public void postAction (final Object action,
                            final Runnable actionPerformedNotifier) {
        debuggerActionsRP.post(new Runnable() {
            @Override
            public void run() {
                try {
                    doAction(action);
                } finally {
                    actionPerformedNotifier.run();
                }
            }
        });
    }

    /**
     * Declarative registration of an ActionsProvider implementation.
     * By marking the implementation class with this annotation,
     * you automatically register that implementation for use by debugger.
     * The class must be public and have a public constructor which takes
     * no arguments or takes {@link ContextProvider} as an argument.
     * @since 1.16
     */
    @Retention(RetentionPolicy.SOURCE)
    @Target({ElementType.TYPE})
    public @interface Registration {
        /**
         * An optional path to register this implementation in.
         * Usually the session ID.
         */
        String path() default "";

        /**
         * Provide the list of actions that this provider supports.
         * This list is used before an instance of the registered class is created,
         * it's necessary when {@link #activateForMIMETypes()} is overriden
         * to prevent from the class instantiation.
         * @return The list of actions.
         * @since 1.23
         */
        String[] actions() default {};

        /**
         * Provide a list of MIME types that are compared to the MIME type of
         * a file currently active in the IDE and when matched, this provider
         * is activated (an instance of the registered class is created).
         * By default, the provider instance is created immediately.
         * This method can be used to delay the instantiation of the
         * implementation class for performance reasons.
         * @return The list of MIME types
         * @since 1.23
         */
        String[] activateForMIMETypes() default {};

    }

    /**
     * Allows registration of multiple {@link Registration} annotations.
     * @since 1.28
     */
    @Retention(RetentionPolicy.SOURCE)
    @Target({ElementType.TYPE})
    public @interface Registrations {
        Registration[] value();
    }

    static class ContextAware extends ActionsProvider implements ContextAwareService<ActionsProvider>,
                                                                 ContextAwareServicePath {

        private static final String ERROR = "error in getting MIMEType";    // NOI18N

        private String path;        // The file path, that declares this service, or null if not known.
        private String serviceName;
        private ContextProvider context;
        private ActionsProvider delegate;

        private Set actions;
        private Set<String> enabledOnMIMETypes;
        private List<ActionsProviderListener> listeners = new ArrayList<ActionsProviderListener>();
        private PropertyChangeListener contextDispatcherListener;

        private ContextAware(String serviceName, Set actions, Set<String> enabledOnMIMETypes) {
            this.serviceName = serviceName;
            this.actions = actions;
            this.enabledOnMIMETypes = enabledOnMIMETypes;
        }

        private ContextAware(String serviceName, Set actions, Set<String> enabledOnMIMETypes,
                             ContextProvider context) {
            this.serviceName = serviceName;
            this.actions = actions;
            this.enabledOnMIMETypes = enabledOnMIMETypes;
            this.context = context;
        }

        private synchronized ActionsProvider getDelegate() {
            if (delegate == null) {
                delegate = (ActionsProvider) ContextAwareSupport.createInstance(serviceName, context);
                if (delegate == null) {
                    throw new IllegalStateException("No instance created for service "+serviceName+", context = "+context+", path = "+path);
                }
                for (ActionsProviderListener l : listeners) {
                    delegate.addActionsProviderListener(l);
                }
                listeners.clear();
                if (contextDispatcherListener != null) {
                    detachContextDispatcherListener();
                }
            }
            return delegate;
        }

        @Override
        public String getServicePath() {
            return path;
        }
        
        @Override
        public Set getActions() {
            ActionsProvider actionsDelegate;
            if (actions != null) {
                synchronized (this) {
                    actionsDelegate = this.delegate;
                }
            } else {
                actionsDelegate = getDelegate();
            }
            if (actionsDelegate == null) {
                return actions;
            }
            return actionsDelegate.getActions();
        }

        @Override
        public void doAction(Object action) {
            getDelegate().doAction(action);
        }

        @Override
        public void postAction(Object action, Runnable actionPerformedNotifier) {
            getDelegate().postAction(action, actionPerformedNotifier);
        }

        @Override
        public boolean isEnabled(Object action) {
            ActionsProvider actionsDelegate;
            if (enabledOnMIMETypes != null) {
                synchronized (this) {
                    actionsDelegate = this.delegate;
                }
            } else {
                actionsDelegate = getDelegate();
            }
            if (actionsDelegate == null) {
                Boolean isEnabledMIME = isCurrentMIMETypeIn(enabledOnMIMETypes);
                if (!Boolean.TRUE.equals(isEnabledMIME)) {
                    //System.err.println("Delegate '"+serviceName+"' NOT enabled on "+currentMIMEType+", enabled MIME types = "+enabledOnMIMETypes);
                    return false;
                }
            }
            return getDelegate().isEnabled(action);
        }

        @Override
        public void addActionsProviderListener(ActionsProviderListener l) {
            ActionsProvider actionsDelegate;
            synchronized (this) {
                actionsDelegate = delegate;
                if (actionsDelegate == null) {
                    listeners.add(l);
                    if (contextDispatcherListener == null && enabledOnMIMETypes != null) {
                        contextDispatcherListener = attachContextDispatcherListener();
                    }
                    return ;
                }
            }
            actionsDelegate.addActionsProviderListener(l);
        }

        @Override
        public void removeActionsProviderListener(ActionsProviderListener l) {
            ActionsProvider actionsDelegate;
            synchronized (this) {
                actionsDelegate = delegate;
                if (actionsDelegate == null) {
                    listeners.remove(l);
                    if (listeners.isEmpty() && contextDispatcherListener != null) {
                        detachContextDispatcherListener();
                    }
                    return ;
                }
            }
            actionsDelegate.removeActionsProviderListener(l);
        }

        @Override
        public ActionsProvider forContext(ContextProvider context) {
            if (context == this.context) {
                return this;
            } else {
                ContextAware ca = new ActionsProvider.ContextAware(serviceName, actions, enabledOnMIMETypes, context);
                ca.path = path;
                return ca;
            }
        }

        /**
         * Creates instance of <code>ContextAwareService</code> based on layer.xml
         * attribute values
         *
         * @param attrs attributes loaded from layer.xml
         * @return new <code>ContextAwareService</code> instance
         */
        static ContextAwareService createService(Map attrs) throws ClassNotFoundException {
            String serviceName = (String) attrs.get(ContextAwareServiceHandler.SERVICE_NAME);
            String actionsStr = (String) attrs.get(ContextAwareServiceHandler.SERVICE_ACTIONS);
            String enabledOnMIMETypesStr = (String) attrs.get(ContextAwareServiceHandler.SERVICE_ENABLED_MIMETYPES);
            String[] actions = parseArray(actionsStr);
            String[] enabledOnMIMETypes = parseArray(enabledOnMIMETypesStr);
            String path = null;
            try {
                Field foField = attrs.getClass().getDeclaredField("fo");
                foField.setAccessible(true);
                FileObject fo = (FileObject) foField.get(attrs);
                path = fo.getPath();
            } catch (Exception ex) {}
            ContextAware ca = new ActionsProvider.ContextAware(serviceName,
                                                               createSet(actions),
                                                               createSet(enabledOnMIMETypes));
            ca.path = path;
            return ca;
        }

        private static String[] parseArray(String strArray) {
            if (strArray == null) {
                return null;
            }
            if (strArray.startsWith("[")) {
                strArray = strArray.substring(1);
            }
            if (strArray.endsWith("]")) {
                strArray = strArray.substring(0, strArray.length() - 1);
            }
            strArray = strArray.trim();
            int index = 0;
            List<String> strings = new ArrayList<String>();
            while (index < strArray.length()) {
                int index2 = strArray.indexOf(',', index);
                if (index2 < 0) {
                    index2 = strArray.length();
                }
                if (index2 > index) {
                    String s = strArray.substring(index, index2).trim();
                    if (s.length() > 0) { // Can be trimmed to 0 length
                        strings.add(s);
                    }
                    index = index2 + 1;
                } else {
                    index++;
                    continue;
                }
            }
            return strings.toArray(new String[0]);
        }

        private static <T> Set<T> createSet(T[] array) {
            if (array != null) {
                return Collections.unmodifiableSet(new HashSet(Arrays.asList(array)));
            } else {
                return null;
            }
        }

        private static Boolean isCurrentMIMETypeIn(Set<String> mimeTypes) {
            // Ask EditorContextDispatcher.getDefault().getMostRecentFile()
            // It's not in a dependent module, therefore we have to find it dynamically:
            try {
                Class editorContextDispatcherClass = Lookup.getDefault().lookup(ClassLoader.class).loadClass("org.netbeans.spi.debugger.ui.EditorContextDispatcher");
                try {
                    try {
                        Object editorContextDispatcher = editorContextDispatcherClass.getMethod("getDefault").invoke(null);
                        java.lang.reflect.Method getMIMETypesOnCurrentLineMethod = editorContextDispatcherClass.getDeclaredMethod("getMIMETypesOnCurrentLine");
                        getMIMETypesOnCurrentLineMethod.setAccessible(true);
                        Set<String> lineMIMETypes = (Set<String>) getMIMETypesOnCurrentLineMethod.invoke(editorContextDispatcher);
                        if (!lineMIMETypes.isEmpty()) {
                            return !Collections.disjoint(mimeTypes, lineMIMETypes);
                        }
                        FileObject file = (FileObject) editorContextDispatcherClass.getMethod("getMostRecentFile").invoke(editorContextDispatcher);
                        if (file != null) {
                            return mimeTypes.contains(file.getMIMEType());
                        } else {
                            return null;
                        }
                    } catch (IllegalAccessException ex) {
                        Exceptions.printStackTrace(ex);
                    } catch (IllegalArgumentException ex) {
                        Exceptions.printStackTrace(ex);
                    } catch (InvocationTargetException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                } catch (NoSuchMethodException ex) {
                    Exceptions.printStackTrace(ex);
                } catch (SecurityException ex) {
                    Exceptions.printStackTrace(ex);
                }
            } catch (ClassNotFoundException ex) {
            }
            return null;
        }

        private PropertyChangeListener attachContextDispatcherListener() {
            // Call EditorContextDispatcher.getDefault().addPropertyChangeListener(String MIMEType, PropertyChangeListener l)
            // It's not in a dependent module, therefore we have to find it dynamically:
            PropertyChangeListener l = null;
            try {
                Class editorContextDispatcherClass = Lookup.getDefault().lookup(ClassLoader.class).loadClass("org.netbeans.spi.debugger.ui.EditorContextDispatcher");
                try {
                    try {
                        Object editorContextDispatcher = editorContextDispatcherClass.getMethod("getDefault").invoke(null);
                        java.lang.reflect.Method m = editorContextDispatcherClass.getMethod(
                                "addPropertyChangeListener",
                                String.class,
                                PropertyChangeListener.class);
                        l = new ContextDispatcherListener();
                        for (String mimeType : enabledOnMIMETypes) {
                            m.invoke(editorContextDispatcher, mimeType, l);
                        }
                    } catch (IllegalAccessException ex) {
                        Exceptions.printStackTrace(ex);
                    } catch (IllegalArgumentException ex) {
                        Exceptions.printStackTrace(ex);
                    } catch (InvocationTargetException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                } catch (NoSuchMethodException ex) {
                    Exceptions.printStackTrace(ex);
                } catch (SecurityException ex) {
                    Exceptions.printStackTrace(ex);
                }
            } catch (ClassNotFoundException ex) {
            }
            return l;
        }

        private void detachContextDispatcherListener() {
            // Call EditorContextDispatcher.getDefault().removePropertyChangeListener(PropertyChangeListener l)
            // It's not in a dependent module, therefore we have to find it dynamically:
            PropertyChangeListener l = null;
            try {
                Class editorContextDispatcherClass = Lookup.getDefault().lookup(ClassLoader.class).loadClass("org.netbeans.spi.debugger.ui.EditorContextDispatcher");
                try {
                    try {
                        Object editorContextDispatcher = editorContextDispatcherClass.getMethod("getDefault").invoke(null);
                        java.lang.reflect.Method m = editorContextDispatcherClass.getMethod(
                                "removePropertyChangeListener",
                                PropertyChangeListener.class);
                        m.invoke(editorContextDispatcher, contextDispatcherListener);
                        contextDispatcherListener = null;
                    } catch (IllegalAccessException ex) {
                        Exceptions.printStackTrace(ex);
                    } catch (IllegalArgumentException ex) {
                        Exceptions.printStackTrace(ex);
                    } catch (InvocationTargetException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                } catch (NoSuchMethodException ex) {
                    Exceptions.printStackTrace(ex);
                } catch (SecurityException ex) {
                    Exceptions.printStackTrace(ex);
                }
            } catch (ClassNotFoundException ex) {
            }
        }

        private class ContextDispatcherListener implements PropertyChangeListener {

            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                List<ActionsProviderListener> ls;
                synchronized (ContextAware.this) {
                    ls = new ArrayList<ActionsProviderListener>(listeners);
                }
                for (ActionsProviderListener l : ls) {
                    for (Object action : actions) {
                        l.actionStateChange(action, isEnabled(action));
                    }
                }
            }
            
        }

        @Override
        public synchronized String toString() {
            return "ActionsProvider.ContextAware for service "+serviceName+", context = "+context+", path = "+path+", delegate = "+delegate;
        }
        
    }
    
}

