/*
 * 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.netbeans.modules.payara.jakartaee.db;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.netbeans.modules.glassfish.javaee.db.JDBCConnectionPool;
import org.netbeans.modules.glassfish.javaee.db.JDBCResource;
import org.netbeans.modules.payara.tooling.TaskState;
import org.netbeans.modules.payara.tooling.admin.CommandGetProperty;
import org.netbeans.modules.payara.tooling.admin.ResultMap;
import org.netbeans.modules.payara.tooling.data.PayaraServer;
import org.netbeans.modules.payara.tooling.data.PayaraPlatformVersionAPI;
import org.netbeans.modules.payara.tooling.utils.StringPrefixTree;
import org.netbeans.modules.j2ee.deployment.common.api.Datasource;

/**
 * Reads data sources from Payara server.
 * <p/>
 * @author Tomas Kraus
 */
public class DataSourcesReader {

    ////////////////////////////////////////////////////////////////////////////
    // Inner classes                                                          //
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Payara server <code>resource</code> property type.
     */
    private static enum ResourceType {
        /** Unknown resource type. */
        UNKNOWN,

        /** JDBC connection pool. */
        CONNECTION_POOL,

        /** JDBC data source. */
        DATA_SOURCE;

        /** A <code>String</code> representation of UNKNOWN value. */
        private static final String UNKNOWN_STR = "UNKNOWN";

        /** A <code>String</code> representation of CONNECTION_POOL value. */
        private static final String CONNECTION_POOL_STR
                = "jdbc-connection-pool";

        /** A <code>String</code> representation of DATA_SOURCE value. */
        private static final String DATA_SOURCE_STR = "jdbc-resource";

        /** Stored <code>String</code> values for backward <code>String</code>
         *  conversion. */
        private static final StringPrefixTree<ResourceType> stringValues
                = new StringPrefixTree<>(false);

        static {
            stringValues.add(CONNECTION_POOL.toString(), CONNECTION_POOL);
            stringValues.add(DATA_SOURCE.toString(), DATA_SOURCE);
        }

        /**
         * Returns a <code>ResourceType</code> with a value represented
         * by the specified <code>String</code>.
         * <p/>
         * The <code>ResourceType</code> returned represents existing value
         * only if specified <code>String</code> matches any <code>String</code>
         * returned by <code>toString</code> method. Otherwise <code>null</code>
         * value is returned.
         * <p>
         * @param name Value containing <code>ResourceType</code>
         *             <code>toString</code> representation.
         * @return <code>ResourceType</code> value represented
         *         by <code>String</code> or <code>null</code> if value
         *         was not recognized.
         */
        public static ResourceType toValue(final String name) {
            if (name != null) {
                ResourceType type = stringValues.match(name.toLowerCase());
                return type != null ? type : UNKNOWN;
            } else {
                return null;
            }
        }

        /**
         * Convert <code>ResourceType</code> value to <code>String</code>.
         * <p/>
         * @return A <code>String</code> representation of the value
         *         of this object.
         */
        @Override
        public String toString() {
            switch (this) {
                case UNKNOWN:         return UNKNOWN_STR;
                case CONNECTION_POOL: return CONNECTION_POOL_STR;
                case DATA_SOURCE:     return DATA_SOURCE_STR;
                // This is unrecheable. Being here means this class does not handle
                // all possible values correctly.
                default:
                    throw new IllegalStateException("Invalid ResourceType value");
            }
        }

    }

    ////////////////////////////////////////////////////////////////////////////
    // Class attributes                                                       //
    ////////////////////////////////////////////////////////////////////////////

    /** Payara server JDBC data sources properties search pattern. */
    public static final String DATA_SOURCES_PATTERN = "resources.*";

    /** Payara server property key split pattern. */
    public static final String PROPERTY_SPLIT_PATTERN = "\\.";

    /** Payara server resource property identifier. */
    public static final String PROPERTY_IDENT = "property";

    /** Default JDBC data source registered in GF. */
    static final String DEFAULT_DATA_SOURCE = "jdbc/__default";

