package com.thinkbiganalytics.nifi.rest.visitor;

/*-
 * #%L
 * thinkbig-nifi-rest-client-api
 * %%
 * Copyright (C) 2017 ThinkBig Analytics
 * %%
 * 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.
 * #L%
 */


import com.thinkbiganalytics.nifi.rest.client.NiFiRestClient;
import com.thinkbiganalytics.nifi.rest.client.NifiComponentNotFoundException;
import com.thinkbiganalytics.nifi.rest.model.visitor.NifiFlowVisitor;
import com.thinkbiganalytics.nifi.rest.model.visitor.NifiVisitableConnection;
import com.thinkbiganalytics.nifi.rest.model.visitor.NifiVisitableProcessGroup;
import com.thinkbiganalytics.nifi.rest.model.visitor.NifiVisitableProcessor;
import com.thinkbiganalytics.nifi.rest.support.NifiConnectionUtil;
import com.thinkbiganalytics.nifi.rest.support.NifiProcessUtil;

import org.apache.nifi.web.api.dto.ConnectableDTO;
import org.apache.nifi.web.api.dto.ConnectionDTO;
import org.apache.nifi.web.api.dto.ProcessGroupDTO;
import org.apache.nifi.web.api.dto.ProcessorDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 */
public class NifiConnectionOrderVisitor implements NifiFlowVisitor {

    private static final Logger log = LoggerFactory.getLogger(NifiConnectionOrderVisitor.class);

    protected NifiVisitableProcessGroup currentProcessGroup;

    private NifiVisitableProcessGroup processGroup;

    private Map<String, ProcessorDTO> processorsMap = new HashMap<>();

    private Map<String, NifiVisitableProcessor> visitedProcessors = new HashMap<>();

    private Map<String, NifiVisitableProcessGroup> visitedProcessGroups = new HashMap<>();

    private Set<NifiVisitableConnection> allConnections = new HashSet<>();

    private NiFiRestClient restClient;

    private NifiConnectionOrderVisitorCache cache;


    public NifiConnectionOrderVisitor(NiFiRestClient restClient, NifiVisitableProcessGroup processGroup, NifiConnectionOrderVisitorCache cache) {
        this.restClient = restClient;
        this.processGroup = processGroup;
        this.currentProcessGroup = processGroup;
        this.processorsMap = NifiProcessUtil.getProcessorsMap(processGroup.getDto());
        this.cache = cache;
        if (cache == null) {
            cache = new NifiConnectionOrderVisitorCache();
        }
    }

    @Override
    public void visitProcessor(NifiVisitableProcessor processor) {

        visitedProcessors.put(processor.getDto().getId(), processor);
        //add the pointer to the ProcessGroup
        currentProcessGroup.addProcessor(processor);
    }

    @Override
    public NifiVisitableProcessor getProcessor(String id) {
        return visitedProcessors.get(id);
    }

    @Override
    public NifiVisitableProcessGroup getProcessGroup(String id) {
        return visitedProcessGroups.get(id);
    }


    @Override
    public void visitConnection(NifiVisitableConnection connection) {
        Set<String> relationships = connection.getDto().getSelectedRelationships();
        String sourceType = connection.getDto().getSource().getType();
        String destType = connection.getDto().getDestination().getType();

        List<NifiVisitableProcessor> destinationProcessors = getDestinationProcessors(connection.getDto(), true);

        List<NifiVisitableProcessor> sourceProcessors = getSourceProcessors(connection.getDto());

        if (destinationProcessors != null) {
            destinationProcessors.forEach(destinationProcessor -> destinationProcessor.addSourceConnectionIdentifier(connection.getDto()));
        }
        if (destinationProcessors != null && sourceProcessors != null) {
            for (NifiVisitableProcessor destination : destinationProcessors) {
                for (NifiVisitableProcessor source : sourceProcessors) {
                    destination.addSource(source);
                    source.addDestination(destination);
                }
            }
        }

        for (NifiVisitableProcessor sourceProcessor : sourceProcessors) {
            sourceProcessor.addDestinationConnectionIdentifier(connection.getDto());
        }

        allConnections.add(connection);

    }

    @Override
    public void visitProcessGroup(NifiVisitableProcessGroup processGroup) {

        log.debug(" Visit Process Group: {}, ({}) ", processGroup.getDto().getName(), processGroup.getDto().getId());

        NifiVisitableProcessGroup group = visitedProcessGroups.get(processGroup.getDto().getId());

        if (group == null) {
            group = processGroup;
        }
        this.currentProcessGroup = group;

        if (processGroup.getParentProcessGroup() == null) {
            try {
                ProcessGroupDTO parent = fetchProcessGroupForNameAndIdentifier(processGroup.getDto().getParentGroupId());
                if (parent != null) {
                    group.setParentProcessGroup(parent);
                }
            } catch (NifiComponentNotFoundException e) {
                //cant find the parent
            }
        }

        group.accept(this);
        this.visitedProcessGroups.put(group.getDto().getId(), group);


    }


