/*
 * 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.jackrabbit.oak.run;


import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.PrintStream;
import java.io.Reader;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;
import javax.jcr.query.Row;
import javax.jcr.query.RowIterator;

import com.google.common.util.concurrent.MoreExecutors;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;

import org.apache.jackrabbit.oak.Oak;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.commons.json.JsonObject;
import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
import org.apache.jackrabbit.oak.commons.json.JsopReader;
import org.apache.jackrabbit.oak.commons.json.JsopTokenizer;
import org.apache.jackrabbit.oak.jcr.Jcr;
import org.apache.jackrabbit.oak.plugins.index.lucene.IndexTracker;
import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexProvider;
import org.apache.jackrabbit.oak.plugins.index.lucene.hybrid.DocumentQueue;
import org.apache.jackrabbit.oak.run.cli.NodeStoreFixtureProvider;
import org.apache.jackrabbit.oak.run.cli.Options;
import org.apache.jackrabbit.oak.run.commons.Command;
import org.apache.jackrabbit.oak.run.cli.NodeStoreFixture;
import org.apache.jackrabbit.oak.spi.commit.Observer;
import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider;
import org.apache.jackrabbit.oak.spi.state.NodeStore;
import org.apache.jackrabbit.oak.stats.StatisticsProvider;

public class JsonIndexCommand implements Command {
    public static final String INDEX = "json-index";

    PrintStream output = System.out;
    Session session;
    private boolean interactive;
    private final Map<String, Object> data = new HashMap<String, Object>();

    @Override
    public void execute(String... args) throws Exception {
        OptionParser parser = new OptionParser();
        OptionSpec<String> scriptOption = parser
                .accepts("script", "Path to Script").withOptionalArg()
                .defaultsTo("");
        OptionSpec<String> userOption = parser
                .accepts("user", "User name").withOptionalArg()
                .defaultsTo("admin");
        OptionSpec<String> passwordOption = parser
                .accepts("password", "Password").withOptionalArg()
                .defaultsTo("admin");

        Options oakOptions = new Options();
        OptionSet options = oakOptions.parseAndConfigure(parser, args);

        System.out.println("Opening nodestore...");
        NodeStoreFixture nodeStoreFixture = NodeStoreFixtureProvider.create(oakOptions);

        NodeStore nodeStore = nodeStoreFixture.getStore();
        String script = scriptOption.value(options);
        String user = userOption.value(options);
        String password = passwordOption.value(options);
        LineNumberReader reader = openScriptReader(script);
        try {
            process(nodeStore, reader, user, password);
        } finally {
            nodeStoreFixture.close();
            reader.close();
        }
    }

    private LineNumberReader openScriptReader(String script)
            throws IOException {
        Reader reader;
        if ("-".equals(script)) {
            reader = new InputStreamReader(System.in);
            interactive = true;
        } else {
            reader = new FileReader(script);
        }
        return new LineNumberReader(new BufferedReader(reader));
    }

    public void process(NodeStore nodeStore, LineNumberReader reader, String user, String password)
            throws Exception {
        session = openSession(nodeStore, user, password);
        System.out.println("Nodestore is open");
        if (interactive) {
            System.out.println("Type \"exit\" to quit");
        }
        while (true) {
            try {
                String json = readJson(reader);
                if (json == null || json.trim().equals("exit")) {
                    break;
                }
                execute(json);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if (session != null) {
            session.logout();
        }
    }

    private static String readJson(LineNumberReader reader) throws IOException {
        StringBuilder buff = new StringBuilder();
        int level = 0;
        while (true) {
            String line = reader.readLine();
            if (line == null) {
                return null;
            } else if (line.trim().startsWith("//")) {
                continue;
            }
            buff.append(line).append('\n');
            for (int i = 0; i < line.length(); i++) {
                char c = line.charAt(i);
                if (c == '\"') {
                    while (true) {
                        c = line.charAt(++i);
                        if (c == '\"') {
                            break;
                        } else if (c == '\\') {
                            ++i;
                        }
                    }
                } else if (c == '{') {
                    level++;
                } else if (c == '}') {
                    level--;
                }
            }
            if (level == 0 && !buff.toString().trim().isEmpty()) {
                return buff.toString();
            }
        }
    }

    void execute(String command) throws RepositoryException {
        JsopTokenizer t = new JsopTokenizer(command);
        t.read('{');
        JsonObject json = JsonObject.create(t);
        Map<String, String> properties = json.getProperties();
        if (properties.containsKey("if")) {
            Object value = getValueOrVariable(properties.get("if"));
            Object equals = getValueOrVariable(properties.get("="));
            if (value == null) {
                if (equals != null) {
                    return;
                }
            } else if (!value.equals(equals)) {
                return;
            }
        }
        for (Entry<String, String> e : properties.entrySet()) {
            String k = e.getKey();
            Object value = getValueOrVariable(e.getValue());
            if ("addNode".equals(k)) {
                String nodePath = value.toString();
                String parent = PathUtils.getParentPath(nodePath);
                if (session.nodeExists(parent)) {
                    Node p = session.getNode(parent);
                    String nodeName = PathUtils.getName(nodePath);
                    if (!p.hasNode(nodeName)) {
                        JsonObject node = json.getChildren().get("node");
                        addNode(p, nodeName, node);
                    }
                }
            } else if ("removeNode".equals(k)) {
                String path = value.toString();
                if (session.nodeExists(path)) {
                    session.getNode(path).remove();
                }
            } else if ("setProperty".equals(k)) {
                String itemPath = value.toString();
                String nodePath = PathUtils.getParentPath(itemPath);
                if (session.nodeExists(nodePath)) {
                    String propertyName = PathUtils.getName(itemPath);
                    Object propertyValue = getValueOrVariable(properties
                            .get("value"));
                    setProperty(session.getNode(nodePath), propertyName,
                            propertyValue);
                }
            } else if ("session".equals(k)) {
                if ("save".equals(value)) {
                    session.save();
                }
            } else if ("xpath".equals(k) || "sql".equals(k)) {
                String language = "xpath".equals(k) ? k : Query.JCR_SQL2;
                String columnName = "xpath".equals(k) ? "jcr:path" : null;
                boolean quiet = properties.containsKey("quiet");
                int depth = properties.containsKey("depth") ? Integer
                        .parseInt(properties.get("depth")) : 0;
                runQuery(value.toString(), language, columnName, quiet, depth);
            } else if ("print".equals(k)) {
                output.println(value);
            } else if ("for".equals(k)) {
                String name = JsopTokenizer.decodeQuoted(properties.get(k));
                Object old = data.get(name);
                String[] commands = (String[]) getValueOrVariable(properties
                        .get("do"));
                for (String x : (String[]) value) {
                    data.put(name, x);
                    for (String c : commands) {
                        execute(c);
                    }
                }
                data.put(name, old);
            } else if ("loop".equals(k)) {
                while (true) {
                    for (String c : (String[]) value) {
                        execute(c);
                        if (data.remove("$break") != null) {
                            return;
                        }
                    }
                }
            } else if (k.startsWith("$")) {
                setVariable(properties, k, value);
            }
        }
    }

    private void setVariable(Map<String, String> properties, String k,
            Object value) {
        if (k.startsWith("$$")) {
            k = "$" + getValueOrVariable("\"" + k.substring(1) + "\"");
        }
        if (properties.containsKey("+")) {
            Object v2 = getValueOrVariable(properties.get("+"));
            if (value == null) {
                value = v2;
            } else if (v2 == null) {
                // keep value
            } else if (v2 instanceof Long && value instanceof Long) {
                value = (Long) value + (Long) v2;
            } else {
                value = value.toString() + v2.toString();
            }
        }
        data.put(k, value);
    }

    private Object getValueOrVariable(String jsonValue) {
        Object v = getValue(jsonValue);
        if (v == null || !v.toString().startsWith("$")) {
            return v;
        }
        String value = v.toString();
        if (value.startsWith("$$")) {
            value = "$" + getValueOrVariable("\"" + value.substring(1) + "\"");
        }
        return data.get(value.toString());
    }

    private void addNode(Node p, String nodeName, JsonObject json)
            throws RepositoryException {
        Map<String, String> properties = json.getProperties();
        Map<String, JsonObject> children = json.getChildren();
        String primaryType = properties.get("jcr:primaryType");
        Node n;
        if (primaryType == null) {
            n = p.addNode(nodeName);
        } else {
            n = p.addNode(nodeName, getValueOrVariable(primaryType).toString());
        }
        for (Entry<String, String> e : properties.entrySet()) {
            String propertyName = e.getKey();
            if (!"jcr:primaryType".equals(propertyName)) {
                Object value = getValueOrVariable(properties.get(propertyName));
                setProperty(n, propertyName, value);
            }
        }
        for (Entry<String, JsonObject> e : children.entrySet()) {
            String k = e.getKey();
            JsonObject v = e.getValue();
            addNode(n, k, v);
        }
    }

    private static Object getValue(String jsonValue) {
        if (jsonValue == null) {
            return null;
        }
        JsopTokenizer t = new JsopTokenizer(jsonValue);
        if (t.matches(JsopReader.NULL)) {
            return null;
        } else if (t.matches(JsopReader.NUMBER)) {
            String n = t.getToken();
            if (n.indexOf('.') < 0) {
                return Long.parseLong(n);
            }
            return Double.parseDouble(n);
        } else if (t.matches(JsopReader.TRUE)) {
            return true;
        } else if (t.matches(JsopReader.FALSE)) {
            return false;
        } else if (t.matches(JsopReader.STRING)) {
            return t.getToken();
        } else if (t.matches('[')) {
            ArrayList<String> list = new ArrayList<String>();
            if (!t.matches(']')) {
                while (true) {
                    list.add(t.readRawValue());
                    if (t.matches(']')) {
                        break;
                    }
                    t.read(',');
                }
            }
            return list.toArray(new String[0]);
        }
        throw new IllegalArgumentException(jsonValue);
    }

    private static void setProperty(Node n, String propertyName, Object value)
            throws RepositoryException {
        int type = PropertyType.UNDEFINED;
        if (propertyName.startsWith("{")) {
            String t = propertyName.substring(1, propertyName.indexOf('}'));
            propertyName = propertyName.substring(t.length() + 2);
            type = PropertyType.valueFromName(t);
        }
        if (value == null) {
            n.setProperty(propertyName, (String) null);
            return;
        }
        if (type == PropertyType.UNDEFINED) {
            if (value instanceof Boolean) {
                type = PropertyType.BOOLEAN;
            } else if (value instanceof Long) {
                type = PropertyType.LONG;
            } else if (value instanceof Double) {
                type = PropertyType.DOUBLE;
            } else {
                type = PropertyType.STRING;
            }
        }
        if (value instanceof String[]) {
            String[] list = (String[]) value;
            for (int i = 0; i < list.length; i++) {
                list[i] = getValue(list[i]).toString();
            }
            n.setProperty(propertyName, list, type);
        } else {
            n.setProperty(propertyName, value.toString(), type);
        }
    }

    private void runQuery(String query, String language, String columnName,
            boolean quiet, int depth) throws RepositoryException {
        ArrayList<String> list = new ArrayList<String>();
        columnName = query.startsWith("explain") ? "plan" : columnName;
        QueryManager qm = session.getWorkspace().getQueryManager();
        Query q = qm.createQuery(query, language);
        for (String b : q.getBindVariableNames()) {
            ValueFactory vf = session.getValueFactory();
            q.bindValue(b, vf.createValue(data.get("$" + b).toString()));
        }
        QueryResult result = q.execute();
        if (depth != 0) {
            NodeIterator ni = result.getNodes();
            JsopBuilder builder = new JsopBuilder().array();
            while (ni.hasNext()) {
                Node n = ni.nextNode();
                builder.key(n.getPath());
                appendNode(builder, n, depth - 1);
            }
            output.println(JsopBuilder.prettyPrint(builder.endArray().toString()));
            return;
        }
        RowIterator ri = result.getRows();
        while (ri.hasNext()) {
            Row r = ri.nextRow();
            if (columnName != null) {
                String x = r.getValue(columnName).getString();
                list.add(x);
                if (!quiet) {
                    output.println(x);
                }
            } else {
                String[] columnNames = result.getColumnNames();
                for (String cn : columnNames) {
                    Value v = r.getValue(cn);
                    String x = v == null ? null : v.getString();
                    if (columnNames.length == 1) {
                        list.add(x);
                        if (!quiet) {
                            output.println(x);
                        }
                    } else {
                        list.add(x);
                        if (!quiet) {
                            output.println(cn + ": " + x);
                        }
                    }
                }
            }
        }
        data.put("$resultSize", (long) list.size());
        data.put("$result", list.toArray(new String[0]));
    }

    private void appendNode(JsopBuilder builder, Node n, int depth)
            throws RepositoryException {
        builder.object();
        for (PropertyIterator it = n.getProperties(); depth != 0 &&
                it.hasNext();) {
            Property p = it.nextProperty();
            String name = (p.getType() == PropertyType.STRING ||
                    p.getName().equals("jcr:primaryType") ? "" : "{" +
                    PropertyType.nameFromValue(p.getType()) + "}") +
                    p.getName();
            builder.key(name);
            if (p.isMultiple()) {
                builder.array();
                for (Value v : p.getValues()) {
                    builder.value(v.getString());
                }
                builder.endArray();
            } else {
                builder.value(p.getValue().getString());
            }
        }
        for (NodeIterator it = n.getNodes(); depth != 0 && it.hasNext();) {
            Node n2 = it.nextNode();
            builder.key(n2.getName());
            appendNode(builder, n2, depth - 1);
        }
        builder.endObject();
    }

    public static Session openSession(NodeStore nodeStore, String user, String password) throws RepositoryException {
        if (nodeStore == null) {
            return null;
        }
        StatisticsProvider statisticsProvider = StatisticsProvider.NOOP;
        Oak oak = new Oak(nodeStore).with(ManagementFactory.getPlatformMBeanServer());
        oak.getWhiteboard().register(StatisticsProvider.class, statisticsProvider, Collections.emptyMap());
        LuceneIndexProvider provider = createLuceneIndexProvider();
        oak.with((QueryIndexProvider) provider)
                .with((Observer) provider)
                .with(createLuceneIndexEditorProvider());
        Jcr jcr = new Jcr(oak);
        Repository repository = jcr.createRepository();
        return repository.login(new SimpleCredentials(user, password.toCharArray()));
    }

    private static LuceneIndexEditorProvider createLuceneIndexEditorProvider() {
        LuceneIndexEditorProvider ep = new LuceneIndexEditorProvider();
        ScheduledExecutorService executorService = MoreExecutors.getExitingScheduledExecutorService(
                (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(5));
        StatisticsProvider statsProvider = StatisticsProvider.NOOP;
        int queueSize = Integer.getInteger("queueSize", 1000);
        long queueTimeout = Long.getLong("queueTimeoutMillis", 100);
        IndexTracker tracker = new IndexTracker();
        DocumentQueue queue = new DocumentQueue(queueSize, queueTimeout, tracker, executorService, statsProvider);
        ep.setIndexingQueue(queue);
        return ep;
    }

    private static LuceneIndexProvider createLuceneIndexProvider() {
        return new LuceneIndexProvider();
    }

}
