/**
 * Copyright 2011-2017 Asakusa Framework Team.
 *
 * 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.asakusafw.testdriver.loader;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;

import com.asakusafw.runtime.core.GroupView;
import com.asakusafw.testdriver.core.DataModelDefinition;
import com.asakusafw.testdriver.core.DataModelReflection;
import com.asakusafw.testdriver.core.DataModelSource;
import com.asakusafw.testdriver.core.DataModelSourceFactory;
import com.asakusafw.testdriver.core.PropertyName;
import com.asakusafw.testdriver.core.PropertyType;
import com.asakusafw.testdriver.core.TestContext;

/**
 * A basic implementation of {@link GroupLoader}.
 * @param <T> the data type
 * @since 0.9.1
 * @see BasicDataLoader
 */
public class BasicGroupLoader<T> implements GroupLoader<T> {

    private final TestContext context;

    private final DataModelDefinition<T> definition;

    private final DataModelSourceFactory factory;

    private final List<PropertyName> grouping;

    private Comparator<DataModelReflection> refComparator;

    BasicGroupLoader(
            TestContext context,
            DataModelDefinition<T> definition,
            DataModelSourceFactory factory,
            List<PropertyName> grouping,
            Comparator<DataModelReflection> comparator) {
        this.context = context;
        this.definition = definition;
        this.factory = factory;
        this.grouping = grouping;
        this.refComparator = comparator;
    }

    @Override
    public GroupLoader<T> order(String... terms) {
        if (refComparator != null) {
            throw new IllegalStateException("order is already defined"); //$NON-NLS-1$
        }
        refComparator = Util.toComparator(definition, terms);
        return this;
    }

    @Override
    public GroupLoader<T> order(Comparator<? super T> comparator) {
        if (refComparator != null) {
            throw new IllegalStateException("order is already defined"); //$NON-NLS-1$
        }
        refComparator = Util.toComparator(definition, comparator);
        return this;
    }

    @Override
    public GroupView<T> asView() {
        Map<List<Object>, List<DataModelReflection>> map = asRefMap();
        Map<List<Object>, List<T>> results = new LinkedHashMap<>();
        for (Map.Entry<List<Object>, List<DataModelReflection>> entry : map.entrySet()) {
            List<Object> key = entry.getKey();
            List<DataModelReflection> refs = entry.getValue();
            List<T> resolved = new ArrayList<>(refs.size());
            for (DataModelReflection ref : refs) {
                T object = definition.toObject(ref);
                resolved.add(object);
            }
            results.put(key, resolved);
        }
        return new MapGroupView<>(definition, grouping, results);
    }

    private Map<List<Object>, List<DataModelReflection>> asRefMap() {
        Map<List<Object>, List<DataModelReflection>> results = new LinkedHashMap<>();
        try (DataModelSource source = factory.createSource(definition, context)) {
            while (true) {
                DataModelReflection ref = source.next();
                if (ref == null) {
                    break;
                }
                List<Object> key = new ArrayList<>(grouping.size());
                for (PropertyName name : grouping) {
                    Object value = ref.getValue(name);
                    key.add(value);
                }
                results.computeIfAbsent(key, k -> new ArrayList<>()).add(ref);
            }
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
        if (refComparator != null) {
            for (List<DataModelReflection> refs : results.values()) {
                refs.sort(refComparator);
            }
        }
        return results;
    }

    private static class MapGroupView<T> implements GroupView<T> {

        private final DataModelDefinition<T> definition;

        private final PropertyType[] types;

        private final Map<List<Object>, List<T>> entity;

        MapGroupView(
                DataModelDefinition<T> definition,
                List<PropertyName> names,
                Map<List<Object>, List<T>> entity) {
            this.definition = definition;
            this.types = names.stream()
                    .sequential()
                    .map(definition::getType)
                    .toArray(PropertyType[]::new);
            this.entity = entity;
        }

        @Override
        public Iterator<T> iterator() {
            Iterator<? extends List<T>> partitions = entity.values().iterator();
            return new Iterator<T>() {
                private Iterator<T> nextPartition;
                @Override
                public boolean hasNext() {
                    while (true) {
                        if (nextPartition == null) {
                            if (partitions.hasNext()) {
                                nextPartition = partitions.next().iterator();
                            } else {
                                return false;
                            }
                        }
                        if (nextPartition.hasNext()) {
                            return true;
                        } else {
                            nextPartition = null;
                        }
                    }
                }
                @Override
                public T next() {
                    if (nextPartition == null) {
                        throw new NoSuchElementException();
                    }
                    return nextPartition.next();
                }
            };
        }

        @Override
        public List<T> find(Object... elements) {
            List<Object> key = toKey(elements);
            return entity.getOrDefault(key, Collections.emptyList());
        }

        private List<Object> toKey(Object[] elements) {
            if (elements.length != types.length) {
                throw new IllegalArgumentException(MessageFormat.format(
                        "invalid key element types: requires ({0})",
                        Arrays.stream(types)
                            .map(Enum::name)
                            .collect(Collectors.joining())));
            }
            List<Object> key = new ArrayList<>(elements.length);
            for (int i = 0; i < elements.length; i++) {
                Object value = definition.resolveRawValue(elements[i]);
                if (value != null) {
                    Class<?> expect = types[i].getRepresentation();
                    Class<?> actual = value.getClass();
                    if (expect.isAssignableFrom(actual) == false) {
                        throw new IllegalArgumentException(MessageFormat.format(
                                "invalid key element type at {0}: required={1}, specified={2}",
                                i,
                                types[i].name(),
                                actual.getSimpleName()));
                    }
                }
                key.add(value);
            }
            return key;
        }
    }
}

@@ -957,32 +957,59 @@
 ementException;%0A
+import java.util.Optional;%0A
 import java.util
@@ -7848,16 +7848,180 @@
         
+ Optional.ofNullable(elements%5Bi%5D)%0A                                    .map(it -%3E it.getClass().getSimpleName())%0A                                    .orElseGet(() -%3E
  actual.
@@ -8037,16 +8037,17 @@
 Name()))
+)
 ;%0A      

Revise error messages.