/*
 * 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.modules.debugger.jpda;

import com.sun.jdi.AbsentInformationException;
import com.sun.jdi.Location;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.StackFrame;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.modules.debugger.jpda.jdi.InternalExceptionWrapper;
import org.netbeans.modules.debugger.jpda.jdi.InvalidStackFrameExceptionWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ObjectCollectedExceptionWrapper;
import org.netbeans.spi.debugger.ContextProvider;

import org.netbeans.api.debugger.jpda.CallStackFrame;
import org.netbeans.api.debugger.jpda.Field;
import org.netbeans.api.debugger.jpda.JPDAClassType;
import org.netbeans.api.debugger.jpda.JPDADebugger;
import org.netbeans.api.debugger.jpda.JPDAThread;
import org.netbeans.modules.debugger.jpda.jdi.LocationWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ReferenceTypeWrapper;
import org.netbeans.modules.debugger.jpda.jdi.StackFrameWrapper;
import org.netbeans.modules.debugger.jpda.jdi.VMDisconnectedExceptionWrapper;
import org.netbeans.modules.debugger.jpda.models.CallStackFrameImpl;
import org.netbeans.spi.debugger.jpda.SourcePathProvider;
import org.netbeans.spi.debugger.jpda.EditorContext;
import org.openide.util.Exceptions;

/**
 * Utility methods for sources.
 *
 * @see Similar class in debuggerjpda/ui when modifying this.
 *
 * @author Jan Jancura
 */
public class SourcePath {

    private ContextProvider          lookupProvider;
    private SourcePathProvider   contextProvider;
    private JPDADebugger            debugger;
    

    public SourcePath (ContextProvider lookupProvider) {
        this.lookupProvider = lookupProvider;
        debugger = lookupProvider.lookupFirst(null, JPDADebugger.class);
    }

    public SourcePathProvider getContext () {
        if (contextProvider == null) {
            List l = lookupProvider.lookup (null, SourcePathProvider.class);
            contextProvider = (SourcePathProvider) l.get (0);
            int i, k = l.size ();
            for (i = 1; i < k; i++) {
                contextProvider = new CompoundContextProvider (
                    (SourcePathProvider) l.get (i), 
                    contextProvider
                );
            }
        }
        return contextProvider;
    }

    
    // ContextProvider methods .................................................
    
    /**
     * Returns relative path for given url.
     *
     * @param url a url of resource file
     * @param directorySeparator a directory separator character
     * @param includeExtension whether the file extension should be included 
     *        in the result
     *
     * @return relative path
     */
    public String getRelativePath (
        String url, 
        char directorySeparator, 
        boolean includeExtension
    ) {
        return getContext ().getRelativePath 
            (url, directorySeparator, includeExtension);
    }
    
    /**
     * Returns the source root (if any) for given url.
     *
     * @param url a url of resource file
     *
     * @return the source root or <code>null</code> when no source root was found.
     */
    public String getSourceRoot(String url) {
        return getContext().getSourceRoot(url);
    }

    /**
     * Translates a relative path ("java/lang/Thread.java") to url 
     * ("file:///C:/Sources/java/lang/Thread.java"). Uses GlobalPathRegistry
     * if global == true.
     *
     * @param relativePath a relative path (java/lang/Thread.java)
     * @param global true if global path should be used
     * @return url
     */
    public String getURL (String relativePath, boolean global) {
        String url = getContext ().getURL (relativePath, global);
        if (url != null) {
            try {
                new java.net.URL(url);
            } catch (java.net.MalformedURLException muex) {
                Logger.getLogger(SourcePath.class.getName()).log(Level.WARNING,
                        "Malformed URL '"+url+"' produced by "+getContext (), muex);
                return null;
            }
        }
        return url;
    }
    
