package org.exist.indexing.sort;

import org.exist.EXistException;
import org.exist.collections.Collection;
import org.exist.dom.persistent.*;
import org.exist.indexing.IndexController;
import org.exist.indexing.IndexWorker;
import org.exist.indexing.MatchListener;
import org.exist.indexing.StreamListener;
import org.exist.indexing.StreamListener.ReindexMode;
import org.exist.storage.DBBroker;
import org.exist.storage.NodePath;
import org.exist.storage.btree.BTreeCallback;
import org.exist.storage.btree.BTreeException;
import org.exist.storage.btree.IndexQuery;
import org.exist.storage.btree.Value;
import org.exist.storage.lock.Lock;
import org.exist.util.*;
import org.exist.xquery.QueryRewriter;
import org.exist.xquery.TerminatedException;
import org.exist.xquery.XQueryContext;
import org.w3c.dom.NodeList;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class SortIndexWorker implements IndexWorker {

    private ReindexMode mode = ReindexMode.STORE;
    private DocumentImpl document = null;
    private SortIndex index;

    public SortIndexWorker(final SortIndex index) {
        this.index = index;
    }

    public void setDocument(final DocumentImpl doc, final ReindexMode mode) {
        this.document = doc;
        this.mode = mode;
    }

    public String getIndexId() {
        return SortIndex.ID;
    }

    public String getIndexName() {
        return index.getIndexName();
    }

    @Override
    public QueryRewriter getQueryRewriter(final XQueryContext context) {
        return null;
    }

    @Override
    public void flush() {
        switch (mode) {
            case REMOVE_ALL_NODES:
                remove(document);
                break;
        }
    }

    /**
     * Create a new sort index identified by a name. The method iterates through all items in
     * the items list and adds the nodes to the index. It assumes that the list is already ordered.
     *
     * @param name  the name by which the index will be identified
     * @param items ordered list of items to store
     * @throws EXistException
     * @throws LockException
     */
    public void createIndex(final String name, final List<SortItem> items) throws EXistException, LockException {
        // get an id for the new index
        final short id = getOrRegisterId(name);
        final Lock lock = index.btree.getLock();
        try {
            lock.acquire(Lock.WRITE_LOCK);
            long idx = 0;
            for (final SortItem item : items) {
                final byte[] key = computeKey(id, item.getNode());
                index.btree.addValue(new Value(key), idx++);
            }
        } catch (final LockException | IOException | BTreeException e) {
            throw new EXistException("Exception caught while creating sort index: " + e.getMessage(), e);
        } finally {
            lock.release(Lock.WRITE_LOCK);
        }
    }

    public boolean hasIndex(final String name) throws EXistException, LockException {
        return getId(name) > 0;
    }

    /**
     * Looks up the given node in the specified index and returns its original position
     * in the ordered set as a long integer.
     *
     * @param name  the name of the index
     * @param proxy the node
     * @return the original position of the node in the ordered set
     * @throws EXistException
     * @throws LockException
     */
    public long getIndex(final String name, final NodeProxy proxy) throws EXistException, LockException {
        final short id = getId(name);
        final Lock lock = index.btree.getLock();
        try {
            lock.acquire(Lock.READ_LOCK);
            final byte[] key = computeKey(id, proxy);
            return index.btree.findValue(new Value(key));
        } catch (final LockException | IOException | BTreeException e) {
            throw new EXistException("Exception caught while reading sort index: " + e.getMessage(), e);
        } finally {
            lock.release(Lock.READ_LOCK);
        }
    }

    /**
     * Completely remove the index identified by its name.
     *
     * @param name the name of the index
     * @throws EXistException
     * @throws LockException
     */
    public void remove(final String name) throws EXistException, LockException {
        final short id = getId(name);
        final Lock lock = index.btree.getLock();
        try {
            lock.acquire(Lock.READ_LOCK);
            final byte[] fromKey = computeKey(id);
            final byte[] toKey = computeKey((short) (id + 1));
            final IndexQuery query = new IndexQuery(IndexQuery.RANGE, new Value(fromKey), new Value(toKey));
            index.btree.remove(query, null);

            removeId(name);
        } catch (final BTreeException | TerminatedException | IOException e) {
            throw new EXistException("Exception caught while deleting sort index: " + e.getMessage(), e);
        } finally {
            lock.release(Lock.READ_LOCK);
        }
    }

    public void remove(final String name, final DocumentImpl doc) throws EXistException, LockException {
        final short id = getId(name);
        remove(doc, id);
    }

    private void remove(final DocumentImpl doc, final short id) throws LockException, EXistException {
        final Lock lock = index.btree.getLock();
        try {
            lock.acquire(Lock.READ_LOCK);
            final byte[] fromKey = computeKey(id, doc.getDocId());
            final byte[] toKey = computeKey(id, doc.getDocId() + 1);
            final IndexQuery query = new IndexQuery(IndexQuery.RANGE, new Value(fromKey), new Value(toKey));
            index.btree.remove(query, null);
        } catch (final BTreeException | TerminatedException | IOException e) {
            throw new EXistException("Exception caught while deleting sort index: " + e.getMessage(), e);
        } finally {
            lock.release(Lock.READ_LOCK);
        }
    }

    public void remove(final DocumentImpl doc) {
        if (index.btree == null)
            return;
        final byte[] fromKey = new byte[]{1};
        final byte[] endKey = new byte[]{2};

        final Lock lock = index.btree.getLock();
        try {
            lock.acquire(Lock.READ_LOCK);
            final IndexQuery query = new IndexQuery(IndexQuery.RANGE, new Value(fromKey), new Value(endKey));
            final FindIdCallback callback = new FindIdCallback(true);
            index.btree.query(query, callback);

            for (final long id : callback.allIds) {
                remove(doc, (short) id);
            }

        } catch (final BTreeException | EXistException | LockException | TerminatedException | IOException e) {
            SortIndex.LOG.debug("Exception caught while reading sort index: " + e.getMessage(), e);
        } finally {
            lock.release(Lock.READ_LOCK);
        }
    }

    /**
     * Register the given index name and return a short id for it.
     *
     * @param name the name of the index
     * @return a unique id to be used for the index entries
     * @throws EXistException
     * @throws LockException
     */
    private short getOrRegisterId(final String name) throws EXistException, LockException {
        short id = getId(name);
        if (id < 0) {
            final byte[] fromKey = {1};
            final byte[] endKey = {2};
            final IndexQuery query = new IndexQuery(IndexQuery.RANGE, new Value(fromKey), new Value(endKey));
            final Lock lock = index.btree.getLock();
            try {
                lock.acquire(Lock.READ_LOCK);
                final FindIdCallback callback = new FindIdCallback(false);
                index.btree.query(query, callback);
                id = (short) (callback.max + 1);
                registerId(id, name);
            } catch (final IOException | TerminatedException | BTreeException e) {
                throw new EXistException("Exception caught while reading sort index: " + e.getMessage(), e);
            } finally {
                lock.release(Lock.READ_LOCK);
            }
        }
        return id;
    }

    private void registerId(final short id, final String name) throws EXistException {
        final byte[] key = new byte[1 + UTF8.encoded(name)];
        key[0] = 1;
        UTF8.encode(name, key, 1);
        final Lock lock = index.btree.getLock();
        try {
            lock.acquire(Lock.READ_LOCK);
            index.btree.addValue(new Value(key), id);
        } catch (final LockException | IOException | BTreeException e) {
            throw new EXistException("Exception caught while reading sort index: " + e.getMessage(), e);
        } finally {
            lock.release(Lock.READ_LOCK);
        }
    }

    private void removeId(final String name) throws EXistException {
        final byte[] key = new byte[1 + UTF8.encoded(name)];
        key[0] = 1;
        UTF8.encode(name, key, 1);
        final Lock lock = index.btree.getLock();
        try {
            lock.acquire(Lock.READ_LOCK);
            index.btree.removeValue(new Value(key));
        } catch (final LockException | IOException | BTreeException e) {
            throw new EXistException("Exception caught while reading sort index: " + e.getMessage(), e);
        } finally {
            lock.release(Lock.READ_LOCK);
        }
    }

    private short getId(final String name) throws EXistException, LockException {
        final byte[] key = new byte[1 + UTF8.encoded(name)];
        key[0] = 1;
        UTF8.encode(name, key, 1);
        final Lock lock = index.btree.getLock();
        try {
            lock.acquire(Lock.READ_LOCK);
            return (short) index.btree.findValue(new Value(key));
        } catch (final BTreeException | IOException e) {
            throw new EXistException("Exception caught while reading sort index: " + e.getMessage(), e);
        } finally {
            lock.release(Lock.READ_LOCK);
        }
    }

    private byte[] computeKey(final short id, final NodeProxy proxy) {
        final byte[] data = new byte[7 + proxy.getNodeId().size()];
        data[0] = 0;
        ByteConversion.shortToByteH(id, data, 1);
        ByteConversion.intToByteH(proxy.getOwnerDocument().getDocId(), data, 3);
        proxy.getNodeId().serialize(data, 7);
        return data;
    }

    private byte[] computeKey(final short id, final int docId) {
        final byte[] data = new byte[7];
        data[0] = 0;
        ByteConversion.shortToByteH(id, data, 1);
        ByteConversion.intToByteH(docId, data, 3);
        return data;
    }

    private byte[] computeKey(final short id) {
        final byte[] data = new byte[3];
        data[0] = 0;
        ByteConversion.shortToByteH(id, data, 1);
        return data;
    }

    public Object configure(final IndexController controller, final NodeList configNodes, final Map<String, String> namespaces) throws DatabaseConfigurationException {
        return null;
    }

    public DocumentImpl getDocument() {
        return document;
    }

    public void setDocument(final DocumentImpl doc) {
        this.document = doc;
    }

    @Override
    public ReindexMode getMode() {
        return mode;
    }

    public void setMode(final ReindexMode mode) {
        this.mode = mode;
    }

    public IStoredNode getReindexRoot(final IStoredNode node, final NodePath path, final boolean insert, final boolean includeSelf) {
        return insert ? null : node;
    }

    public StreamListener getListener() {
        return null;
    }

    public MatchListener getMatchListener(final DBBroker broker, final NodeProxy proxy) {
        return null;
    }

    public void removeCollection(final Collection collection, final DBBroker broker, final boolean reindex) {
    }

    public boolean checkIndex(final DBBroker broker) {
        return false;
    }

    public Occurrences[] scanIndex(final XQueryContext context, final DocumentSet docs, final NodeSet contextSet, final Map hints) {
        return new Occurrences[0];
    }

    private final static class FindIdCallback implements BTreeCallback {
        long max = 0;
        List<Long> allIds = null;

        private FindIdCallback(final boolean findIds) {
            if (findIds)
                allIds = new ArrayList<>(10);
        }

        public boolean indexInfo(final Value value, final long pointer) throws TerminatedException {
            max = Math.max(max, pointer);
            if (allIds != null) {
                allIds.add(pointer);
            }
            return true;
        }
    }
}
