package net.ranides.assira.reflection;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * ResolveConfiguration class is configuration used by ResolvePattern.
 * It is just collection of ResolveModel instances which will try to process object.
 *
 * Order of items inside configuration is used at processing time (first Model has higher priority than last)
 *
 * When you compile expression to ResolvePattern, you can pass your own ResolveConfiguration.
 * If you don't do it, then DEFAULT configuration is used.
 *
 * Please note, that this configuration is immutable, you have to use builder pattern to create new one.
 */
public class ResolveConfiguration implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * Default configuration. It uses models:
     *   - {@link ResolveModel#CONTEXT}
     *   - {@link ResolveModel#ARRAY}
     *   - {@link ResolveModel#LIST}
     *   - {@link ResolveModel#MAP}
     *   - {@link ResolveModel#MATCHER}
     *   - {@link ResolveModel#CHARS}
     *   - {@link ResolveModel#BEAN}
     */
    public static final ResolveConfiguration DEFAULT = new ResolveConfiguration(new ResolveModel[]{
            ResolveModel.CONTEXT,
            ResolveModel.ARRAY,
            ResolveModel.LIST,
            ResolveModel.MAP,
            ResolveModel.MATCHER,
            ResolveModel.CHARS,
            ResolveModel.BEAN
    });

    private final ResolveModel[] models;

    private ResolveConfiguration(ResolveModel[] models) {
        this.models = models;
    }

    /**
     * Factory: creates new builder for configuration
     *
     * @return Builder
     */
    public static ResolveModelsBuilder builder() {
        return new ResolveModelsBuilder();
    }

    /**
     * Selects first matching model and uses it to read property value.
     *
     * If property does not exist, it sets "status.error" flag and returns null.
     *
     * @param status output status
     * @param that that
     * @param name name
     * @return Object
     */
    public Object get(ResolveModel.ResolveStatus status, Object that, String name) {
        for(ResolveModel m : models) {
            if(m.matches(that, name)) {
                return m.get(status, that, name);
            }
        }
        status.error = true;
        return null;
    }

    /**
     * Selects first matching model and uses it to change property value.
     *
     * If property does not exist, it sets "status.error" flag and does nothing.
     *
     * @param status output status
     * @param that that
     * @param name name
     * @param value value
     */
    public void set(ResolveModel.ResolveStatus status, Object that, String name, Object value) {
        for(ResolveModel m : models) {
            if(m.matches(that, name)) {
                m.set(status, that, name, value);
                return;
            }
        }
        status.error = true;
    }

    /**
     * Selects first matching model and uses it to change property value.
     * Returns previously assigned value.
     *
     * If property does not exist, it sets "status.error" flag and does nothing.
     *
     * @param status status
     * @param that that
     * @param name name
     * @param value value
     * @return Object
     */
    public Object replace(ResolveModel.ResolveStatus status, Object that, String name, Object value) {
        for(ResolveModel m : models) {
            if(m.matches(that, name)) {
                return m.replace(status, that, name, value);
            }
        }
        status.error = true;
        return null;
    }

    /**
     * Selects first matching model and uses it to deduce property type.
     *
     * If property does not exist, it sets "status.error" flag and returns null.
     *
     * @param status output status
     * @param that that
     * @param name name
     * @return IClass
     */
    public IClass<?> type(ResolveModel.ResolveStatus status, IClass<?> that, String name) {
        for(ResolveModel m : models) {
            if(m.matchesType(that, name)) {
                return m.type(status, that, name);
            }
        }
        status.error = true;
        return null;
    }

    /**
     * Builder pattern for this configuration object
     */
    public static class ResolveModelsBuilder {

        private final List<ResolveModel> models = new ArrayList<>();

        /**
         * Adds all models from provided configuration
         *
         * @param configuration models
         * @return Builder
         */
        public ResolveModelsBuilder add(ResolveConfiguration configuration) {
            this.models.addAll(Arrays.asList(configuration.models));
            return this;
        }

        /**
         * Adds provided model
         *
         * @param model model
         * @return Builder
         */
        public ResolveModelsBuilder add(ResolveModel model) {
            this.models.add(model);
            return this;
        }

        /**
         * Creates final immutable configuration object, usable by ResolvePattern.
         *
         * @return ResolveConfiguration
         */
        public ResolveConfiguration build() {
            return new ResolveConfiguration(models.toArray(new ResolveModel[0]));
        }
    }

}
