/**
 *  Copyright 2005-2016 Red Hat, Inc.
 *
 *  Red Hat 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 io.fabric8.apmagent;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CopyOnWriteArrayList;

public class ApmConfiguration implements ApmConfigurationMBean {
    public enum STRATEGY {
        TRACE,
        SAMPLE,;

        static STRATEGY getStrategy(String name) {
            for (STRATEGY v : values()) {
                if (v.name().equals(name.toUpperCase())) {
                    return v;
                }
            }
            return SAMPLE;
        }
    };

    final static Logger logger = LoggerFactory.getLogger(ApmConfiguration.class);
    private boolean trace = false;
    private boolean debug = false;
    private boolean asyncTransformation = false;
    private boolean startJolokiaAgent = false;
    private boolean autoStartMetrics = false;
    private boolean usePlatformMBeanServer = true;
    private boolean verifyClasses = false;
    private int methodMetricDepth = 10;
    private int threadMetricDepth = 5;
    private boolean filterChanged = false;
    private boolean methodMetricDepthChanged = false;
    private boolean threadMetricDepthChanged = false;
    private boolean strategyChanged = false;
    private int samplingInterval = 1;
    private List<FilterItem> whiteFilterList = new ArrayList<>();
    private List<FilterItem> blackFilterList = new ArrayList<>();
    private List<ApmConfigurationChangeListener> changeListeners = new CopyOnWriteArrayList<>();
    private STRATEGY strategy = STRATEGY.TRACE;

    ApmConfiguration() {
        addToBlackList("java");
        addToBlackList("com.sun");
        addToBlackList("sun");
        addToBlackList("$Proxy");
        addToBlackList("ByCGLIB$$");
        addToBlackList("io.fabric8.apmagent");
        addToBlackList("org.apache.camel.spring.remoting");
        addToBlackList("org.jolokia");
        addToBlackList("org.springframework");
        addToBlackList("org.eclipse");
        addToBlackList("org.apache.xbean");
        addToBlackList("org.slf4j");
        addToBlackList("org.omg");
        addToBlackList("com.apple");
        addToBlackList("oracle");
        addToBlackList("org.apache.log4j");
        addToBlackList("org.objectweb.asm");
        addToBlackList("org.apache.commons");
        addToBlackList("org.apache.jasper");
        addToBlackList("jrockit");
        addToBlackList("org.json");
        addToBlackList("org.fusesource.hawtbuf");
        addToBlackList("com.intellij");
        addToBlackList("org.w3c.dom");
        addToBlackList("com.codahale");
        //for testing only
        addToWhiteList("io.fabric8.testApp");

    }

    @Override
    public String getWhiteList() {
        return getListAsString(whiteFilterList);
    }

    @Override
    public void setWhiteList(String whiteList) {
        whiteFilterList = new ArrayList<>();
        initializeList(whiteList, this.whiteFilterList);
        filterChanged = true;
        fireConfigurationChanged();
    }

    @Override
    public String getBlackList() {
        return getListAsString(blackFilterList);
    }

    @Override
    public void setBlackList(String blackList) {
        this.blackFilterList = new ArrayList<>();
        initializeList(blackList, this.blackFilterList);
        filterChanged = true;
        fireConfigurationChanged();
    }

    @Override
    public void addToBlackList(String s) {
        FilterItem filterItem = new FilterItem();
        String[] classAndMethod = s.split("@");
        filterItem.setClassName(classAndMethod[0]);
        if (classAndMethod.length > 1) {
            filterItem.setMethodName(classAndMethod[1]);
        }
        blackFilterList.add(filterItem);
        filterChanged = true;
        fireConfigurationChanged();
    }

    @Override
    public void addToWhiteList(String s) {
        FilterItem filterItem = new FilterItem();
        String[] classAndMethod = s.split("@");
        filterItem.setClassName(classAndMethod[0]);
        if (classAndMethod.length > 1) {
            filterItem.setMethodName(classAndMethod[1]);
        }
        whiteFilterList.add(filterItem);
        filterChanged = true;
        fireConfigurationChanged();
    }

    @Override
    public boolean isTrace() {
        return trace;
    }

    @Override
    public void setTrace(boolean trace) {
        this.trace = trace;
    }

    @Override
    public boolean isDebug() {
        return debug;
    }

    @Override
    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    @Override
    public boolean isAsyncTransformation() {
        return asyncTransformation;
    }

    @Override
    public void setAsyncTransformation(boolean asyncTransformation) {
        this.asyncTransformation = asyncTransformation;
    }

    public boolean isStartJolokiaAgent() {
        return startJolokiaAgent;
    }

    public void setStartJolokiaAgent(boolean startJolokiaAgent) {
        this.startJolokiaAgent = startJolokiaAgent;
    }

    public boolean isAutoStartMetrics() {
        return autoStartMetrics;
    }

    public void setAutoStartMetrics(boolean autoStartMetrics) {
        this.autoStartMetrics = autoStartMetrics;
    }

    public boolean isUsePlatformMBeanServer() {
        return usePlatformMBeanServer;
    }

    public void setUsePlatformMBeanServer(boolean usePlatformMBeanServer) {
        this.usePlatformMBeanServer = usePlatformMBeanServer;
    }

    public boolean isVerifyClasses() {
        return verifyClasses;
    }

    public void setVerifyClasses(boolean verifyClasses) {
        this.verifyClasses = verifyClasses;
    }

    public int getThreadMetricDepth() {
        return threadMetricDepth;
    }

    public void setThreadMetricDepth(int threadMetricDepth) {
        this.threadMetricDepth = threadMetricDepth;
        this.threadMetricDepthChanged = true;
        fireConfigurationChanged();
    }

    public int getMethodMetricDepth() {
        return methodMetricDepth;
    }

    public void setMethodMetricDepth(int methodMetricDepth) {
        this.methodMetricDepth = methodMetricDepth;
        this.methodMetricDepthChanged = true;
        fireConfigurationChanged();
    }

    public boolean isStrategyChanged() {
        return strategyChanged;
    }

    public boolean isThreadMetricDepthChanged() {
        return threadMetricDepthChanged;
    }

    public boolean isMethodMetricDepthChanged() {
        return methodMetricDepthChanged;
    }

    public boolean isFilterChanged() {
        return filterChanged;
    }

    public int getSamplingInterval() {
        return samplingInterval;
    }

    public void setSamplingInterval(int samplingInterval) {
        this.samplingInterval = samplingInterval;
    }

    public String getStrategy() {
        return strategy.name();
    }

    public void setStrategy(String name) {
        STRATEGY newStrategy = STRATEGY.getStrategy(name);
        if (!this.strategy.equals(newStrategy)) {
            this.strategy = STRATEGY.getStrategy(name);
            this.strategyChanged = true;
            fireConfigurationChanged();
        }
    }

    public STRATEGY getStrategyImpl() {
        return strategy;
    }

    public void initalizeFromProperties(Properties properties) {
        for (Map.Entry entry : properties.entrySet()) {
            if (entry.getKey() != null && entry.getValue() != null) {
                setProperty(entry.getKey().toString(), entry.getValue().toString());
            }
        }
    }

    public boolean isAudit(String className) {
        return isWhiteListed(className) || !isBlackListed(className);
    }

    public boolean isAudit(String className, String methodName) {
        return isWhiteListed(className, methodName) || !isBlackListed(className, methodName);
    }

    public boolean isBlackListed(String className) {
        for (FilterItem item : blackFilterList) {
            if (item.matches(className)) {
                return true;
            }
        }
        return false;
    }

    public boolean isBlackListed(String className, String methodName) {
        for (FilterItem item : blackFilterList) {
            if (item.matches(className, methodName, true)) {
                return true;
            }
        }
        return false;
    }

    public boolean isWhiteListed(String className) {

        if (whiteFilterList.isEmpty())
            return false;
        for (FilterItem item : whiteFilterList) {
            if (className.matches(item.getClassName()))
                return true;
        }
        return false;
    }

    public boolean isWhiteListed(String className, String methodName) {
        for (FilterItem item : whiteFilterList) {
            if (item.matches(className, methodName, false)) {
                return true;
            }
        }
        return false;
    }

    public void addChangeListener(ApmConfigurationChangeListener changeListener) {
        changeListeners.add(changeListener);
    }

    public void removeChangeListener(ApmConfigurationChangeListener changeListener) {
        changeListeners.remove(changeListener);
    }

    private void fireConfigurationChanged() {
        for (ApmConfigurationChangeListener apmConfigurationChangeListener : this.changeListeners) {
            apmConfigurationChangeListener.configurationChanged();
        }
        resetChanged();
    }

    private void resetChanged() {
        filterChanged = false;
        methodMetricDepthChanged = false;
        threadMetricDepthChanged = false;
        strategyChanged = false;
    }

    private void setProperty(String name, Object value) {
        try {
            Method setter = findSetterMethod(name);
            if (setter != null) {
                if (value == null || value.getClass().equals(setter.getParameterTypes()[0])) {
                    setter.invoke(this, value);
                } else {
                    setter.invoke(this, convert(value, setter.getParameterTypes()[0]));
                }
            }
        } catch (Throwable e) {
            logger.warn("Failed to set property: " + e.getMessage(), e);
        }
    }

    private Method findSetterMethod(String name) {
        String methodName = "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
        Method[] methods = ApmConfiguration.class.getMethods();
        for (Method method : methods) {
            Class params[] = method.getParameterTypes();
            if (method.getName().equals(methodName) && params.length == 1) {
                return method;
            }
        }
        return null;
    }

    private Object convert(Object value, Class type) throws Exception {
        PropertyEditor editor = PropertyEditorManager.findEditor(type);
        if (editor != null) {
            editor.setAsText(value.toString());
            return editor.getValue();
        }
        if (type == URI.class) {
            return new URI(value.toString());
        }
        return null;
    }

    private void initializeList(String str, List<FilterItem> list) {

        String[] split = str.split(",");

        for (String s : split) {
            FilterItem filterItem = new FilterItem();
            String[] classAndMethod = s.split("@");
            filterItem.setClassName(classAndMethod[0]);
            if (classAndMethod.length > 1) {
                filterItem.setMethodName(classAndMethod[1]);
            }
            list.add(filterItem);

        }
    }

    private String getListAsString(List<FilterItem> list) {
        String result = "";
        if (list != null && !list.isEmpty()) {
            for (int i = 0; i < list.size(); i++) {
                FilterItem filterItem = list.get(i);
                result += filterItem.getClassName();
                String methodName = filterItem.getMethodName();
                if (methodName != null && methodName.length() > 0) {
                    result += "@" + methodName;
                }
                if (i < (list.size() - 1)) {
                    result += ",";
                }
            }
        }
        return result;
    }
}
