/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2022 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 org.jboss.installer.screens;

import org.jboss.installer.common.FontResources;
import org.jboss.installer.common.UiResources;
import org.jboss.installer.core.InstallationData;
import org.jboss.installer.core.LanguageUtils;
import org.jboss.installer.core.Screen;
import org.jboss.installer.core.SocketBindingUtils;
import org.jboss.installer.core.ValidationResult;
import org.jboss.installer.postinstall.task.PortBindingConfig;
import org.jboss.installer.validators.PortConfigurationValidator;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.JTextComponent;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

public abstract class CustomPortConfigurationScreen extends DefaultScreen implements Screen {

    public static final String KEY_PREFIX = "port.config.common.";
    public static final String TITLE_KEY = "port.config.common.title";
    public static final String PORT_KEY = "port.config.common.port";
    public static final String DEFAULT_VALUE_KEY = "port.config.common.default_value";
    public static final String SYSTEM_PROPERTY_KEY = "port.config.common.system_property";
    public static final String AJP_KEY = "port.config.common.ajp";
    public static final String HTTP_KEY = "port.config.common.http";
    public static final String HTTPS_KEY = "port.config.common.https";
    public static final String IIOP_KEY = "port.config.common.iiop";
    public static final String IIOP_SSL_KEY = "port.config.common.iiop-ssl";
    public static final String JGROUPS_MPING_ADDRESS_KEY = "port.config.common.jgroups-mping-address";
    public static final String JGROUPS_MPING_MCAST_PORT_KEY = "port.config.common.jgroups-mping";
    public static final String JGROUPS_TCP_KEY = "port.config.common.jgroups-tcp";
    public static final String JGROUPS_TCP_FD_KEY = "port.config.common.jgroups-tcp-fd";
    public static final String JGROUPS_UDP_KEY = "port.config.common.jgroups-udp";
    public static final String JGROUPS_UDP_FD_KEY = "port.config.common.jgroups-udp-fd";
    public static final String JGROUPS_UDP_ADDRESS_KEY = "port.config.common.jgroups-udp-address";
    public static final String JGROUPS_UDP_MCAST_PORT_KEY = "port.config.common.jgroups-udp-port";
    public static final String MODCLUSTER_ADDRESS_KEY = "port.config.common.modcluster_address";
    public static final String MODCLUSTER_MCAST_PORT_KEY = "port.config.common.modcluster";
    public static final String MANAGEMENT_HTTP_KEY = "port.config.common.management-http";
    public static final String MANAGEMENT_HTTPS_KEY = "port.config.common.management-https";
    public static final String TXN_RECOVERY_ENVIRONMENT_KEY = "port.config.common.txn-recovery-environment";
    public static final String TXN_STATUS_MANAGER_KEY = "port.config.common.txn-status-manager";
    public static final String DISTINCT_PORT_ERROR_KEY = "port.config.common.distinct_port_error";
    public static final String EMPTY_FIELD_ERROR_KEY = "port.config.common.empty_field_error";
    public static final String INVALID_IP_ADDRESS_ERROR_KEY = "port.config.common.invalid_ip_address_error";
    private static final String INVALID_MULTICAST_ADDRESS_ERROR_KEY = "port.config.common.invalid_multicast_address_error";
    public static final List<String> portOnlyBindings = Arrays.asList(AJP_KEY, HTTP_KEY, HTTPS_KEY, IIOP_KEY,
            IIOP_SSL_KEY, JGROUPS_TCP_KEY, JGROUPS_TCP_FD_KEY, JGROUPS_UDP_FD_KEY, MANAGEMENT_HTTP_KEY,
            MANAGEMENT_HTTPS_KEY, TXN_RECOVERY_ENVIRONMENT_KEY, TXN_STATUS_MANAGER_KEY);


    private final PortConfigurationValidator portConfigValidator;
    private final HashMap<String, String> systemPropertyDefaultMap = createDefaultSystemPropertyMap();
    public static final Map<String, String> DEFAULT_PORT_VALUES = createDefaultPortMap();
    private final HashMap<String, JTextField> systemPropertyFieldMap = new HashMap<>();
    private final HashMap<String, JTextField> ipAddressFieldMap = new HashMap<>();
    private final HashMap<String, JFormattedTextField> portFieldMap = new HashMap<>();
    protected List<String> properties;

    public CustomPortConfigurationScreen(Screen parent, LanguageUtils langUtils, boolean isActive) {
        super(parent, langUtils, isActive);
        portConfigValidator = new PortConfigurationValidator();
    }

    @Override
    public JPanel getContent() {
        JPanel finalPanel = new JPanel();
        finalPanel.setLayout(new BorderLayout());
        JLabel subHeader = createSubHeader(getSideNavName());
        finalPanel.add(subHeader, BorderLayout.NORTH);
        finalPanel.add(generateContent(properties));
        return finalPanel;
    }