    /** JavaEE 7 new default data source linked to old default data source. */
    static final String DEFAULT_DATA_SOURCE_EE7
            = "java:comp/DefaultDataSource";

    ////////////////////////////////////////////////////////////////////////////
    // Instance attributes                                                    //
    ////////////////////////////////////////////////////////////////////////////

    /** Payara server to read data sources from. */
    private final PayaraServer server;

    ////////////////////////////////////////////////////////////////////////////
    // Constructors                                                           //
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Creates an instance of Payara server data sources reader.
     * <p/>
     * @param server Payara server to read data sources from.
     */
    public DataSourcesReader(final PayaraServer server) {
        this.server = server;
    }

    ////////////////////////////////////////////////////////////////////////////
    // Methods                                                                //
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Retrieve data sources from Payara server.
     * <p/>
     * @return Data sources from Payara server or <code>null</code>
     *         when retrieving of data sources failed.
     */
    public Set<Datasource> getDataSourcesFromServer() {
        Map<String, String> properties = getPropertiesFromServer();
        if (properties != null) {
            Map<String, JDBCConnectionPool> pools = new HashMap<>();
            Map<String, JDBCResource> resources = new HashMap<>();
            buildJDBCContentObjects(properties, pools, resources);
            assignConnectionPoolsToResources(pools, resources);
            // Add Java EE 7 comp/DefaultDataSource data source (since PF 4).
            if (server.getPlatformVersion().isEE7Supported()) {
                addNewJavaEE7dataSource(resources);
            }
            return new HashSet<>(resources.values());
        } else {
            return null;
        }
    }

    /**
     * Build JDBC connection pools and resources content objects
     * from server properties.
     * <p/>
     * @param properties Server <code>resources.*</code> properties map.
     * @param pools      Existing JDBC connection pools map.
     * @param resources  Existing JDBC resources map.
     */
    private void buildJDBCContentObjects(final Map<String, String> properties,
            final Map<String, JDBCConnectionPool> pools,
            final Map<String, JDBCResource> resources) {
        for (Map.Entry<String, String> entry : properties.entrySet()) {
            String key = entry.getKey();
            if (key != null && key.length() > 0) {
                String[] elements = key.split(PROPERTY_SPLIT_PATTERN);
                if (elements.length >= 4) {
                    JDBCConnectionPool.PropertyType propertyType;
                    ResourceType type = ResourceType.toValue(elements[1]);
                    switch (type) {
                        case CONNECTION_POOL:
                            buildJDBCConnectionPool(
                                    pools, elements, entry.getValue());
                            break;
                        case DATA_SOURCE:
                            buildJDBCResource(
                                    resources, elements, entry.getValue());
                            break;
                    }
                }
            }
        }
    }

    /**
     * Assign JDBC connection pools to resources (data sources).
     * <p/>
     * @param pools          Existing JDBC connection pools map.
     * @param resources      Existing JDBC resources map.
     */
    private void assignConnectionPoolsToResources(
            final Map<String, JDBCConnectionPool> pools,
            final Map<String, JDBCResource> resources) {
        for (Map.Entry<String, JDBCResource> entry : resources.entrySet()) {
            JDBCResource resource = entry.getValue();
            if (resource != null && resource.getPoolName() != null) {
                resource.setPool(pools.get(resource.getPoolName()));
            }
        }
    }

    /**
     * Add new Java EE 7 <code>comp/DefaultDataSource</code> data source as old
     * default <code>jdbc/__default</code> data source clone.
     * <p/>
     * Data source will be added only if <code>jdbc/__default</code> data source
     * exists and Java EE 7 new data source is not registered yet. Old data
     * source must be already fully initialized and linked with connection pool.
     * <p/>
     * @param resources Existing JDBC resources map.
     */
    private void addNewJavaEE7dataSource(
            final Map<String, JDBCResource> resources) {
        JDBCResource defaultResource = resources.get(DEFAULT_DATA_SOURCE);
        if (defaultResource != null
                && !resources.containsKey(DEFAULT_DATA_SOURCE_EE7)) {
            resources.put(DEFAULT_DATA_SOURCE_EE7,
                    defaultResource.copy(DEFAULT_DATA_SOURCE_EE7));
        }
    }

