/**
 * 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.hadoop.hive.ql;

import com.google.common.collect.ImmutableMap;
import org.apache.hadoop.hive.ql.exec.Task;
import org.apache.hadoop.hive.ql.exec.TaskResult;
import org.apache.hadoop.hive.ql.plan.api.StageType;

import java.io.Serializable;
import java.util.*;

import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonWriteNullProperties;
import org.codehaus.jackson.annotate.JsonIgnore;

/**
 * Some limited query information to save for WebUI.
 *
 * The class is synchronized, as WebUI may access information about a running query.
 */
public class QueryDisplay {

  // Member variables
  private String queryStr;
  private String explainPlan;
  private String errorMessage;
  private String queryId;

  private final Map<Phase, Map<String, Long>> hmsTimingMap = new HashMap<Phase, Map<String, Long>>();
  private final Map<Phase, Map<String, Long>> perfLogStartMap = new HashMap<Phase, Map<String, Long>>();
  private final Map<Phase, Map<String, Long>> perfLogEndMap = new HashMap<Phase, Map<String, Long>>();

  private final LinkedHashMap<String, TaskDisplay> tasks = new LinkedHashMap<String, TaskDisplay>();

  public synchronized <T extends Serializable> void updateTaskStatus(Task<T> tTask) {
    if (!tasks.containsKey(tTask.getId())) {
      tasks.put(tTask.getId(), new TaskDisplay(tTask));
    }
    tasks.get(tTask.getId()).updateStatus(tTask);
  }

  //Inner classes
  public enum Phase {
    COMPILATION,
    EXECUTION,
  }

  @JsonWriteNullProperties(false)
  @JsonIgnoreProperties(ignoreUnknown = true)
  public static class TaskDisplay {

    private Integer returnValue;  //if set, determines that task is complete.
    private String errorMsg;

    private Long beginTime;
    private Long endTime;

    private String taskId;
    private String externalHandle;

    public Task.TaskState taskState;
    private StageType taskType;
    private String name;
    private boolean requireLock;
    private boolean retryIfFail;
    private String statusMessage;

    // required for jackson
    public TaskDisplay() {

    }
    public TaskDisplay(Task task) {
      taskId = task.getId();
      externalHandle = task.getExternalHandle();
      taskType = task.getType();
      name = task.getName();
      requireLock = task.requireLock();
      retryIfFail = task.ifRetryCmdWhenFail();
    }
    @JsonIgnore
    public synchronized String getStatus() {
      if (returnValue == null) {
        return "Running";
      } else if (returnValue == 0) {
        return "Success, ReturnVal 0";
      } else {
        return "Failure, ReturnVal " + String.valueOf(returnValue);
      }
    }

    public synchronized Long getElapsedTime() {
      if (endTime == null) {
        if (beginTime == null) {
          return null;
        }
        return System.currentTimeMillis() - beginTime;
      } else {
        return endTime - beginTime;
      }
    }

    public synchronized Integer getReturnValue() {
      return returnValue;
    }

    public synchronized String getErrorMsg() {
      return errorMsg;
    }

    public synchronized Long getBeginTime() {
      return beginTime;
    }

    public synchronized Long getEndTime() {
      return endTime;
    }

    public synchronized String getTaskId() {
      return taskId;
    }

    public synchronized StageType getTaskType() {
      return taskType;
    }

    public synchronized String getName() {
      return name;
    }
    @JsonIgnore
    public synchronized boolean isRequireLock() {
      return requireLock;
    }
    @JsonIgnore
    public synchronized boolean isRetryIfFail() {
      return retryIfFail;
    }

    public synchronized String getExternalHandle() {
      return externalHandle;
    }

    public synchronized <T extends Serializable> void updateStatus(Task<T> tTask) {
      this.taskState = tTask.getTaskState();
      if (externalHandle == null && tTask.getExternalHandle() != null) {
        this.externalHandle = tTask.getExternalHandle();
      }
      setStatusMessage(tTask.getStatusMessage());
      switch (taskState) {
        case RUNNING:
          if (beginTime == null) {
            beginTime = System.currentTimeMillis();
          }
          break;
        case FINISHED:
          if (endTime == null) {
            endTime = System.currentTimeMillis();
          }
          break;
      }
    }

