/*
 * 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.apache.empire.jsf2.utils;

import java.io.Serializable;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Hashtable;
import java.util.Locale;

import org.apache.empire.commons.DateUtils;
import org.apache.empire.commons.StringUtils;
import org.apache.empire.db.DBRowSet;
import org.apache.empire.exceptions.InternalException;
import org.apache.empire.exceptions.InvalidArgumentException;
import org.apache.empire.exceptions.UnexpectedReturnValueException;
import org.apache.empire.jsf2.pages.PageDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class manages request parameters in a way that they cannot be analyzed and modified by the user 
 * @author doebele
 *
 */
public class ParameterMap implements Serializable
{
    private static final long             serialVersionUID = 1L;

    private static final Logger           log              = LoggerFactory.getLogger(ParameterMap.class);

    private static final SimpleDateFormat dateFormat       = new SimpleDateFormat("yyyy.MM.dd hh:mm:ss", Locale.GERMAN);
    
    static private MessageDigest          md5              = null;
    {
        try
        {
            ParameterMap.md5 = MessageDigest.getInstance("MD5");
        }
        catch (NoSuchAlgorithmException e)
        {
            ParameterMap.log.error("MessageDigest NoSuchAlgorithmException.", e);
            throw new InternalException(e);
        }
    }
    
    private final byte[] salt;

    protected Hashtable<String, String> codeMap = new Hashtable<String, String>();

    protected final Hashtable<String, Hashtable<String, Object>> typeMap = new Hashtable<String, Hashtable<String, Object>>();
    
    public ParameterMap()
    {
        String dateTime = dateFormat.format(DateUtils.getTimeNow());
        salt = dateTime.getBytes();
    }
    
    public synchronized String encodeString(String valueAsString)
    {
        if (valueAsString==null)
            throw new InvalidArgumentException("valueAsString", valueAsString);
        // log
        if (log.isTraceEnabled())
            log.trace("Generating code for value {}.", valueAsString);
        // generate code
        md5.reset();
        if (salt!=null)
            md5.update(salt);
        md5.update(valueAsString.getBytes());
        byte s[] = ParameterMap.md5.digest();
        StringBuilder hash = new StringBuilder(32);
        for (int i = 0; i < s.length; i++)
        {   // add the hash part
            // String check = Integer.toHexString((0x000000ff & s[i]) | 0xffffff00).substring(6);;
            String part = Integer.toHexString(0x000000ff & s[i]);
            switch(part.length())
            {
                case 1: hash.append('0');
                case 2: hash.append(part);
                        break;
                default:
                        throw new UnexpectedReturnValueException(part, "Integer.toHexString");
                        // hash.append(part.substring(2));
            }
        }
        return hash.toString();
    }

    public String encodeStringWithCache(String valueAsString)
    {
        String code = codeMap.get(valueAsString);
        if (code==null)
        {   // generate code
            code = encodeString(valueAsString);
            codeMap.put(valueAsString, code);
        }
        return code;
    }

    /**
     * gets a unique name for a given rowset  
     * @param rowset
     * @return a unique name for the given rowset
     */
    protected String getRowSetTypeName(DBRowSet rowset)
    {
        /*
         * alternatively use:
         *     rowset.getName();
         * or
         *     rowset.getFullName();
         */
        return rowset.getClass().getName();
    }

    /**
     * puts an object into the parameter map
     * @param typeName
     * @param id
     * @param value
     */
    protected void putValue(String typeName, String encodedId, Object item)
    {   // put in Table
        if (encodedId==null)
        {
            throw new InvalidArgumentException("encodedId", encodedId);
        }
        Hashtable<String, Object> map = typeMap.get(typeName);
        if (map==null)
        {   map = new Hashtable<String, Object>(1);
            typeMap.put(typeName, map);
        }
        map.put(encodedId, item);
    }
    
