package com.laytonsmith.core.constructs;
import com.laytonsmith.PureUtilities.Version;
import com.laytonsmith.annotations.typeof;
import com.laytonsmith.core.CHLog;
import com.laytonsmith.core.CHVersion;
import com.laytonsmith.core.LogLevel;
import com.laytonsmith.core.Static;
import com.laytonsmith.core.exceptions.CRE.CRECastException;
import com.laytonsmith.core.exceptions.CRE.CREFormatException;
import com.laytonsmith.core.exceptions.CRE.CREIndexOverflowException;
import com.laytonsmith.core.exceptions.CRE.CRERangeException;
import com.laytonsmith.core.exceptions.ConfigRuntimeException;
import com.laytonsmith.core.functions.ArrayHandling;
import com.laytonsmith.core.functions.BasicLogic;
import com.laytonsmith.core.functions.DataHandling;
import com.laytonsmith.core.natives.interfaces.ArrayAccess;
import com.laytonsmith.core.natives.interfaces.Mixed;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.Stack;
import java.util.TreeMap;
/**
* A class that represents a dynamic array.
*
* For subclasses, the ArrayAccess methods are the most commonly
* overridden methods. There are several overloaded methods in this
* class, you need only to override the non-final ones for the
* same effect.
*/
@typeof("array")
public class CArray extends Construct implements ArrayAccess{
private boolean associative_mode = false;
private long next_index = 0;
private List<Construct> array;
private SortedMap<String, Construct> associative_array;
private String mutVal;
CArray parent = null;
private boolean valueDirty = true;
public CArray(Target t) {
this(t, 0, (Construct[]) null);
}
public CArray(Target t, Construct... items) {
this(t, 0, items);
}
public CArray(Target t, int initialCapacity) {
this(t, initialCapacity, (Construct[]) null);
}
public CArray(Target t, Collection<Construct> items) {
this(t, 0, getArray(items));
}
public CArray(Target t, int initialCapacity, Collection<Construct> items) {
this(t, initialCapacity, getArray(items));
}
public CArray(Target t, int initialCapacity, Construct... items) {
super("{}", ConstructType.ARRAY, t);
if(initialCapacity == -1){
associative_mode = true;
} else if(items != null){
for(Construct item : items){
if(item instanceof CEntry){
//it's an associative array
associative_mode = true;
break;
}
}
}
associative_array = new TreeMap<>(comparator);
array = associative_mode ? new ArrayList<Construct>() : initialCapacity > 0 ? new ArrayList<Construct>(initialCapacity) : items != null ? new ArrayList<Construct>(items.length) : new ArrayList<Construct>();
if(associative_mode){
if(items != null){
for(Construct item : items){
if(item instanceof CEntry){
associative_array.put(normalizeConstruct(((CEntry)item).ckey), ((CEntry)item).construct);
} else {
int max = Integer.MIN_VALUE;
for (String key : associative_array.keySet()) {
try {
int i = Integer.parseInt(key);
max = java.lang.Math.max(max, i);
} catch(NumberFormatException e){}
}
if(max == Integer.MIN_VALUE){
max = -1; //Special case, there are no integer indexes in here yet.
}
associative_array.put(Integer.toString(max + 1), item);
if(item instanceof CArray){
((CArray)item).parent = this;
}
}
}
}
} else {
if(items != null){
for(Construct item : items){
array.add(item);
if(item instanceof CArray){
((CArray)item).parent = this;
}
}
}
this.next_index = array.size();
}
regenValue(new HashSet<CArray>());
}
/**
* Returns if this array is in associative mode or not.
* @return
*/
@Override
public boolean isAssociative(){
return associative_mode;
}
/**
* Returns the backing array.
* @return
*/
protected List<Construct> getArray(){
return array;
}
/**
* Returns a List based on the array. This is only applicable if this
* is a normal array.
* @return
*/
public List<Construct> asList(){
if(inAssociativeMode()){
throw new RuntimeException("asList can only be called on a normal array");
} else {
return new ArrayList<Construct>(array);
}
}
/**
* Returns the backing associative array.
* @return
*/
protected SortedMap<String, Construct> getAssociativeArray(){
return associative_array;
}
private static Construct [] getArray(Collection<Construct> items){
Construct c [] = new Construct[items.size()];
int count = 0;
for(Construct cc : items){
c[count++] = cc;
}
return c;
}
/**
* @return Whether or not this array is operating in associative mode
*/
public boolean inAssociativeMode() {
return associative_mode;
}
/**
* Returns a new empty CArray that is in associative mode.
* @param t
* @return
*/
public static CArray GetAssociativeArray(Target t){
return new CArray(t, -1);
}
public static CArray GetAssociativeArray(Target t, Construct[] args){
return new CArray(t, -1, args);
}
/**
* This must be called every time the underlying model is changed, which
* sets the toString value to dirty, which means that the value will be regenerated
* next time it is requested.
*/
private void regenValue(Set<CArray> arrays) {
if(arrays.contains(this)){
return; //Recursive, so don't continue.
}
arrays.add(this);
valueDirty = true;
if(parent != null){
parent.regenValue(arrays);
}
}
/**
* Reverses the array in place, if it is a normal array, otherwise, if associative, it throws
* an exception.
* @param t
*/
public void reverse(Target t){
if(!associative_mode){
Collections.reverse(array);
regenValue(new HashSet<CArray>());
} else {
throw new CRECastException("Cannot reverse an associative array.", t);
}
}
/**
* Pushes a new Construct onto the end of the array.
* @param c
* @param t
*/
public final void push(Construct c, Target t){
push(c, null, t);
}
/**
* Pushes a new Construct onto the end of the array. If the index is specified, this works like
* a "insert" operation, in that all values are shifted to the right, starting with the value
* at that index. If the array is associative though, you MUST send null, otherwise an
* {@link IllegalArgumentException} is thrown. Ideally, you should use {@link #set} anyways
* for an associative array.
* @param c The Construct to add to the array
* @throws IllegalArgumentException If index is not null, and this is an associative array.
* @throws IndexOutOfBoundsException If the index is not null, and the index specified is out of
* range.
*/
public void push(Construct c, Integer index, Target t) throws IllegalArgumentException, IndexOutOfBoundsException {
if (!associative_mode) {
if(index != null){
array.add(index, c);
} else {
array.add(c);
}
next_index++;
} else {
if(index != null){
throw new IllegalArgumentException("Cannot insert into an associative array");
}
int max = 0;
for (String key : associative_array.keySet()) {
try{
int i = Integer.parseInt(key);
max = java.lang.Math.max(max, i);
} catch(NumberFormatException e){}
}
if(c instanceof CEntry){
associative_array.put(Integer.toString(max + 1), ((CEntry)c).construct());
} else {
associative_array.put(Integer.toString(max + 1), c);
}
}
if(c instanceof CArray){
((CArray)c).parent = this;
}
regenValue(new HashSet<CArray>());
}
/**
* Returns the key set for this array. If it's an associative array, it simply returns
* the key set of the map, otherwise it generates a set of CInts from 0 to size-1, and
* returns that.
* @return
*/
@Override
public Set<Construct> keySet(){
Set<Construct> set = !associative_mode?new LinkedHashSet<Construct>(array.size()):new LinkedHashSet<Construct>(associative_array.size());
if(!associative_mode){
for(int i = 0; i < array.size(); i++){
set.add(new CInt(i, Target.UNKNOWN));
}
} else {
for(String key : associative_array.keySet()){
set.add(new CString(key, Target.UNKNOWN));
}
}
return set;
}
/**
* Returns the string based key set for this array. If it's an associative array, it
* simply returns the key set of the map, otherwise it generates a set from 0 to size-1,
* toStrings the integers, and returns that.
* @return
*/
public Set<String> stringKeySet(){
Set<String> set = !associative_mode?new LinkedHashSet<String>(array.size()):new HashSet<String>(associative_array.size());
if(!associative_mode){
for(int i = 0; i < array.size(); i++){
set.add(Integer.toString(i));
}
} else {
set = associative_array.keySet();
}
return set;
}
/**
*
* @param index
* @param c
*/
public void set(Construct index, Construct c, Target t) {
if (!associative_mode) {
try {
int indx = Static.getInt32(index, t);
if (indx > next_index || indx < 0) {
throw new CREIndexOverflowException("", Target.UNKNOWN);
} else if(indx == next_index){
this.push(c, t);
} else {
array.set(indx, c);
}
} catch (ConfigRuntimeException e) {
//Not a number. Convert to associative.
associative_array = new TreeMap<String, Construct>(comparator);
for (int i = 0; i < array.size(); i++) {
associative_array.put(Integer.toString(i), array.get(i));
}
associative_mode = true;
array = null; // null out the original array container so it can be GC'd
}
}
if (associative_mode) {
associative_array.put(normalizeConstruct(index), c);
}
if(c instanceof CArray){
((CArray)c).parent = this;
}
regenValue(new HashSet<CArray>());
}
public final void set(int index, Construct c, Target t){
this.set(new CInt(index, Target.UNKNOWN), c, t);
}
/* Shortcuts */
public final void set(String index, Construct c, Target t){
set(new CString(index, c.getTarget()), c, t);
}
public final void set(String index, String value, Target t){
set(index, new CString(value, t), t);
}
public final void set(String index, String value){
set(index, value, Target.UNKNOWN);
}
@Override
public Construct get(Construct index, Target t) {
if(!associative_mode){
try {
return array.get(Static.getInt32(index, t));
} catch (IndexOutOfBoundsException e) {
throw new CREIndexOverflowException("The element at index \"" + index.val() + "\" does not exist", t, e);
}
} else {
if(associative_array.containsKey(normalizeConstruct(index))){
Construct val = associative_array.get(normalizeConstruct(index));
if(val instanceof CEntry){
return ((CEntry)val).construct();
}
return val;
} else {
//Create this so we can at least attach a stacktrace.
@SuppressWarnings({"ThrowableInstanceNotThrown", "ThrowableInstanceNeverThrown"})
IndexOutOfBoundsException ioobe = new IndexOutOfBoundsException();
throw new CREIndexOverflowException("The element at index \"" + index.val() + "\" does not exist", t, ioobe);
}
}
}
public final Construct get(long index, Target t){
return this.get(new CInt(index, t), t);
}
@Override
public final Construct get(int index, Target t){
return this.get(new CInt(index, t), t);
}
@Override
public final Construct get(String index, Target t){
return this.get(new CString(index, t), t);
}
public boolean containsKey(String c){
if(associative_mode){
return associative_array.containsKey(c);
} else {
try {
return Integer.valueOf(c) < array.size();
} catch (NumberFormatException e) {
return false;
}
}
}
public final boolean containsKey(int i){
return this.containsKey(Integer.toString(i));
}
public boolean contains(Construct c){
if(associative_mode){
return associative_array.containsValue(c);
} else {
return array.contains(c);
}
}
public final boolean contains(String c){
return contains(new CString(c, Target.UNKNOWN));
}
public final boolean contains(int i){
return contains(new CString(Integer.toString(i), Target.UNKNOWN));
}
/**
* Returns an array of the keys of all the values that are
* equal to the value specified
* @param value
* @return
*/
public CArray indexesOf(Construct value){
CArray ret = new CArray(Target.UNKNOWN);
if(associative_mode){
for(String key : associative_array.keySet()){
if(BasicLogic.equals.doEquals(associative_array.get(key), value)){
ret.push(new CString(key, Target.UNKNOWN), Target.UNKNOWN);
}
}
} else {
for(int i = 0; i < array.size(); i++){
if(BasicLogic.equals.doEquals(array.get(i), value)){
ret.push(new CInt(i, Target.UNKNOWN), Target.UNKNOWN);
}
}
}
return ret;
}
@Override
public String val() {
if(valueDirty){
mutVal = getString(new Stack<CArray>(), this.getTarget());
valueDirty = false;
}
return mutVal;
}
@Override
public String toString() {
return val();
}
/**
* Returns a string version of this array. The arrays
* that have been accounted for so far are stored in arrays,
* to prevent recursion. Subclasses may override this method
* if a more efficient or concise string can be generated.
* @param arrays The values accounted for so far
* @param t
* @return
*/
protected String getString(Stack<CArray> arrays, Target t){
StringBuilder b = new StringBuilder();
b.append("{");
if (!inAssociativeMode()) {
for (int i = 0; i < this.size(); i++) {
Mixed value = this.get(i, t);
String v;
if(value instanceof CArray){
if(arrays.contains((CArray)value)){
//Check for recursion
v = "*recursion*";
} else {
arrays.add(((CArray)value));
v = ((CArray)value).getString(arrays, t);
arrays.pop();
}
} else {
v = value.val();
}
if (i > 0) {
b.append(", ");
}
b.append(v);
}
} else {
boolean first = true;
for(String key : this.stringKeySet()){
if(!first){
b.append(", ");
}
first = false;
String v;
if(this.get(key, t) == null){
v = "null";
} else {
Mixed value = this.get(key, t);
if(value instanceof CArray){
if(arrays.contains(((CArray)value))){
v = "*recursion*";
} else {
arrays.add(((CArray)value));
v = ((CArray)value).getString(arrays, t);
}
} else {
v = value.val();
}
}
b.append(key).append(": ").append(v);
}
}
b.append("}");
String ret = b.toString();
return ret;
}
@Override
public long size() {
if(associative_mode){
return associative_array.size();
} else {
return array.size();
}
}
@Override
public CArray clone() {
CArray clone;
try {
clone = (CArray) super.clone();
} catch (CloneNotSupportedException ex) {
throw new RuntimeException(ex);
}
clone.associative_mode = associative_mode;
if(!associative_mode){
if (array != null) {
clone.array = new ArrayList<Construct>(this.array);
}
} else {
if(associative_array != null){
clone.associative_array = new TreeMap<String, Construct>(this.associative_array);
}
}
clone.regenValue(new HashSet<CArray>());
return clone;
}
public CArray deepClone(Target t) {
CArray clone = deepClone(this, t, new ArrayList<CArray[]>());
return clone;
}
private static CArray deepClone(CArray array, Target t, ArrayList<CArray[]> cloneRefs) {
// Return the clone reference if this array has been cloned before (both clones will have the same reference).
for(CArray[] refCouple : cloneRefs) {
if(refCouple[0] == array) {
return refCouple[1];
}
}
// Create the clone to put array in and add it to the cloneRefs list.
CArray clone = new CArray(t, (int) array.size());
clone.associative_mode = array.associative_mode;
cloneRefs.add(new CArray[] {array, clone});
// Iterate over the array, recursively calling this method to perform a deep clone.
for (Construct key : array.keySet()) {
Construct value = array.get(key, t);
if(value instanceof CArray) {
value = deepClone((CArray) value, t, cloneRefs);
}
clone.set(key, value, t);
}
return clone;
}
private String normalizeConstruct(Construct c){
if(c instanceof CArray){
throw new CRECastException("Arrays cannot be used as the key in an associative array", c.getTarget());
} else if(c instanceof CString || c instanceof CInt){
return c.val();
} else if(c instanceof CNull){
return "";
} else if(c instanceof CBoolean){
if(((CBoolean)c).getBoolean()){
return "1";
} else {
return "0";
}
} else if(c instanceof CLabel){
return normalizeConstruct(((CLabel)c).cVal());
} else {
return c.val();
}
}
/**
* Removes the value at the specified integer key.
* @param i
* @return
*/
public Construct remove(int i){
return remove(new CInt(i, Target.UNKNOWN));
}
/**
* Removes the value at the specified string key.
* @param s
* @return
*/
public Construct remove(String s){
return remove(new CString(s, Target.UNKNOWN));
}
/**
* Removes the value at the specified key
* @param construct
* @return
*/
public Construct remove(Construct construct) {
String c = normalizeConstruct(construct);
Construct ret;
if(!associative_mode){
try{
ret = array.remove(Integer.parseInt(c));
next_index--;
} catch(NumberFormatException e){
throw new CRECastException("Expecting an integer, but received \"" + c + "\" (were you expecting an associative array? This array is a normal array.)", construct.getTarget());
} catch(IndexOutOfBoundsException e){
throw new CRERangeException("Cannot remove the value at '" + c + "', as no such index exists in the array", construct.getTarget());
}
} else {
ret = associative_array.remove(c);
}
regenValue(new HashSet<CArray>());
return ret;
}
/**
* Removes all values that are equal to the specified construct
* from this array
* @param construct
*/
public void removeValues(Construct construct){
if(associative_mode){
Iterator<Construct> it;
it = associative_array.values().iterator();
while(it.hasNext()){
Construct c = it.next();
if(BasicLogic.equals.doEquals(c, construct)){
it.remove();
}
}
} else {
for(int i = array.size() - 1; i >= 0; i--){
Construct c = array.get(i);
if(BasicLogic.equals.doEquals(c, construct)){
array.remove(i);
}
}
}
regenValue(new HashSet<CArray>());
}
/**
* Creates a new, empty array, with the same type. Note to subclasses: By default,
* this method expects a constructor that accepts a {@link Target}. If this assumption
* is not valid, you may override this method as needed.
* @param t
* @return
*/
public CArray createNew(Target t){
try {
Constructor<CArray> con = (Constructor<CArray>) this.getClass().getConstructor(Target.class);
try {
return con.newInstance(t);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
throw new RuntimeException(ex);
}
} catch (NoSuchMethodException ex) {
throw new RuntimeException(this.typeof() + " does not support creating a new value.");
} catch (SecurityException ex) {
throw new RuntimeException(ex);
}
}
private Comparator<String> comparator = new Comparator<String>(){
private int normalize(int value){
if(value < 0){
return -1;
} else if(value > 0){
return 1;
} else {
return 0;
}
}
@Override
public int compare(String o1, String o2) {
// Null checks!
if (o1 == null && o2 != null) {
return -1;
} else if (o1 == null) {
return 0;
} else if (o2 == null) {
return 1;
}
// This fixes a bug where occasionally (I can't totally figure out the pattern) a value
// would be missing from the list. I think this is ok in all cases, except that it may
// change the order of certain associative array's key display, however, this has never
// been a guaranteed property of the arrays.
return normalize(o1.compareTo(o2));
/*
//Due to a dumb behavior in Double.parseDouble,
//we need to check to see if there are non-digit characters in
//the keys, and if so, do a string comparison.
if(o1.matches(".*[^0-9\\.]+.*") || o2.matches(".*[^0-9\\.]+.*")){
return normalize(o1.compareTo(o2));
}
try{
int i1 = Integer.parseInt(o1);
int i2 = Integer.parseInt(o2);
//They're both integers, do an integer comparison
return new Integer(i1).compareTo(new Integer(i2));
} catch(NumberFormatException e){
try{
double d1 = Double.parseDouble(o1);
double d2 = Double.parseDouble(o2);
//They're both doubles, do a double comparison
return new Double(d1).compareTo(new Double(d2));
} catch(NumberFormatException ee){
//Just do a string comparison
return normalize(o1.compareTo(o2));
}
}*/
}
};
@Override
public boolean isDynamic() {
//The CArray is static, despite what you might first think.
//The only way to get a static array is to use the array function,
//which WILL return a static array. A function that takes an array
//as an argument will accept the static array, and can be optimized possibly,
//however, it is likely that the array is stored in a variable, which of couse
//is NOT static. So, if just the array function is run, it's static, if the static
//array is put into a variable, the staticness is lost (as it is with a number or string)
return false;
}
@Override
public boolean canBeAssociative() {
return true;
}
@Override
public Construct slice(int begin, int end, Target t) {
return new ArrayHandling.array_get().exec(t, null, new CSlice(begin, end, t));
}
@Override
public String docs() {
return "An array is a data type, which contains any number of other values.";
}
@Override
public Version since() {
return CHVersion.V3_0_1;
}
public enum SortType{
/**
* Sorts the elements without converting types first. If a non-numeric
* string is compared to a numeric string, it is compared as a string,
* otherwise, it's compared as a natural ordering.
*/
REGULAR,
/**
* All strings are considered numeric, that is, 001 comes before 2.
*/
NUMERIC,
/**
* All values are considered strings.
*/
STRING,
/**
* All values are considered strings, but the comparison is case-insensitive.
*/
STRING_IC
}
public void sort(final SortType sort){
List<Construct> list = array;
if(this.associative_mode){
list = new ArrayList(associative_array.values());
this.associative_array.clear();
this.associative_array = null;
this.associative_mode = false;
CHLog.GetLogger().Log(CHLog.Tags.GENERAL, LogLevel.VERBOSE, "Attempting to sort an associative array; key values will be lost.", this.getTarget());
}
Collections.sort(array, new Comparator<Construct>() {
@Override
public int compare(Construct o1, Construct o2) {
//o1 < o2 -> -1
//o1 == o2 -> 0
//o1 > o2 -> 1
for(int i = 0; i < 2; i++){
Construct c;
if(i == 0){
c = o1;
} else {
c = o2;
}
if(c instanceof CArray){
throw new CRECastException("Cannot sort an array of arrays.", CArray.this.getTarget());
}
if(!(c instanceof CBoolean || c instanceof CString || c instanceof CInt ||
c instanceof CDouble || c instanceof CNull)){
throw new CREFormatException("Unsupported type being sorted: " + c.getCType(), CArray.this.getTarget());
}
}
if(o1 instanceof CNull || o2 instanceof CNull){
if(o1 instanceof CNull && o2 instanceof CNull){
return 0;
} else if(o1 instanceof CNull){
return "".compareTo(o2.getValue());
} else {
return o1.val().compareTo("");
}
}
if(o1 instanceof CBoolean || o2 instanceof CBoolean){
if(Static.getBoolean(o1) == Static.getBoolean(o2)){
return 0;
} else {
int oo1 = Static.getBoolean(o1)==true?1:0;
int oo2 = Static.getBoolean(o2)==true?1:0;
return (oo1 < oo2) ? -1 : 1;
}
}
//At this point, things will either be numbers or strings
switch(sort){
case REGULAR:
return compareRegular(o1, o2);
case NUMERIC:
return compareNumeric(o1, o2);
case STRING:
return compareString(o1.val(), o2.val());
case STRING_IC:
return compareString(o1.val().toLowerCase(), o2.val().toLowerCase());
}
throw ConfigRuntimeException.CreateUncatchableException("Missing implementation for " + sort.name(), Target.UNKNOWN);
}
public int compareRegular(Construct o1, Construct o2){
if(Static.getBoolean(new DataHandling.is_numeric().exec(Target.UNKNOWN, null, o1))
&& Static.getBoolean(new DataHandling.is_numeric().exec(Target.UNKNOWN, null, o2))){
return compareNumeric(o1, o2);
} else if(Static.getBoolean(new DataHandling.is_numeric().exec(Target.UNKNOWN, null, o1))){
//The first is a number, the second is a string
return -1;
} else if(Static.getBoolean(new DataHandling.is_numeric().exec(Target.UNKNOWN, null, o2))){
//The second is a number, the first is a string
return 1;
} else {
//They are both strings
return compareString(o1.val(), o2.val());
}
}
public int compareNumeric(Construct o1, Construct o2){
double d1 = Static.getNumber(o1, o1.getTarget());
double d2 = Static.getNumber(o2, o2.getTarget());
return Double.compare(d1, d2);
}
public int compareString(String o1, String o2){
return o1.compareTo(o2);
}
});
this.array = list;
this.regenValue(new HashSet<CArray>());
}
public boolean isEmpty(){
return size() == 0;
}
/**
* Clears all the values out of this array
*/
public void clear(){
this.array.clear();
this.associative_array.clear();
this.next_index = 0;
this.parent = null;
this.valueDirty = true;
}
public void ensureCapacity(int capacity) {
((ArrayList) array).ensureCapacity(capacity);
}
}