package com.jsoniter.any;

import com.jsoniter.*;
import com.jsoniter.output.JsonStream;
import com.jsoniter.spi.JsonException;
import com.jsoniter.spi.TypeLiteral;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

class ObjectLazyAny extends LazyAny {

    private final static TypeLiteral<Map<String, Any>> typeLiteral = new TypeLiteral<Map<String, Any>>(){};
    private Map<String, Any> cache;
    private int lastParsedPos;

    public ObjectLazyAny(byte[] data, int head, int tail) {
        super(data, head, tail);
        lastParsedPos = head;
    }

    @Override
    public ValueType valueType() {
        return ValueType.OBJECT;
    }

    @Override
    public Object object() {
        fillCache();
        return cache;
    }

    @Override
    public boolean toBoolean() {
        try {
            JsonIterator iter = parse();
            try {
                return CodegenAccess.readObjectStart(iter);
            } finally {
                JsonIteratorPool.returnJsonIterator(iter);
            }
        } catch (IOException e) {
            throw new JsonException(e);
        }
    }

    @Override
    public int toInt() {
        return size();
    }

    @Override
    public long toLong() {
        return size();
    }

    @Override
    public float toFloat() {
        return size();
    }

    @Override
    public double toDouble() {
        return size();
    }

    @Override
    public BigInteger toBigInteger() {
        return BigInteger.valueOf(size());
    }

    @Override
    public BigDecimal toBigDecimal() {
        return BigDecimal.valueOf(size());
    }

    @Override
    public int size() {
        fillCache();
        return cache.size();
    }

    @Override
    public Set<String> keys() {
        fillCache();
        return (Set) cache.keySet();
    }

    @Override
    public Any get(Object key) {
        Any element = fillCacheUntil(key);
        if (element == null) {
            return new NotFoundAny(key, object());
        }
        return element;
    }

    @Override
    public Any get(Object[] keys, int idx) {
        if (idx == keys.length) {
            return this;
        }
        Object key = keys[idx];
        if (isWildcard(key)) {
            fillCache();
            HashMap<String, Any> result = new HashMap<String, Any>();
            for (Map.Entry<String, Any> entry : cache.entrySet()) {
                Any mapped = entry.getValue().get(keys, idx + 1);
                if (mapped.valueType() != ValueType.INVALID) {
                    result.put(entry.getKey(), mapped);
                }
            }
            return Any.rewrap(result);
        }
        Any child = fillCacheUntil(key);
        if (child == null) {
            return new NotFoundAny(keys, idx, object());
        }
        return child.get(keys, idx+1);
    }

    private Any fillCacheUntil(Object target) {
        if (lastParsedPos == tail) {
            return cache.get(target);
        }
        if (cache == null) {
            cache = new HashMap<String, Any>(4);
        }
        Any value = cache.get(target);
        if (value != null) {
            return value;
        }
        JsonIterator iter = JsonIteratorPool.borrowJsonIterator();
        try {
            iter.reset(data, lastParsedPos, tail);
            if (lastParsedPos == head) {
                if (!CodegenAccess.readObjectStart(iter)) {
                    lastParsedPos = tail;
                    return null;
                }
                String field = CodegenAccess.readObjectFieldAsString(iter);
                value = iter.readAny();
                cache.put(field, value);
                if (field.hashCode() == target.hashCode() && field.equals(target)) {
                    lastParsedPos = CodegenAccess.head(iter);
                    return value;
                }
            }
            while (CodegenAccess.nextToken(iter) == ',') {
                String field = CodegenAccess.readObjectFieldAsString(iter);
                value = iter.readAny();
                cache.put(field, value);
                if (field.hashCode() == target.hashCode() && field.equals(target)) {
                    lastParsedPos = CodegenAccess.head(iter);
                    return value;
                }
            }
            lastParsedPos = tail;
            return null;
        } catch (IOException e) {
            throw new JsonException(e);
        } finally {
            JsonIteratorPool.returnJsonIterator(iter);
        }
    }

    private void fillCache() {
        if (lastParsedPos == tail) {
            return;
        }
        if (cache == null) {
            cache = new HashMap<String, Any>(4);
        }
        JsonIterator iter = JsonIteratorPool.borrowJsonIterator();
        try {
            iter.reset(data, lastParsedPos, tail);
            if (lastParsedPos == head) {
                if (!CodegenAccess.readObjectStart(iter)) {
                    lastParsedPos = tail;
                    return;
                }
                String field = CodegenAccess.readObjectFieldAsString(iter);
                cache.put(field, iter.readAny());
            }
            while (CodegenAccess.nextToken(iter) == ',') {
                String field = CodegenAccess.readObjectFieldAsString(iter);
                cache.put(field, iter.readAny());
            }
            lastParsedPos = tail;
        } catch (IOException e) {
            throw new JsonException(e);
        } finally {
            JsonIteratorPool.returnJsonIterator(iter);
        }
    }

    @Override
    public EntryIterator entries() {
        return new LazyIterator();
    }


    private class LazyIterator implements EntryIterator {

        private Iterator<Map.Entry<String, Any>> mapIter;
        private String key;
        private Any value;

        public LazyIterator() {
            if (cache == null) {
                cache = new HashMap<String, Any>();
            }
            mapIter = new HashMap<String, Any>(cache).entrySet().iterator();
            try {
                if (lastParsedPos == head) {
                    JsonIterator iter = JsonIteratorPool.borrowJsonIterator();
                    try {
                        iter.reset(data, lastParsedPos, tail);
                        if (!CodegenAccess.readObjectStart(iter)) {
                            lastParsedPos = tail;
                        } else {
                            lastParsedPos = CodegenAccess.head(iter);
                        }
                    } finally {
                        JsonIteratorPool.returnJsonIterator(iter);
                    }
                }
            } catch (IOException e) {
                throw new JsonException(e);
            }
        }

        @Override
        public boolean next() {
            if (lastParsedPos == tail) {
                return false;
            }
            if (mapIter != null) {
                if (mapIter.hasNext()) {
                    Map.Entry<String, Any> entry = mapIter.next();
                    key = entry.getKey();
                    value = entry.getValue();
                    return true;
                } else {
                    mapIter = null;
                }
            }
            JsonIterator iter = JsonIteratorPool.borrowJsonIterator();
            try {
                iter.reset(data, lastParsedPos, tail);
                key = CodegenAccess.readObjectFieldAsString(iter);
                value = iter.readAny();
                cache.put(key, value);
                if (CodegenAccess.nextToken(iter) == ',') {
                    lastParsedPos = CodegenAccess.head(iter);
                } else {
                    lastParsedPos = tail;
                }
            } catch (IOException e) {
                throw new JsonException(e);
            } finally {
                JsonIteratorPool.returnJsonIterator(iter);
            }
            return true;
        }

        @Override
        public String key() {
            return key;
        }

        @Override
        public Any value() {
            return value;
        }
    }

    @Override
    public void writeTo(JsonStream stream) throws IOException {
        if (lastParsedPos == head) {
            super.writeTo(stream);
        } else {
            // there might be modification
            fillCache();
            stream.writeVal(typeLiteral, (Map) cache);
        }
    }

    @Override
    public String toString() {
        if (lastParsedPos == head) {
            return super.toString();
        } else {
            fillCache();
            return JsonStream.serialize(cache);
        }
    }
}
