/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.brooklyn.feed.http;

import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import java.util.NoSuchElementException;

import javax.annotation.Nullable;

import org.apache.brooklyn.util.guava.Functionals;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.guava.MaybeFunctions;

import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.jayway.jsonpath.JsonPath;

public class JsonFunctions {

    private JsonFunctions() {} // instead use static utility methods
    
    /** @deprecated since 0.9.0 kept only to allow conversion of anonymous inner classes */
    @SuppressWarnings("unused") @Deprecated 
    private static Function<String, JsonElement> asJsonOld() {
        // TODO PERSISTENCE WORKAROUND
        return new Function<String, JsonElement>() {
            @Override public JsonElement apply(String input) {
                return new JsonParser().parse(input);
            }
        };
    }

    /** @deprecated since 0.9.0 kept only to allow conversion of anonymous inner classes */
    @SuppressWarnings("unused") @Deprecated 
    private static <T> Function<JsonElement, List<T>> forEachOld(final Function<JsonElement, T> func) {
        // TODO PERSISTENCE WORKAROUND
        return new Function<JsonElement, List<T>>() {
            @Override public List<T> apply(JsonElement input) {
                JsonArray array = (JsonArray) input;
                List<T> result = Lists.newArrayList();
                for (int i = 0; i < array.size(); i++) {
                    result.add(func.apply(array.get(i)));
                }
                return result;
            }
        };
    }

    /** @deprecated since 0.9.0 kept only to allow conversion of anonymous inner classes */
    @SuppressWarnings("unused") @Deprecated 
    private static Function<JsonElement, JsonElement> walkOld(final Iterable<String> elements) {
        // TODO PERSISTENCE WORKAROUND
        return new Function<JsonElement, JsonElement>() {
            @Override public JsonElement apply(JsonElement input) {
                JsonElement curr = input;
                for (String element : elements) {
                    JsonObject jo = curr.getAsJsonObject();
                    curr = jo.get(element);
                    if (curr==null)
                        throw new NoSuchElementException("No element '"+element+" in JSON, when walking "+elements);
                }
                return curr;
            }
        };
    }

    /** as {@link #walk(Iterable))} but if any element is not found it simply returns null */
    /** @deprecated since 0.9.0 kept only to allow conversion of anonymous inner classes */
    @SuppressWarnings("unused") @Deprecated 
    private static Function<JsonElement, JsonElement> walkNOld(final Iterable<String> elements) {
        // TODO PERSISTENCE WORKAROUND
        return new Function<JsonElement, JsonElement>() {
            @Override public JsonElement apply(JsonElement input) {
                JsonElement curr = input;
                for (String element : elements) {
                    if (curr==null) return null;
                    JsonObject jo = curr.getAsJsonObject();
                    curr = jo.get(element);
                }
                return curr;
            }
        };
    }

    /** @deprecated since 0.9.0 kept only to allow conversion of anonymous inner classes */
    @SuppressWarnings("unused") @Deprecated 
    private static Function<Maybe<JsonElement>, Maybe<JsonElement>> walkMOld(final Iterable<String> elements) {
        // TODO PERSISTENCE WORKAROUND
        return new Function<Maybe<JsonElement>, Maybe<JsonElement>>() {
            @Override public Maybe<JsonElement> apply(Maybe<JsonElement> input) {
                Maybe<JsonElement> curr = input;
                for (String element : elements) {
                    if (curr.isAbsent()) return curr;
                    JsonObject jo = curr.get().getAsJsonObject();
                    JsonElement currO = jo.get(element);
                    if (currO==null) return Maybe.absent("No element '"+element+" in JSON, when walking "+elements);
                    curr = Maybe.of(currO);
                }
                return curr;
            }
        };
    }

    /** @deprecated since 0.9.0 kept only to allow conversion of anonymous inner classes */
    @SuppressWarnings("unused") @Deprecated 
    private static <T> Function<JsonElement,T> getPathOld(final String path) {
        // TODO PERSISTENCE WORKAROUND
        return new Function<JsonElement, T>() {
            @SuppressWarnings("unchecked")
            @Override public T apply(JsonElement input) {
                String jsonString = input.toString();
                Object rawElement = JsonPath.read(jsonString, path);
                return (T) rawElement;
            }
        };
    }

    /** @deprecated since 0.9.0 kept only to allow conversion of anonymous inner classes */
    @SuppressWarnings("unused") @Deprecated 
    private static <T> Function<JsonElement, T> castOld(final Class<T> expected) {
        // TODO PERSISTENCE WORKAROUND
        return new Function<JsonElement, T>() {
            @Override public T apply(JsonElement input) {
                return doCast(input, expected);
            }
        };
    }
    
