package org.monospark.spongematchers.matcher;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;

import org.spongepowered.api.data.DataQuery;
import org.spongepowered.api.data.DataView;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

public final class DataViewMatcher implements SpongeMatcher<DataView> {

    private DataEntry root;

    private DataViewMatcher(DataEntry root) {
        this.root = root;
    }

    @Override
    public boolean matches(DataView v) {
        return root.matchesEntry(Collections.emptyList(), v);
    }

    public static abstract class DataEntry {

        abstract boolean matchesEntry(List<String> currentPath, DataView v);
    }
    
    public static final class DataMap extends DataEntry {
        
        private Map<String, DataEntry> entries;

        public DataMap(Map<String, DataEntry> entries) {
            entries = ImmutableMap.copyOf(entries);
        }

        @Override
        boolean matchesEntry(List<String> currentPath, DataView v) {
            for (Entry<String,DataEntry> mapEntry : entries.entrySet()) {
                if (!mapEntry.getValue().matchesEntry(createPathCopy(currentPath, mapEntry.getKey()), v)) {
                    return false;
                }
            }
            return true;
        }
        
        private List<String> createPathCopy(List<String> path, String next) {
            List<String> copy = Lists.newArrayList(path);
            copy.add(next);
            return copy;
        }
    }
    
    public static final class DataList extends DataEntry {
        
        private List<DataEntry> entries;

        public DataList(List<DataEntry> entries) {
            this.entries = ImmutableList.copyOf(entries);
        }

        @Override
        boolean matchesEntry(List<String> currentPath, DataView v) {
            Optional<List<DataView>> viewList = v.getViewList(DataQuery.of(currentPath));
            if (!viewList.isPresent() || viewList.get().size() != entries.size()) {
                return false;
            }
            
            for (int i = 0; i < entries.size(); i++) {
                boolean matches = entries.get(i).matchesEntry(currentPath, viewList.get().get(i));
                if (!matches) {
                    return false;
                }
            }
            return true;
        }
    }
    
    public static final class DataValue<T> extends DataEntry {
        
        private Type<T> valueType;
        
        private SpongeMatcher<T> valueMatcher;

        public DataValue(Type<T> valueType, SpongeMatcher<T> valueMatcher) {
            this.valueType = valueType;
            this.valueMatcher = valueMatcher;
        }

        @SuppressWarnings("unchecked")
        @Override
        boolean matchesEntry(List<String> currentPath, DataView v) {
            DataQuery query = DataQuery.of(currentPath);
            Optional<Object> queried = v.get(query);
            return queried.isPresent() && valueType.getTypeClass().isAssignableFrom(queried.get().getClass()) ?
                    valueMatcher.matches((T) queried.get()) : false;
        }
        
        public static class Type<T> {
            
            public static final Type<Integer> INTEGER = new Type<Integer>(Integer.class);
            
            private Class<?> typeClass;

            private Type(Class<?> typeClass) {
                this.typeClass = typeClass;
            }

            private Class<?> getTypeClass() {
                return typeClass;
            }
        }
    }
}

@@ -169,16 +169,54 @@
 ptional;
+%0Aimport java.util.function.BiFunction;
 %0A%0Aimport
@@ -2887,47 +2887,8 @@
  %7D%0A%0A
-        @SuppressWarnings(%22unchecked%22)%0A
     
@@ -3052,14 +3052,19 @@
 nal%3C
-Object
+? extends T
 %3E qu
@@ -3072,21 +3072,36 @@
 ried = v
-.get(
+alueType.getData(v, 
 query);%0A
@@ -3143,100 +3143,9 @@
 t() 
-&& valueType.getTypeClass().isAssignableFrom(queried.get().getClass()) ?%0A                   
+?
  val
@@ -3166,12 +3166,8 @@
 hes(
-(T) 
 quer
@@ -3227,16 +3227,22 @@
  static 
+final 
 class Ty
@@ -3340,226 +3340,666 @@
 er%3E(
-Integer.class);%0A            %0A            private Class%3C?%3E typeClass;%0A%0A            private Type(Class%3C?%3E typeClass) %7B%0A                this.typeClass = typeClass;%0A            %7D%0A%0A            private Class%3C?%3E getTypeClass(
+(v, q) -%3E v.getInt(q));%0A            public static final Type%3CList%3C?%3E%3E LIST = new Type%3CList%3C?%3E%3E((v, q) -%3E v.getList(q));%0A            public static final Type%3CMap%3C?,?%3E%3E MAP = new Type%3CMap%3C?,?%3E%3E((v, q) -%3E v.getMap(q));%0A            public static final Type%3CList%3CMap%3C?,?%3E%3E%3E MAP_LIST = new Type%3CList%3CMap%3C?,?%3E%3E%3E((v, q) -%3E v.getMapList(q));%0A%0A            private BiFunction%3CDataView, DataQuery, Optional%3C? extends T%3E%3E dataFunction;%0A%0A            private Type(BiFunction%3CDataView, DataQuery, Optional%3C? extends T%3E%3E dataFunction) %7B%0A                this.dataFunction = dataFunction;%0A            %7D%0A%0A            private Optional%3C? extends T%3E getData(DataView v, DataQuery q
 ) %7B%0A
@@ -4025,17 +4025,32 @@
 urn 
-typeClass
+dataFunction.apply(v, q)
 ;%0A  

Improve the usage of data Types in DataViewMatchers