    public String getURL(JPDAClassType clazz, String stratum) {
        SourcePathProvider context = getContext();
        try {
            String url = (String) context.getClass().
                    getMethod("getURL", JPDAClassType.class, String.class).
                    invoke(context, clazz, stratum);
            if (url != null) {
                try {
                    new java.net.URL(url);
                } catch (java.net.MalformedURLException muex) {
                    Logger.getLogger(SourcePath.class.getName()).log(Level.WARNING,
                            "Malformed URL '"+url+"' produced by "+getContext (), muex);
                    return null;
                }
                return url;
            }
        } catch (NoSuchMethodException ex) {
        } catch (SecurityException ex) {
        } catch (IllegalAccessException ex) {
        } catch (IllegalArgumentException ex) {
        } catch (InvocationTargetException ex) {
        }
        String typePath = EditorContextBridge.getRelativePath (clazz.getName());
        return getURL(typePath, true);
    }
    
    public String getURL (
        CallStackFrame csf,
        String stratumn
    ) throws InternalExceptionWrapper, VMDisconnectedExceptionWrapper, InvalidStackFrameExceptionWrapper, ObjectCollectedExceptionWrapper {
        return getURL(StackFrameWrapper.location(((CallStackFrameImpl) csf).getStackFrame()), stratumn);
    }
    
    public String getURL (
        StackFrame sf,
        String stratumn
    ) throws InternalExceptionWrapper, VMDisconnectedExceptionWrapper, InvalidStackFrameExceptionWrapper, ObjectCollectedExceptionWrapper {
        return getURL(StackFrameWrapper.location(sf), stratumn);
    }
    
    public String getURL(JPDAThread t, String stratum) throws InternalExceptionWrapper, VMDisconnectedExceptionWrapper, InvalidStackFrameExceptionWrapper, ObjectCollectedExceptionWrapper {
        String url;
        try {
            CallStackFrame[] callStacks = t.getCallStack(0, 1);
            if (callStacks.length > 0) {
                url = getURL(callStacks[0], stratum);
            } else {
                String sourcePath = convertSlash (t.getSourcePath (stratum));
                url = getURL (sourcePath, true);
            }
        } catch (AbsentInformationException e) {
            String sourcePath = convertClassNameToRelativePath (t.getClassName ());
            url = getURL (sourcePath, true);
        }
        return url;
        
    }
    
    public String getURL (
        Location loc,
        String stratumn
    ) throws InternalExceptionWrapper, VMDisconnectedExceptionWrapper, ObjectCollectedExceptionWrapper {
        ReferenceType declaringType = LocationWrapper.declaringType(loc);
        JPDAClassType classType = ((JPDADebuggerImpl) debugger).getClassType(declaringType);
        return getURL(classType, loc, stratumn);
    }
    
    private String getURL (
        JPDAClassType classType,
        Location loc,
        String stratumn
    ) throws InternalExceptionWrapper, VMDisconnectedExceptionWrapper, ObjectCollectedExceptionWrapper {
        if (classType != null) {
            SourcePathProvider context = getContext();
            try {
                String url = (String) context.getClass().
                        getMethod("getURL", JPDAClassType.class, String.class).
                        invoke(context, classType, stratumn);
                if (url != null) {
                    try {
                        new java.net.URL(url);
                        return url;
                    } catch (java.net.MalformedURLException muex) {
                        Logger.getLogger(SourcePath.class.getName()).log(Level.WARNING,
                                "Malformed URL '"+url+"' produced by "+getContext (), muex);
                    }
                }
            } catch (NoSuchMethodException ex) {
            } catch (SecurityException ex) {
            } catch (IllegalAccessException ex) {
            } catch (IllegalArgumentException ex) {
            } catch (InvocationTargetException ex) {
            }
        }
        String sourcePath;
        if (loc == null) {
            sourcePath = EditorContextBridge.getRelativePath (classType.getName());
        } else {
            try {
                sourcePath = convertSlash(LocationWrapper.sourcePath(loc, stratumn));
            } catch (AbsentInformationException e) {
                sourcePath = convertClassNameToRelativePath (
                                 ReferenceTypeWrapper.name(LocationWrapper.declaringType(loc))
                             );
            }
        }
        return getURL(sourcePath, true);
    }
    
    /**
     * Returns array of source roots.
     */
    public String[] getSourceRoots () {
        return getContext ().getSourceRoots ();
    }
    