    /** @deprecated since 0.9.0 kept only to allow conversion of anonymous inner classes */
    @SuppressWarnings("unused") @Deprecated 
    private static <T> Function<Maybe<JsonElement>, T> castMOld(final Class<T> expected, final T defaultValue) {
        // TODO PERSISTENCE WORKAROUND
        return new Function<Maybe<JsonElement>, T>() {
            @Override
            public T apply(Maybe<JsonElement> input) {
                if (input.isAbsent()) return defaultValue;
                return cast(expected).apply(input.get());
            }
        };
    }

    public static Function<String, JsonElement> asJson() {
        return new AsJson();
    }

    protected static class AsJson implements Function<String, JsonElement> {
        @Override public JsonElement apply(String input) {
            return new JsonParser().parse(input);
        }
    }

    public static <T> Function<JsonElement, List<T>> forEach(final Function<JsonElement, T> func) {
        return new ForEach<T>(func);
    }

    protected static class ForEach<T> implements Function<JsonElement, List<T>> {
        private final Function<JsonElement, T> func;
        public ForEach(Function<JsonElement, T> func) {
            this.func = func;
        }

        @Override public List<T> apply(JsonElement input) {
            JsonArray array = (JsonArray) input;
            List<T> result = Lists.newArrayList();
            for (int i = 0; i < array.size(); i++) {
                result.add(func.apply(array.get(i)));
            }
            return result;
        }
    }

    /** as {@link #walkM(Iterable)} taking a single string consisting of a dot separated path */
    public static Function<JsonElement, JsonElement> walk(String elementOrDotSeparatedElements) {
        return walk( Splitter.on('.').split(elementOrDotSeparatedElements) );
    }

    /** as {@link #walkM(Iterable)} taking a series of strings (dot separators not respected here) */
    public static Function<JsonElement, JsonElement> walk(final String... elements) {
        return walk(Arrays.asList(elements));
    }

    /** returns a function which traverses the supplied path of entries in a json object (maps of maps of maps...), 
     * @throws NoSuchElementException if any path is not present as a key in that map */
    public static Function<JsonElement, JsonElement> walk(final Iterable<String> elements) {
        // could do this instead, pointing at Maybe for this, and for walkN, but it's slightly less efficient
//      return Functionals.chain(MaybeFunctions.<JsonElement>wrap(), walkM(elements), MaybeFunctions.<JsonElement>get());
        return new Walk(elements);
    }

    protected static class Walk implements Function<JsonElement, JsonElement> {
        private final Iterable<String> elements;
        public Walk(Iterable<String> elements) {
            this.elements = elements;
        }
        @Override public JsonElement apply(JsonElement input) {
            JsonElement curr = input;
            for (String element : elements) {
                JsonObject jo = curr.getAsJsonObject();
                curr = jo.get(element);
                if (curr==null)
                    throw new NoSuchElementException("No element '"+element+" in JSON, when walking "+elements);
            }
            return curr;
        }
    }
    
    /** as {@link #walk(String)} but if any element is not found it simply returns null */
    public static Function<JsonElement, JsonElement> walkN(@Nullable String elements) {
        return walkN( Splitter.on('.').split(elements) );
    }

    /** as {@link #walk(String...))} but if any element is not found it simply returns null */
    public static Function<JsonElement, JsonElement> walkN(final String... elements) {
        return walkN(Arrays.asList(elements));
    }

    /** as {@link #walk(Iterable))} but if any element is not found it simply returns null */
    public static Function<JsonElement, JsonElement> walkN(final Iterable<String> elements) {
        return new WalkN(elements);
    }

    protected static class WalkN implements Function<JsonElement, JsonElement> {
        private final Iterable<String> elements;

        public WalkN(Iterable<String> elements) {
            this.elements = elements;
        }
        @Override public JsonElement apply(JsonElement input) {
            JsonElement curr = input;
            for (String element : elements) {
                if (curr==null) return null;
                JsonObject jo = curr.getAsJsonObject();
                curr = jo.get(element);
            }
            return curr;
        }
    }

    /** as {@link #walk(String))} and {@link #walk(Iterable)} */
    public static Function<Maybe<JsonElement>, Maybe<JsonElement>> walkM(@Nullable String elements) {
        return walkM( Splitter.on('.').split(elements) );
    }

    /** as {@link #walk(String...))} and {@link #walk(Iterable)} */
    public static Function<Maybe<JsonElement>, Maybe<JsonElement>> walkM(final String... elements) {
        return walkM(Arrays.asList(elements));
    }

    /** as {@link #walk(Iterable))} but working with objects which {@link Maybe} contain {@link JsonElement},
     * simply preserving a {@link Maybe#absent()} object if additional walks are requested upon it
     * (cf jquery) */
    public static Function<Maybe<JsonElement>, Maybe<JsonElement>> walkM(final Iterable<String> elements) {
        return new WalkM(elements);
    }

