/*
 * Copyright Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags and
 * the COPYRIGHT.txt file distributed with this work.
 *
 * Licensed 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.teiid.olingo.service;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentSkipListMap;

import javax.servlet.ServletException;
import javax.xml.stream.XMLStreamException;

import org.apache.olingo.server.api.OData;
import org.apache.olingo.server.api.ODataHttpHandler;
import org.apache.olingo.server.api.ServiceMetadata;
import org.apache.olingo.server.core.OData4Impl;
import org.teiid.adminapi.DVMetadataCache;
import org.teiid.adminapi.Model;
import org.teiid.adminapi.impl.VDBMetaData;
import org.teiid.core.TeiidProcessingException;
import org.teiid.dqp.internal.process.DQPWorkContext;
import org.teiid.metadata.DVMetadataRecordHolder;
import org.teiid.metadata.Schema;
import org.teiid.metadata.odata.ODataSchemaBuilder;
import org.teiid.metadata.odata.TeiidEdmProvider;
import org.teiid.odata.api.Client;
import org.teiid.olingo.ODataPlugin;
import org.teiid.metadata.odata.ODataSchemaBuilder.ODataSchemaInfo;

import static org.teiid.metadata.odata.ODataMetadata.RESERVED;

public class OlingoBridge {

    public static class HandlerInfo {
        public final ODataHttpHandler oDataHttpHandler;
        public final ServiceMetadata serviceMetadata;

        HandlerInfo(ODataHttpHandler handler, ServiceMetadata serviceMetadata) {
            this.oDataHttpHandler = handler;
            this.serviceMetadata = serviceMetadata;
        }
    }

    private String singleVDBContext;

    private DQPWorkContext dqpWorkContext;

    private Set<String> aliases = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);

    private volatile boolean loaded;

    /**
     *
     * @param singleVDBContext null if multi-vdb
     */
    public OlingoBridge(String singleVDBContext) {
        this.singleVDBContext = singleVDBContext;
        aliases.addAll(RESERVED);
    }

    //the schema name is handled as case insensitive
    private ConcurrentSkipListMap<String, HandlerInfo> handlers = new ConcurrentSkipListMap<>(String.CASE_INSENSITIVE_ORDER);
    private Map<String, ODataSchemaInfo> infoMap = new LinkedHashMap<>();

    public HandlerInfo getHandlers(String baseUri, Client client, String schemaName) throws ServletException, TeiidProcessingException {
        // we should reload the schema for every request otherwise it won't be possible to see recently created views
        /*HandlerInfo handler = this.handlers.get(schemaName);
        if (handler != null) {
            return handler;
        }*/
        VDBMetaData vdb = client.getVDB();

        org.teiid.metadata.Schema teiidSchema = client.getMetadataStore().getSchema(schemaName);
        if (teiidSchema == null || !isVisible(vdb, teiidSchema)) {
            handlers.remove(schemaName);
            throw new TeiidProcessingException(ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16022));
        }

        synchronized (this) {
            // we should reload the schema for every request otherwise it won't be possible to see recently created views
            /*handler = this.handlers.get(schemaName);
            if (handler != null) {
                return handler;
            }*/
            if (!loaded) {
                loadAllHandlers(baseUri, client, vdb);
                loaded = true;
            } else
                reloadHandler(baseUri, client, vdb, teiidSchema);
            return handlers.get(schemaName);
        }
    }

    private void loadAllHandlers(String baseUri, Client client, VDBMetaData vdb) throws ServletException {
        //process the base metadata structure
        for (Schema s : client.getMetadataStore().getSchemaList()) {
            if (!isVisible(vdb, s)) {
                 continue;
            }

            getSchemaInfo(vdb, s);
        }

        processNavigationLinks(baseUri, client, vdb);

        OData odata = OData4Impl.newInstance();

        for (Map.Entry<String, ODataSchemaInfo> entry : infoMap.entrySet()) {
            TeiidEdmProvider edmProvider = entry.getValue().edmProvider;
            ServiceMetadata metadata = odata.createServiceMetadata(edmProvider, edmProvider.getReferences());
            ODataHttpHandler handler = odata.createHandler(metadata);
            handler.register(new TeiidServiceHandler(entry.getKey()));
            this.handlers.put(entry.getKey(), new HandlerInfo(handler, metadata));
        }
    }

    private void reloadHandler(String baseUri, Client client, VDBMetaData vdb, Schema s) throws ServletException {
        String schemaName = s.getName();
        ODataSchemaInfo info = getSchemaInfo(vdb, s);
        if (info != null) {
            // re-process navigation links
            processNavigationLinks(baseUri, client, vdb, info, s);

            OData odata = OData4Impl.newInstance();
            TeiidEdmProvider edmProvider = info.edmProvider;
            ServiceMetadata metadata = odata.createServiceMetadata(edmProvider, edmProvider.getReferences());
            ODataHttpHandler handler = odata.createHandler(metadata);
            handler.register(new TeiidServiceHandler(schemaName));
            this.handlers.put(schemaName, new HandlerInfo(handler, metadata));
        }
    }

    private ODataSchemaInfo getSchemaInfo(VDBMetaData vdb, Schema s) {
        ODataSchemaInfo schemaInfo;
        DVMetadataRecordHolder recordHolder = (DVMetadataRecordHolder) vdb.getAttachment(DVMetadataCache.class);
        if (recordHolder != null) {
            schemaInfo = recordHolder.buildODataStructuralMetadata(s,
                    resources -> ODataSchemaBuilder.getNotPermittedObjects(resources, dqpWorkContext));
        } else {
            if (infoMap.containsKey(s.getName()))
                return null;
            // to prevent failed unit tests
            //prevent collisions with the reserved, and then with any other schemas
            int i = 1;
            String alias = s.getName();
            while (!aliases.add(alias)) {
                alias = alias + "_" + i++; //$NON-NLS-1$
            }
            schemaInfo = ODataSchemaBuilder.buildStructuralMetadata(vdb.getFullName(), s, alias, dqpWorkContext);
        }
        infoMap.put(s.getName(), schemaInfo);
        return schemaInfo;
    }

    private void processNavigationLinks(String baseUri, Client client, VDBMetaData vdb) throws ServletException {
        for (Map.Entry<String, ODataSchemaInfo> entry : infoMap.entrySet()) {
            final Schema s = client.getMetadataStore().getSchema(entry.getKey());
            final ODataSchemaInfo schemaInfo = entry.getValue();
            processNavigationLinks(baseUri, client, vdb, schemaInfo, s);
        }
    }

    private void processNavigationLinks(String baseUri, Client client, VDBMetaData vdb, ODataSchemaInfo schemaInfo, Schema s) throws ServletException {
        for (Map.Entry<String, ODataSchemaBuilder.ODataTableInfo> entryTable : schemaInfo.tableInfos.entrySet()) {
            ODataSchemaBuilder.ODataTableInfo tableInfo = entryTable.getValue();
            tableInfo.entityType.getNavigationProperties().clear();
            tableInfo.entitySet.getNavigationPropertyBindings().clear();
        }
        try {
            if (!schemaInfo.edmProvider.isInitialized())
                schemaInfo.edmProvider.init(getClass().getClassLoader(), baseUri, client.getProperty(Client.INVALID_CHARACTER_REPLACEMENT));
        } catch (XMLStreamException e) {
            throw new ServletException(ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16054));
        }

        String context = singleVDBContext;
        if (context == null) {
            context = vdb.getFullName();
        }
        ODataSchemaBuilder.DefaultSchemaResolver defaultSchemaResolver =
                new ODataSchemaBuilder.DefaultSchemaResolver(schemaInfo, infoMap, context);
        ODataSchemaBuilder.buildNavigationProperties(s, schemaInfo, defaultSchemaResolver);
    }

    private static boolean isVisible(VDBMetaData vdb, org.teiid.metadata.Schema schema) {
        String schemaName = schema.getName();
        Model model = vdb.getModel(schemaName);
        if (model == null) {
            //system models are not visible by default
            return false;
        }
        return model.isVisible();
    }

    /**
     *
     * @param singleVDBContext
     */
    public void setSingleVDBContext(String singleVDBContext) {
        this.singleVDBContext = singleVDBContext;
    }

    public void setDqpWorkContext(DQPWorkContext dqpWorkContext) {
        this.dqpWorkContext = dqpWorkContext;
    }
}
