package org.gcube.application.geoportal.common.model;


import com.jayway.jsonpath.*;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.gcube.application.geoportal.common.JSONSerializationProvider;
import org.reflections.Reflections;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Slf4j
public class JSONPathWrapper {

    /**
     * Based on
     *
     * https://github.com/json-path/JsonPath
     */

    public static Configuration JSON_PATH_ALWAYS_LIST_CONFIG=null;
    public static Configuration JSON_PATH_PATHS_CONFIGURATION=null;

    static {
        Reflections reflections = new Reflections(
                new ConfigurationBuilder()
                        .forPackage("org.gcube.application")
                        .filterInputsBy(new FilterBuilder().includePackage("org.gcube.application")));

        reflections.getSubTypesOf(JSONSerializationProvider.class).iterator().forEachRemaining(providerClass->{
            if(!providerClass.isInterface()){
                try {
                    log.warn("Loading JSON Provider {} ",providerClass);
                    JSONSerializationProvider provider = providerClass.newInstance();
                    provider.setJSONWrapperDefaults();
                }catch (Throwable t){
                    log.error("Unable to instantiate provider "+providerClass,t);
                }
            }

        });

        JSON_PATH_ALWAYS_LIST_CONFIG= Configuration.builder().options(Option.ALWAYS_RETURN_LIST,Option.SUPPRESS_EXCEPTIONS,Option.DEFAULT_PATH_LEAF_TO_NULL).build();
        JSON_PATH_PATHS_CONFIGURATION = Configuration.builder().options(Option.AS_PATH_LIST,Option.SUPPRESS_EXCEPTIONS,Option.DEFAULT_PATH_LEAF_TO_NULL).build();

    }



    @Getter
    DocumentContext valueCTX =null;
    DocumentContext pathsCTX =null;


    public JSONPathWrapper(String json) {

        valueCTX =JsonPath.using(JSON_PATH_ALWAYS_LIST_CONFIG).parse(json);
        pathsCTX =JsonPath.using(JSON_PATH_PATHS_CONFIGURATION).parse(json);
    }

    public List<Object> getByPath(String path){
        return getByPath(path,Object.class);
    }

    public List<String> getMatchingPaths(String path){
            List<String> l=pathsCTX.read(path);
            l.removeIf(p->p==null);
            return l;
    }

    public <T> List<T> getByPath(String path,Class<T> clazz){
        List<T> l= valueCTX.read(path, new TypeRef<List<T>>() {});
        l.removeIf(p->p==null);
        return l;
    }

    /**
     * Changes the value of an existent field at @path
     *
     * @param path
     * @param toSet
     * @return
     */
    public JSONPathWrapper setElement(String path, Object toSet){
        log.debug("Setting Path {} = {}",path,toSet);
        JsonPath jPath=JsonPath.compile(path);
        if(jPath.isDefinite()){
            valueCTX.set(path,toSet);
        } else{
            log.debug("Path is not definte, evaluating matching paths..");
            for(String p : getMatchingPaths(path)){
                log.debug("Actually setting {} as {} ",p,toSet);
                setElement(p,toSet);
            }
        }
        return this;
    }

    /**
     *  Creates a new element @elementName == @toSet as child of all matching @path
     *
     * @param path
     * @param elementName
     * @param toSet
     * @return
     */
    public JSONPathWrapper putElement(String path,String elementName, Object toSet){
        log.debug("Putting {} = {} at Path {}",elementName,toSet,path);
        JsonPath jPath=JsonPath.compile(path);
        if(jPath.isDefinite()){
            valueCTX.put(path,elementName,toSet);
        } else{
            log.debug("Path is not definte, evaluating matching paths..");
            for(String p : getMatchingPaths(path)){
                putElement(p,elementName,toSet);
            }
        }
        return this;
    }

