/*
* 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.vxquery.context;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

import javax.xml.namespace.QName;

import org.apache.commons.lang3.tuple.Pair;
import org.apache.vxquery.collations.Collation;
import org.apache.vxquery.functions.Function;
import org.apache.vxquery.types.AttributeType;
import org.apache.vxquery.types.ElementType;
import org.apache.vxquery.types.SchemaType;
import org.apache.vxquery.types.SequenceType;

public class StaticContextImpl implements StaticContext {
    private final StaticContext parent;

    private final Map<String, String> namespaceMap;

    private final Map<QName, XQueryVariable> variableMap;

    protected final Map<String, Collation> collationMap;

    protected final Map<QName, Function[]> functionMap;

    protected final Map<String, SequenceType> documentTypeMap;

    protected final Map<String, SequenceType> collectionTypeMap;

    protected final List<Pair<String, List<String>>> moduleImports;

    protected final List<Pair<String, List<String>>> schemaImports;

    protected final Map<QName, SchemaType> schemaTypeMap;

    protected final Map<SequenceType, Integer> sequenceTypeMap;

    protected final List<SequenceType> sequenceTypeList;

    protected final Map<QName, AttributeType> attributeDeclarationMap;

    protected final Map<QName, ElementType> elementDeclarationMap;

    protected final Map<QName, String> options;

    private BoundarySpaceProperty boundarySpaceProperty;

    private String defaultFunctionNamespaceUri;

    private String defaultElementNamespaceUri;

    private OrderingModeProperty orderingModeProperty;

    private EmptyOrderProperty emptyOrderProperty;

    private String defaultCollation;

    private String baseUri;

    private ConstructionModeProperty constructionModeProperty;

    private CopyNamespacesModeProperty copyNamespacesModeProperty;

    private SequenceType defaultCollectionType;

    private int typeCounter;

    public StaticContextImpl(StaticContext parent) {
        this.parent = parent;
        namespaceMap = new LinkedHashMap<String, String>();
        variableMap = new LinkedHashMap<QName, XQueryVariable>();
        collationMap = new LinkedHashMap<String, Collation>();
        functionMap = new LinkedHashMap<QName, Function[]>();
        documentTypeMap = new LinkedHashMap<String, SequenceType>();
        collectionTypeMap = new LinkedHashMap<String, SequenceType>();
        moduleImports = new ArrayList<Pair<String, List<String>>>();
        schemaImports = new ArrayList<Pair<String, List<String>>>();
        schemaTypeMap = new LinkedHashMap<QName, SchemaType>();
        sequenceTypeMap = new HashMap<SequenceType, Integer>();
        sequenceTypeList = new ArrayList<SequenceType>();
        attributeDeclarationMap = new LinkedHashMap<QName, AttributeType>();
        elementDeclarationMap = new LinkedHashMap<QName, ElementType>();
        options = new LinkedHashMap<QName, String>();
        typeCounter = parent == null ? 0 : parent.getMaxSequenceTypeCode();
    }

    @Override
    public StaticContext getParent() {
        return parent;
    }

    @Override
    public String lookupNamespaceUri(String prefix) {
        if (namespaceMap.containsKey(prefix)) {
            return namespaceMap.get(prefix);
        }
        if (parent != null) {
            return parent.lookupNamespaceUri(prefix);
        }
        return null;
    }

    @Override
    public void registerNamespaceUri(String prefix, String uri) {
        namespaceMap.put(prefix, uri);
    }

    @Override
    public Collation lookupCollation(String collationName) {
        if (collationMap.containsKey(collationName)) {
            return collationMap.get(collationName);
        }
        if (parent != null) {
            return parent.lookupCollation(collationName);
        }
        return null;
    }

    @Override
    public void registerCollation(String collationName, Collation collation) {
        collationMap.put(collationName, collation);
    }

    @Override
    public Function lookupFunction(QName functionName, int arity) {
        if (functionMap.containsKey(functionName)) {
            Function[] fns = functionMap.get(functionName);
            if (fns != null && fns.length > arity && fns[arity] != null) {
                return fns[arity];
            }
        }
        if (parent != null) {
            return parent.lookupFunction(functionName, arity);
        }
        return null;
    }

    @Override
    public Function[] lookupFunctions(QName functionName) {
        if (functionMap.containsKey(functionName)) {
            return functionMap.get(functionName);
        }
        if (parent != null) {
            return parent.lookupFunctions(functionName);
        }
        return null;
    }

    @Override
    public void registerFunction(Function function) {
        Function fns[] = functionMap.get(function.getName());
        int arity = function.getSignature().getArity();
        if (fns == null) {
            fns = new Function[arity + 1];
            fns[arity] = function;
            functionMap.put(function.getName(), fns);
        } else if (fns.length <= arity) {
            Function newFns[] = new Function[arity + 1];
            System.arraycopy(fns, 0, newFns, 0, fns.length);
            newFns[arity] = function;
            functionMap.put(function.getName(), newFns);
        } else {
            fns[arity] = function;
        }
    }

    @Override
    public Iterator<Function> listFunctions() {
        return new Iterator<Function>() {
            private Iterator<Function[]> faIter = functionMap.values().iterator();
            private Function[] fa = null;
            private int faIdx = 0;

            @Override
            public boolean hasNext() {
                fetchNext();
                return fa != null;
            }

            @Override
            public Function next() {
                fetchNext();
                if (fa == null) {
                    throw new NoSuchElementException();
                }
                return fa[faIdx++];
            }

            private void fetchNext() {
                while (true) {
                    if (fa != null && faIdx < fa.length) {
                        if (fa[faIdx] != null) {
                            break;
                        }
                        ++faIdx;
                    } else {
                        if (faIter.hasNext()) {
                            fa = faIter.next();
                            faIdx = 0;
                        } else {
                            fa = null;
                            break;
                        }
                    }
                }
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    @Override
    public SequenceType lookupDocumentType(String docUri) {
        if (documentTypeMap.containsKey(docUri)) {
            return documentTypeMap.get(docUri);
        }
        if (parent != null) {
            return parent.lookupDocumentType(docUri);
        }
        return null;
    }

    @Override
    public void registerDocumentType(String docUri, SequenceType type) {
        documentTypeMap.put(docUri, type);
    }

    @Override
    public XQueryVariable lookupVariable(QName name) {
        if (variableMap.containsKey(name)) {
            return variableMap.get(name);
        }
        if (parent != null) {
            return parent.lookupVariable(name);
        }
        return null;
    }

    @Override
    public void registerVariable(XQueryVariable var) {
        variableMap.put(var.getName(), var);
    }

    @Override
    public Iterator<XQueryVariable> listVariables() {
        return Collections.unmodifiableCollection(variableMap.values()).iterator();
    }

    @Override
    public SequenceType lookupCollectionType(String collectionUri) {
        if (collectionTypeMap.containsKey(collectionUri)) {
            return collectionTypeMap.get(collectionUri);
        }
        if (parent != null) {
            return parent.lookupCollectionType(collectionUri);
        }
        return null;
    }

    @Override
    public void registerCollectionType(String collectionUri, SequenceType type) {
        collectionTypeMap.put(collectionUri, type);
    }

    @Override
    public Iterator<Pair<String, List<String>>> listModules() {
        return new ConcatenatingIterator<Pair<String, List<String>>>() {
            @Override
            protected Iterator<Pair<String, List<String>>> getCurrentIterator() {
                return moduleImports.iterator();
            }

            @Override
            protected Iterator<Pair<String, List<String>>> getParentIterator() {
                if (parent != null) {
                    return parent.listModules();
                }
                return null;
            }
        };
    }

    @Override
    public void registerModuleImport(String uri, List<String> locations) {
        moduleImports.add(Pair.<String, List<String>> of(uri, locations));
    }

    @Override
    public Iterator<Pair<String, List<String>>> listSchemas() {
        return new ConcatenatingIterator<Pair<String, List<String>>>() {
            @Override
            protected Iterator<Pair<String, List<String>>> getCurrentIterator() {
                return schemaImports.iterator();
            }

            @Override
            protected Iterator<Pair<String, List<String>>> getParentIterator() {
                if (parent != null) {
                    return parent.listSchemas();
                }
                return null;
            }
        };
    }

    @Override
    public void registerSchemaImport(String uri, List<String> locations) {
        schemaImports.add(Pair.<String, List<String>> of(uri, locations));
    }

    @Override
    public SchemaType lookupSchemaType(QName name) {
        if (schemaTypeMap.containsKey(name)) {
            return schemaTypeMap.get(name);
        }
        if (parent != null) {
            return parent.lookupSchemaType(name);
        }
        return null;
    }

    @Override
    public void registerSchemaType(QName name, SchemaType type) {
        schemaTypeMap.put(name, type);
    }

    @Override
    public int lookupSequenceType(SequenceType type) {
        if (sequenceTypeMap.containsKey(type)) {
            return sequenceTypeMap.get(type);
        }
        if (parent != null) {
            return parent.lookupSequenceType(type);
        }
        return -1;
    }

    @Override
    public SequenceType lookupSequenceType(int code) {
        int maxParentTypeCode = parent == null ? 0 : parent.getMaxSequenceTypeCode();
        if (code >= maxParentTypeCode) {
            return sequenceTypeList.get(code - maxParentTypeCode);
        }
        return parent.lookupSequenceType(code);
    }

    @Override
    public int encodeSequenceType(SequenceType type) {
        int code = lookupSequenceType(type);
        if (code == -1) {
            code = typeCounter++;
            sequenceTypeMap.put(type, code);
            sequenceTypeList.add(type);
            return code;
        }
        return code;
    }

    List<SequenceType> getSequenceTypeList() {
        return sequenceTypeList;
    }

    @Override
    public int getMaxSequenceTypeCode() {
        return typeCounter;
    }

    @Override
    public AttributeType lookupAttributeDeclaration(QName name) {
        if (attributeDeclarationMap.containsKey(name)) {
            return attributeDeclarationMap.get(name);
        }
        if (parent != null) {
            return parent.lookupAttributeDeclaration(name);
        }
        return null;
    }

    @Override
    public void registerAttributeDeclaration(QName name, AttributeType attrDecl) {
        attributeDeclarationMap.put(name, attrDecl);
    }

    @Override
    public ElementType lookupElementDeclaration(QName name) {
        if (elementDeclarationMap.containsKey(name)) {
            return elementDeclarationMap.get(name);
        }
        if (parent != null) {
            return parent.lookupElementDeclaration(name);
        }
        return null;
    }

    @Override
    public void registerElementDeclaration(QName name, ElementType elemDecl) {
        elementDeclarationMap.put(name, elemDecl);
    }

    @Override
    public BoundarySpaceProperty getBoundarySpaceProperty() {
        if (boundarySpaceProperty != null) {
            return boundarySpaceProperty;
        }
        if (parent != null) {
            return parent.getBoundarySpaceProperty();
        }
        return null;
    }

    @Override
    public void setBoundarySpaceProperty(BoundarySpaceProperty boundarySpaceProperty) {
        this.boundarySpaceProperty = boundarySpaceProperty;
    }

    @Override
    public String getDefaultFunctionNamespaceUri() {
        if (defaultFunctionNamespaceUri != null) {
            return defaultFunctionNamespaceUri;
        }
        if (parent != null) {
            return parent.getDefaultFunctionNamespaceUri();
        }
        return null;
    }

    @Override
    public void setDefaultFunctionNamespaceUri(String uri) {
        this.defaultFunctionNamespaceUri = uri;
    }

    @Override
    public String getDefaultElementNamespaceUri() {
        if (defaultElementNamespaceUri != null) {
            return defaultElementNamespaceUri;
        }
        if (parent != null) {
            return parent.getDefaultElementNamespaceUri();
        }
        return null;
    }

    @Override
    public void setDefaultElementNamespaceUri(String uri) {
        this.defaultElementNamespaceUri = uri;
    }

    @Override
    public OrderingModeProperty getOrderingModeProperty() {
        if (orderingModeProperty != null) {
            return orderingModeProperty;
        }
        if (parent != null) {
            return parent.getOrderingModeProperty();
        }
        return null;
    }

    @Override
    public void setOrderingModeProperty(OrderingModeProperty orderingMode) {
        this.orderingModeProperty = orderingMode;
    }

    @Override
    public EmptyOrderProperty getEmptyOrderProperty() {
        if (emptyOrderProperty != null) {
            return emptyOrderProperty;
        }
        if (parent != null) {
            return parent.getEmptyOrderProperty();
        }
        return null;
    }

    @Override
    public void setEmptyOrderProperty(EmptyOrderProperty emptyOrder) {
        this.emptyOrderProperty = emptyOrder;
    }

    @Override
    public String getDefaultCollation() {
        if (defaultCollation != null) {
            return defaultCollation;
        }
        if (parent != null) {
            return parent.getDefaultCollation();
        }
        return null;
    }

    @Override
    public void setDefaultCollation(String defaultCollation) {
        this.defaultCollation = defaultCollation;
    }

    @Override
    public String getBaseUri() {
        if (baseUri != null) {
            return baseUri;
        }
        if (parent != null) {
            return parent.getBaseUri();
        }
        return null;
    }

    @Override
    public void setBaseUri(String baseUri) {
        this.baseUri = baseUri;
    }

    @Override
    public ConstructionModeProperty getConstructionModeProperty() {
        if (constructionModeProperty != null) {
            return constructionModeProperty;
        }
        if (parent != null) {
            return parent.getConstructionModeProperty();
        }
        return null;
    }

    @Override
    public void setConstructionModeProperty(ConstructionModeProperty constructionMode) {
        this.constructionModeProperty = constructionMode;
    }

    @Override
    public CopyNamespacesModeProperty getCopyNamespacesModeProperty() {
        if (copyNamespacesModeProperty != null) {
            return copyNamespacesModeProperty;
        }
        if (parent != null) {
            return parent.getCopyNamespacesModeProperty();
        }
        return null;
    }

    @Override
    public void setCopyNamespacesModeProperty(CopyNamespacesModeProperty copyNamespacesMode) {
        this.copyNamespacesModeProperty = copyNamespacesMode;
    }

    @Override
    public SequenceType getDefaultCollectionType() {
        if (defaultCollectionType != null) {
            return defaultCollectionType;
        }
        if (parent != null) {
            return parent.getDefaultCollectionType();
        }
        return null;
    }

    @Override
    public void setDefaultCollectionType(SequenceType type) {
        this.defaultCollectionType = type;
    }

    @Override
    public void setOption(QName name, String value) {
        options.put(name, value);
    }

    @Override
    public String getOption(QName name) {
        if (options.containsKey(name)) {
            return options.get(name);
        }
        if (parent != null) {
            return parent.getOption(name);
        }
        return null;
    }

    public IStaticContextFactory createFactory() {
        return StaticContextImplFactory.createInstance(this);
    }

    private abstract class ConcatenatingIterator<T> implements Iterator<T> {
        Iterator<T> currListIter = getCurrentIterator();
        Iterator<T> parentIter = null;
        T nextItem = null;

        @Override
        public boolean hasNext() {
            fetchNext();
            return nextItem != null;
        }

        protected abstract Iterator<T> getCurrentIterator();

        protected abstract Iterator<T> getParentIterator();

        @Override
        public T next() {
            if (hasNext()) {
                T item = nextItem;
                nextItem = null;
                return item;
            }
            return null;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }

        private void fetchNext() {
            if (nextItem != null) {
                return;
            }
            if (currListIter != null) {
                if (currListIter.hasNext()) {
                    nextItem = currListIter.next();
                } else {
                    currListIter = null;
                    parentIter = getParentIterator();
                }
            }
            if (nextItem == null && parentIter != null) {
                if (parentIter.hasNext()) {
                    nextItem = parentIter.next();
                } else {
                    parentIter = null;
                }
            }
        }
    }
}
