/*
 * Copyright 2017 the original author or authors.
 *
 * Licensed 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.gradle.plugins.javascript.rhino.worker.internal;

import org.gradle.api.Action;
import org.gradle.api.UncheckedIOException;
import org.gradle.util.GFileUtils;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.FunctionObject;
import org.mozilla.javascript.Scriptable;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.LinkedHashMap;
import java.util.Map;

public abstract class RhinoWorkerUtils {

    public interface ScopeOperation<T> {
        void initContext(Context context);
        T action(Scriptable scope, Context context);
    }

    public static class DefaultScopeOperation<T> implements ScopeOperation<T> {
        public void initContext(Context context) {}
        public T action(Scriptable scope, Context context) {
            return null;
        }
    }

    public static String readFile(File file, String encoding) {
        return GFileUtils.readFile(file, encoding);
    }

    public static void writeFile(String content, File destination, String encoding) {
        GFileUtils.writeFile(content, destination, encoding);
    }

    public static Scriptable parse(File source, String encoding) {
        return parse(source, encoding, null);
    }

    public static <T> T parseRhino(File rhinoScript, ScopeOperation<T> operation) {
        Context context = Context.enter();
        try {
            operation.initContext(context);
            Scriptable scope = context.initStandardObjects();
            String printFunction = "function print(message) {}";
            context.evaluateString(scope, printFunction, "print", 1, null);
            context.evaluateString(scope, readFile(rhinoScript, "UTF-8"), rhinoScript.getName(), 1, null);
            return operation.action(scope, context);
        } finally {
            Context.exit();
        }
    }

    public static Scriptable parse(File source, String encoding, Action<Context> contextConfig) {
        Context context = Context.enter();
        if (contextConfig != null) {
            contextConfig.execute(context);
        }

        Scriptable scope = context.initStandardObjects();
        try {
            Reader reader = new InputStreamReader(new FileInputStream(source), encoding);
            try {
                context.evaluateReader(scope, reader, source.getName(), 0, null);
            } finally {
                reader.close();
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        } finally {
            Context.exit();
        }

        return scope;
    }

    public static <R> R childScope(Scriptable parentScope, ScopeOperation<R> operation) {
        Context context = Context.enter();
        try {
            operation.initContext(context);
            Scriptable childScope = context.newObject(parentScope);
            childScope.setParentScope(parentScope);
            return operation.action(childScope, context);
        } finally {
            Context.exit();
        }
    }

    public static Map<String, Object> toMap(Scriptable obj) {
        Map<String, Object> map = new LinkedHashMap<String, Object>();

        for (Object id : obj.getIds()) {
            String key;
            Object value;
            if (id instanceof String) {
                key = (String) id;
                value = obj.get(key, obj);
            } else if (id instanceof Integer) {
                key = id.toString();
                value = obj.get((Integer) id, obj);
            } else {
                throw new IllegalArgumentException(String.format("Unexpected key type: %s (value: %s)", id.getClass().getName(), id));
            }

            map.put(key, toJavaValue(value));
        }

        return map;
    }

    public static Object toJavaValue(Object object) {
        if (object == null || object.equals(Context.getUndefinedValue())) {
            return null;
        } else if (object.getClass().getPackage().getName().startsWith("java.")) {
            return object;
        } else if (object instanceof FunctionObject) {
            throw new IllegalArgumentException(String.format("Cannot convert function object to value (object: %s)", object));
        } else if (object instanceof Scriptable) {
            return toMap((Scriptable) object);
        } else {
            throw new IllegalArgumentException(String.format("Can't convert JS object %s (type: %s) to native Java object", object, object.getClass().getName()));
        }
    }


}