    /**
     * encodes the objectKey and stores the item in the parameter map
     * @param typeName
     * @param id
     * @param value
     */
    protected String encodeAndStore(String typeName, String objectKey, Object item, boolean useCache)
    {   // Generate the id
        String encodedId = (useCache ? encodeStringWithCache(objectKey) : encodeString(objectKey));
        // store
        putValue(typeName, encodedId, item);
        // return id
        return encodedId;
    }

    public String put(String type, String key, boolean useCache)
    {
        // Generate id and put in map
        return encodeAndStore(type, key, key, useCache);
    }

    /**
     * Puts an object into the paramter map that implements the ParameterObject interface  
     * @param paramObject
     * @return
     */
    public String put(ParameterObject paramObject)
    {
        String objectKey;
        // check param
        if (paramObject==null || StringUtils.isEmpty((objectKey=paramObject.getObjectKey())))
            throw new InvalidArgumentException("paramObject", paramObject);
        // Generate id and put in map
        String type = paramObject.getClass().getName();
        return encodeAndStore(type, objectKey, paramObject, false);
    }

    public String put(DBRowSet rowset, Object[] key)
    {
        // Generate id and put in map
        String rowKey = StringUtils.valueOf(key);
        String type = getRowSetTypeName(rowset);
        return encodeAndStore(type, rowKey, key, false);
    }

    /*
     * do we really need this?
     * 
    public String put(Class<? extends Object> c, Object[] key)
    {
        // Generate id and put in map
        String ref = StringUtils.valueOf(key);
        String type = c.getName();
        return encodeAndStore(type, ref, key, false);
    }
    */
    
    /**
     * Generates an idParam which is only valid for the given page.
     * @param targetPage
     * @param rowset
     * @param key
     * @return
     */
    public String put(PageDefinition targetPage, DBRowSet rowset, Object[] key) {
        // Generate id and put in map
        String ref  = StringUtils.valueOf(key);
        String type = targetPage.getPageBeanName() + "$" + getRowSetTypeName(rowset);
        return encodeAndStore(type, ref, key, false);
    }

    /**
     * Gets an object from the parameter map for a given type and id
     * @param type the object type (typically the class name)
     * @param id the encoded idParam
     * @return the object
     */
    public Object get(String type, String id)
    {
        Hashtable<String, Object> map = typeMap.get(type);
        return (map!=null ? map.get(id) : null);
    }

    public void clear(String type)
    {
        Hashtable<String, Object> map = typeMap.get(type);
        if (map!=null)
            map.clear();
    }

    /**
     * Puts an object into the paramter map that implements the ParameterObject interface  
     * @param paramType
     * @param id
     * @return
     */
    @SuppressWarnings("unchecked")
    public <T extends ParameterObject> T get(Class<T> paramType, String id)
    {
        String type = paramType.getName();
        Hashtable<String, Object> map = typeMap.get(type);
        return (T)(map!=null ? map.get(id) : null);
    }

    public void clear(Class<? extends ParameterObject> paramType)
    {
        String type = paramType.getName();
        clear(type);
    }

    /*
     * do we really need this?
     * 
    public Object[] getKey(Class<? extends Object> c, String id)
    {
        String type = c.getName();
        Hashtable<String, Object> map = typeMap.get(type);
        return (map!=null ? ((Object[])map.get(id)) : null);
    }
    */

    public Object[] getKey(DBRowSet rowset, String id)
    {
        String type = getRowSetTypeName(rowset);
        Hashtable<String, Object> map = typeMap.get(type);
        return (map!=null ? ((Object[])map.get(id)) : null);
    }

    public void clear(DBRowSet rowset)
    {
        String type = getRowSetTypeName(rowset);
        clear(type);
    }

    /**
     * returns an record key for a given page
     * @param page
     * @param rowset
     * @param id
     * @return
     */
    public Object[] getKey(PageDefinition page, DBRowSet rowset, String id)
    {
        String type = page.getPageBeanName() + "$" + getRowSetTypeName(rowset);
        Hashtable<String, Object> map = typeMap.get(type);
        return (map!=null ? ((Object[])map.get(id)) : null);
    }
}