    protected static class WalkM implements Function<Maybe<JsonElement>, Maybe<JsonElement>> {
        private final Iterable<String> elements;

        public WalkM(Iterable<String> elements) {
            this.elements = elements;
        }

        @Override public Maybe<JsonElement> apply(Maybe<JsonElement> input) {
            Maybe<JsonElement> curr = input;
            for (String element : elements) {
                if (curr.isAbsent()) return curr;
                JsonObject jo = curr.get().getAsJsonObject();
                JsonElement currO = jo.get(element);
                if (currO==null) return Maybe.absent("No element '"+element+" in JSON, when walking "+elements);
                curr = Maybe.of(currO);
            }
            return curr;
        }
    }
    
    /**
     * returns an element from a single json primitive value given a full path {@link com.jayway.jsonpath.JsonPath}
     */
    public static <T> Function<JsonElement,T> getPath(final String path) {
        return new GetPath<T>(path);
    }

    protected static class GetPath<T> implements Function<JsonElement, T> {
        private final String path;

        public GetPath(String path) {
            this.path = path;
        }
        @SuppressWarnings("unchecked")
        @Override public T apply(JsonElement input) {
            String jsonString = input.toString();
            Object rawElement = JsonPath.read(jsonString, path);
            return (T) rawElement;
        }
    };

    public static <T> Function<JsonElement, T> cast(final Class<T> expected) {
        return new Cast<T>(expected);
    }
    
    protected static class Cast<T> implements Function<JsonElement, T> {
        private final Class<T> expected;

        public Cast(Class<T> expected) {
            this.expected = expected;
        }

        @Override public T apply(JsonElement input) {
            return doCast(input, expected);
        }
    };

    @SuppressWarnings("unchecked")
    protected static <T> T doCast(JsonElement input, Class<T> expected) {
        if (input == null) {
            return null;
        } else if (input.isJsonNull()) {
            return null;
        } else if (expected == boolean.class || expected == Boolean.class) {
            return (T) (Boolean) input.getAsBoolean();
        } else if (expected == char.class || expected == Character.class) {
            return (T) (Character) input.getAsCharacter();
        } else if (expected == byte.class || expected == Byte.class) {
            return (T) (Byte) input.getAsByte();
        } else if (expected == short.class || expected == Short.class) {
            return (T) (Short) input.getAsShort();
        } else if (expected == int.class || expected == Integer.class) {
            return (T) (Integer) input.getAsInt();
        } else if (expected == long.class || expected == Long.class) {
            return (T) (Long) input.getAsLong();
        } else if (expected == float.class || expected == Float.class) {
            return (T) (Float) input.getAsFloat();
        } else if (expected == double.class || expected == Double.class) {
            return (T) (Double) input.getAsDouble();
        } else if (expected == BigDecimal.class) {
            return (T) input.getAsBigDecimal();
        } else if (expected == BigInteger.class) {
            return (T) input.getAsBigInteger();
        } else if (Number.class.isAssignableFrom(expected)) {
            // TODO Will result in a class-cast if it's an unexpected sub-type of Number not handled above
            return (T) input.getAsNumber();
        } else if (expected == String.class) {
            return (T) input.getAsString();
        } else if (expected.isArray()) {
            JsonArray array = input.getAsJsonArray();
            Class<?> componentType = expected.getComponentType();
            if (JsonElement.class.isAssignableFrom(componentType)) {
                JsonElement[] result = new JsonElement[array.size()];
                for (int i = 0; i < array.size(); i++) {
                    result[i] = array.get(i);
                }
                return (T) result;
            } else {
                Object[] result = (Object[]) Array.newInstance(componentType, array.size());
                for (int i = 0; i < array.size(); i++) {
                    result[i] = cast(componentType).apply(array.get(i));
                }
                return (T) result;
            }
        } else {
            throw new IllegalArgumentException("Cannot cast json element to type "+expected);
        }
    }
    
    public static <T> Function<Maybe<JsonElement>, T> castM(final Class<T> expected) {
        return Functionals.chain(MaybeFunctions.<JsonElement>get(), cast(expected));
    }
    
    public static <T> Function<Maybe<JsonElement>, T> castM(final Class<T> expected, final T defaultValue) {
        return new CastM<T>(expected, defaultValue);
    }

    protected static class CastM<T> implements Function<Maybe<JsonElement>, T> {
        private final Class<T> expected;
        private final T defaultValue;

        public CastM(Class<T> expected, T defaultValue) {
            this.expected = expected;
            this.defaultValue = defaultValue;
        }
        @Override
        public T apply(Maybe<JsonElement> input) {
            if (input.isAbsent()) return defaultValue;
            return cast(expected).apply(input.get());
        }
    }
}
