/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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 com.github.rjeschke.weel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map.Entry;
/**
* Weel map implementation.
*
* @author René Jeschke <rene_jeschke@yahoo.de>
*/
public final class ValueMap
{
/** Integer keys. */
private HashMap<Integer, Integer> intKeys = new HashMap<Integer, Integer>();
/** String keys. */
private HashMap<String, Integer> strKeys = new HashMap<String, Integer>();
/** The size. */
int size;
/** Is this map ordered? */
boolean ordered = true;
/** The data. */
ArrayList<Value> data;
/** The keys. */
ArrayList<Value> keys;
/** The highest integer key value for unordered maps. */
int highestIntKey;
/**
* Creates a new ValueMap.
*/
public ValueMap()
{
this.data = new ArrayList<Value>();
this.keys = new ArrayList<Value>();
}
/**
* Gets the size of this map.
*
* @return The size.
*/
public int size()
{
return this.size;
}
/**
* Get the value at the given index.
*
* @param index
* The index.
* @return The value or a Value of type NULL.
* @throws WeelException
* If the index is invalid.
*/
public Value get(final Value index)
{
final Value out = new Value();
this.get(index, out);
return out;
}
/**
* Get the value at the given index.
*
* @param index
* The index.
* @return The value or a Value of type NULL.
* @throws WeelException
* If the index is invalid.
*/
public Value get(final int index)
{
if (this.ordered)
{
return index >= 0 && index < this.size ? this.data.get(index)
.clone() : new Value();
}
return this.get(new Value(index));
}
/**
* Get the value at the given index.
*
* @param index
* The index.
* @param out
* The output Value.
* @throws WeelException
* If the index is invalid.
* @return out.
*/
public Value get(final int index, final Value out)
{
if (this.ordered)
{
if (index >= 0 && index < this.size)
this.data.get(index).copyTo(out);
else
out.setNull();
}
else
{
final Integer idx2 = this.intKeys.get(index);
if (idx2 != null)
this.data.get(idx2).copyTo(out);
else
out.setNull();
}
return out;
}
/**
* Get the value at the given index.
*
* @param index
* The index.
* @return The value or a Value of type NULL.
* @throws WeelException
* If the index is invalid.
*/
public Value get(final String index)
{
return this.get(index, new Value());
}
/**
* Get the value at the given index.
*
* @param index
* The index.
* @param out
* The output Value.
* @return out.
* @throws WeelException
* If the index is invalid.
*/
public Value get(final String index, final Value out)
{
if (this.ordered)
{
out.setNull();
}
else
{
final Integer idx2 = this.strKeys.get(index);
if (idx2 != null)
this.data.get(idx2).copyTo(out);
else
out.setNull();
}
return out;
}
/**
* Gets the value at the given index.
*
* @param index
* The index.
* @param out
* The output Value.
* @throws WeelException
* If the index is invalid.
*/
public void get(final Value index, final Value out)
{
if (index.type == ValueType.NUMBER)
{
final int idx = (int) index.number;
if (this.ordered)
{
if (idx < 0 || idx >= this.size)
out.setNull();
else
this.data.get(idx).copyTo(out);
}
else
{
final Integer idx2 = this.intKeys.get(idx);
if (idx2 != null)
this.data.get(idx2).copyTo(out);
else
out.setNull();
}
}
else if (index.type == ValueType.STRING)
{
final Integer idx = this.strKeys.get(index.object);
if (idx != null)
this.data.get(idx).copyTo(out);
else
out.setNull();
}
else
{
throw new WeelException("Illegal map index type: " + index.type);
}
}
/**
* Check if this map contains the given key.
*
* @param key
* The key
* @return <code>true</code> if it contains the given key.
*/
public boolean hasKey(final String key)
{
return this.hasKey(new Value(key));
}
/**
* Check if this map contains the given key.
*
* @param key
* The key
* @return <code>true</code> if it contains the given key.
*/
public boolean hasKey(final int key)
{
return this.hasKey(new Value(key));
}
/**
* Check if this map contains the given key.
*
* @param key
* The key
* @return <code>true</code> if it contains the given key.
*/
public boolean hasKey(final Value key)
{
if (key.type == ValueType.NUMBER)
{
final int idx = (int) key.number;
if (this.ordered)
{
return idx >= 0 && idx < this.size;
}
return this.intKeys.containsKey(idx);
}
if (key.type == ValueType.STRING)
{
if (this.ordered)
return false;
return this.strKeys.containsKey(key.object);
}
throw new WeelException("Illegal map index type: " + key.type);
}
/**
* Creates integer key mappings for ordered to unordered transition.
*/
private void unorder()
{
this.ordered = false;
// There can only be integer keys inside the map right now
for (int i = 0; i < this.size; i++)
{
this.keys.add(new Value(i));
this.intKeys.put(i, i);
}
this.highestIntKey = this.size - 1;
}
/**
* Sets the value at the given index. Maps grow automatically.
*
* @param index
* The index.
* @param value
* The value.
* @throws WeelException
* If the index is invalid.
*/
public void set(final String index, final Value value)
{
if (this.ordered)
{
this.unorder();
}
final Integer idx = this.strKeys.get(index);
if (idx != null)
{
value.copyTo(this.data.get(idx));
}
else
{
this.strKeys.put(index, this.size);
this.keys.add(new Value(index));
this.data.add(value.clone());
this.size++;
}
}
/**
* Sets the value at the given index. Maps grow automatically.
*
* @param index
* The index.
* @param value
* The value.
* @throws WeelException
* If the index is invalid.
*/
public void set(final int index, final Value value)
{
if (this.ordered && index >= 0 && index <= this.size)
{
if (index == this.size)
{
this.data.add(value.clone());
this.size++;
}
else
{
value.copyTo(this.data.get(index));
}
}
else
{
if (this.ordered)
{
this.unorder();
}
final Integer index2 = this.intKeys.get(index);
if (index2 != null)
{
value.copyTo(this.data.get(index2));
}
else
{
this.intKeys.put(index, this.size);
this.keys.add(new Value(index));
this.data.add(value.clone());
this.highestIntKey = index;
this.size++;
}
}
}
/**
* Sets the value at the given index. Maps grow automatically.
*
* @param index
* The index.
* @param value
* The value.
* @throws WeelException
* If the index is invalid.
*/
public void set(final Value index, final Value value)
{
if (index.type == ValueType.NUMBER)
{
final int idx = (int) index.number;
if (this.ordered && idx >= 0 && idx <= this.size)
{
if (idx == this.size)
{
this.data.add(value.clone());
this.size++;
}
else
{
value.copyTo(this.data.get(idx));
}
}
else
{
if (this.ordered)
{
this.unorder();
}
final Integer idx2 = this.intKeys.get(idx);
if (idx2 != null)
{
value.copyTo(this.data.get(idx2));
}
else
{
this.intKeys.put(idx, this.size);
this.keys.add(new Value(idx));
this.data.add(value.clone());
this.highestIntKey = idx;
this.size++;
}
}
}
else if (index.type == ValueType.STRING)
{
if (this.ordered)
{
this.unorder();
}
final Integer idx = this.strKeys.get(index.object);
if (idx != null)
{
value.copyTo(this.data.get(idx));
}
else
{
this.strKeys.put((String)index.object, this.size);
this.keys.add(index.clone());
this.data.add(value.clone());
this.size++;
}
}
else
{
throw new WeelException("Illegal map index type: " + index.type);
}
}
/**
* Appends the given value to this map using an auto generated integer key.
*
* @param value
* The value to append.
*/
public void append(final Value value)
{
if (this.ordered)
{
this.data.add(value.clone());
}
else
{
this.keys.add(new Value(++this.highestIntKey));
this.intKeys.put(this.highestIntKey, this.size);
this.data.add(value.clone());
}
this.size++;
}
/** @see java.lang.Object#clone() */
@Override
public ValueMap clone()
{
final ValueMap ret = new ValueMap();
final Value k = new Value();
final Value v = new Value();
for (final ValueMapIterator i = new ValueMapIterator(this); i
.next(k, v);)
{
ret.set(k, v.isMap() ? new Value(v.getMap().clone()) : v);
}
return ret;
}
private void remap(final int r)
{
for(final Entry<Integer, Integer> e : this.intKeys.entrySet())
{
final int i = e.getValue();
if(i > r)
{
this.intKeys.put(e.getKey(), i - 1);
}
}
for(final Entry<String, Integer> e : this.strKeys.entrySet())
{
final int i = e.getValue();
if(i > r)
{
this.strKeys.put(e.getKey(), i - 1);
}
}
}
/**
* Removes the last entry in this map.
*/
public Value removeLast()
{
if(this.size < 1)
return new Value();
this.size--;
final Value rem = this.data.remove(this.size);
if(!this.ordered)
{
final Value k = this.keys.remove(this.size);
this.data.remove(this.size);
if(k.type == ValueType.NUMBER)
{
this.intKeys.remove((int)k.number);
}
else
{
this.strKeys.remove(k.object);
}
}
return rem;
}
public void remove(final Value index)
{
if(index.type == ValueType.NUMBER)
{
final int idx = (int) index.number;
if(this.ordered)
{
if(idx < 0 || idx >= this.size)
return;
if(idx + 1 == this.size)
{
this.size--;
this.data.remove(this.size);
return;
}
this.unorder();
}
final Integer idx2 = this.intKeys.get(idx);
if(idx2 != null)
{
final int r = idx2;
this.data.remove(r);
this.keys.remove(r);
this.intKeys.remove(idx);
this.remap(r);
}
}
else if(index.type == ValueType.STRING)
{
if(this.ordered)
return;
final Integer idx2 = this.strKeys.get(index.object);
if(idx2 != null)
{
final int r = idx2;
this.data.remove(r);
this.keys.remove(r);
this.strKeys.remove(index.object);
this.remap(r);
}
}
else
{
throw new WeelException("Illegal map index type: " + index.type);
}
}
/**
* Reverses this map.
*
* @return This map.
*/
public ValueMap reverse()
{
// FIXME ... hä?
Collections.reverse(this.data);
Collections.reverse(this.keys);
return this;
}
/**
* Creates an iterator.
*
* @return The iterator.
*/
public ValueMapIterator createIterator()
{
return new ValueMapIterator(this);
}
/**
* Iterator implementation.
*
* @author René Jeschke <rene_jeschke@yahoo.de>
*/
public final static class ValueMapIterator
{
/** The ValueMap. */
private final ValueMap map;
/** Current cursor. */
private int cursor = 0;
/**
* Constructor.
*
* @param map
* The ValueMap to iterate over.
*/
ValueMapIterator(final ValueMap map)
{
this.map = map;
}
/**
* Gets the next key-value pair.
*
* @param key
* The key.
* @param value
* The value.
* @return <code>false</code> if there are no more elements.
*/
public boolean next(final Value key, final Value value)
{
if (this.cursor < this.map.size)
{
if (this.map.ordered)
{
key.type = ValueType.NUMBER;
key.number = this.cursor;
}
else
{
this.map.keys.get(this.cursor).copyTo(key);
}
this.map.data.get(this.cursor++).copyTo(value);
return true;
}
return false;
}
}
/** @see java.lang.Object#toString() */
@Override
public String toString()
{
if(this.size == 0)
return "{}";
final StringBuilder sb = new StringBuilder();
sb.append('{');
if(this.ordered)
{
sb.append(this.data.get(0).toIntString());
for(int i = 1; i < this.size; i++)
{
sb.append(',');
sb.append(this.data.get(i).toIntString());
}
}
else
{
final Value k = new Value(), v = new Value();
final ValueMapIterator it = new ValueMapIterator(this);
while(it.next(k, v))
{
if(sb.length() > 1)
sb.append(',');
sb.append('[');
sb.append(k.toIntString());
sb.append("]=");
sb.append(v.toIntString());
}
}
sb.append('}');
return sb.toString();
}
}