/*
 * 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.apache.brooklyn.util.javalang;

import java.util.Iterator;
import java.util.Map;

import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.text.Strings;

import com.google.common.base.Preconditions;
import com.google.common.reflect.TypeToken;

public class JavaClassNames {

    private static final StackTraceSimplifier STACK_TRACE_SIMPLIFIER_EXCLUDING_UTIL_JAVALANG = 
            StackTraceSimplifier.newInstance(StackTraceSimplifier.class.getPackage().getName()+".");
    
    /** returns the Class of anything which isn't a class; if input is class it is pass-through */
    public static Class<?> type(Object x) {
        if (x==null) return null;
        if (x instanceof Class) return (Class<?>)x;
        if (x instanceof TypeToken) return ((TypeToken<?>)x).getRawType();
        return x.getClass();
    }

    /** like type, but removes any array modifiers */
    public static Class<?> componentType(Object x) {
        Class<?> c = type(x);
        if (c==null) return null;
        while (c.isArray()) {
            c = c.getComponentType();
        }
        return c;
    }

    /** as {@link #simpleClassName(Class)} but returning json types if appropriate,
     * e.g. `map`, `string`, etc; else falling back to detailed type */
    public static String superSimpleClassName(Class<?> t) {
        if (Map.class.isAssignableFrom(t)) return "map";
        if (Iterable.class.isAssignableFrom(t) || Iterator.class.isAssignableFrom(t) ||
            t.isArray()) return "list";
        if (CharSequence.class.isAssignableFrom(t)) return "string";
        if (Number.class.isAssignableFrom(t)) return "number";
        if (Boolean.class.isAssignableFrom(t)) return "boolean";
        return simpleClassName(t);
    }
    
    /** as {@link #simpleClassName(Object)} but looking up the type if needed */
    public static String superSimpleClassName(Object o) {
        return superSimpleClassName(type(o));
    }
    
    /**  returns a simplified name of the class, just the simple name if it seems useful, else the full name */
    public static String simpleClassName(Class<?> t) {
        if (t==null) return null;
        int arrayCount = 0;
        while (t.isArray()) {
            arrayCount++;
            t = t.getComponentType();
        }
        Class<?> ct = componentType(t);
        
        String result = ct.getSimpleName();
        if (Strings.isBlank(result) || result.length()<=4) {
            result = ct.getName();
        }
        return result+Strings.repeat("[]", arrayCount);
    }
    
    /** as {@link #simpleClassName(Class)} but if something is an inner class it drops everything before the $ */
    public static String verySimpleClassName(Class<?> t) {
        t = componentType(t);
        String result = t.getSimpleName();
        result = result.substring(result.lastIndexOf('.')+1);
        result = result.substring(result.lastIndexOf('$')+1);
        if (Strings.isBlank(result)) {
            result = t.getName();
        }
        return result;
    }
    
    /** as {@link #simpleClassName(Class)} but taking the type of the object if it is not already a class
     * or a type-token; callers should usually do the getClass themselves, unless they aren't sure whether
     * it is already a Class-type object */
    public static String simpleClassName(Object x) {
        return simpleClassName(type(x));
    }

    /** as {@link #simpleClassName(Class)} but taking a string rep'n of the class name,
     * and doing best effort to simplify it (without instantiating) */
    public static String simplifyClassName(String className) {
        if (className==null) return null;
        int lastDot = className.lastIndexOf('.');
        if (lastDot < className.length()-5)
            return className.substring(lastDot+1);
        return className;
    }

    /** as {@link #simpleClassName(Object)} but making the result clean for use on filesystems and as java identifiers */
    public static String cleanSimpleClassName(Object x) {
        return Strings.makeValidFilename(simpleClassName(x));
    }
    
    /** as {@link #simpleClassName(Object)} but making the result clean for use on filesystems and as java identifiers */
    public static String cleanSimpleClassName(Class<?> x) {
        return Strings.makeValidFilename(simpleClassName(x));
    }
    
    public static String packageName(Object x) {
        return componentType(x).getPackage().getName();
    }

    /** returns e.g. "/com/acme/" for an object in package com.acme */ 
    public static String packagePath(Object x) {
        return Urls.mergePaths("/", componentType(x).getPackage().getName().replace('.', '/'), "/");
    }

    /** returns path relative to the package of x, unless path is absolute.
     * useful to mimic Class.getResource(path) behaviour, cf Class.resolveName where the first argument below is the class. */
    public static String resolveName(Object context, String path) {
        Preconditions.checkNotNull(path, "path must not be null");
        if (path.startsWith("/") || Urls.isUrlWithProtocol(path)) return path;
        Preconditions.checkNotNull(context, "context must not be null when path is relative");
        return packagePath(context)+path;
    }

    /** returns a "classpath:" URL given a context object and a file to be found in that directory or a sub-directory
     * (ignoring the context object if the given path is absolute, i.e. starting with "/" or "protocol:") 
     * e.g. "classpath://com/acme/foo.txt" given a context object com.acme.SomeClass and "foo.txt" */
    public static String resolveClasspathUrl(Object context, String path) {
        if (Urls.isUrlWithProtocol(path)) return path;
        // additional / comes from resolve name
        return "classpath:/"+resolveName(context, path);
    }

    /** returns a cleaned stack trace; caller is usually at the top */
    public static StackTraceElement[] currentStackTraceCleaned() {
        return STACK_TRACE_SIMPLIFIER_EXCLUDING_UTIL_JAVALANG.clean(
                Thread.currentThread().getStackTrace());
    }
    
    /** returns top of cleaned stack trace; usually the caller's location */
    public static StackTraceElement currentStackElement() {
        return STACK_TRACE_SIMPLIFIER_EXCLUDING_UTIL_JAVALANG.nthUseful(0,
                Thread.currentThread().getStackTrace());
    }

    /** returns element in cleaned stack trace; usually the caller's location is at the top,
     * and caller of that is up one, etc */
    public static StackTraceElement callerStackElement(int depth) {
        return STACK_TRACE_SIMPLIFIER_EXCLUDING_UTIL_JAVALANG.nthUseful(depth,
                Thread.currentThread().getStackTrace());
    }

    /** returns nice class name and method for the given element */
    public static String niceClassAndMethod(StackTraceElement st) {
        return simplifyClassName(st.getClassName())+"."+st.getMethodName();
    }

    /** returns nice class name and method for the caller, going up the stack (filtered to remove invocation etc),
     * with 0 typically being the context where this method is called, 1 being its caller, etc */
    public static String callerNiceClassAndMethod(int depth) {
        return niceClassAndMethod(callerStackElement(depth));
    }

    /** convenience for {@link #callerNiceClassAndMethod(int)} with depth 0
     * <p>
     * useful for tests and other debug-facing log messages! */
    public static String niceClassAndMethod() {
        return callerNiceClassAndMethod(0);
    }

}
