/*
 * Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.jfr.internal;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;

import jdk.jfr.internal.EventControl.NamedControl;
import jdk.jfr.internal.event.EventConfiguration;
import jdk.jfr.internal.util.Utils;

final class SettingsManager {

    private static class InternalSetting {

        private final String identifier;
        private Map<String, Set<String>> enabledMap = new LinkedHashMap<>(5);
        private Map<String, Set<String>> allMap = new LinkedHashMap<>(5);
        private boolean enabled;

        /**
         * Settings identifier, for example "com.example.HelloWorld" or "56"
         * (id of event)
         *
         * @param settingsId
         */
        public InternalSetting(String settingsId) {
            this.identifier = settingsId;
        }

        public Set<String> getValues(String key) {
            if (enabled) {
                return enabledMap.get(key);
            } else {
                return allMap.get(key);
            }
        }

        public void add(String attribute, String value) {
            if ("enabled".equals(attribute) && "true".equals(value)) {
                enabled = true;
                allMap = null; // no need to keep these around
            }
            addToMap(enabledMap, attribute, value);
            if (allMap != null) {
                addToMap(allMap, attribute, value);
            }
        }

        private void addToMap(Map<String, Set<String>> map, String attribute, String value) {
            Set<String> values = map.get(attribute);
            if (values == null) {
                values = HashSet.newHashSet(4);
                map.put(attribute, values);
            }
            values.add(value);

        }

        public String getSettingsId() {
            return identifier;
        }

        public void add(InternalSetting enabled) {
            for (Map.Entry<String, Set<String>> entry : enabled.enabledMap.entrySet()) {
                for (String value : entry.getValue()) {
                    add(entry.getKey(), value);
                }
            }
        }

        public boolean isEnabled() {
            return enabled;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(identifier);
            sb.append(": ");
            sb.append(enabledMap.toString());
            return sb.toString();
        }

        public void finish() {
            if (!enabled) {
                // settings from disabled
                // events should not impact results, but
                // we can't clear enabledMap since enabled=false
                // needs be there, so events that are enabled
                // by default are turned off
                Map<String, Set<String>> disabledMap = new HashMap<>(2);
                Set<String> values = new HashSet<>(2);
                values.add("false");
                disabledMap.put("enabled", values);
                enabledMap = disabledMap;
            }
        }
    }

   private Map<String, InternalSetting> availableSettings = new LinkedHashMap<>();

    void setSettings(List<Map<String, String>> activeSettings, boolean writeSettingEvents) {
        // store settings so they are available if a new event class is loaded
        availableSettings = createSettingsMap(activeSettings);
        List<EventControl> eventControls = MetadataRepository.getInstance().getEventControls();
        if (!JVM.isRecording()) {
            for (EventControl ec : eventControls) {
                ec.disable();
            }
        } else {
            if (Logger.shouldLog(LogTag.JFR_SETTING, LogLevel.INFO)) {
                eventControls.sort(Comparator.comparing(x -> x.getEventType().getName()));
            }
            long timestamp = JVM.counterTime();
            for (EventControl ec : eventControls) {
                setEventControl(ec, writeSettingEvents, timestamp);
            }
        }
        if (JVM.getAllowedToDoEventRetransforms()) {
            updateRetransform(JVM.getAllEventClasses());
        }
    }

    public void updateRetransform(List<Class<? extends jdk.internal.event.Event>> eventClasses) {
        List<Class<?>> classes = new ArrayList<>();
        for(Class<? extends jdk.internal.event.Event> eventClass: eventClasses) {
            EventConfiguration ec = JVMSupport.getConfiguration(eventClass);
            if (ec != null ) {
                PlatformEventType eventType = ec.getPlatformEventType();
                if (eventType.isMarkedForInstrumentation()) {
                    classes.add(eventClass);
                    eventType.markForInstrumentation(false);
                    // A bit premature to set it here, but hard to check
                    // after call to retransformClasses.
                    eventType.setInstrumented();
                }
            }
        }
        if (!classes.isEmpty()) {
            JVM.retransformClasses(classes.toArray(new Class<?>[0]));
        }
    }

    private Map<String, InternalSetting> createSettingsMap(List<Map<String,String>> activeSettings) {
        Map<String, InternalSetting> map = LinkedHashMap.newLinkedHashMap(activeSettings.size());
        for (Map<String, String> rec : activeSettings) {
            for (InternalSetting internal : makeInternalSettings(rec)) {
                InternalSetting is = map.get(internal.getSettingsId());
                if (is == null) {
                    map.put(internal.getSettingsId(), internal);
                } else {
                    is.add(internal);
                }
            }
        }
        return map;
    }

    private Collection<InternalSetting> makeInternalSettings(Map<String, String> rec) {
        Map<String, InternalSetting> internals = LinkedHashMap.newLinkedHashMap(rec.size());
        for (Map.Entry<String, String> entry : rec.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            int index = key.indexOf("#");
            if (index > 1 && index < key.length() - 2) {
                String eventName = key.substring(0, index);
                eventName = Utils.upgradeLegacyJDKEvent(eventName);
                InternalSetting s = internals.get(eventName);
                String settingName = key.substring(index + 1).trim();
                if (s == null) {
                    s = new InternalSetting(eventName);
                    internals.put(eventName, s);
                }
                s.add(settingName, value);
            }
        }
      for (InternalSetting s : internals.values()) {
         s.finish();
      }

      return internals.values();
    }

    void setEventControl(EventControl ec, boolean writeSettingEvents, long timestamp) {
        InternalSetting is = getInternalSetting(ec);
        boolean shouldLog = Logger.shouldLog(LogTag.JFR_SETTING, LogLevel.INFO);
        if (shouldLog) {
            Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, "Applied settings for " + ec.getEventType().getLogName() + " {");
        }
        for (NamedControl nc: ec.getNamedControls()) {
            Set<String> values = null;
            String settingName = nc.name();
            if (is != null) {
                values = is.getValues(settingName);
            }
            Control control = nc.control();
            if (values != null) {
                control.apply(values);
                String after = control.getLastValue();
                if (shouldLog) {
                    if (control.isVisible(ec.getEventType().hasEventHook())) {
                        if (values.size() > 1) {
                            StringJoiner sj = new StringJoiner(", ", "{", "}");
                            for (String s : values) {
                                sj.add("\"" + s + "\"");
                            }
                            String message = "  " + settingName + "= " + sj.toString() + " => \"" + after + "\"";
                            Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, message);
                        } else {
                            String message = "  " + settingName + "=\"" + control.getLastValue() + "\"";
                            Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, message);
                        }
                    }
                }
            } else {
                control.setDefault();
                if (shouldLog) {
                    String message = "  " + settingName + "=\"" + control.getLastValue() + "\"";
                    Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, message);
                }
            }
        }
        if (writeSettingEvents) {
            ec.writeActiveSettingEvent(timestamp);
        }
        if (shouldLog) {
            Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, "}");
        }
    }

    private InternalSetting getInternalSetting(EventControl ec) {
        String name = ec.getEventType().getName();
        InternalSetting nameBased = availableSettings.get(name);
        InternalSetting idBased = availableSettings.get(ec.getSettingsId());

        if (nameBased == null && idBased == null) {
            return null;
        }
        if (idBased == null) {
            return nameBased;
        }
        if (nameBased == null) {
            return idBased;
        }
        InternalSetting mixed = new InternalSetting(nameBased.getSettingsId());
        mixed.add(nameBased);
        mixed.add(idBased);
        return mixed;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (InternalSetting enabled : availableSettings.values()) {
            sb.append(enabled.toString());
            sb.append("\n");
        }
        return sb.toString();
    }

    boolean isEnabled(String eventName) {
        InternalSetting is = availableSettings.get(eventName);
        if (is == null) {
            return false;
        }
        return is.isEnabled();
    }
}
