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


import java.io.StringWriter;

import org.slf4j.Logger;

import java.util.Properties;
import java.util.Collection;
import java.util.Map;
import java.util.List;
import java.util.LinkedList;

import groovy.xml.MarkupBuilder;
import groovy.lang.Closure;

import org.apache.zeppelin.interpreter.InterpreterContext;
import org.apache.zeppelin.interpreter.InterpreterContextRunner;

import org.apache.zeppelin.display.AngularObjectRegistry;
import org.apache.zeppelin.display.AngularObject;
import org.apache.zeppelin.display.GUI;
import org.apache.zeppelin.display.ui.OptionInput.ParamOption;
import org.apache.zeppelin.annotation.ZeppelinApi;
import org.apache.zeppelin.interpreter.RemoteWorksController;
import org.apache.zeppelin.interpreter.InterpreterException;

/**
 * Groovy interpreter for Zeppelin.
 */
public class GObject extends groovy.lang.GroovyObjectSupport {

  Logger log;
  StringWriter out;
  Properties props;
  InterpreterContext interpreterContext;
  Map<String, Object> bindings;


  public GObject(Logger log, StringWriter out, Properties p, InterpreterContext ctx,
      Map<String, Object> bindings) {
    this.log = log;
    this.out = out;
    this.interpreterContext = ctx;
    this.props = p;
    this.bindings = bindings;
  }

  public Object getProperty(String key) {
    if ("log".equals(key)) {
      return log;
    }
    return props.getProperty(key);
  }

  public void setProperty(String key, Object value) {
    throw new RuntimeException("Set properties not supported: " + key + "=" + value);
  }

  public Properties getProperties() {
    return props;
  }

  private void startOutputType(String type) {
    StringBuffer sb = out.getBuffer();
    if (sb.length() > 0) {
      if (sb.length() < type.length() || !type.equals(sb.substring(0, type.length()))) {
        log.error("try to start output `" + type + "` after non-" + type + " started");
      }
    } else {
      out.append(type);
      out.append('\n');
    }
  }

  /**
   * returns gui object
   */
  public GUI getGui() {
    return interpreterContext.getGui();
  }

  @ZeppelinApi
  public Object input(String name) {
    return input(name, "");
  }

  @ZeppelinApi
  public Object input(String name, Object defaultValue) {
    return getGui().input(name, defaultValue);
  }

  private ParamOption[] toParamOptions(Map<Object, String> options) {
    ParamOption[] paramOptions = new ParamOption[options.size()];
    int i = 0;
    for (Map.Entry<Object, String> e : options.entrySet()) {
      paramOptions[i++] = new ParamOption(e.getKey(), e.getValue());
    }
    return paramOptions;
  }

  @ZeppelinApi
  public Object select(String name, Map<Object, String> options) {
    return select(name, "", options);
  }

  @ZeppelinApi
  public Object select(String name, Object defaultValue, Map<Object, String> options) {
    return getGui().select(name, defaultValue, toParamOptions(options));
  }

  @ZeppelinApi
  public Collection<Object> checkbox(String name, Map<Object, String> options) {
    return checkbox(name, options.keySet(), options);
  }

  @ZeppelinApi
  public Collection<Object> checkbox(String name, Collection<Object> defaultChecked,
      Map<Object, String> options) {
    return getGui().checkbox(name, defaultChecked, toParamOptions(options));
  }


  /**
   * Returns shared variable if it was previously set. The same as getting groovy script variables
   * but this method will return null if script variable not assigned. To understand groovy script
   * variables see groovy.transform.Field annotation for more information.
   *
   * @see #put
   */
  public Object get(String varName) {
    return bindings.get(varName);
  }

  /**
   * Returns script (shared) variable value but if value was not set returns default value. The same
   * as getting groovy script variables but this method will return default value if script variable
   * not assigned. To understand groovy script variables see groovy.transform.Field annotation for
   * more information.
   *
   * @see #put
   */
  public Object get(String varName, Object defValue) {
    return bindings.containsKey(varName) ? bindings.get(varName) : defValue;
  }

  /**
   * Sets a new value to interpreter's shared variables.
   * Could be set by <code>put('varName', newValue )</code>
   * or by just assigning <code>varName = value</code> without declaring a variable.
   */
  public Object put(String varName, Object newValue) {
    return bindings.put(varName, newValue);
  }

  /**
   * starts or continues rendering html/angular and returns MarkupBuilder to build html.
   * <pre> g.html().with{
   * 	h1("hello")
   *  h2("world")
   * }</pre>
   */
  public MarkupBuilder html() {
    startOutputType("%angular");
    return new MarkupBuilder(out);
  }

