package org.exist.xquery.functions.map;

import com.ibm.icu.text.Collator;
import io.lacuna.bifurcan.IEntry;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.dom.QName;
import org.exist.xquery.*;
import org.exist.xquery.value.*;

import javax.annotation.Nullable;
import java.util.List;

/**
 * Abstract base class for map types. A map item is also a function item. This class thus extends
 * {@link FunctionReference} to allow the item to be called in a dynamic function
 * call.
 *
 * @author Wolfgang Meier
 */
public abstract class AbstractMapType extends FunctionReference
        implements IEntry<AtomicValue, Sequence>, Iterable<IEntry<AtomicValue, Sequence>>,
        Lookup.LookupSupport {

    /**
     * Used when the type of the keys for the
     * map is unknown.
     */
    public static final int UNKNOWN_KEY_TYPE = Type.ANY_SIMPLE_TYPE;

    /**
     * Used when a map contains keys of various xs:anyAtomicType
     */
    public static final int MIXED_KEY_TYPES = Type.ATOMIC;

    private final static Logger LOG = LogManager.getLogger(AbstractMapType.class);

    // the signature of the function which is evaluated if the map is called as a function item
    private static final FunctionSignature ACCESSOR =
        new FunctionSignature(
            new QName("get", MapModule.NAMESPACE_URI, MapModule.PREFIX),
            "Internal accessor function for maps.",
            new SequenceType[]{
                    new FunctionParameterSequenceType("key", Type.ATOMIC, Cardinality.EXACTLY_ONE, "The key to look up")
            },
            new SequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE));

    private InternalFunctionCall accessorFunc = null;

    protected XQueryContext context;

    public AbstractMapType(XQueryContext context) {
        super(null);
        this.context = context;
    }

    public abstract Sequence get(AtomicValue key);

    public abstract AbstractMapType put(AtomicValue key, Sequence value) throws XPathException;

    public abstract AbstractMapType merge(Iterable<AbstractMapType> others);

    public abstract boolean contains(AtomicValue key);

    public abstract Sequence keys();

    public abstract AbstractMapType remove(AtomicValue[] keysAtomicValues) throws XPathException;

    public abstract int getKeyType();

    public abstract int size();

    @Override
    public int getItemType() {
        return Type.MAP;
    }

    @Override
    public int getType() {
        return Type.MAP;
    }

    @Override
    public void analyze(AnalyzeContextInfo contextInfo) throws XPathException {
        getAccessorFunc().analyze(contextInfo);
    }

    @Override
    public Sequence eval(Sequence contextSequence) throws XPathException {
        return getAccessorFunc().eval(contextSequence);
    }

    @Override
    public void setArguments(List<Expression> arguments) throws XPathException {
        getAccessorFunc().setArguments(arguments);
    }

    @Override
    public void resetState(boolean postOptimization) {
        getAccessorFunc().resetState(postOptimization);
    }

    protected static boolean keysEqual(@Nullable final Collator collator, final AtomicValue k1, final AtomicValue k2) {
        try {
            return ValueComparison.compareAtomic(collator, k1, k2, Constants.StringTruncationOperator.NONE, Constants.Comparison.EQ);
        } catch (final XPathException e) {
            LOG.warn("Unable to compare with collation '" + collator + "', will fallback to non-collation comparision. Error: " + e.getMessage(), e);
        }

        // fallback
        return k1.equals(k2);
    }

    @Override
    public String toString() {
        final StringBuilder buf = new StringBuilder("map {");

        boolean first = true;
        final Sequence keys = keys();
        if(keys != null && !keys.isEmpty()) {
            try {
                final SequenceIterator iterator = keys.iterate();
                while (iterator.hasNext()) {
                    final Item key = iterator.nextItem();

                    if (!first) {
                        buf.append(", ");
                    }

                    if(key instanceof StringValue) {
                        buf.append('\"');
                    }

                    buf.append(key);

                    if(key instanceof StringValue) {
                        buf.append('\"');
                    }

                    buf.append(": ");
                    final Sequence value = get((AtomicValue) key);

                    if(value != null && value.hasOne() && value instanceof StringValue) {
                        buf.append('\"');
                    }

                    buf.append(value);
                    if(value != null && value.hasOne() && value instanceof StringValue) {
                        buf.append('\"');
                    }

                    first = false;
                }
            } catch (final XPathException e) {
                LOG.error(e.getMessage(), e);
            }
        }

        buf.append('}');

        return buf.toString();
    }

    /**
     * Return the accessor function. Will be created on demand.
     *
     * @return function item to access the map
     */
    protected InternalFunctionCall getAccessorFunc() {
        initFunction();
        return accessorFunc;
    }

    /**
     * Lazy initialization of the accessor function. Creating
     * this for every map would be too expensive and we thus
     * only instantiate it on demand.
     */
    protected void initFunction() {
        if (this.accessorFunc != null)
            {return;}
        final Function fn = new AccessorFunc(this.context);
        this.accessorFunc = new InternalFunctionCall(fn);
    }

    /**
     * The accessor function which will be evaluated if the map is called
     * as a function item.
     */
    private class AccessorFunc extends BasicFunction {

        public AccessorFunc(XQueryContext context) {
            super(context, ACCESSOR);
        }

        public Sequence eval(Sequence[] args, Sequence contextSequence) throws XPathException {
            return AbstractMapType.this.get((AtomicValue) args[0].itemAt(0));
        }
    }
}
@@ -2809,32 +2809,335 @@
 quence);%0A    %7D%0A%0A
+    @Override%0A    public Sequence evalFunction(final Sequence contextSequence, final Item contextItem, final Sequence%5B%5D seq) throws XPathException %7B%0A        final AccessorFunc accessorFunc =  (AccessorFunc) getAccessorFunc().getFunction();%0A        return accessorFunc.eval(seq, contextSequence);%0A    %7D%0A%0A
     @Override%0A  
@@ -5907,16 +5907,18 @@
 != null)
+ %7B
 %0A       
@@ -5926,16 +5926,24 @@
     
-%7B
 return;
+%0A        
 %7D%0A  
@@ -5985,28 +5985,29 @@
 sorFunc(
-this.
 context
+, this
 );%0A     
@@ -6195,16 +6195,23 @@
 private 
+static 
 class Ac
@@ -6244,16 +6244,59 @@
 nction %7B
+%0A        private final AbstractMapType map;
 %0A%0A      
@@ -6317,16 +6317,22 @@
 sorFunc(
+final 
 XQueryCo
@@ -6336,32 +6336,59 @@
 yContext context
+, final AbstractMapType map
 ) %7B%0A            
@@ -6413,34 +6413,80 @@
 ESSOR);%0A        
-%7D%0A
+    this.map = map;%0A        %7D%0A%0A        @Override
 %0A        public 
@@ -6491,32 +6491,38 @@
 c Sequence eval(
+final 
 Sequence%5B%5D args,
@@ -6521,16 +6521,22 @@
 %5B%5D args,
+ final
  Sequenc
@@ -6601,28 +6601,11 @@
 urn 
-AbstractMapType.this
+map
 .get

Fix an NPE when evaluating a map instance which has been passed as a function reference