    @Override
    public JComponent getDefaultFocusComponent() {
        final String firstKey = properties.get(0);
        if (portFieldMap.containsKey(firstKey)) {
            return portFieldMap.get(firstKey);
        } else if (ipAddressFieldMap.containsKey(firstKey)) {
            return ipAddressFieldMap.get(firstKey);
        } else {
            return null;
        }
    }

    private JLabel createSubHeader(String subHeaderName) {
        JLabel subHeader = new JLabel(subHeaderName);
        subHeader.setBorder(BorderFactory.createEmptyBorder(0, 0, 30, 0));
        subHeader.setFont(FontResources.getOpenSansLightSubHeading());
        return subHeader;
    }

    private JComponent generateContent(List<String> ports) {
        JPanel content = new JPanel();
        content.setLayout(new GridBagLayout());
        GridBagConstraints c = new GridBagConstraints();
        setInitialConstraints(c);
        addHeader(content, c);
        for (String portKey : ports) {
            addRow(content, c, portKey);
            c.gridy++;
        }
        c.gridy++;
        c.weighty = 1;
        content.add(Box.createVerticalBox(), c);
        return content;
    }

    private void addHeader(JPanel content, GridBagConstraints c) {
        c.insets = FIELD_ROW_INSET;
        content.add(createFieldLabel(PORT_KEY), c);

        c.insets = SUBSECTION_INSET;
        content.add(createFieldLabel(SYSTEM_PROPERTY_KEY), c);

        content.add(createFieldLabel(DEFAULT_VALUE_KEY), c);
        c.gridy++;
    }

    private void addRow(JPanel content, GridBagConstraints c, String key) {
        c.insets = FIELD_ROW_INSET;
        content.add(createFieldLabel(key), c);

        c.insets = SUBSECTION_INSET;
        JTextField systemPropertyField = createPropertyField(key);

        content.add(systemPropertyField, c);
        systemPropertyFieldMap.put(key, systemPropertyField);

        if (key.contains("address")) {
            JTextField ipAddressField = createIpAddressField(key);
            ipAddressFieldMap.put(key, ipAddressField);
            content.add(ipAddressField, c);
        } else {
            JFormattedTextField portField = createPortField(key);
            portFieldMap.put(key, portField);
            content.add(portField, c);
        }
    }

    private HashMap<String, String> createDefaultSystemPropertyMap() {
        HashMap<String, String> map = new HashMap<>();
        map.put(AJP_KEY, "jboss.ajp.port");
        map.put(HTTP_KEY, "jboss.http.port");
        map.put(HTTPS_KEY, "jboss.https.port");
        map.put(IIOP_KEY, "");
        map.put(IIOP_SSL_KEY, "");
        map.put(JGROUPS_MPING_ADDRESS_KEY, "jboss.default.multicast.address");
        map.put(JGROUPS_MPING_MCAST_PORT_KEY, "");
        map.put(JGROUPS_TCP_KEY, "");
        map.put(JGROUPS_TCP_FD_KEY, "");
        map.put(JGROUPS_UDP_KEY, "");
        map.put(JGROUPS_UDP_FD_KEY, "");
        map.put(JGROUPS_UDP_ADDRESS_KEY, "jboss.default.multicast.address");
        map.put(JGROUPS_UDP_MCAST_PORT_KEY, "");
        map.put(MODCLUSTER_ADDRESS_KEY, "jboss.modcluster.multicast.address");
        map.put(MODCLUSTER_MCAST_PORT_KEY, "");
        map.put(MANAGEMENT_HTTP_KEY, "jboss.management.http.port");
        map.put(MANAGEMENT_HTTPS_KEY, "jboss.management.https.port");
        map.put(TXN_RECOVERY_ENVIRONMENT_KEY, "");
        map.put(TXN_STATUS_MANAGER_KEY, "");
        return map;
    }

    private static Map<String, String> createDefaultPortMap() {
        HashMap<String, String> map = new HashMap<>();
        map.put(AJP_KEY, "8009");
        map.put(HTTP_KEY, "8080");
        map.put(HTTPS_KEY, "8443");
        map.put(IIOP_KEY, "3528");
        map.put(IIOP_SSL_KEY, "3529");
        map.put(JGROUPS_MPING_ADDRESS_KEY, "230.0.0.4");
        map.put(JGROUPS_MPING_MCAST_PORT_KEY, "45700");
        map.put(JGROUPS_TCP_KEY, "7600");
        map.put(JGROUPS_TCP_FD_KEY, "57600");
        map.put(JGROUPS_UDP_KEY, "55200");
        map.put(JGROUPS_UDP_FD_KEY, "54200");
        map.put(JGROUPS_UDP_ADDRESS_KEY, "230.0.0.4");
        map.put(JGROUPS_UDP_MCAST_PORT_KEY, "45688");
        map.put(MODCLUSTER_ADDRESS_KEY, "224.0.1.105");
        map.put(MODCLUSTER_MCAST_PORT_KEY, "23364");
        map.put(MANAGEMENT_HTTP_KEY, "9990");
        map.put(MANAGEMENT_HTTPS_KEY, "9993");
        map.put(TXN_RECOVERY_ENVIRONMENT_KEY, "4712");
        map.put(TXN_STATUS_MANAGER_KEY, "4713");
        return Collections.unmodifiableMap(map);
    }

