package org.embulk.filter.column;

import io.github.medjed.jsonpathcompiler.expressions.Path;
import io.github.medjed.jsonpathcompiler.expressions.path.ArrayIndexOperation;
import io.github.medjed.jsonpathcompiler.expressions.path.ArrayPathToken;
import io.github.medjed.jsonpathcompiler.expressions.path.PathCompiler;
import io.github.medjed.jsonpathcompiler.expressions.path.PathToken;
import io.github.medjed.jsonpathcompiler.expressions.path.PropertyPathToken;
import io.github.medjed.jsonpathcompiler.expressions.path.RootPathToken;
import io.github.medjed.jsonpathcompiler.expressions.path.WildcardPathToken;
import org.embulk.config.ConfigException;
import org.embulk.spi.type.Type;
import org.msgpack.value.StringValue;
import org.msgpack.value.Value;
import org.msgpack.value.ValueFactory;

public class JsonColumn
{
    private final String path;
    private final Type type;
    private final Value defaultValue;
    private final String src;

    private StringValue pathValue = null;
    private String parentPath = null;
    private Long tailIndex = null;
    private StringValue parentPathValue = null;
    private Value tailNameValue = null;

    private StringValue srcValue = null;
    private String srcParentPath = null;
    private Long srcTailIndex = null;
    private StringValue srcParentPathValue = null;
    private Value srcTailNameValue = null;

    public static final int WILDCARD_INDEX = -1;

    public JsonColumn(String path, Type type)
    {
        this(path, type, null, null);
    }

    public JsonColumn(String path, Type type, Value defaultValue)
    {
        this(path, type, defaultValue, null);
    }

    public JsonColumn(String path, Type type, Value defaultValue, String src)
    {
        Path compiledPath = PathCompiler.compile(path);
        Path compiledSrc = src == null ? compiledPath : PathCompiler.compile(src);
        RootPathToken compiledRoot = (RootPathToken) compiledPath.getRoot();
        RootPathToken compiledSrcRoot = (RootPathToken) compiledSrc.getRoot();
        this.path = compiledPath.toString();
        this.type = type;
        this.defaultValue = (defaultValue == null ? ValueFactory.newNil() : defaultValue);
        this.src = compiledSrc.toString();

        this.pathValue = ValueFactory.newString(path);
        this.parentPath = compiledPath.getParentPath();

        this.tailIndex = getTailIndex(compiledRoot);
        this.parentPathValue = ValueFactory.newString(parentPath);
        String tailName = getTailName(compiledRoot);
        this.tailNameValue = tailName == null ? ValueFactory.newNil() : ValueFactory.newString(tailName);

        this.srcValue = ValueFactory.newString(this.src);
        this.srcParentPath = compiledSrc.getParentPath();
        this.srcTailIndex = getTailIndex(compiledSrcRoot);
        this.srcParentPathValue = ValueFactory.newString(this.srcParentPath);
        String srcTailName = getTailName(compiledSrcRoot);
        this.srcTailNameValue = srcTailName == null ? ValueFactory.newNil() : ValueFactory.newString(srcTailName);

        if (!srcParentPath.equals(parentPath)) {
            throw new ConfigException(String.format("The branch (parent path) of src \"%s\" must be same with of name \"%s\" yet", src, path));
        }
    }

    // $['foo'] or $.foo => foo
    // $['foo'][0] or $.foo[0] or $['foo'][*] or $.foo[*] => null
    private String getTailName(RootPathToken root)
    {
        PathToken pathToken = root.getTail();
        if (pathToken instanceof PropertyPathToken) {
            if (!((PropertyPathToken) pathToken).singlePropertyCase()) {
                throw new ConfigException(String.format("Multiple property is not supported \"%s\"", root.toString()));
            }
            return ((PropertyPathToken) pathToken).getProperties().get(0);
        }
        else {
            return null;
        }
    }

    private Long getTailIndex(RootPathToken root)
    {
        PathToken tail = root.getTail();
        if (tail instanceof ArrayPathToken) {
            ArrayIndexOperation arrayIndexOperation = ((ArrayPathToken) tail).getArrayIndexOperation();
            JsonPathUtil.assertSupportedArrayPathToken(arrayIndexOperation, path);
            return arrayIndexOperation.indexes().get(0).longValue();
        }
        else if (tail instanceof WildcardPathToken) {
            return Long.valueOf(WILDCARD_INDEX);
        }
        else {
            return null;
        }
    }

    public String getPath()
    {
        return path;
    }

    public Type getType()
    {
        return type;
    }

    public Value getDefaultValue()
    {
        return defaultValue;
    }

    public String getSrc()
    {
        return src;
    }

    public StringValue getPathValue()
    {
        return pathValue;
    }

    public String getParentPath()
    {
        return parentPath;
    }

    public Long getTailIndex()
    {
        return tailIndex;
    }

    public StringValue getParentPathValue()
    {
        return parentPathValue;
    }

    public Value getTailNameValue()
    {
        return tailNameValue;
    }

    public StringValue getSrcValue()
    {
        return srcValue;
    }

    public String getSrcParentPath()
    {
        return srcParentPath;
    }

    public Long getSrcTailIndex()
    {
        return srcTailIndex;
    }

    public StringValue getSrcParentPathValue()
    {
        return srcParentPathValue;
    }

    public Value getSrcTailNameValue()
    {
        return srcTailNameValue;
    }

    // like File.dirname
    public static String parentPath(String path)
    {
        return PathCompiler.compile(path).getParentPath();
    }

    public static String tailName(String path)
    {
        return ((RootPathToken) PathCompiler.compile(path).getRoot()).getTailPath();
    }

    public static Long getTailIndex(String path)
    {
        Path compiledPath = PathCompiler.compile(path);
        PathToken tail = ((RootPathToken) compiledPath.getRoot()).getTail();
        if (tail instanceof ArrayPathToken) {
            ArrayIndexOperation arrayIndexOperation = ((ArrayPathToken) tail).getArrayIndexOperation();
            if (arrayIndexOperation == null) {
                throw new ConfigException(String.format("Array Slice Operation is not supported \"%s\"", path));
            }
            if (arrayIndexOperation.isSingleIndexOperation()) {
                return arrayIndexOperation.indexes().get(0).longValue();
            }
            else {
                throw new ConfigException(String.format("Multi Array Indexes is not supported \"%s\"", path));
            }
        }
        else {
            return null;
        }
    }
}