/*
 * 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 accord.verify;

import clojure.java.api.Clojure;
import clojure.lang.ArraySeq;
import clojure.lang.IFn;
import clojure.lang.IMapEntry;
import clojure.lang.IPersistentCollection;
import clojure.lang.IPersistentMap;
import clojure.lang.ISeq;
import clojure.lang.IteratorSeq;
import clojure.lang.Keyword;
import clojure.lang.PersistentArrayMap;
import clojure.lang.PersistentVector;
import clojure.lang.RT;
import com.google.common.base.StandardSystemProperty;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.RandomAccess;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class ElleVerifier implements Verifier
{
    public static class Support
    {
        public static boolean allowed()
        {
            if (!Boolean.parseBoolean(System.getProperty("accord.verify.elle", "true")))
                return false;

            // Elle only works on JDK 11
            int jdkVersion = Integer.parseInt(StandardSystemProperty.JAVA_VERSION.value().split("\\.")[0]);
            return !(jdkVersion == 1 /* 1.8 */ || jdkVersion == 8);
        }
    }

    // In order to build the jepsen history, we need the full history... so must buffer everything
    private final List<Event> events = new ArrayList<>();

    @Override
    public Checker witness(Object description, int start, int end)
    {
        List<Action> invoked = new ArrayList<>();
        List<Action> witnessed = new ArrayList<>();
        return new Checker()
        {
            @Override
            public void read(int index, int[] seq)
            {
                invoked.add(new Read(index, null));
                witnessed.add(new Read(index, seq));
            }

            @Override
            public void write(int index, int value)
            {
                Append e = new Append(index, value);
                invoked.add(e);
                witnessed.add(e);
            }

            @Override
            public void close()
            {
                // When a range read is performed, if the result was no matching keys then history isn't clear.
                // Since StrictSerializabilityVerifier uses indexes and not pk values, it is not possible to find expected keys and putting empty result for them...
                if (witnessed.isEmpty())
                    return;
                events.add(new Event(start, Event.Type.invoke, start, invoked));
                events.add(new Event(start, Event.Type.ok, end, witnessed));
            }
        };
    }

    @Override
    public void close()
    {
        if (events.isEmpty())
            throw new IllegalArgumentException("No events seen");
        // invoke and ok are mixed together in order, but there could be time gaps, so order based off time...
        events.sort(Comparator.comparingLong(a -> a.time));

        Object eventHistory = Clj.history.invoke(Event.toClojure(events));
        events.clear();
        PersistentArrayMap result = (PersistentArrayMap) Clj.check.invoke(Clj.elleListAppendOps, eventHistory);
        Object isValid = result.get(Keys.valid);
        if (isValid == Boolean.TRUE)
            return;
        if (isValid == Keys.unknown)
        {
            // Elle couldn't figure out if the history is bad or not... why?
            Object anomalyTypes = result.get(Keys.anomalyTypes);
            if (anomalyTypes != null)
            {
                ArraySeq seq = (ArraySeq) anomalyTypes;
                if (!seq.isEmpty())
                {
                    for (Object type : seq)
                    {
                        if (type == Keys.emptyTransactionGraph)
                            continue; // nothing to see here
                        throw new AssertionError("Unexpected anomaly type detected: " + type);
                    }
                    return;  // all good
                }
            }
        }
        throw new HistoryViolation("", -1, "Violation detected: " + result);
    }

    private static abstract class Action extends java.util.AbstractList<Object> implements RandomAccess
    {
        enum Type
        {
            append, r;

            final Keyword keyword;

            Type()
            {
                keyword = RT.keyword(null, name());
            }
        }
        private final Action.Type type;
        private final int key;
        private final Object value;

        protected Action(Action.Type type, int key, @Nullable Object value)
        {
            this.type = type;
            this.key = key;
            this.value = value;
        }

        @Override
        public Object get(int index)
        {
            switch (index)
            {
                case 0:
                    return type.keyword;
                case 1:
                    return key;
                case 2:
                    if (value != null)
                        return value;
                default:
                    throw new IndexOutOfBoundsException();
            }
        }

        @Override
        public int size()
        {
            return value == null ? 2 : 3;
        }
    }

    private static class Read extends Action
    {
        protected Read(int key, int[] seq)
        {
            // TODO (optimization): rather than vector of boxed int, can we use the interfaces so we can stay primitive array?
            super(Type.r, key, seq == null ? null : PersistentVector.create(IntStream.of(seq).boxed().collect(Collectors.toList())));
        }
    }

    private static class Append extends Action
    {
        protected Append(int key, int value)
        {
            super(Type.append, key, value);
        }
    }

    private static class Event extends ObjectPersistentMap
    {
        enum Type
        {
            invoke, ok, fail; // info is left out as burn test does not have access to the original request, so can't populate an "invoke" event

            final Keyword keyword;

            Type()
            {
                keyword = RT.keyword(null, name());
            }
        }

        private final int process;
        private final Event.Type type;
        private final List<Action> actions;
        private final long time;
        private long index = -1;

        private Event(int process, Type type, long time, List<Action> actions)
        {
            super(Keys.eventKeys);
            this.process = process;
            this.type = type;
            this.actions = actions;
            this.time = time;
        }

        public static Object toClojure(List<Event> events)
        {
            return PersistentVector.create(events);
        }

        @Override
        public boolean containsKey(Object key)
        {
            if (key == Keys.index)
                return index != -1;
            return super.containsKey(key);
        }

        @Override
        public Object valAt(Object key, Object notFound)
        {
            if      (key == Keys.process) return process;
            else if (key == Keys.index)   return index == -1 ? notFound : index;
            else if (key == Keys.time)    return time;
            else if (key == Keys.type)    return type.keyword;
            else if (key == Keys.value)   return actions;
            return notFound;
        }

        @Override
        public IPersistentMap assoc(Object key, Object val)
        {
            if (key == Keys.index)
                index = ((Long) val).longValue();
            else
                throw new UnsupportedOperationException("Unable to update key " + key);
            return this;
        }
    }

    private static class Keys
    {
        // event keys
        private final static Keyword process = RT.keyword(null, "process");
        private final static Keyword index = RT.keyword(null, "index");
        private final static Keyword time = RT.keyword(null, "time");
        private final static Keyword type = RT.keyword(null, "type");
        private final static Keyword value = RT.keyword(null, "value");

        // elle check results
        private final static Keyword valid = RT.keyword(null, "valid?");
        private final static Keyword unknown = RT.keyword(null, "unknown");
        private final static Keyword anomalyTypes = RT.keyword(null, "anomaly-types");
        private final static Keyword emptyTransactionGraph = RT.keyword(null, "empty-transaction-graph");

        private static final Set<Keyword> eventKeys = ImmutableSet.of(Keys.process, Keys.time, Keys.type, Keys.value);
    }

    private static class Clj
    {
        static
        {
            // Needed else elle loads rhizome which then would call java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() which fails in CI
            System.setProperty("java.awt.headless", "true");

            IFn require = Clojure.var("clojure.core", "require");
            require.invoke(Clojure.read("elle.list-append"));
            require.invoke(Clojure.read("jepsen.history"));
        }

        private static final IFn check = Clojure.var("elle.list-append", "check");
        private static final IFn history = Clojure.var("jepsen.history", "history");
        private static final Object elleListAppendOps = Clojure.read("{:consistency-models [:strict-serializable]}");
    }

    private static abstract class ObjectPersistentMap implements clojure.lang.IPersistentMap
    {
        private Set<Keyword> keys;

        private ObjectPersistentMap(Set<Keyword> keys)
        {
            this.keys = keys;
        }

        @Override
        public boolean containsKey(Object key)
        {
            if (!(key instanceof Keyword))
                throw new AssertionError(String.format("Unexpected key %s; type %s", key, key == null ? null : key.getClass()));
            return keys.contains(key);
        }

        @Override
        public IMapEntry entryAt(Object key)
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public IPersistentMap assoc(Object key, Object val)
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public IPersistentMap assocEx(Object key, Object val)
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public IPersistentMap without(Object key)
        {
            keys = Sets.filter(keys, k -> !k.equals(key));
            return this;
        }

        @Override
        public Object valAt(Object key)
        {
            return valAt(key, null);
        }

        @Override
        public abstract Object valAt(Object key, Object notFound);

        @Override
        public int count()
        {
            return keys.size();
        }

        @Override
        public IPersistentCollection cons(Object o)
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public IPersistentCollection empty()
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean equiv(Object o)
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public ISeq seq()
        {
            return IteratorSeq.create(iterator());
        }

        @Override
        public Iterator iterator()
        {
            return keys.iterator();
        }
    }
}