    /**
     * Process one step of building <code>Map</code> of JDBC connection pool
     * content objects.
     * <p/>
     * @param pools          Existing JDBC connection pools map.
     * @param elements       Payara server JDBC connection pool property key
     *                       elements.
     * @param attributeValue Payara server JDBC connection pool property
     *                       value.
     */
    private void buildJDBCConnectionPool(
            final Map<String, JDBCConnectionPool> pools,
            final String[] elements, final String attributeValue) {
        JDBCConnectionPool.PropertyType propertyType = null;
        final String name = elements[2];
        switch(elements.length) {
            case 4:
                propertyType = null;
                break;
            case 5:
                if (PROPERTY_IDENT.equalsIgnoreCase(elements[3])) {
                    propertyType = JDBCConnectionPool
                            .PropertyType.toValue(elements[4]);
                } else {
                    propertyType = null;
                }
                break;
        }
        if (propertyType != null
                && propertyType != JDBCConnectionPool.PropertyType.UNKNOWN) {
            JDBCConnectionPool pool = getJDBCConnectionPool(pools, name);
            pool.setProperty(propertyType, attributeValue);
        }
        
    }

    /**
     * Process one step of building <code>Map</code> of JDBC resource
     * content objects.
     * <p/>
     * @param resources      Existing JDBC resources map.
     * @param elements       Payara server JDBC resource property key
     *                       elements.
     * @param attributeValue Payara server JDBC resource property value.
     */
     private void buildJDBCResource(final Map<String, JDBCResource> resources,
             final String[] elements, final String attributeValue) {
         JDBCResource.AttrType attrType = null;
         final String name = elements[2];
         if (elements.length == 4) {
             attrType = JDBCResource.AttrType.toValue(elements[3]);
         }
         if (attrType != null && attrType != JDBCResource.AttrType.UNKNOWN) {
             JDBCResource resource = getJDBCResource(resources, name);
             resource.setProperty(attrType, attributeValue);
         }
     }

     /**
     * Get existing JDBC connection pool content object from pools
     * <code>Map</code> or create new one and put it into pools <code>Map</code>
     * when no such object exists.
     * <p/>
     * @param pools Existing JDBC connection pools map.
     * @param name  JDBC connection pool name (<code>Map</code> key).
     * @return JDBC connection pool content object. Never returns
     *         <code>null</code>.
     */
    private JDBCConnectionPool getJDBCConnectionPool(
            final Map<String, JDBCConnectionPool> pools, final String name) {
        JDBCConnectionPool pool = pools.get(name);
        if (pool == null) {
            pool = new JDBCConnectionPool();
            pools.put(name, pool);
        }
        return pool;
    }

    /**
     * Get existing JDBC resources content object from resources
     * <code>Map</code> or create new one and put it into resources
     * <code>Map</code> when no such object exists.
     * <p/>
     * @param pools Existing JDBC resources map.
     * @param name  JDBC resource name (<code>Map</code> key).
     * @return JDBC resource content object. Never returns <code>null</code>.
     */
    private JDBCResource getJDBCResource(
            final Map<String, JDBCResource> resources, final String name) {
        JDBCResource resource = resources.get(name);
        if (resource == null) {
            resource = new JDBCResource();
            resources.put(name, resource);
        }
        return resource;
    }

    /**
     * Retrieve data sources properties from Payara server.
     * <p/>
     * @return Data sources properties from Payara server
     *         or <code>null</code> when retrieving using server
     *         <code>get</code> administration command failed.
     */
    private Map<String, String> getPropertiesFromServer() {
        ResultMap<String, String> result = CommandGetProperty
                .getProperties(server, DATA_SOURCES_PATTERN);
        if (result != null && result.getState() == TaskState.COMPLETED) {
            return result.getValue();
        } else {
            return null;
        }
    }

}