    public synchronized String getStatusMessage() {
      return statusMessage;
    }

    public synchronized void setStatusMessage(String statusMessage) {
      this.statusMessage = statusMessage;
    }
  }
  public synchronized void setTaskResult(String taskId, TaskResult result) {
    TaskDisplay taskDisplay = tasks.get(taskId);
    if (taskDisplay != null) {
      taskDisplay.returnValue = result.getExitVal();
      if (result.getTaskError() != null) {
        taskDisplay.errorMsg = result.getTaskError().toString();
      }
    }
  }
  public synchronized List<TaskDisplay> getTaskDisplays() {
    List<TaskDisplay> taskDisplays = new ArrayList<TaskDisplay>();
    taskDisplays.addAll(tasks.values());
    return taskDisplays;
  }

  public synchronized void setQueryStr(String queryStr) {
    this.queryStr = queryStr;
  }

  public synchronized String getQueryString() {
    return returnStringOrUnknown(queryStr);
  }

  public synchronized String getExplainPlan() {
    return returnStringOrUnknown(explainPlan);
  }

  public synchronized void setExplainPlan(String explainPlan) {
    this.explainPlan = explainPlan;
  }

  /**
   * @param phase phase of query
   * @return map of HMS Client method-calls and duration in miliseconds, during given phase.
   */
  public synchronized Map<String, Long> getHmsTimings(Phase phase) {
    return hmsTimingMap.get(phase);
  }

  /**
   * @param phase phase of query
   * @param hmsTimings map of HMS Client method-calls and duration in miliseconds, during given phase.
   */
  public synchronized void setHmsTimings(Phase phase, ImmutableMap<String, Long> hmsTimings) {
    hmsTimingMap.put(phase, hmsTimings);
  }

  /**
   * @param phase phase of query
   * @return map of PerfLogger call-trace name and start time in miliseconds, during given phase.
   */
  public synchronized Map<String, Long> getPerfLogStarts(Phase phase) {
    return perfLogStartMap.get(phase);
  }

  /**
   * @param phase phase of query
   * @param perfLogStarts map of PerfLogger call-trace name and start time in miliseconds, during given phase.
   */
  public synchronized void setPerfLogStarts(Phase phase, ImmutableMap<String, Long> perfLogStarts) {
    perfLogStartMap.put(phase, perfLogStarts);
  }

  /**
   * @param phase phase of query
   * @return map of PerfLogger call-trace name and end time in miliseconds, during given phase.
   */
  public synchronized Map<String, Long> getPerfLogEnds(Phase phase) {
    return perfLogEndMap.get(phase);
  }

  /**
   * @param phase phase of query
   * @param perfLogEnds map of PerfLogger call-trace name and end time in miliseconds, during given phase.
   */
   public synchronized void setPerfLogEnds(Phase phase, ImmutableMap<String, Long> perfLogEnds) {
    perfLogEndMap.put(phase, perfLogEnds);
  }

  /**
   * @param phase phase of query
   * @return map of PerfLogger call-trace name and duration in miliseconds, during given phase.
   */
  public synchronized Map<String, Long> getPerfLogTimes(Phase phase) {
    Map<String, Long> times = new HashMap<>();
    Map<String, Long> startTimes = perfLogStartMap.get(phase);
    Map<String, Long> endTimes = perfLogEndMap.get(phase);
    if (endTimes != null && startTimes != null) {
      for (String timeKey : endTimes.keySet()) {
        Long endTime = endTimes.get(timeKey);
        Long startTime = startTimes.get(timeKey);
        if (startTime != null) {
          times.put(timeKey, endTime - startTime);
        }
      }
    }
    return times;
  }

  public synchronized String getErrorMessage() {
    return errorMessage;
  }

  public synchronized void setErrorMessage(String errorMessage) {
    this.errorMessage = errorMessage;
  }

  public synchronized String getQueryId() {
    return returnStringOrUnknown(queryId);
  }

  public synchronized void setQueryId(String queryId) {
    this.queryId = queryId;
  }

  private String returnStringOrUnknown(String s) {
    return s == null ? "UNKNOWN" : s;
  }
}