    /**
     * Sets array of source roots.
     *
     * @param sourceRoots a new array of sourceRoots
     */
    public void setSourceRoots (String[] sourceRoots) {
        getContext ().setSourceRoots (sourceRoots);
    }
    
    /**
     * Returns set of original source roots.
     *
     * @return set of original source roots
     */
    public String[] getOriginalSourceRoots () {
        return getContext ().getOriginalSourceRoots ();
    }
    
    /**
     * Returns the project's source roots.
     * 
     * @return array of source roots belonging to the project
     */
    public String[] getProjectSourceRoots() {
        try {
            java.lang.reflect.Method getProjectSourceRootsMethod = getContext().getClass().getMethod("getProjectSourceRoots", new Class[] {}); // NOI18N
            String[] projectSourceRoots = (String[]) getProjectSourceRootsMethod.invoke(getContext(), new Object[] {});
            return projectSourceRoots;
        } catch (Exception ex) {
            Exceptions.printStackTrace(ex);
            return new String[] {};
        }
    }
    
    /**
     * Adds property change listener.
     *
     * @param l new listener.
     */
    public void addPropertyChangeListener (PropertyChangeListener l) {
        getContext ().addPropertyChangeListener (l);
    }

    /**
     * Removes property change listener.
     *
     * @param l removed listener.
     */
    public void removePropertyChangeListener (
        PropertyChangeListener l
    ) {
        getContext ().removePropertyChangeListener (l);
    }
    
    
    // utility methods .........................................................

    public static String convertSlash (String original) {
        return original.replace (File.separatorChar, '/');
    }

    public static String convertClassNameToRelativePath (
        String className
    ) {
        int i = className.indexOf ('$');
        if (i > 0) className = className.substring (0, i);
        String sourceName = className.replace
            ('.', '/') + ".java";
        return sourceName;
    }

    
    // innerclasses ............................................................

    private static class CompoundContextProvider extends SourcePathProvider {

        private SourcePathProvider cp1, cp2;

        CompoundContextProvider (
            SourcePathProvider cp1,
            SourcePathProvider cp2
        ) {
            this.cp1 = cp1;
            this.cp2 = cp2;
        }

        public String getURL (String relativePath, boolean global) {
            String p1 = cp1.getURL (relativePath, global);
            if (p1 != null) {
                try {
                    new java.net.URL(p1);
                    return p1;
                } catch (java.net.MalformedURLException muex) {
                    Logger.getLogger(SourcePath.class.getName()).log(Level.WARNING,
                            "Malformed URL '"+p1+"' produced by "+cp1, muex);
                }
            }
            p1 = cp2.getURL (relativePath, global);
            if (p1 != null) {
                try {
                    new java.net.URL(p1);
                } catch (java.net.MalformedURLException muex) {
                    Logger.getLogger(SourcePath.class.getName()).log(Level.WARNING,
                            "Malformed URL '"+p1+"' produced by "+cp2, muex);
                    p1 = null;
                }
            }
            return p1;
        }
        
        public String getURL(JPDAClassType clazz, String stratum) {
            try {
                java.lang.reflect.Method getURLMethod = cp1.getClass().getMethod("getURL", JPDAClassType.class, String.class); // NOI18N
                String url = (String) getURLMethod.invoke(cp1, clazz, stratum);
                if (url != null) {
                    return url;
                }
            } catch (IllegalAccessException ex) {
            } catch (IllegalArgumentException ex) {
            } catch (NoSuchMethodException ex) {
            } catch (SecurityException ex) {
            } catch (InvocationTargetException ex) {
            }
            try {
                java.lang.reflect.Method getURLMethod = cp2.getClass().getMethod("getURL", JPDAClassType.class, String.class); // NOI18N
                String url = (String) getURLMethod.invoke(cp2, clazz, stratum);
                if (url != null) {
                    return url;
                }
            } catch (IllegalAccessException ex) {
            } catch (IllegalArgumentException ex) {
            } catch (NoSuchMethodException ex) {
            } catch (SecurityException ex) {
            } catch (InvocationTargetException ex) {
            }
            return null;
        }