  /**
   * starts or continues rendering table rows
   *
   * @param obj: 1. List(rows) of List(columns) where first line is a header
   */
  public void table(Object obj) {
    if (obj == null) {
      return;
    }
    StringBuffer sb = out.getBuffer();
    startOutputType("%table");
    if (obj instanceof groovy.lang.Closure) {
      //if closure run and get result collection
      obj = ((Closure) obj).call();
    }
    if (obj instanceof Collection) {
      int count = 0;
      for (Object row : ((Collection) obj)) {
        count++;
        boolean rowStarted = false;
        if (row instanceof Collection) {
          for (Object field : ((Collection) row)) {
            if (rowStarted) {
              sb.append('\t');
            }
            sb.append(field);
            rowStarted = true;
          }
        } else {
          sb.append(row);
        }
        sb.append('\n');
      }
    } else {
      throw new RuntimeException("Not supported table value :" + obj.getClass());
    }
  }

  private AngularObject getAngularObject(String name) {
    AngularObjectRegistry registry = interpreterContext.getAngularObjectRegistry();
    String noteId = interpreterContext.getNoteId();
    // try get local object
    AngularObject paragraphAo = registry.get(name, noteId, interpreterContext.getParagraphId());
    AngularObject noteAo = registry.get(name, noteId, null);

    AngularObject ao = paragraphAo != null ? paragraphAo : noteAo;

    if (ao == null) {
      // then global object
      ao = registry.get(name, null, null);
    }
    return ao;
  }

  /**
   * Get angular object. Look up notebook scope first and then global scope
   *
   * @param name variable name
   * @return value
   */
  public Object angular(String name) {
    AngularObject ao = getAngularObject(name);
    if (ao == null) {
      return null;
    } else {
      return ao.get();
    }
  }

  @SuppressWarnings("unchecked")
  public void angularBind(String name, Object o, String noteId) {
    AngularObjectRegistry registry = interpreterContext.getAngularObjectRegistry();

    if (registry.get(name, noteId, null) == null) {
      registry.add(name, o, noteId, null);
    } else {
      registry.get(name, noteId, null).set(o);
    }
  }

  /**
   * Create angular variable in notebook scope and bind with front end Angular display system.
   * If variable exists, it'll be overwritten.
   *
   * @param name name of the variable
   * @param o value
   */
  public void angularBind(String name, Object o) {
    angularBind(name, o, interpreterContext.getNoteId());
  }

  /*------------------------------------------RUN----------------------------------------*/
  @ZeppelinApi
  public List<InterpreterContextRunner> getInterpreterContextRunner(String noteId,
      String paragraphId, InterpreterContext interpreterContext) {
    RemoteWorksController remoteWorksController = interpreterContext.getRemoteWorksController();
    if (remoteWorksController != null) {
      return remoteWorksController.getRemoteContextRunner(noteId, paragraphId);
    }
    return new LinkedList<InterpreterContextRunner>();
  }

  @ZeppelinApi
  public List<InterpreterContextRunner> getInterpreterContextRunner(String noteId,
      InterpreterContext interpreterContext) {
    RemoteWorksController remoteWorksController = interpreterContext.getRemoteWorksController();
    if (remoteWorksController != null) {
      return remoteWorksController.getRemoteContextRunner(noteId);
    }
    return new LinkedList<InterpreterContextRunner>();
  }

  /**
   * Run paragraph by id
   */
  @ZeppelinApi
  public void run(String noteId, String paragraphId) {
    run(noteId, paragraphId, interpreterContext);
  }

  /**
   * Run paragraph by id
   */
  @ZeppelinApi
  public void run(String paragraphId) {
    String noteId = interpreterContext.getNoteId();
    run(noteId, paragraphId, interpreterContext);
  }

  /**
   * Run paragraph by id
   */
  @ZeppelinApi
  public void run(String noteId, String paragraphId, InterpreterContext context) {
    if (paragraphId.equals(context.getParagraphId())) {
      throw new InterpreterException("Can not run current Paragraph");
    }
    List<InterpreterContextRunner> runners = getInterpreterContextRunner(noteId, paragraphId,
        context);
    if (runners.size() <= 0) {
      throw new InterpreterException("Paragraph " + paragraphId + " not found " + runners.size());
    }
    for (InterpreterContextRunner r : runners) {
      r.run();
    }
  }

  public void runNote(String noteId) {
    runNote(noteId, interpreterContext);
  }

  public void runNote(String noteId, InterpreterContext context) {
    String runningNoteId = context.getNoteId();
    String runningParagraphId = context.getParagraphId();
    List<InterpreterContextRunner> runners = getInterpreterContextRunner(noteId, context);

    if (runners.size() <= 0) {
      throw new InterpreterException("Note " + noteId + " not found " + runners.size());
    }

    for (InterpreterContextRunner r : runners) {
      if (r.getNoteId().equals(runningNoteId) && r.getParagraphId().equals(runningParagraphId)) {
        continue;
      }
      r.run();
    }
  }

  /**
   * Run all paragraphs. except this.
   */
  @ZeppelinApi
  public void runAll() {
    runAll(interpreterContext);
  }

  /**
   * Run all paragraphs. except this.
   */
  @ZeppelinApi
  public void runAll(InterpreterContext context) {
    runNote(context.getNoteId());
  }


}
