/*
 * 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.exec;

import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.TreeMap;

import com.mapr.fs.MapRFileSystem;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hive.ql.CompilationOpContext;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.plan.MapWork;
import org.apache.hadoop.hive.serde2.Deserializer;
import org.apache.hadoop.hive.serde2.SerDeException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Writable;

import static org.apache.hadoop.hive.common.SymLinkUtils.isSymLinkSupportEnabled;

/**
 * Abstract Map operator. Common code of MapOperator and VectorMapOperator.
 **/
@SuppressWarnings("deprecation")
public abstract class AbstractMapOperator extends Operator<MapWork>
    implements Serializable, Cloneable {

  private static final long serialVersionUID = 1L;
  private Configuration internalConf;

  /**
   * Initialization call sequence:
   *
   *   (Operator)                     Operator.setConf(MapWork conf);
   *   (Operator)                     Operator.initialize(
   *                                      Configuration hconf, ObjectInspector[] inputOIs);
   *
   *   ([Vector]MapOperator)          @Override setChildren(Configuration hconf)
   *
   *   (Operator)                     Operator.passExecContext(ExecMapperContext execContext)
   *   (Operator)                     Operator.initializeLocalWork(Configuration hconf)
   *
   *   (AbstractMapOperator)          initializeMapOperator(Configuration hconf)
   *
   * [ (AbstractMapOperator)          initializeContexts() ]   // exec.tez.MapRecordProcessor only.
   *
   *   (Operator)                     Operator.setReporter(Reporter rep)
   *
   */

  /**
   * Counter.
   *
   */
  public static enum Counter {
    DESERIALIZE_ERRORS,
    RECORDS_IN
  }

  protected final transient LongWritable deserialize_error_count = new LongWritable();
  protected final transient LongWritable recordCounter = new LongWritable();
  protected transient long numRows = 0;

  private final Map<Integer, DummyStoreOperator> connectedOperators
  = new TreeMap<Integer, DummyStoreOperator>();

  private transient final Map<Path, Path> normalizedPaths = new HashMap<>();

  private Path normalizePath(Path onefile, boolean schemaless) {
    //creating Path is expensive, so cache the corresponding
    //Path object in normalizedPaths
    Path path = normalizedPaths.get(onefile);
    if (path == null) {
      path = onefile;
      if (schemaless && path.toUri().getScheme() != null) {
        path = new Path(path.toUri().getPath());
      }
      normalizedPaths.put(onefile, path);
    }
    return path;
  }

  protected String getNominalPath(Path fpath) {
    Path nominal = null;
    boolean schemaless = fpath.toUri().getScheme() == null;
    for (Path onefile : conf.getPathToAliases().keySet()) {
      Path onepath = normalizePath(onefile, schemaless);
      // onepath can be a symlink, we need to check and fix it
      try {
        if (FileSystem.get(internalConf) instanceof MapRFileSystem && isSymLinkSupportEnabled(internalConf)) {
          FileSystem maprFS = MapRFileSystem.get(internalConf);
          FileStatus fileStatus = maprFS.getFileStatus(onepath);
          boolean isFound = false;
          if (fileStatus.isSymlink()) { // symlink to a dir; MAPR-HIVE-880
            onepath = FileUtil.fixSymlinkFileStatus(fileStatus);
            isFound = true;
          } else { // symlink to a file; MAPR-HIVE-884
            FileStatus[] fileStatuses = maprFS.listStatus(onepath);
            for (FileStatus each : fileStatuses) {
              if (each.isSymlink()) {
                Path p = FileUtil.fixSymlinkFileStatus(each);
                if (maprFS.getFileStatus(p).compareTo(maprFS.getFileStatus(fpath)) == 0) {
                  onepath = p.getParent();
                  isFound = true;
                  break;
                }
              }
            }
          }

          // This is to consider symlink somewhere in the middle of the path
          if (!isFound) {
            // We have already considered the case when symlink points full link
            // So, skipping first node
            Stack<String> nodes = new Stack<>();
            nodes.push(onepath.getName());
            Path p = onepath.getParent();
            while (p != null) {
              fileStatus = maprFS.getFileStatus(p);
              if (fileStatus.isSymlink()) {
                p = FileUtil.fixSymlinkFileStatus(fileStatus);
                isFound = true;
                break;
              }
              nodes.push(p.getName());
              p = p.getParent();
            }
            if (isFound) {
              String processedPath = p.toString();
              while (!nodes.isEmpty()) {
                processedPath = processedPath + Path.SEPARATOR + nodes.pop();
              }
              onepath = new Path(processedPath);
            }
          }
        }
      } catch (IOException e) {
        // can be ignored, because we check file status before attempting to fix symlink
        // the exception can be thrown only when it is attempted to fix non-symlink as symlink
      }

      Path curfpath = fpath;
      if(!schemaless && onepath.toUri().getScheme() == null) {
        curfpath = new Path(fpath.toUri().getPath());
      }
      // check for the operators who will process rows coming to this Map Operator
      if (onepath.toUri().relativize(curfpath.toUri()).equals(curfpath.toUri())) {
        // not from this
        continue;
      }
      if (nominal != null) {
        throw new IllegalStateException("Ambiguous input path " + fpath);
      }
      nominal = onefile;
      break;
    }
    if (nominal == null) {
      throw new IllegalStateException("Invalid input path " + fpath);
    }
    return nominal.toString();
  }

  public abstract void initEmptyInputChildren(List<Operator<?>> children, Configuration hconf)
      throws SerDeException, Exception;


  /** Kryo ctor. */
  protected AbstractMapOperator() {
    super();
  }

  public AbstractMapOperator(CompilationOpContext ctx) {
    super(ctx);
  }

  public abstract void setChildren(Configuration hconf) throws Exception;


  public void initializeMapOperator(Configuration hconf) throws HiveException {
    // set that parent initialization is done and call initialize on children
    state = State.INIT;

    // gets the conf to use for symlink check in nominal path method
    this.internalConf = hconf;

    statsMap.put(Counter.DESERIALIZE_ERRORS.toString(), deserialize_error_count);

    numRows = 0;

    String context = hconf.get(Operator.CONTEXT_NAME_KEY, "");
    if (context != null && !context.isEmpty()) {
      context = "_" + context.replace(" ","_");
    }
    statsMap.put(Counter.RECORDS_IN + context, recordCounter);
  }

  public abstract void initializeContexts() throws HiveException;

  public abstract Deserializer getCurrentDeserializer();

  public abstract void process(Writable value) throws HiveException;

  @Override
  public void closeOp(boolean abort) throws HiveException {
    recordCounter.set(numRows);
    super.closeOp(abort);
  }

  public void clearConnectedOperators() {
    connectedOperators.clear();
  }

  public void setConnectedOperators(int tag, DummyStoreOperator dummyOp) {
    connectedOperators.put(tag, dummyOp);
  }

  public Map<Integer, DummyStoreOperator> getConnectedOperators() {
    return connectedOperators;
  }
}