        public String getRelativePath (
            String url, 
            char directorySeparator, 
            boolean includeExtension
        ) {
            String p1 = cp1.getRelativePath (
                url, 
                directorySeparator, 
                includeExtension
            );
            if (p1 != null) return p1;
            return cp2.getRelativePath (
                url, 
                directorySeparator, 
                includeExtension
            );
        }

        public String getSourceRoot(String url) {
            String sourceRoot = cp1.getSourceRoot(url);
            if (sourceRoot == null) {
                sourceRoot = cp2.getSourceRoot(url);
            }
            return sourceRoot;
        }
        
        public String[] getSourceRoots () {
            String[] fs1 = cp1.getSourceRoots ();
            String[] fs2 = cp2.getSourceRoots ();
            String[] fs = new String [fs1.length + fs2.length];
            System.arraycopy (fs1, 0, fs, 0, fs1.length);
            System.arraycopy (fs2, 0, fs, fs1.length, fs2.length);
            return fs;
        }
    
        public String[] getOriginalSourceRoots () {
            String[] fs1 = cp1.getOriginalSourceRoots ();
            String[] fs2 = cp2.getOriginalSourceRoots ();
            String[] fs = new String [fs1.length + fs2.length];
            System.arraycopy (fs1, 0, fs, 0, fs1.length);
            System.arraycopy (fs2, 0, fs, fs1.length, fs2.length);
            return fs;
        }
        
        public String[] getProjectSourceRoots() {
            String[] projectSourceRoots1;
            String[] projectSourceRoots2;
            //System.err.println("\nCompoundContextProvider["+toString()+"].getProjectSourceRoots()...\n");
            try {
                java.lang.reflect.Method getProjectSourceRootsMethod = cp1.getClass().getMethod("getProjectSourceRoots", new Class[] {}); // NOI18N
                projectSourceRoots1 = (String[]) getProjectSourceRootsMethod.invoke(cp1, new Object[] {});
            } catch (Exception ex) {
                projectSourceRoots1 = new String[0];
            }
            try {
                java.lang.reflect.Method getProjectSourceRootsMethod = cp2.getClass().getMethod("getProjectSourceRoots", new Class[] {}); // NOI18N
                projectSourceRoots2 = (String[]) getProjectSourceRootsMethod.invoke(cp2, new Object[] {});
            } catch (Exception ex) {
                projectSourceRoots2 = new String[0];
            }
            if (projectSourceRoots1.length == 0) {
                //System.err.println("\nCompoundContextProvider.getProjectSourceRoots() = "+java.util.Arrays.toString(projectSourceRoots2)+"\n");
                return projectSourceRoots2;
            }
            if (projectSourceRoots2.length == 0) {
                //System.err.println("\nCompoundContextProvider.getProjectSourceRoots() = "+java.util.Arrays.toString(projectSourceRoots1)+"\n");
                return projectSourceRoots1;
            }
            String[] projectSourceRoots = new String[projectSourceRoots1.length + projectSourceRoots2.length];
            System.arraycopy (projectSourceRoots1, 0, projectSourceRoots, 0, projectSourceRoots1.length);
            System.arraycopy (projectSourceRoots2, 0, projectSourceRoots, projectSourceRoots1.length, projectSourceRoots2.length);
            //System.err.println("\nCompoundContextProvider.getProjectSourceRoots() = "+java.util.Arrays.toString(projectSourceRoots)+"\n");
            return projectSourceRoots;
        }

        public void setSourceRoots (String[] sourceRoots) {
            cp1.setSourceRoots (sourceRoots);
            cp2.setSourceRoots (sourceRoots);
        }

        public void addPropertyChangeListener (PropertyChangeListener l) {
            cp1.addPropertyChangeListener (l);
            cp2.addPropertyChangeListener (l);
        }

        public void removePropertyChangeListener (PropertyChangeListener l) {
            cp1.removePropertyChangeListener (l);
            cp2.removePropertyChangeListener (l);
        }

        @Override
        public String toString() {
            return "CompoundContextProvider["+cp1.toString()+", "+cp2.toString()+"]";
        }
        
        
    }

}