    /**
     *  Appends a new element @toSet as child of all arrays matching @path
     *
     * @param path
     * @param toAdd
     * @return
     */
    public JSONPathWrapper addElementToArray(String path, Object toAdd){
        log.debug("Setting Path {} = {}",path,toAdd);
        JsonPath jPath=JsonPath.compile(path);
        if(jPath.isDefinite()){
            valueCTX.add(path,toAdd);
        } else{
            log.debug("Path is not definte, evaluating matching paths..");
            for(String p : getMatchingPaths(path)){
                addElementToArray(p,toAdd);
            }
        }
        return this;
    }


    //***** EXTENSIONS

   private static Pattern DOTTED_PATH_PATTERN=Pattern.compile("(\\$?\\.\\w*(\\[((\\?\\(.*\\))|(\\d+|\\*))\\])?)(?=(\\.|$)?)");
    private static Pattern BRACKET_PATH_PATTERN=Pattern.compile("(\\$?\\['\\w*'\\](\\[((\\?\\(.*\\))|(\\d+|\\*))\\])?)");


/*    private static final DocumentContext addElement(DocumentContext ctx,String path,Object value) throws JsonPathException{
        log.debug("Inserting object at {}",path);
        JsonPath jPath=JsonPath.compile(path);
        if(jPath.isDefinite()) {
            List<String> tokens=tokenizePath(path);
            DocumentContext currentCtx=ctx;
            String parentPath = null;
            for(int i = 0; i<tokens.size();i++){
                String s= tokens.get(i);
                log.trace("Managing {}",s);
                if(parentPath== null) parentPath="$";
                else parentPath +=s;

                String elementName = extractFieldNameFromPathElement(s);

                if(i==tokens.size()-1) {
                    // IF LEAF SET
                    log.trace("{} is leaf, setting as {}", s, value);
                    currentCtx.put("$",elementName,value);
                }else {
                    // Intermediate element

                    List<String> foundElements = currentCtx.read(s);
                    foundElements.removeIf(v->(v==null));

                    if(foundElements.isEmpty()){
                        Document newElement=null;
                        log.trace("{} not found..");
                        // add child
                        if (s.matches("")){
                            log.trace("{} is array, checking existence",s,value);
                            // ARRAY
                            // if array exists add elements else create new
                            List<?> foundArray= currentCtx.read(elementName,List.class);
                            throw new RuntimeException("Implement array support");
                        } else {
                            // Map
                            newElement = new Document();
                        }
                        currentCtx.put("$",elementName,newElement);
                        log.trace("Set {}, json is {}",s,currentCtx.jsonString());
                        currentCtx= JsonPath.parse(newElement.toJson());
                    }else currentCtx = JsonPath.parse(currentCtx.read(s,Document.class));
                }
            } return ctx;
        }else throw new JsonPathException("Unable to initialize non-definite path : "+path);
    }

*/
    static List<String> tokenizePath(String path){
        List<String> toReturn=null;
        log.debug("Tokenizing JSON Path {} ",path);
        // if path is $.element.child
        if(path.matches("(\\$\\.)?\\w.*"))
            toReturn= getByRegex(path, DOTTED_PATH_PATTERN);
        else if (path.matches("(\\$)?\\[\\'.*"))
            toReturn =  getByRegex(path, BRACKET_PATH_PATTERN);

        log.debug("Path {} as tokens {}",path,toReturn);
        return toReturn;
    }

    private static List<String> getByRegex(String s,Pattern p){
        ArrayList<String> toReturn = new ArrayList<>();
        Matcher m =  p.matcher(s);
        log.trace("Groups from {} with {} : ",s,p.pattern());
        while(m.find()) {
            String found=m.group();
            toReturn.add(found);
        }
        return toReturn;
    }

    static String extractFieldNameFromPathElement(String p){
        Matcher m=Pattern.compile("\\$?\\.?(\\[')?([a-zA-Z_]+)").matcher(p);
        m.find();
        return m.group(2);
    }

}
