/*
 *  @author Ranides Atterwim <ranides@gmail.com>
 *  @copyright Ranides Atterwim
 *  @license WTFPL
 *  @url http://ranides.net/projects/assira
 */

package net.ranides.assira.config;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.ranides.assira.collection.SingleCollection;
import net.ranides.assira.collection.list.ListAdapter;
import net.ranides.assira.generic.TypeToken;
import net.ranides.assira.text.LexicalCast;
import net.ranides.assira.text.StringUtils;
import net.ranides.assira.text.Strings;

/**
 *
 * @author ranides
 */
@SuppressWarnings({
    "SIC_INNER_SHOULD_BE_STATIC_ANON"
})
public abstract class AbstractBranch implements Branch {

    protected enum NoResult { CODE };


    protected AbstractBranch() {
        // do nothing
    }

    @Override
    public Branch branch(String path) {
        return new BasicBranch(path, get(path));
    }

    @Override
    public List<Branch> branchList(String path) {
        List<?> list = getList(path);
        List<Branch> result = new ArrayList<>();
        for(Object item : list) {
            String home = path+"[" + result.size() + "]";
            result.add(new BasicBranch(home, item));
        }
        return result;
    }

    @Override
    public final Map<String,Object> asFlatMap() {
        return flat(asMap(), "");
    }

    @Override
    public final <T> T get(Class<T> type, String path, T ddefault) throws ConfigException {
        return LexicalCast.cast(get(path, ddefault), type);
    }

    @Override
    public final <T> T get(Class<T> type, String path) throws ConfigException {
        return LexicalCast.cast(get(path), type);
    }

    @Override
    public final <T> T get(TypeToken<T> type, String path, T ddefault) throws ConfigException {
        return LexicalCast.cast(get(path, ddefault), type);
    }

    @Override
    public final <T> T get(TypeToken<T> type, String path) throws ConfigException {
        return LexicalCast.cast(get(path), type);
    }

    @Override
    public final Object get(String path) throws ConfigException {
        return get(path, NoResult.CODE);
    }

    @Override
    public final List<?> getList(String path) throws ConfigException {
        Object value = get(path, Collections.emptyList());
        if(value instanceof List) {
            return (List<?>)value;
        } else {
            return Arrays.asList(value);
        }
    }

    @Override
    public final <T> List<T> getList(Class<T> type, String path) {
        Object value = get(path, Collections.emptyList());
        if(value instanceof List) {
            return new CastList<>(type, (List<?>)value);
        } else {
            return SingleCollection.of(LexicalCast.cast(value, type));
        }
    }

    @Override
    public final <T> List<T> getList(TypeToken<T> type, String path) {
        Object value = get(path, Collections.emptyList());
        if(value instanceof List) {
            return new TokenCastList<>(type, (List<?>)value);
        } else {
            return SingleCollection.of(LexicalCast.cast(value, type));
        }
    }

    @Override
    public final <T> void put(Class<T> type, String path, Object value) throws ConfigException {
        put(path, LexicalCast.cast(value, type));
    }

    @Override
    public final <T> void put(TypeToken<T> type, String path, Object value) throws ConfigException {
        put(path, LexicalCast.cast(value, type));
    }

    @Override
    public final <T> void putList(Class<T> type, String path, List<?> value) throws ConfigException {
        put(path, new CastList<>(type, value));
    }

    @Override
    public final <T> void putList(TypeToken<T> type, String path, List<?> value) throws ConfigException {
        put(path, new TokenCastList<>(type, value));
    }

    @Override
    public final void putList(String path, List<?> value) throws ConfigException {
        put(path, value);
    }

    @SuppressWarnings("unchecked")
    protected void put(Object context, String path, Object value) throws ConfigException {
        String[] keys   = path.split("\\.");

        Object target = context;
        for(int i=0, n=keys.length-1; i<n; i++) {
            String key = keys[i];
            if(target instanceof Map) {
                Map<Object,Object> map = (Map)target;
                if( !map.containsKey(key) ) {
                    map.put(key, target = new HashMap<>());
                } else {
                    target = map.get(key);
                }
            } else {
                throw new ConfigException("property not found: " + StringUtils.join(keys, "."));
            }
        }

        if(target instanceof Map) {
            Map<Object,Object> map = (Map)target;
            map.put(keys[keys.length-1], value);
        } else {
            throw new ConfigException("can't change property: " + path);
        }
    }