    public List<NifiVisitableProcessor> getDestinationProcessors(ConnectionDTO connection, boolean getSource) {
        ConnectableDTO dest = connection.getDestination();
        List<NifiVisitableProcessor> destinationProcessors = new ArrayList<>();
        if (dest != null) {

            if ("INPUT_PORT".equalsIgnoreCase(dest.getType())) {
                boolean isNew = false;
                NifiVisitableProcessGroup group = visitedProcessGroups.get(dest.getGroupId());
                if (group == null) {
                    group = fetchProcessGroup(dest.getGroupId());
                }
                ConnectionDTO conn = group.getConnectionMatchingSourceId(dest.getId());
                if (conn != null) {
                    destinationProcessors = getDestinationProcessors(conn, getSource);

                    if (getSource) {
                        List<NifiVisitableProcessor> outputProcessors = getSourceProcessors(connection);
                        if (outputProcessors != null) {
                            for (NifiVisitableProcessor outputProcessor : outputProcessors) {
                                //outputProcessor.setOutputPortId(dest.getId());
                                currentProcessGroup.addOutputPortProcessor(dest.getId(), outputProcessor);
                            }
                        }
                    }
                }


            } else if ("OUTPUT_PORT".equals(dest.getType())) {
                boolean isNew = false;
                //get parent processgroup connection to input port
                NifiVisitableProcessGroup group = visitedProcessGroups.get(dest.getGroupId());
                if (group == null) {
                    group = fetchProcessGroup(dest.getGroupId());
                }
                ConnectionDTO conn = group.getConnectionMatchingSourceId(dest.getId());
                if (conn == null) {
                    conn = searchConnectionMatchingSource(group.getDto().getParentGroupId(), dest.getId());
                }
                if (conn != null) {
                    //get the processor whos source matches this connection Id
                    List<NifiVisitableProcessor> destinations = getDestinationProcessors(conn, getSource);
                    if (destinations != null) {
                        destinationProcessors.addAll(destinations);
                    }
                    if (getSource) {
                        List<NifiVisitableProcessor> outputProcessors = getSourceProcessors(connection);
                        if (outputProcessors != null) {
                            for (NifiVisitableProcessor outputProcessor : outputProcessors) {
                                currentProcessGroup.addOutputPortProcessor(dest.getId(), outputProcessor);
                            }
                        }
                    }
                }

            } else if ("FUNNEL".equals(dest.getType())) {
                List<ConnectionDTO>
                    passThroughConnections =
                    NifiConnectionUtil.findConnectionsMatchingSourceId(currentProcessGroup.getDto().getContents().getConnections(), connection.getDestination().getId());
                if (passThroughConnections != null) {
                    for (ConnectionDTO dto : passThroughConnections) {
                        ConnectionDTO newConnection = new ConnectionDTO();
                        newConnection.setId(connection.getSource().getId());
                        newConnection.setSource(connection.getSource());
                        newConnection.setDestination(dto.getDestination());
                        List<NifiVisitableProcessor> destinations = getDestinationProcessors(newConnection, getSource);
                        if (destinations != null) {
                            //  destinationProcessor.setOutputPortId(dest.getId());
                            destinationProcessors.addAll(destinations);
                        }
                    }
                }
            } else if ("PROCESSOR".equals(dest.getType())) {
                NifiVisitableProcessor destinationProcessor = getConnectionProcessor(dest.getGroupId(), dest.getId());
                destinationProcessors.add(destinationProcessor);
            }
        }
        for (NifiVisitableProcessor destinationProcessor : destinationProcessors) {
            destinationProcessor.addSourceConnectionIdentifier(connection);
        }
        return destinationProcessors;
    }


