/*
 * Firebird Open Source J2ee connector - jdbc driver
 *
 * Distributable under LGPL license.
 * You may obtain a copy of the License at http://www.gnu.org/copyleft/lgpl.html
 *
 * This program 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
 * LGPL License for more details.
 *
 * This file was created by members of the firebird development team.
 * All individual contributions remain the Copyright (C) of those
 * individuals.  Contributors to this file are either listed here or
 * can be obtained from a CVS history command.
 *
 * All rights reserved.
 */

package org.firebirdsql.jdbc;

import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.StringTokenizer;

import org.firebirdsql.encodings.EncodingFactory;


/**
 * Manager of the DPB properties.
 */
public class FBDriverPropertyManager {

    private static final String RES = "driver_property_info";
    
    private static ResourceBundle info;
    static {
        try {
            info = ResourceBundle.getBundle(RES);
        } catch(MissingResourceException ex) {
            info = null;
        }
    }

    /**
     * Container class for the driver properties.
     */
    private static class PropertyInfo {
        private String alias;
        private String dpbName;
        private Integer dpbKey;
        private String description;
        
        private int hashCode;
        
        public PropertyInfo(String alias, String dpbName, Integer dpbKey,
                String description) {
            this.alias = alias;
            this.dpbName = dpbName;
            this.dpbKey = dpbKey;
            this.description = description;
            
            hashCode = 17;
            if (alias != null)
                hashCode ^= alias.hashCode();
            
            hashCode ^= dpbName.hashCode();
            hashCode ^= dpbKey.intValue();
        }
        
        public int hashCode() {
            return hashCode;
        }
        
        public boolean equals(Object obj) {
            if (obj == this) return true;
            if (!(obj instanceof PropertyInfo)) return false;
            
            PropertyInfo that = (PropertyInfo)obj;
            
            boolean result = true;
            
            result &= this.alias.equals(that.alias);
            result &= this.dpbName.equals(that.dpbName);
            result &= this.dpbKey.equals(that.dpbKey);
            
            return result;
        }
    }
    
    private static HashMap aliases = new HashMap();
    private static HashMap dpbMap = new HashMap();
    private static HashMap reversedDpbMap = new HashMap();

    static {
        // process aliases and descriptions first
        if (info != null) {
            for (Enumeration en = info.getKeys(); en.hasMoreElements();) {
                String key = (String) en.nextElement();
                String value = info.getString(key);
                
                int hashIndex = value.indexOf('#');
                
                String dpbName;
                String description = "";
                if (hashIndex != -1) {
                    dpbName = value.substring(0, hashIndex).trim();
                    description = value.substring(hashIndex + 1).trim();
                } else
                    dpbName = value.trim();
                
                // skip incorrect mappings
                if (!dpbName.startsWith(FBConnectionHelper.DPB_PREFIX))
                    continue;
                
                Integer dpbKey = FBConnectionHelper.getDpbKey(dpbName);
                
                // skip unknown elements
                if (dpbKey == null)
                    continue;
                
                PropertyInfo propInfo = new PropertyInfo(key, dpbName, 
                    dpbKey, description);
                
                aliases.put(propInfo.alias, propInfo);
                dpbMap.put(propInfo.dpbName, propInfo);
                reversedDpbMap.put(dpbKey, propInfo);
            }
        }
        
        // fill rest of the properties
        Map tempDpbMap = FBConnectionHelper.getDpbMap();
        for (Iterator iter = tempDpbMap.entrySet().iterator(); iter.hasNext();) {
            Map.Entry entry = (Map.Entry) iter.next();
            String dpbName = (String)entry.getKey();
            Integer dpbKey = (Integer)entry.getValue();
            
            if (!dpbName.startsWith(FBConnectionHelper.DPB_PREFIX))
                continue;

            if (dpbMap.containsKey(dpbName))
                continue;

            PropertyInfo propInfo = new PropertyInfo(null, dpbName, dpbKey, "");
            
            dpbMap.put(dpbName, propInfo);
            reversedDpbMap.put(dpbKey, propInfo);
        }
    }
    
    /**
     * Normalize the properties. This method resolves the aliases to their
     * original names. Also it restores the short syntax for the DPB parameters.
     * 
     * @param props instance of {@link Properties} containing original properties.
     * 
     * @return instance of {@link Properties} containing the normalized ones.
     * 
     * @throws SQLException if original properties reference the same DPB 
     * parameter using both alias and original name.
     */
    public static HashMap normalize(String url, Map props) throws SQLException {
        
        HashMap tempProps = new HashMap();
        tempProps.putAll(props);
        
        convertUrlParams(url, tempProps);
        
        HashMap result = new HashMap();
        
        for (Iterator iter = tempProps.entrySet().iterator(); iter.hasNext();) {
            Map.Entry entry = (Map.Entry) iter.next();
            String propName = (String)entry.getKey();
            Object propValue = entry.getValue();
            
            PropertyInfo propInfo = (PropertyInfo)aliases.get(propName);
            
            // check if alias is not used together with original property
            if (propInfo != null) {
                String originalName = propInfo.dpbName;
                String shortName = propInfo.dpbName.substring(
                    FBConnectionHelper.DPB_PREFIX.length());
                
                boolean hasDuplicate = tempProps.keySet().contains(originalName)
                        || tempProps.keySet().contains(shortName);
                
                hasDuplicate &= !propName.equals(shortName);
                hasDuplicate &= !propName.equals(originalName);
                
                if (hasDuplicate)
                    throw new FBSQLException("Specified properties contain " +
                            "reference to a DPB parameter under original and " +
                            "alias names: original name " + propInfo.dpbName + 
                            ", alias : " + propInfo.alias);
            }
            
            // if the specified property is not an alias, check
            // the full list
            if (propInfo == null) {
                String tempKey = propName;
                if (!tempKey.startsWith(FBConnectionHelper.DPB_PREFIX))
                    tempKey = FBConnectionHelper.DPB_PREFIX + tempKey;
                
                propInfo = (PropertyInfo)dpbMap.get(tempKey);
            }
            
            // skip the element if nothing if found
            if (propInfo == null)
                continue;
            
            result.put(propInfo.dpbName, propValue);
        }
        
        handleEncodings(result);
        
        return result;
    }
    
