/*
 * Copyright (c) 2000-2009 TeamDev Ltd. All rights reserved.
 * TeamDev PROPRIETARY and CONFIDENTIAL.
 * Use is subject to license terms.
 */
package com.jniwrapper.win32.registry;

import com.jniwrapper.*;
import com.jniwrapper.util.Enums;
import com.jniwrapper.util.Logger;

import java.util.*;

/**
 * This class provides access to values of a registry key.
 * A value can be obtained via the {@link #get(Object key)}. To get a default value for a key,
 * pass an empty string to {@link #get(Object key)} method.
 * For example, the following code obtains the command for the default browser:<br>
 * <pre>
 * RegistryKey rkey = RegistryKey.CLASSES_ROOT.openSubKey("http\\shell\\open\\command");
 * String browserCommand = rkey.values().get("").toString();
 * </pre>
 *
 * @author Serge Piletsky
 */
public class RegistryKeyValues implements Map
{
    private static final Logger LOG = Logger.getInstance(RegistryKeyValues.class);

    private RegistryKey _registryKey;

    private static final Map TYPE_ASSOCIATIONS = new HashMap();
    private static final List BACK_TYPE_ASSOCIATIONS = new ArrayList();

    static
    {
        // order does matter
        registerAssociation(RegistryKeyType.DWORD, RegistryValueTransformer.INTEGER_TRANSFORMER);
        registerAssociation(RegistryKeyType.SZ, RegistryValueTransformer.STRING_TRANSFORMER);
        registerAssociation(RegistryKeyType.BINARY, RegistryValueTransformer.BINARY_TRANSFORMER);
        registerAssociation(RegistryKeyType.EXPAND_SZ, RegistryValueTransformer.STRING_TRANSFORMER);
        registerAssociation(RegistryKeyType.QWORD, RegistryValueTransformer.QWORD_TRANSFORMER);
        registerAssociation(RegistryKeyType.RESOURCE_LIST, RegistryValueTransformer.BINARY_TRANSFORMER);
        registerAssociation(RegistryKeyType.FULL_RESOURCE_DESCRIPTOR, RegistryValueTransformer.BINARY_TRANSFORMER);
        registerAssociation(RegistryKeyType.RESOURCE_REQUIREMENTS_LIST, RegistryValueTransformer.BINARY_TRANSFORMER);
        registerAssociation(RegistryKeyType.MULTI_SZ, RegistryValueTransformer.MULTISTRING_TRANSFORMER);
        registerAssociation(RegistryKeyType.NONE, RegistryValueTransformer.BINARY_TRANSFORMER);
    }

    /**
     * Associates a value transformer with a specified registry type.
     *
     * @param type        is a registry value type.
     * @param transformer is a custom transformer.
     */
    public static void registerAssociation(RegistryKeyType type, RegistryValueTransformer transformer)
    {
        TYPE_ASSOCIATIONS.put(type, transformer);
        BACK_TYPE_ASSOCIATIONS.add(new Pair(transformer, type));
    }

    public RegistryKeyValues(RegistryKey registryKey)
    {
        _registryKey = registryKey;
    }

    private Map loadWholeMap()
    {
        Map entries = new HashMap();
        for (int i = 0, errorCode = RegistryKey.NO_ERROR; errorCode == RegistryKey.NO_ERROR; i++)
        {
            Str valueName = new Str("", RegistryKey.MAX_PATH);
            UInt32 type = new UInt32();
            UInt32 size = new UInt32();
            errorCode = (int)WinRegistry.enumValue(_registryKey, i, valueName, new UInt32(RegistryKey.MAX_PATH), type, null, size);
            if (errorCode == RegistryKey.NO_ERROR)
            {
                final RegistryKeyType registryKeyType = (RegistryKeyType)Enums.getItem(RegistryKeyType.class, (int)type.getValue());
                final String valName = valueName.getValue();
                final RegistryValueEntry registryValueEntry =
                        new RegistryValueEntry(valName, registryKeyType, (int)size.getValue());
                entries.put(valName, registryValueEntry);
            }
        }
        return entries;
    }

    public int size()
    {
        UInt32 valueCount = new UInt32();
        try
        {
            _registryKey.checkError(WinRegistry.queryInfoKey(_registryKey, (Str)null, null, null, null, null, valueCount, null, null));
        }
        catch (Exception e)
        {
            return 0;
        }
        return (int) valueCount.getValue();
    }

    public boolean isEmpty()
    {
        return size() == 0;
    }

    public boolean containsKey(Object key)
    {
        String keyName = (String) key;
        final long res = WinRegistry.getValueInfo(_registryKey, keyName, new Int32(0), new Int32(0));
        return res != RegistryKey.ERROR_FILE_NOT_FOUND;
    }

    public boolean containsValue(Object value)
    {
        return values().contains(value);
    }

    public Object get(Object key)
    {
        String keyName = (String) key;

        try
        {
            final Int32 type = new Int32(0);
            final Int32 size = new Int32(0);
            _registryKey.checkError(WinRegistry.getValueInfo(_registryKey, keyName, type, size));

            final int typeId = (int) type.getValue();

            final RegistryKeyType keyType = new RegistryKeyType(typeId);
            final RegistryValueTransformer valueTransformer = getValueTransformer(keyType);
            final Parameter valueParameter = valueTransformer.createRegistryValueParameter((int) size.getValue());

            _registryKey.checkError(WinRegistry.getValue(_registryKey, keyName, typeId, valueParameter));
            return valueTransformer.fromRegistryValue(valueParameter);
        }
        catch (RegistryException ex)
        {
            return null; // Means key not found
        }
    }

