/*
 * Copyright 2014 NAVER Corp.
 *
 * 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 com.navercorp.pinpoint.plugin.tomcat.interceptor;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.navercorp.pinpoint.bootstrap.context.ServerMetaDataHolder;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.Host;
import org.apache.catalina.loader.WebappLoader;

import com.navercorp.pinpoint.bootstrap.context.TraceContext;
import com.navercorp.pinpoint.bootstrap.interceptor.AroundInterceptor;
import com.navercorp.pinpoint.bootstrap.logging.PLogger;
import com.navercorp.pinpoint.bootstrap.logging.PLoggerFactory;

/**
 * @author hyungil.jeong
 */
public class WebappLoaderStartInterceptor implements AroundInterceptor {

    private final PLogger logger = PLoggerFactory.getLogger(this.getClass());
    
    private final TraceContext traceContext;

    public WebappLoaderStartInterceptor(TraceContext traceContext) {
        this.traceContext = traceContext;
    }

    @Override
    public void before(Object target, Object[] args) {
        // Do Nothing
    }

    @Override
    public void after(Object target, Object[] args, Object result, Throwable throwable) {
        // target should be an instance of WebappLoader.
        if (target instanceof WebappLoader) {
            WebappLoader webappLoader = (WebappLoader)target;
            try {
                String contextKey = extractContextKey(webappLoader);
                List<String> loadedJarNames = extractLibJars(webappLoader);
                dispatchLibJars(contextKey, loadedJarNames);
            } catch (Exception e) {
                if (logger.isWarnEnabled()) {
                    logger.warn(e.getMessage(), e);
                }
            }
        } else {
            logger.warn("Webapp loader is not an instance of org.apache.catalina.loader.WebappLoader. Found [{}]", target.getClass().toString());
        }
    }
    
    private String extractContextKey(WebappLoader webappLoader) {
        final String defaultContextName = "";
        try {
            Container container = extractContext(webappLoader);
            // WebappLoader's associated Container should be a Context.
            if (container instanceof Context) {
                Context context = (Context)container;
                String contextName = context.getName();
                Host host = (Host)container.getParent();
                Engine engine = (Engine)host.getParent();
                StringBuilder sb = new StringBuilder();
                sb.append(engine.getName()).append("/").append(host.getName());
                if (!contextName.startsWith("/")) {
                    sb.append('/');
                }
                sb.append(contextName);
                return sb.toString();
            }
        } catch (Exception e) {
            // Same action for any and all exceptions.
            logger.warn("Error extracting context name.", e);
        }
        return defaultContextName;
    }

    // FIXME Use reflection until we provide separate packages for instrumented libraries.
    // Tomcat 8's WebappLoader does not have getContainer() method.
    // Providing an optional package that calls WebappLoader.getContext() method could be an option.
    private Container extractContext(WebappLoader webappLoader) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
        Method m;
        try {
            // Tomcat 6, 7 - org.apache.catalina.loader.getContainer() 
            m = webappLoader.getClass().getDeclaredMethod("getContainer");
        } catch (NoSuchMethodException e1) {
            try {
                // Tomcat 8 - org.apache.catalina.loader.getContainer()
                m = webappLoader.getClass().getDeclaredMethod("getContext");
            } catch (NoSuchMethodException e2) {
                logger.warn("Webapp loader does not have access to its container.");
                return null;
            }
        }
        Object container = m.invoke(webappLoader);
        if (container instanceof Container) {
            return (Container)container;
        }
        return null;
    }
    
    private List<String> extractLibJars(WebappLoader webappLoader) {
        ClassLoader classLoader = webappLoader.getClassLoader();
        if (classLoader instanceof URLClassLoader) {
            URLClassLoader webappClassLoader = (URLClassLoader)classLoader;
            URL[] urls = webappClassLoader.getURLs();
            return extractLibJarNamesFromURLs(urls);
        } else {
            logger.warn("Webapp class loader is not an instance of URLClassLoader. Found [{}]", classLoader.getClass().toString());
            return Collections.emptyList();
        } 
    }
    
    private List<String> extractLibJarNamesFromURLs(URL[] urls) {
        if (urls == null) {
            return Collections.emptyList();
        }
        List<String> libJarNames = new ArrayList<String>(urls.length);
        for (URL url : urls) {
            try {
                URI uri =  url.toURI();
                String libJarName = extractLibJarName(uri);
                if (libJarName.length() > 0) {
                    libJarNames.add(libJarName);
                }
            } catch (URISyntaxException e) {
                // ignore invalid formats
                logger.warn("Invalid library url found : [{}]", url, e);
            } catch (Exception e) {
                logger.warn("Error extracting library name", e);
            }
        }
        return libJarNames;
    }
    
    private String extractLibJarName(URI uri) {
        String jarName = uri.toString();
        if (jarName == null) {
            return "";
        }
        int lastIndexOfSeparator = jarName.lastIndexOf("/");
        if (lastIndexOfSeparator < 0) {
            return jarName;
        } else {
            return jarName.substring(lastIndexOfSeparator + 1);
        }
    }
    
    private void dispatchLibJars(String contextKey, List<String> libJars) {
        ServerMetaDataHolder holder = this.traceContext.getServerMetaDataHolder();
        holder.addServiceInfo(contextKey, libJars);
        holder.notifyListeners();
    }

}
