package com.thinkbiganalytics.nifi.rest.model.flow;

/*-
 * #%L
 * thinkbig-nifi-flow-visitor-model
 * %%
 * 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.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.Lists;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * Object in a NifiFlow that has pointers to all its sources (parents)  and children (destinations)
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public class NifiFlowProcessor implements Serializable {

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

    @JsonProperty("id")
    private String id;
    @JsonProperty("name")
    private String name;

    private String type;

    private String parentGroupId;

    private boolean isFailure;
    private boolean isEnd;

    private String flowId;

    private NifiFlowProcessGroup processGroup;

    @JsonIgnore
    private Set<NifiFlowProcessor> sources; //parents

    @JsonIgnore
    private Set<NifiFlowProcessor> destinations; //children

    private Set<String> sourceIds;

    private Set<String> destinationIds;

    private Set<NiFiFlowProcessorConnection> sourceConnectionIds;

    private Set<NiFiFlowProcessorConnection> destinationConnectionIds;


    public NifiFlowProcessor() {

    }

    public NifiFlowProcessor(@JsonProperty("id") String id, @JsonProperty("name") String name) {
        this.id = id;
        this.name = name;
    }

    public NifiFlowProcessor(@JsonProperty("id") String id, @JsonProperty("name") String name, @JsonProperty("type") String type) {
        this.id = id;
        this.name = name;
        this.type = type;
    }

    /**
     * Return the processor id
     *
     * @return the processor id
     */
    public String getId() {
        return id;
    }

    /**
     * set the processor id
     *
     * @param id the processor id
     */
    public void setId(String id) {
        this.id = id;
    }

    /**
     * Return the processor name
     *
     * @return the processor name
     */
    public String getName() {
        return name;
    }

    /**
     * set the processor name
     *
     * @param name the processor name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Return the set of destinations coming from this processor
     *
     * @return the set of destination processors
     */
    public Set<NifiFlowProcessor> getDestinations() {
        if (destinations == null) {
            destinations = new HashSet<>();
        }
        return destinations;
    }

    /**
     * set the destination processors
     *
     * @param destinations the destination processors
     */
    public void setDestinations(Set<NifiFlowProcessor> destinations) {
        this.destinations = destinations;
    }

    /**
     * Return the source processors
     *
     * @return the source processors
     */
    public Set<NifiFlowProcessor> getSources() {
        if (sources == null) {
            sources = new HashSet<>();
        }
        return sources;
    }

    /**
     * set the source processors
     *
     * @param sources the source processors
     */
    public void setSources(Set<NifiFlowProcessor> sources) {
        this.sources = sources;
    }

    /**
     * Return the group associated with this processor
     *
     * @return the group
     */
    public NifiFlowProcessGroup getProcessGroup() {
        return processGroup;
    }

    /**
     * Set the group associated with this processor
     *
     * @param processGroup the group
     */
    public void setProcessGroup(NifiFlowProcessGroup processGroup) {
        this.processGroup = processGroup;
    }


    /**
     * Return the set of ids that are incoming source processors
     *
     * @return the set of ids that are incoming source processors
     */
    public Set<String> getSourceIds() {
        if (sourceIds == null) {
            sourceIds = new HashSet<>();
        }
        return sourceIds;
    }

    /**
     * set the set of ids that are incoming source processors
     *
     * @param sourceIds the set of ids that are incoming source processors
     */
    public void setSourceIds(Set<String> sourceIds) {
        this.sourceIds = sourceIds;
    }

    /**
     * Return the set of ids that are destination processors
     *
     * @return the set of ids that are destination processors
     */
    public Set<String> getDestinationIds() {
        if (destinationIds == null) {
            destinationIds = new HashSet<>();
        }
        return destinationIds;
    }

    /**
     * set  the set of ids that are destination processors
     *
     * @param destinationIds the set of ids that are destination processors
     */
    public void setDestinationIds(Set<String> destinationIds) {
        this.destinationIds = destinationIds;
    }

    /**
     * Return the set of this processors destination ids, and all of its children destination ids
     *
     * @return the set of this processors destination ids, and all of its children destination ids
     */
    public Set<String> getAllDestinationIds() {
        Set<String> destinationIds = new HashSet<>();
        destinationIds.addAll(getDestinationIds());
        for (NifiFlowProcessor destination : getDestinations()) {
            destinationIds.addAll(destination.getAllDestinationIds());
        }
        return destinationIds;

    }

    /**
     * Return the set of connection identifiers for the incoming  connections
     *
     * @return the set of connection identifiers for the incoming  connections
     */
    public Set<NiFiFlowProcessorConnection> getSourceConnectionIds() {
        if (sourceConnectionIds == null) {
            sourceConnectionIds = new HashSet<>();
        }
        return sourceConnectionIds;
    }

    /**
     * set the set of connection identifiers for the incoming  connections
     *
     * @param sourceConnectionIds the set of connection identifiers for the incoming  connections
     */
    public void setSourceConnectionIds(Set<NiFiFlowProcessorConnection> sourceConnectionIds) {
        this.sourceConnectionIds = sourceConnectionIds;
    }

    /**
     * Return the set of outgoing destination connection ids
     *
     * @return the set of outgoing destination connection ids
     */
    public Set<NiFiFlowProcessorConnection> getDestinationConnectionIds() {
        if (destinationConnectionIds == null) {
            destinationConnectionIds = new HashSet<>();
        }
        return destinationConnectionIds;
    }

    /**
     * set the set of outgoing destination connection ids
     *
     * @param destinationConnectionIds the set of outgoing destination connection ids
     */
    public void setDestinationConnectionIds(Set<NiFiFlowProcessorConnection> destinationConnectionIds) {
        this.destinationConnectionIds = destinationConnectionIds;
    }

    /**
     * prints the flow id for the processor and its destinations
     */
    public void print() {

        print(0, null);
    }

    /**
     * prints the flow id for the processor and its destinations
     */
    public void print(Integer level, Set<String> printed) {

        log.info(flowId + ", " + level + ". " + getName());
        System.out.println(level + ". " + getName());
        printed = printed == null ? new HashSet<>() : printed;
        printed.add(this.getId());
        Integer nextLevel = level + 1;

        for (NifiFlowProcessor child : getDestinations()) {
            if (!child.containsDestination(this) && !child.containsDestination(child) && !child.equals(this) && !printed.contains(child.getId())) {
                child.print(nextLevel, printed);
                printed.add(child.getId());
            }
        }
    }

    /**
     * Sort the destination processors so the flow id generation will be the same each time a similar flow of the same template is walked
     *
     * @return the sorted destinations
     */
    public List<NifiFlowProcessor> getSortedDestinations() {
        if (destinations == null) {
            destinations = new HashSet<>();
        }
        List<NifiFlowProcessor> list = Lists.newArrayList(destinations);
        Collections.sort(list, new NifiFlowProcessor.FlowIdComparator());
        return list;
    }

    /**
     * Assign a flow identifier to the processor. Flow ids are an attempt to assign a id relative the the location of the processor in the graph as it is walked so different instances of the same
     * flow/template can relate given processors to each other
     *
     * @param flowId the id representing its placement in the graph
     * @return the assigned numeric id
     */
    public Integer assignFlowIds(Integer flowId) {
        flowId++;
        setFlowId(flowId + "__" + StringUtils.substringAfterLast(this.type, "."));
        Set<String> printed = new HashSet<>();
        printed.add(this.getId());

        for (NifiFlowProcessor child : getSortedDestinations()) {
            if (StringUtils.isBlank(child.getFlowId()) && !child.containsDestination(this) && !child.containsDestination(child) && !child.equals(this) && !printed.contains(child.getId())) {
                flowId = child.assignFlowIds(flowId);
                printed.add(child.getId());
            }
        }
        return flowId;
    }

    /**
     * Count the number of distinct destination processor nodes
     *
     * @return the number of distinct destination processor nodes
     */
    public Integer countNodes() {
        return countNodes(null);
    }

    /**
     * Count the number of distinct destination processor nodes
     *
     * @param printed a set containing those nodes that were already printed
     * @return the number of distinct destination processor nodes
     */
    public Integer countNodes(Set<String> printed) {
        Integer count = getDestinations().size();
        printed = printed == null ? new HashSet<>() : printed;
        printed.add(this.getId());
        for (NifiFlowProcessor child : getDestinations()) {
            if (!child.containsDestination(this) && !child.containsDestination(child) && !child.equals(this) && !printed
                .contains(child.getId())) {
                count += child.countNodes(printed);
                printed.add(child.getId());
            }
        }
        return count;
    }

    /**
     * Check to see if the supplied processor is part of this processors graph
     *
     * @param parent a processor
     * @return {@code true} if the processor is already part of this destination set, {@code false} if the processor is not part of the destination set
     */
    public boolean containsDestination(NifiFlowProcessor parent) {
        final String thisId = getId();
        final String parentId = parent.getId();

        return getDestinations().stream().anyMatch(processor -> processor.getId().equalsIgnoreCase(thisId) || processor.getId()
            .equalsIgnoreCase(parentId));
    }

    /**
     * Return the unique flow id for this processor
     *
     * @return the flow if
     */
    public String getFlowId() {
        return flowId;
    }

    /**
     * set the flow id for the processor
     *
     * @param flowId the flow id
     */
    public void setFlowId(String flowId) {
        this.flowId = flowId;
    }


    /**
     * Return the type of processor
     *
     * @return the type of processor
     */
    public String getType() {
        return type;
    }

    /**
     * set the type of processor
     *
     * @param type the type of processor
     */
    public void setType(String type) {
        this.type = type;
    }


    /**
     * check if there are any destinations
     *
     * @return {@code true} if this is a ending processor with no more connections, {@code false} if there are other processors connected to this
     */
    public boolean isLeaf() {
        return getDestinationIds().isEmpty();
    }

    /**
     * check if there are any sources
     *
     * @return {@code true} if this is a starting processor with no incoming connections, {@code false} if it is connected to other incoming processors
     */
    public boolean isStart() {
        return getSourceIds().isEmpty();
    }

    /**
     * check to see if its a leaf processor.  same check as the {@link this#isLeaf()}, but supplied when building the object in the {@link
     * com.thinkbiganalytics.nifi.rest.model.visitor.NifiFlowBuilder}
     */
    public boolean isEnd() {
        return isEnd;
    }

    /**
     * set the processor as being a leaf/ending processor,  supplied when building the object in the {@link com.thinkbiganalytics.nifi.rest.model.visitor.NifiFlowBuilder}
     */
    public void setIsEnd(boolean isEnd) {
        this.isEnd = isEnd;
    }


    /**
     * return the process group id for this processor
     *
     * @return the process group id
     */
    public String getParentGroupId() {
        return parentGroupId;
    }

    /**
     * set the process group id
     *
     * @param parentGroupId the process group id
     */
    public void setParentGroupId(String parentGroupId) {
        this.parentGroupId = parentGroupId;
    }


    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        NifiFlowProcessor that = (NifiFlowProcessor) o;
        return Objects.equals(id, that.id);
    }


    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("NifiFlowProcessor{");
        sb.append("id='").append(id).append('\'');
        sb.append(", name='").append(name).append('\'');
        sb.append('}');
        return sb.toString();
    }


    public static class FlowIdComparator implements Comparator<NifiFlowProcessor> {

        @Override
        public int compare(NifiFlowProcessor o1, NifiFlowProcessor o2) {
            int compare = 0;

            if (o1 == null && o2 == null) {
                compare = 0;
            } else if (o1 == null && o2 != null) {
                compare = -1;
            } else if (o1 != null && o2 == null) {
                compare = 1;
            } else if (o1.getName() != null && o2.getName() != null) {
                compare = o1.getName().compareTo(o2.getName());
            }

            if (compare == 0 && o1.getType() != null && o2.getType() != null) {
                compare = o1.getType().compareTo(o2.getType());
            }
            return compare;
        }
    }


}
