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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;

/**
 * InterpreterMessageOutputStream
 */
public class InterpreterResultMessageOutput extends OutputStream {
  private static final Logger LOGGER = LoggerFactory.getLogger(InterpreterResultMessageOutput.class);
  private final int NEW_LINE_CHAR = '\n';
  private List<String> resourceSearchPaths;

  ByteArrayOutputStream buffer = new ByteArrayOutputStream();

  private final List<Object> outList = new LinkedList<>();
  private InterpreterOutputChangeWatcher watcher;
  private final InterpreterResultMessageOutputListener flushListener;
  private InterpreterResult.Type type = InterpreterResult.Type.TEXT;
  private boolean firstWrite = true;
  private boolean enableTableAppend = true;

  public InterpreterResultMessageOutput(
      InterpreterResult.Type type,
      InterpreterResultMessageOutputListener listener) {
    this.type = type;
    this.flushListener = listener;
  }

  public InterpreterResultMessageOutput(
      InterpreterResult.Type type,
      InterpreterResultMessageOutputListener flushListener,
      InterpreterOutputChangeListener listener) throws IOException {
    this.type = type;
    this.flushListener = flushListener;
    watcher = new InterpreterOutputChangeWatcher(listener);
    watcher.start();
  }

  public void setEnableTableAppend(boolean enableTableAppend) {
    this.enableTableAppend = enableTableAppend;
  }

  public InterpreterResult.Type getType() {
    return type;
  }

  public void setType(InterpreterResult.Type type) {
    if (this.type != type) {
      clear();
      this.type = type;
    }
  }

  public void clear() {
     clear(true);
  }

  /**
   *
   * @param sendUpdateToFrontend Whether send empty result to frontend to clear the paragraph output
   */
  public void clear(boolean sendUpdateToFrontend) {
    synchronized (outList) {
      buffer.reset();
      outList.clear();
      if (watcher != null) {
        watcher.clear();
      }

      if (flushListener != null && sendUpdateToFrontend) {
        flushListener.onUpdate(this);
      }
    }
  }

  @Override
  public void write(int b) throws IOException {
    synchronized (outList) {
      buffer.write(b);
      if (b == NEW_LINE_CHAR) {
        // first time use of this outputstream.
        if (firstWrite) {
          // clear the output on gui
          if (flushListener != null) {
            flushListener.onUpdate(this);
          }
          firstWrite = false;
        }

        if (isAppendSupported()) {
          flush(true);
        }
      }
    }
  }

  @Override
  public void write(byte [] b) throws IOException {
    write(b, 0, b.length);
  }

  @Override
  public void write(byte [] b, int off, int len) throws IOException {
    synchronized (outList) {
      for (int i = off; i < len; i++) {
        write(b[i]);
      }
    }
  }

  /**
   * In dev mode, it monitors file and update ZeppelinServer
   * @param file
   * @throws IOException
   */
  public void write(File file) throws IOException {
    outList.add(file);
    if (watcher != null) {
      watcher.watch(file);
    }
  }

  public void write(String string) throws IOException {
    write(string.getBytes());
  }

  /**
   * write contents in the resource file in the classpath
   * @param url
   * @throws IOException
   */
  public void write(URL url) throws IOException {
    outList.add(url);
  }

  public void setResourceSearchPaths(List<String> resourceSearchPaths) {
    this.resourceSearchPaths = resourceSearchPaths;
  }

  public void writeResource(String resourceName) throws IOException {
    // search file under provided paths first, for dev mode
    for (String path : resourceSearchPaths) {
      File res = new File(path + "/" + resourceName);
      if (res.isFile()) {
        write(res);
        return;
      }
    }

    // search from classpath
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    if (cl == null) {
      cl = this.getClass().getClassLoader();
    }
    if (cl == null) {
      cl = ClassLoader.getSystemClassLoader();
    }

    write(cl.getResource(resourceName));
  }

  public byte[] toByteArray() throws IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    List<Object> all = new LinkedList<>();

    synchronized (outList) {
      all.addAll(outList);
    }

    for (Object o : all) {
      if (o instanceof File) {
        File f = (File) o;
        FileInputStream fin = new FileInputStream(f);
        copyStream(fin, out);
        fin.close();
      } else if (o instanceof byte[]) {
        out.write((byte[]) o);
      } else if (o instanceof Integer) {
        out.write((int) o);
      } else if (o instanceof URL) {
        InputStream fin = ((URL) o).openStream();
        copyStream(fin, out);
        fin.close();
      } else {
        // can not handle the object
      }
    }
    out.close();
    return out.toByteArray();
  }

  public InterpreterResultMessage toInterpreterResultMessage() throws IOException {
    return new InterpreterResultMessage(type, new String(toByteArray()));
  }

  private void flush(boolean append) throws IOException {
    synchronized (outList) {
      buffer.flush();
      byte[] bytes = buffer.toByteArray();
      if (bytes != null && bytes.length > 0) {
        outList.add(bytes);
        if (append) {
          if (flushListener != null) {
            flushListener.onAppend(this, bytes);
          }
        } else {
          if (flushListener != null) {
            flushListener.onUpdate(this);
          }
        }
      }
      buffer.reset();
    }
  }

  public void flush() throws IOException {
    flush(isAppendSupported());
  }

  public boolean isAppendSupported() {
    return type == InterpreterResult.Type.TEXT || (type == InterpreterResult.Type.TABLE && enableTableAppend);
  }

  private void copyStream(InputStream in, OutputStream out) throws IOException {
    int bufferSize = 8192;
    byte[] buffer = new byte[bufferSize];

    while (true) {
      int bytesRead = in.read(buffer);
      if (bytesRead == -1) {
        break;
      } else {
        out.write(buffer, 0, bytesRead);
      }
    }
  }

  @Override
  public void close() throws IOException {
    flush();
    if (watcher != null) {
      watcher.clear();
      watcher.shutdown();
    }
  }

  public String toString() {
    try {
      return "%" + type.name().toLowerCase() + " " + new String(toByteArray());
    } catch (IOException e) {
      LOGGER.error(e.getMessage(), e);
      return "%" + type.name().toLowerCase() + "\n";
    }
  }
}
