/*
 * Copyright 2013 MovingBlocks
 *
 * 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 org.terasology.reflection.copy;

import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.reflection.metadata.ClassMetadata;
import org.terasology.reflection.metadata.DefaultClassMetadata;
import org.terasology.reflection.MappedContainer;
import org.terasology.reflection.copy.strategy.ListCopyStrategy;
import org.terasology.reflection.copy.strategy.MapCopyStrategy;
import org.terasology.reflection.copy.strategy.MappedContainerCopyStrategy;
import org.terasology.reflection.copy.strategy.SetCopyStrategy;
import org.terasology.reflection.reflect.ReflectFactory;
import org.terasology.reflection.ReflectionUtil;

import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A library of copy strategies.
 * <br><br>
 * This library is should be initialised by registering strategies for a number of core types.  Then as strategies are requested for unknown types,
 * new strategies are generated for those types.
 * The library knows how to generate strategies for Lists, Sets, Maps and types marked with the MappedContainer annotation.
 * If there is any trouble generating a strategy for a type, or it is unknown and generation is not appropriate for the type, a default strategy of returning the value
 * to be copied unaltered is returned.
 *
 */
public class CopyStrategyLibrary {
    private static final Logger logger = LoggerFactory.getLogger(CopyStrategyLibrary.class);

    private Map<Class<?>, CopyStrategy<?>> strategies = Maps.newHashMap();
    private CopyStrategy<?> defaultStrategy = new ReturnAsIsStrategy<>();
    private ReflectFactory reflectFactory;

    public CopyStrategyLibrary(ReflectFactory reflectFactory) {
        this.reflectFactory = reflectFactory;
    }


    /**
     * This constructor is not public, as it allows the direct setting of an internal field without a save copy.
     * @param strategies must not be used after it has been passed to this constructor.
     */
    private CopyStrategyLibrary(Map<Class<?>, CopyStrategy<?>> strategies, ReflectFactory reflectFactory) {
        this.strategies = strategies;
        this.reflectFactory = reflectFactory;
    }


    /**
     * Registers a copy strategy for a base type.
     *
     * @param type     The type
     * @param strategy The strategy for copying the type
     * @param <T>      The type
     */
    public <T> void register(Class<T> type, CopyStrategy<T> strategy) {
        strategies.put(type, strategy);
    }

    public void clear() {
        strategies.clear();
    }

    // TODO: Consider CopyStrategyFactory system for Collections and similar
    public CopyStrategy<?> getStrategy(Type genericType) {
        Class<?> typeClass = ReflectionUtil.getRawType(genericType);
        if (typeClass == null) {
            logger.error("Cannot obtain class for type {}, using default strategy", genericType);
            return defaultStrategy;
        }

        if (List.class.isAssignableFrom(typeClass)) {
            // For lists, create the handler for the contained type and wrap in a list type handler
            Type parameter = ReflectionUtil.getTypeParameter(genericType, 0);
            if (parameter != null) {
                CopyStrategy<?> contentStrategy = getStrategy(parameter);
                return new ListCopyStrategy<>(contentStrategy);
            }
            logger.error("List field is not parametrized - using default strategy");
            return new ListCopyStrategy<>(defaultStrategy);

        } else if (Set.class.isAssignableFrom(typeClass)) {
            // For sets:
            Type parameter = ReflectionUtil.getTypeParameter(genericType, 0);
            if (parameter != null) {
                CopyStrategy<?> contentStrategy = getStrategy(parameter);
                return new SetCopyStrategy<>(contentStrategy);
            }
            logger.error("Set field is not parametrized - using default strategy");
            return new SetCopyStrategy<>(defaultStrategy);

        } else if (Map.class.isAssignableFrom(typeClass)) {
            // For Maps, create the handler for the value type
            Type keyParameter = ReflectionUtil.getTypeParameter(genericType, 0);
            CopyStrategy<?> keyStrategy;
            if (keyParameter != null) {
                keyStrategy = getStrategy(keyParameter);
            } else {
                logger.error("Map field is missing key parameter - using default strategy");
                keyStrategy = defaultStrategy;
            }

            Type valueParameter = ReflectionUtil.getTypeParameter(genericType, 1);
            CopyStrategy<?> valueStrategy;
            if (valueParameter != null) {
                valueStrategy = getStrategy(valueParameter);
            } else {
                logger.error("Map field is missing value parameter - using default strategy");
                valueStrategy = defaultStrategy;
            }
            return new MapCopyStrategy<>(keyStrategy, valueStrategy);

        } else if (strategies.containsKey(typeClass)) {
            // For known types, just use the handler
            return strategies.get(typeClass);

        } else if (typeClass.getAnnotation(MappedContainer.class) != null) {
            if (Modifier.isAbstract(typeClass.getModifiers())
                    || typeClass.isLocalClass()
                    || (typeClass.isMemberClass() && !Modifier.isStatic(typeClass.getModifiers()))) {
                logger.error("Type {} is not a valid mapped class", typeClass);
                return defaultStrategy;
            }

            try {
                // TODO: How to create an empty ResourceUrn ?
                ClassMetadata<?, ?> classMetadata = new DefaultClassMetadata<>(null, typeClass, reflectFactory, this);
                return new MappedContainerCopyStrategy<>(classMetadata);
            } catch (NoSuchMethodException e) {
                logger.error("Unable to create copy strategy for field of type {}: no publicly accessible default constructor", typeClass.getSimpleName());
                return defaultStrategy;
            }
        } else {
            logger.debug("Using default copy strategy for {}", typeClass);
            strategies.put(typeClass, defaultStrategy);
            return defaultStrategy;
        }
    }

    /**
     * @return a copy of the this library that uses the specified stategy for the specified type.
     */
    public <T>  CopyStrategyLibrary createCopyOfLibraryWithStrategy(Class<T> type, CopyStrategy<T> strategy) {
        Map<Class<?>, CopyStrategy<?>> newStrategies = Maps.newHashMap(strategies);
        newStrategies.put(type, strategy);
        return new CopyStrategyLibrary(newStrategies, this.reflectFactory);
    }

    /**
     * The default copy strategy - returns the original value.
     *
     * @param <T>
     */
    private static class ReturnAsIsStrategy<T> implements CopyStrategy<T> {

        @Override
        public T copy(T value) {
            return value;
        }
    }
}