    public List<NifiVisitableProcessor> getSourceProcessors(ConnectionDTO connection) {

        ConnectableDTO source = connection.getSource();
        List<NifiVisitableProcessor> sourceProcessors = new ArrayList<>();
        if (source != null) {
            if ("INPUT_PORT".equalsIgnoreCase(source.getType())) {
                NifiVisitableProcessGroup group = visitedProcessGroups.get(source.getGroupId());
                if (group == null) {
                    group = processGroup;
                }
                NifiVisitableProcessGroup parent = visitedProcessGroups.get(group.getDto().getParentGroupId());
                //if the parent is null the parent is the starting process group
                if (parent == null) {
                    parent = processGroup;
                }

                ConnectionDTO conn = parent.getConnectionMatchingDestinationId(source.getId());
                if (conn == null) {
                    //if its null get it from the cache and process with that.
                    conn =
                        cache.getProcessGroupCache().values().stream().flatMap(g -> g.getContents().getConnections().stream())
                            .filter(connectionDTO -> connectionDTO.getDestination().getId().equalsIgnoreCase(source.getId())).findFirst().orElse(null);
                }
                if (conn != null && conn != connection) {
                    //get the processor whos source matches this connection Id
                    sourceProcessors = getSourceProcessors(conn);
                    //assign the inputPortProcessor == the the destination of this connection
                }
                List<NifiVisitableProcessor> inputProcessors = getDestinationProcessors(connection, false);
                if (inputProcessors != null) {
                    for (NifiVisitableProcessor inputProcessor : inputProcessors) {
                        currentProcessGroup.addInputPortProcessor(source.getId(), inputProcessor);
                    }
                }

            } else if ("OUTPUT_PORT".equals(source.getType())) {
                //get the sources group id then get the ending processor for that group
                NifiVisitableProcessGroup group = visitedProcessGroups.get(source.getGroupId());
                if (group != null) {
                    Set<NifiVisitableProcessor> sources = group.getOutputPortProcessors(source.getId());
                    if (sourceProcessors != null && sources != null) {
                        sourceProcessors.addAll(sources);
                    }
                  /*
                   If a process group is connected to another process group without any processors there will be no source processors
                    having sourceProcessors as null here is ok.
                    */
                }
            } else if ("FUNNEL".equalsIgnoreCase(source.getType())) {
                List<ConnectionDTO> passThroughConnections = NifiConnectionUtil.findConnectionsMatchingDestinationId(currentProcessGroup.getDto().getContents().getConnections(),
                                                                                                                     connection.getSource().getId());
                if (passThroughConnections != null) {
                    for (ConnectionDTO dto : passThroughConnections) {
                        ConnectionDTO newConnection = new ConnectionDTO();
                        newConnection.setSource(dto.getSource());
                        newConnection.setId(connection.getSource().getId());
                        newConnection.setDestination(connection.getDestination());
                        visitConnection(new NifiVisitableConnection(currentProcessGroup, newConnection));
                    }
                }

            } else if ("PROCESSOR".equals(source.getType())) {
                NifiVisitableProcessor sourceProcessor = getConnectionProcessor(source.getGroupId(), source.getId());
                sourceProcessors.add(sourceProcessor);
            }
            for (NifiVisitableProcessor sourceProcessor : sourceProcessors) {
                sourceProcessor.addDestinationConnectionIdentifier(connection);
            }
        }
        return sourceProcessors;
    }


    private ProcessGroupDTO fetchProcessGroupForNameAndIdentifier(String groupId) {
        //fetch it
        ProcessGroupDTO processGroupEntity = null;
        try {
            try {
                log.debug("fetchProcessGroup {} ", groupId);
                processGroupEntity = getGroup(groupId);
            } catch (NifiComponentNotFoundException e) {
                log.debug("Unable to find the process group " + groupId);
            }
        } catch (Exception e) {
            log.error("Exception fetching the process group " + groupId);
        }
        return processGroupEntity;
    }


    private NifiVisitableProcessGroup fetchProcessGroup(String groupId) {
        NifiVisitableProcessGroup group = processGroup;
        //fetch it
        ProcessGroupDTO processGroupEntity = null;
        try {
            try {
                log.debug("fetchProcessGroup {} ", groupId);
                processGroupEntity = getGroup(groupId);

            } catch (NifiComponentNotFoundException e) {
                log.debug("Unable to find the process group " + groupId);
            }
            //if the parent is null the parent is the starting process group
            if (processGroupEntity != null) {
                group = new NifiVisitableProcessGroup(processGroupEntity);
            }
        } catch (Exception e) {
            log.error("Exception fetching the process group " + groupId);
        }
        return group;
    }


    private ConnectionDTO searchConnectionMatchingSource(String parentGroupId, String destinationId) {
        //search up to find the connection that matches this dest id

        try {
            ProcessGroupDTO parent = null;
            try {
                log.debug("fetch ProcessGroup for searchConnectionMatchingSource {} ", parentGroupId);
                parent = getGroup(parentGroupId);
            } catch (NifiComponentNotFoundException e) {
                log.debug("Exception searching Connection matching the source. Parent Group ID: " + parentGroupId + ", and destinationId of  " + destinationId);
            }
            if (parent != null) {
                //processGroup.getDto().setParent(parentParent.getProcessGroup());
                //get Contents of this parent
                NifiVisitableProcessGroup visitableProcessGroup = new NifiVisitableProcessGroup(parent);
                ConnectionDTO conn = visitableProcessGroup.getConnectionMatchingSourceId(destinationId);
                if (conn != null) {
                    return conn;
                }
                if (conn == null && parent.getParentGroupId() != null) {
                    return searchConnectionMatchingSource(parent.getParentGroupId(), destinationId);
                }
            }

        } catch (Exception e) {
            log.error("Exception searching Connection matching the source.  Parent Group ID: " + parentGroupId + ", and destinationId of  " + destinationId);
        }
        return null;

    }