    private void setInitialConstraints(GridBagConstraints c) {
        c.fill = GridBagConstraints.BOTH;
        c.weightx = 1;
        c.gridwidth = 3;
        c.gridy = 0;
    }

    private JTextField createPropertyField(String key) {
        JTextField field =  createTextField(systemPropertyDefaultMap.get(key));
        if (systemPropertyFieldMap.containsKey(key)) {
            field.setText(systemPropertyFieldMap.get(key).getText());
        }
        return field;
    }

    private JTextField createIpAddressField(String key) {
        JTextField ipField = createTextField(DEFAULT_PORT_VALUES.get(key));
        if (ipAddressFieldMap.containsKey(key)) {
            ipField.setText(ipAddressFieldMap.get(key).getText());
        }
        ipField.getDocument().addDocumentListener(new InvalidValueHandler(ipField,
                (txt)->portConfigValidator.isIpAddressFormat(txt) && portConfigValidator.isMulticastAddress(txt)));
        return ipField;
    }

    private JFormattedTextField createPortField(String key) {
        JFormattedTextField portField = new JFormattedTextField(getPortValueFormatter());
        portField.setFont(FontResources.getOpenSansRegular());
        portField.setText(DEFAULT_PORT_VALUES.get(key));
        if (portFieldMap.containsKey(key)) {
            portField.setText(portFieldMap.get(key).getText());
        }
        portField.getDocument().addDocumentListener(new InvalidValueHandler(portField, (txt)->txt != null && !txt.equals("")));
        UiResources.withScrollBarListener(portField);
        return portField;
    }

    @Override
    public ValidationResult validate() {
        if (!portConfigValidator.portsDistinct(portFieldMap)) {
            return ValidationResult.error(langUtils.getString(DISTINCT_PORT_ERROR_KEY));
        }
        if (portConfigValidator.valueFieldEmpty(portFieldMap, ipAddressFieldMap)) {
            return ValidationResult.error(langUtils.getString(EMPTY_FIELD_ERROR_KEY));
        }
        if (!portConfigValidator.validIpAddresses(ipAddressFieldMap)) {
            return ValidationResult.error(langUtils.getString(INVALID_IP_ADDRESS_ERROR_KEY));
        }
        final Optional<String> invalidMulticast = ipAddressFieldMap.values().stream()
                .map(JTextComponent::getText)
                .filter(key -> !portConfigValidator.isMulticastAddress(key))
                .findFirst();
        if (invalidMulticast.isPresent()) {
            return ValidationResult.error(langUtils.getString(INVALID_MULTICAST_ADDRESS_ERROR_KEY, invalidMulticast.get()));
        }
        return ValidationResult.ok();
    }

    @Override
    public void record(InstallationData installationData) {
        SocketBindingUtils socketBindingUtils = new SocketBindingUtils
                (systemPropertyFieldMap, ipAddressFieldMap, portFieldMap);
        List<PortBindingConfig.SocketBinding> socketBindings = socketBindingUtils.createSocketBindings(properties);
        PortBindingConfig config = installationData.getConfig(PortBindingConfig.class);
        addPortsToConfig(socketBindings, config);
        installationData.putConfig(config);
    }

    @Override
    public void rollback(InstallationData installationData) {
        PortBindingConfig config = installationData.getConfig(PortBindingConfig.class);
        removePortsFromConfig(config);
        installationData.putConfig(config);
    }

    private void addPortsToConfig(List<PortBindingConfig.SocketBinding> socketBindings, PortBindingConfig config) {
        if (getConfigName() == null) {
            config.setManagementInterfacePort(socketBindings.get(0));
        } else {
            config.setPorts(getConfigName(), socketBindings);
        }
    }

    private void removePortsFromConfig(PortBindingConfig config) {
        if (getConfigName() == null) {
            config.removeManagementInterfacePort();
        } else {
            config.removePorts(getConfigName());
        }
    }

    protected abstract String getConfigName();

    @Override
    public abstract String getSideNavName();

    private static class InvalidValueHandler implements DocumentListener {

        private final JTextField field;
        private final Function<String, Boolean> check;
        private final Border defaultBorder;

        InvalidValueHandler(JTextField field, Function<String, Boolean> check) {
            this.field = field;
            this.check = check;
            defaultBorder = field.getBorder();
        }

        private void doCheck() {
            if (check.apply(field.getText())) {
                field.setBorder(defaultBorder);
            } else {
                field.setBorder(new LineBorder(Color.RED));
            }
        }

        @Override
        public void insertUpdate(DocumentEvent e) {
            doCheck();
        }

        @Override
        public void removeUpdate(DocumentEvent e) {
            doCheck();
        }

        @Override
        public void changedUpdate(DocumentEvent e) {
            doCheck();
        }
    }
}
