/*
 * Copyright 2012 LinkedIn, Inc
 *
 * 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 azkaban.executor;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import azkaban.flow.Edge;
import azkaban.flow.Flow;
import azkaban.flow.FlowProps;
import azkaban.flow.Node;
import azkaban.flow.SpecialJobTypes;
import azkaban.project.Project;
import azkaban.utils.TypedMapWrapper;

public class ExecutableFlowBase extends ExecutableNode {
  public static final String FLOW_ID_PARAM = "flowId";
  public static final String NODES_PARAM = "nodes";
  public static final String PROPERTIES_PARAM = "properties";
  public static final String SOURCE_PARAM = "source";
  public static final String INHERITED_PARAM = "inherited";

  private HashMap<String, ExecutableNode> executableNodes =
      new HashMap<String, ExecutableNode>();
  private ArrayList<String> startNodes;
  private ArrayList<String> endNodes;

  private HashMap<String, FlowProps> flowProps =
      new HashMap<String, FlowProps>();
  private String flowId;

  public ExecutableFlowBase(Project project, Node node, Flow flow,
      ExecutableFlowBase parent) {
    super(node, parent);

    setFlow(project, flow);
  }

  public ExecutableFlowBase() {
  }

  public int getExecutionId() {
    if (this.getParentFlow() != null) {
      return this.getParentFlow().getExecutionId();
    }

    return -1;
  }

  public int getProjectId() {
    if (this.getParentFlow() != null) {
      return this.getParentFlow().getProjectId();
    }

    return -1;
  }

  public int getVersion() {
    if (this.getParentFlow() != null) {
      return this.getParentFlow().getVersion();
    }

    return -1;
  }

  public Collection<FlowProps> getFlowProps() {
    return flowProps.values();
  }

  public String getFlowId() {
    return flowId;
  }

  protected void setFlow(Project project, Flow flow) {
    this.flowId = flow.getId();
    flowProps.putAll(flow.getAllFlowProps());

    for (Node node : flow.getNodes()) {
      String id = node.getId();
      if (node.getType().equals(SpecialJobTypes.EMBEDDED_FLOW_TYPE)) {
        String embeddedFlowId = node.getEmbeddedFlowId();
        Flow subFlow = project.getFlow(embeddedFlowId);

        ExecutableFlowBase embeddedFlow =
            new ExecutableFlowBase(project, node, subFlow, this);
        executableNodes.put(id, embeddedFlow);
      } else {
        ExecutableNode exNode = new ExecutableNode(node, this);
        executableNodes.put(id, exNode);
      }
    }

    for (Edge edge : flow.getEdges()) {
      ExecutableNode sourceNode = executableNodes.get(edge.getSourceId());
      ExecutableNode targetNode = executableNodes.get(edge.getTargetId());

      if (sourceNode == null) {
        System.out.println("Source node " + edge.getSourceId()
            + " doesn't exist");
      }
      sourceNode.addOutNode(edge.getTargetId());
      targetNode.addInNode(edge.getSourceId());
    }
  }

  public List<ExecutableNode> getExecutableNodes() {
    return new ArrayList<ExecutableNode>(executableNodes.values());
  }

  public ExecutableNode getExecutableNode(String id) {
    return executableNodes.get(id);
  }

  public ExecutableNode getExecutableNodePath(String ids) {
    String[] split = ids.split(":");
    return getExecutableNodePath(split);
  }

  public ExecutableNode getExecutableNodePath(String... ids) {
    return getExecutableNodePath(this, ids, 0);
  }

  private ExecutableNode getExecutableNodePath(ExecutableFlowBase flow,
      String[] ids, int currentIdIdx) {
    ExecutableNode node = flow.getExecutableNode(ids[currentIdIdx]);
    currentIdIdx++;

    if (node == null) {
      return null;
    }

    if (ids.length == currentIdIdx) {
      return node;
    } else if (node instanceof ExecutableFlowBase) {
      return getExecutableNodePath((ExecutableFlowBase) node, ids, currentIdIdx);
    } else {
      return null;
    }

  }

  public List<String> getStartNodes() {
    if (startNodes == null) {
      startNodes = new ArrayList<String>();
      for (ExecutableNode node : executableNodes.values()) {
        if (node.getInNodes().isEmpty()) {
          startNodes.add(node.getId());
        }
      }
    }

    return startNodes;
  }

  public List<String> getEndNodes() {
    if (endNodes == null) {
      endNodes = new ArrayList<String>();
      for (ExecutableNode node : executableNodes.values()) {
        if (node.getOutNodes().isEmpty()) {
          endNodes.add(node.getId());
        }
      }
    }

    return endNodes;
  }

  public Map<String, Object> toObject() {
    Map<String, Object> mapObj = new HashMap<String, Object>();
    fillMapFromExecutable(mapObj);

    return mapObj;
  }

  protected void fillMapFromExecutable(Map<String, Object> flowObjMap) {
    super.fillMapFromExecutable(flowObjMap);

    flowObjMap.put(FLOW_ID_PARAM, flowId);

    ArrayList<Object> nodes = new ArrayList<Object>();
    for (ExecutableNode node : executableNodes.values()) {
      nodes.add(node.toObject());
    }
    flowObjMap.put(NODES_PARAM, nodes);

    // Flow properties
    ArrayList<Object> props = new ArrayList<Object>();
    for (FlowProps fprop : flowProps.values()) {
      HashMap<String, Object> propObj = new HashMap<String, Object>();
      String source = fprop.getSource();
      String inheritedSource = fprop.getInheritedSource();

      propObj.put(SOURCE_PARAM, source);
      if (inheritedSource != null) {
        propObj.put(INHERITED_PARAM, inheritedSource);
      }
      props.add(propObj);
    }
    flowObjMap.put(PROPERTIES_PARAM, props);
  }

  @Override
  public void fillExecutableFromMapObject(
      TypedMapWrapper<String, Object> flowObjMap) {
    super.fillExecutableFromMapObject(flowObjMap);

    this.flowId = flowObjMap.getString(FLOW_ID_PARAM);
    List<Object> nodes = flowObjMap.<Object> getList(NODES_PARAM);

    if (nodes != null) {
      for (Object nodeObj : nodes) {
        @SuppressWarnings("unchecked")
        Map<String, Object> nodeObjMap = (Map<String, Object>) nodeObj;
        TypedMapWrapper<String, Object> wrapper =
            new TypedMapWrapper<String, Object>(nodeObjMap);

        String type = wrapper.getString(TYPE_PARAM);
        if (type != null && type.equals(SpecialJobTypes.EMBEDDED_FLOW_TYPE)) {
          ExecutableFlowBase exFlow = new ExecutableFlowBase();
          exFlow.fillExecutableFromMapObject(wrapper);
          exFlow.setParentFlow(this);

          executableNodes.put(exFlow.getId(), exFlow);
        } else {
          ExecutableNode exJob = new ExecutableNode();
          exJob.fillExecutableFromMapObject(nodeObjMap);
          exJob.setParentFlow(this);

          executableNodes.put(exJob.getId(), exJob);
        }
      }
    }

    List<Object> properties = flowObjMap.<Object> getList(PROPERTIES_PARAM);
    for (Object propNode : properties) {
      @SuppressWarnings("unchecked")
      HashMap<String, Object> fprop = (HashMap<String, Object>) propNode;
      String source = (String) fprop.get("source");
      String inheritedSource = (String) fprop.get("inherited");

      FlowProps flowProps = new FlowProps(inheritedSource, source);
      this.flowProps.put(source, flowProps);
    }
  }

  public Map<String, Object> toUpdateObject(long lastUpdateTime) {
    Map<String, Object> updateData = super.toUpdateObject();

    List<Map<String, Object>> updatedNodes =
        new ArrayList<Map<String, Object>>();
    for (ExecutableNode node : executableNodes.values()) {
      if (node instanceof ExecutableFlowBase) {
        Map<String, Object> updatedNodeMap =
            ((ExecutableFlowBase) node).toUpdateObject(lastUpdateTime);
        // We add only flows to the list which either have a good update time,
        // or has updated descendants.
        if (node.getUpdateTime() > lastUpdateTime
            || updatedNodeMap.containsKey(NODES_PARAM)) {
          updatedNodes.add(updatedNodeMap);
        }
      } else {
        if (node.getUpdateTime() > lastUpdateTime) {
          Map<String, Object> updatedNodeMap = node.toUpdateObject();
          updatedNodes.add(updatedNodeMap);
        }
      }
    }

    // if there are no updated nodes, we just won't add it to the list. This is
    // good
    // since if this is a nested flow, the parent is given the option to include
    // or
    // discard these subflows.
    if (!updatedNodes.isEmpty()) {
      updateData.put(NODES_PARAM, updatedNodes);
    }
    return updateData;
  }

  public void applyUpdateObject(TypedMapWrapper<String, Object> updateData,
      List<ExecutableNode> updatedNodes) {
    super.applyUpdateObject(updateData);

    if (updatedNodes != null) {
      updatedNodes.add(this);
    }

    List<Map<String, Object>> nodes =
        (List<Map<String, Object>>) updateData
            .<Map<String, Object>> getList(NODES_PARAM);
    if (nodes != null) {
      for (Map<String, Object> node : nodes) {
        TypedMapWrapper<String, Object> nodeWrapper =
            new TypedMapWrapper<String, Object>(node);
        String id = nodeWrapper.getString(ID_PARAM);
        if (id == null) {
          // Legacy case
          id = nodeWrapper.getString("jobId");
        }

        ExecutableNode exNode = executableNodes.get(id);
        if (updatedNodes != null) {
          updatedNodes.add(exNode);
        }

        if (exNode instanceof ExecutableFlowBase) {
          ((ExecutableFlowBase) exNode).applyUpdateObject(nodeWrapper,
              updatedNodes);
        } else {
          exNode.applyUpdateObject(nodeWrapper);
        }
      }
    }
  }

  public void applyUpdateObject(Map<String, Object> updateData,
      List<ExecutableNode> updatedNodes) {
    TypedMapWrapper<String, Object> typedMapWrapper =
        new TypedMapWrapper<String, Object>(updateData);
    applyUpdateObject(typedMapWrapper, updatedNodes);
  }

  @Override
  public void applyUpdateObject(Map<String, Object> updateData) {
    TypedMapWrapper<String, Object> typedMapWrapper =
        new TypedMapWrapper<String, Object>(updateData);
    applyUpdateObject(typedMapWrapper, null);
  }

  public void reEnableDependents(ExecutableNode... nodes) {
    for (ExecutableNode node : nodes) {
      for (String dependent : node.getOutNodes()) {
        ExecutableNode dependentNode = getExecutableNode(dependent);

        if (dependentNode.getStatus() == Status.KILLED) {
          dependentNode.setStatus(Status.READY);
          dependentNode.setUpdateTime(System.currentTimeMillis());
          reEnableDependents(dependentNode);

          if (dependentNode instanceof ExecutableFlowBase) {

            ((ExecutableFlowBase) dependentNode).reEnableDependents();
          }
        } else if (dependentNode.getStatus() == Status.SKIPPED) {
          dependentNode.setStatus(Status.DISABLED);
          dependentNode.setUpdateTime(System.currentTimeMillis());
          reEnableDependents(dependentNode);
        }
      }
    }
  }

  /**
   * Only returns true if the status of all finished nodes is true.
   * 
   * @return
   */
  public boolean isFlowFinished() {
    for (String end : getEndNodes()) {
      ExecutableNode node = getExecutableNode(end);
      if (!Status.isStatusFinished(node.getStatus())) {
        return false;
      }
    }

    return true;
  }

  /**
   * Finds all jobs which are ready to run. This occurs when all of its
   * dependency nodes are finished running.
   * 
   * It will also return any subflow that has been completed such that the
   * FlowRunner can properly handle them.
   * 
   * @param flow
   * @return
   */
  public List<ExecutableNode> findNextJobsToRun() {
    ArrayList<ExecutableNode> jobsToRun = new ArrayList<ExecutableNode>();

    if (isFlowFinished() && !Status.isStatusFinished(getStatus())) {
      jobsToRun.add(this);
    } else {
      nodeloop: for (ExecutableNode node : executableNodes.values()) {
        if (Status.isStatusFinished(node.getStatus())) {
          continue;
        }

        if ((node instanceof ExecutableFlowBase)
            && Status.isStatusRunning(node.getStatus())) {
          // If the flow is still running, we traverse into the flow
          jobsToRun.addAll(((ExecutableFlowBase) node).findNextJobsToRun());
        } else if (Status.isStatusRunning(node.getStatus())) {
          continue;
        } else {
          for (String dependency : node.getInNodes()) {
            // We find that the outer-loop is unfinished.
            if (!Status.isStatusFinished(getExecutableNode(dependency)
                .getStatus())) {
              continue nodeloop;
            }
          }

          jobsToRun.add(node);
        }
      }
    }

    return jobsToRun;
  }

  public String getFlowPath() {
    if (this.getParentFlow() == null) {
      return this.getFlowId();
    } else {
      return this.getParentFlow().getFlowPath() + "," + this.getId() + ":"
          + this.getFlowId();
    }
  }
}