    /**
     * Get the type of specified value
     *
     * @param key name of registry value
     * @return type of registry value 
     */
    public RegistryKeyType getType(Object key)
    {
        String keyName = (String) key;

        final Int32 type = new Int32(0);
        final Int32 size = new Int32(0);
        _registryKey.checkError(WinRegistry.getValueInfo(_registryKey, keyName, type, size));
        final int typeId = (int) type.getValue();

        return new RegistryKeyType(typeId);
    }

    public Object put(Object key, Object value)
    {
        return putImpl(key, value, null);
    }

    public Object put(Object key, Object value, RegistryKeyType valueType)
    {
        if (valueType == null)
        {
            throw new IllegalArgumentException("valueType can not be Null");
        }
        return putImpl(key, value, valueType);
    }

    private Object putImpl(Object key, Object value, RegistryKeyType valueType)
    {
        Object prevValue = get(key);

        String keyName = (String) key;

        if (valueType == null)
        {
            valueType = getValueType(value);
        }
        RegistryValueTransformer transformer = getValueTransformer(valueType);
        Parameter result = transformer.toRegistryValue(value);
        _registryKey.checkError(WinRegistry.setValue(_registryKey, keyName, valueType.getValue(), result, result.getLength()));

        return prevValue;
    }

    public Object remove(Object key)
    {
        Object prevValue = get(key);

        WinRegistry.deleteValue(_registryKey, key.toString());

        return prevValue;
    }

    public void putAll(Map t)
    {
        for (Iterator i = t.entrySet().iterator(); i.hasNext();)
        {
            Entry entry = (Entry)i.next();
            put(entry.getKey(), entry.getValue());
        }
    }

    public void clear()
    {
        for (Iterator i = keySet().iterator(); i.hasNext();)
        {
            String name = (String)i.next();
            _registryKey.checkError(WinRegistry.deleteValue(_registryKey, name));
        }
    }

    public Set keySet()
    {
        Map cached = loadWholeMap();
        return cached.keySet();
    }

    public Collection values()
    {
        Map cached = loadWholeMap();

        final int size = size();
        List result = new ArrayList(size);
        for (Iterator i = cached.values().iterator(); i.hasNext();)
        {
            Entry entry = (Entry)i.next();
            final Object value = entry.getValue();
            result.add(value);
        }
        return result;
    }

    public Set entrySet()
    {
        Map cached = loadWholeMap();
        return cached.entrySet();
    }

    public List getEntries()
    {
        Map cached = loadWholeMap();
        return Collections.list(Collections.enumeration(cached.values()));
    }

    private static RegistryValueTransformer getValueTransformer(RegistryKeyType type)
    {
        final RegistryValueTransformer res = (RegistryValueTransformer) TYPE_ASSOCIATIONS.get(type);
        if (res == null)
        {
            throw new IllegalArgumentException("Unable to get value transformer for given value type: " + type);
        }
        return res;
    }

    private static RegistryKeyType getValueType(Object value)
    {
        RegistryKeyType result = RegistryKeyType.NONE;
        for (Iterator i = BACK_TYPE_ASSOCIATIONS.iterator(); i.hasNext();)
        {
            Pair pair = (Pair)i.next();
            RegistryValueTransformer transformer = (RegistryValueTransformer) pair.getKey();
            if (transformer.isTypeSupported(value))
            {
                result = (RegistryKeyType) pair.getValue();
                break;
            }
        }
        return result;
    }
    
    // TODO [Sanders]: Why cannot have this class private?
    public class RegistryValueEntry implements Map.Entry
    {
        private String _name;
        private RegistryKeyType _type = RegistryKeyType.NONE;
        private int _size;
        private Object _value;

        public RegistryValueEntry(String name, RegistryKeyType type, int size)
        {
            _name = name;
            _type = type;
            _size = size;
        }

        public RegistryValueEntry(String name)
        {
            _name = name;
        }

        public Object getKey()
        {
            return _name;
        }

        public Object getValue()
        {
            if (_value == null)
            {
                if (!_registryKey.isNull())
                {
                    final RegistryKeyType type = getType();
                    RegistryValueTransformer transformer = getValueTransformer(type);
                    _value = transformer.createRegistryValueParameter(_size);
                    _registryKey.checkError(WinRegistry.getValue(_registryKey, _name, type.getValue(), (Parameter)_value));
                    _value = transformer.fromRegistryValue((Parameter)_value);
                }
            }
            return _value;
        }

        public Object setValue(Object value)
        {
            final RegistryKeyType type = getType();
            RegistryValueTransformer transformer = getValueTransformer(type);
            Parameter result = transformer.toRegistryValue(value);
            _registryKey.checkError(WinRegistry.setValue(_registryKey, _name, type.getValue(), result, result.getLength()));
            _value = value;
            return result;
        }

        public RegistryKeyType getType()
        {
            return _type;
        }

        public void setType(RegistryKeyType type)
        {
            _type = type;
        }

        public int getSize()
        {
            return _size;
        }

        public String toString()
        {
            StringBuffer result = new StringBuffer("RegistryValueEntry [Name=");
            result.append(_name).append(";type=").append(_type).append(']');
            return result.toString();
        }
    }

    private static final class Pair
    {
        private Object key;
        private Object value;

        public Pair(Object key, Object value)
        {
            this.key = key;
            this.value = value;
        }

        public Object getKey()
        {
            return key;
        }

        public Object getValue()
        {
            return value;
        }
    }
}