    public static String getCanonicalName(String propertyName) {
        PropertyInfo propInfo = (PropertyInfo)aliases.get(propertyName);
        
        if (propInfo == null) {
            String tempKey = propertyName;
            if (!tempKey.startsWith(FBConnectionHelper.DPB_PREFIX))
                tempKey = FBConnectionHelper.DPB_PREFIX + tempKey;
            
            propInfo = (PropertyInfo)dpbMap.get(tempKey);
        }
        
        if (propInfo == null)
            return propertyName;
        
        return propInfo.dpbName;
    }
    
    /**
     * Extract properties specified as URL parameter into the specified list
     * of properties.
     * 
     * @param url specified URL.
     * 
     * @param info instance of {@link Properties} into which values should
     * be extracted.
     */
    private static void convertUrlParams(String url, HashMap info) {
        if (url == null)
            return;
        
        int iQuestionMark = url.indexOf("?");

        if (iQuestionMark == -1) 
            return;

        String propString = url.substring(iQuestionMark+1);
        
        StringTokenizer st = new StringTokenizer(propString,"&;");
        while(st.hasMoreTokens()) {
            String propertyString = st.nextToken();
            int iIs = propertyString.indexOf("=");
            if(iIs > -1) {
                String property = propertyString.substring(0, iIs);
                String value = propertyString.substring(iIs+1);
                info.put(property,value);
            } else {
                info.put(propertyString, "");
            }
        }
    }
    
    /**
     * Handle character encoding parameters. This method ensures that both
     * java encoding an client connection encodings are correctly set. 
     * Additionally method handles the character translation stuff.
     * 
     * @param info connection properties
     * 
     * @throws SQLException if both isc_dpb_local_encoding and charSet are
     * specified.
     */
    public static void handleEncodings(HashMap info) throws SQLException {
        String iscEncoding = (String)info.get("isc_dpb_lc_ctype");
        String localEncoding = (String)info.get("isc_dpb_local_encoding");
        
        if (iscEncoding != null && localEncoding == null) {
            String javaEncoding = FBConnectionHelper.getJavaEncoding(iscEncoding);
            
            if (javaEncoding != null)
                info.put("isc_dpb_local_encoding", javaEncoding);
        }
        
        if (iscEncoding == null && localEncoding != null) {
            iscEncoding = FBConnectionHelper.getIscEncoding(localEncoding); 
            info.put("isc_dpb_lc_ctype", iscEncoding);
        }
        
        // ensure that we fail before any connection is obtained
        // in case when incorrect mapping path is specified 
        // (note, EncodingFactory.getEncoding(String, String) throws exception)
        String mappingPath = (String)info.get("isc_dpb_mapping_path");
        if (mappingPath != null) {
            EncodingFactory.getEncoding(localEncoding, mappingPath);
        }
    }
    
    /**
     * Get property information for the specified properties.
     * 
     * @param props instance of {@link Properties}.
     * 
     * @return array of {@link DriverPropertyInfo} instances.
     */
    public static DriverPropertyInfo[] getDriverPropertyInfo(Properties props) {
        ArrayList result = new ArrayList();
       
        for (Iterator iter = props.entrySet().iterator(); iter.hasNext();) {
            Map.Entry entry = (Map.Entry) iter.next();
            String propName = (String)entry.getKey();
            Object propValue = entry.getValue();
            
            PropertyInfo propInfo = (PropertyInfo)aliases.get(propName);
            
            // if the specified property is not an alias, check
            // the full list
            if (propInfo == null) {
                String tempKey = propName;
                if (!tempKey.startsWith(FBConnectionHelper.DPB_PREFIX))
                    tempKey = FBConnectionHelper.DPB_PREFIX + tempKey;
                
                propInfo = (PropertyInfo)dpbMap.get(tempKey);
            }
            
            DriverPropertyInfo driverPropInfo = new DriverPropertyInfo(
                    propName, propValue != null ? propValue.toString() : "");

            if (propInfo != null)
                driverPropInfo.description = propInfo.description;
            
            result.add(driverPropInfo);
        
        }
        
        return (DriverPropertyInfo[])result.toArray(
            new DriverPropertyInfo[result.size()]);
    }
}