    protected Object get(Object context, String path, Object ddefault) throws ConfigException {
        Object now = context;
        for(String key : path.split("\\.") ) {
            Matcher hit = Pattern.compile("(.*)\\[([0-9]+)\\]").matcher(key);
            if(hit.find()) {
                now = next(now, hit.group(1), Integer.parseInt(hit.group(2)));
            } else {
                now = next(now, key, -1);
            }
            if(now == AbstractConfiguration.NoResult.CODE) {
                return reportNotFound(path, ddefault);
            }
        }
        return now;
    }

    protected Object reportNotFound(String path, Object ddefault) {
        if(NoResult.CODE == ddefault) { // NOPMD
            throw new ConfigException("property not found: " + path);
        } else {
            return ddefault;
        }
    }

    private Object next(Object context, String key, int index) {
        if(!(context instanceof Map)) {
            return AbstractConfiguration.NoResult.CODE;
        }
        final Map<?,?> now = (Map<?,?>)context;
        if(!now.containsKey(key)) {
            return AbstractConfiguration.NoResult.CODE;
        }
        Object value = now.get(key);
        if(index == -1) {
            return value;
        }
        if(value instanceof List) {
            List<?> list = (List)value;
            return index < list.size() ? list.get(index) : AbstractConfiguration.NoResult.CODE;
        } else {
            return (index == 0) ? value : AbstractConfiguration.NoResult.CODE;
        }
    }

    protected Map<String,Object> flat(Object source, String seed) {
        return flat(new LinkedHashMap<String, Object>(), source, seed);
    }

    private Map<String,Object> flat(Map<String,Object> target, Object source, String seed) {
        if(source instanceof List) {
            int index = 0;
            for(Object item : (List<?>)source) {
                flat(target, item, seed+"[" + index+ "]");
                index++;
            }
            return target;
        }

        if(source instanceof Map) {
            String key = Strings.isEmpty(seed) ? "" : seed + ".";
            for (Map.Entry<?, ?> entry : ((Map<?,?>)source).entrySet()) {
                flat(target, entry.getValue(), key + entry.getKey());
            }
            return target;
        }

        target.put(seed, source);
        return target;
    }

    private static class CastList<T> extends ListAdapter<T, Object> {

        private final Class<T> type;

        @SuppressWarnings("unchecked")
        public CastList(Class<T> type, List<?> content) {
            super((List<Object>)content);
            this.type = type;
        }

        @Override
        public T apply(Object source) {
            return LexicalCast.cast(source, type);
        }

    }

    private static class TokenCastList<T> extends ListAdapter<T, Object> {

        private final TypeToken<T> type;

        @SuppressWarnings("unchecked")
        public TokenCastList(TypeToken<T> type, List<?> content) {
            super((List<Object>)content);
            this.type = type;
        }

        @Override
        public T apply(Object source) {
            return LexicalCast.cast(source, type);
        }

    }

    public class BasicBranch extends AbstractBranch {

        private final String home;
        private Object context;

        public BasicBranch(String home, Object context) {
            this.home = home;
            initContext(context);
        }

        private void initContext(Object context) {
            if(context == null) {
                this.context = AbstractBranch.this.get(home);
            } else {
                this.context = context;
            }
            if( !(this.context instanceof Map) ) {
                throw new ConfigException("path is not branch: " + home);
            }
        }

        @Override
        public Branch branch(String path) {
            return AbstractBranch.this.branch(home + "." + path);
        }

        @Override
        public List<Branch> branchList(String path) {
            return AbstractBranch.this.branchList(home + "." + path);
        }

        @Override
        public Map<String, Object> asMap() {
            return flat(context, "");
        }

        @Override
        public Object get(String path, Object ddefault) throws ConfigException {
            return get(context, path, ddefault);
        }

        @Override
        public void put(String path, Object value) throws ConfigException {
            put(context, path, value);
        }

    }
}