    private ConnectionDTO searchConnectionMatchingDestination(String parentGroupId, String sourceId) {
        //search up to find the connectoin that matches this dest id

        try {
            ProcessGroupDTO parent = null;
            try {
                parent = getGroup(parentGroupId);
            } catch (NifiComponentNotFoundException e) {
                log.debug("Exception searching Connection matching the destination. Parent Group ID: " + parentGroupId + ", and destinationId of  " + sourceId);
            }
            if (parent != null) {
                //get Contents of this parent
                NifiVisitableProcessGroup visitableProcessGroup = new NifiVisitableProcessGroup(parent);
                ConnectionDTO conn = visitableProcessGroup.getConnectionMatchingDestinationId(sourceId);
                if (conn != null) {
                    return conn;
                }
                if (conn == null && parent.getParentGroupId() != null) {
                    return searchConnectionMatchingSource(parent.getParentGroupId(), sourceId);
                }
            }

        } catch (Exception e) {
            log.error("Exception searching Connection matching the destination.  Parent Group ID: " + parentGroupId + ", and source of  " + sourceId);
        }
        return null;

    }


    private NifiVisitableProcessor getConnectionProcessor(String groupId, String id) {
        NifiVisitableProcessor processor = visitedProcessors.get(id);
        if (processor == null) {
            if (!this.processorsMap.containsKey(id)) {
                //if the current group is not related to this processgroup then attempt to walk this processors processgroup
                try {
                    log.debug("fetch ProcessGroup for getConnectionProcessor {} ", groupId);
                    ProcessGroupDTO processGroupEntity = getGroup(groupId);
                    if (processGroupEntity != null) {

                        ProcessorDTO processorDTO = NifiProcessUtil.findFirstProcessorsById(
                            processGroupEntity.getContents().getProcessors(), id);
                        if (processorDTO != null) {
                            this.processorsMap.put(id, processorDTO);
                        }
                        if (processGroup.getDto().getId() != groupId && !visitedProcessGroups.containsKey(processGroupEntity.getId())) {
                            visitProcessGroup(new NifiVisitableProcessGroup(processGroupEntity));
                        }
                    } else {
                        log.error("getConnectionProcessor error.  Unable to find Process Group for process group id: {}, and processor: {} {} ", groupId, id);
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
            //
            processor = visitedProcessors.get(id);
            if (processor == null) {
                processor = new NifiVisitableProcessor(this.processorsMap.get(id));
                //visit the group?
                processor.accept(this);
            }


        }
        return processor;
    }


    public NifiVisitableProcessGroup getProcessGroup() {
        return processGroup;
    }

    public Map<String, ProcessorDTO> getProcessorsMap() {
        return processorsMap;
    }

    public Map<String, NifiVisitableProcessor> getVisitedProcessors() {
        return visitedProcessors;
    }

    public Map<String, NifiVisitableProcessGroup> getVisitedProcessGroups() {
        return visitedProcessGroups;
    }

    public Set<NifiVisitableConnection> getAllConnections() {
        return allConnections;
    }

    public NifiConnectionOrderVisitorCachedItem toCachedItem() {
        return new NifiConnectionOrderVisitorCachedItem(this);
    }


    public Integer getNumberOfSplits() {
        int count = 0;
        for (NifiVisitableProcessor processor : visitedProcessors.values()) {
            Set<NifiVisitableProcessor> destinations = processor.getDestinations();
            if (destinations != null && !destinations.isEmpty()) {
                count += (destinations.size() - 1);
            }
        }
        return count;
    }

    /**
     * inspect the current status and determine if it has data in queue
     */
    public boolean isProcessingData() {
        return false;
    }

    public void printOrder() {
        for (NifiVisitableProcessor processor : processGroup.getStartingProcessors()) {
            processor.print();
        }
    }

    private ProcessGroupDTO getGroup(String processGroupId) {
        ProcessGroupDTO group = null;
        if (cache.getProcessGroup(processGroupId).isPresent()) {
            group = cache.getProcessGroup(processGroupId).get();
        } else {
            group = getRestClient().processGroups().findById(processGroupId, true, true).orElse(null);
            if (group != null) {
                cache.add(group);
                NifiProcessUtil.getProcessGroups(group).stream().forEach(processGroupDTO -> cache.add(processGroupDTO));
            }
        }
        return group;
    }


    private NiFiRestClient getRestClient() {
        return restClient;
    }
}
