package tech.tablesaw.plotly.components;

import com.mitchellbosecke.pebble.PebbleEngine;
import com.mitchellbosecke.pebble.error.PebbleException;
import com.mitchellbosecke.pebble.template.PebbleTemplate;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import tech.tablesaw.plotly.components.threeD.Scene;

public class Layout {

  private static final int DEFAULT_HEIGHT = 600;
  private static final int DEFAULT_WIDTH = 800;
  private static final String DEFAULT_TITLE = "";
  private static final String DEFAULT_PAPER_BG_COLOR = "#fff";
  private static final String DEFAULT_PLOT_BG_COLOR = "#fff";
  private static final String DEFAULT_DECIMAL_SEPARATOR = ".";
  private static final String DEFAULT_THOUSANDS_SEPARATOR = ",";
  private static final boolean DEFAULT_AUTO_SIZE = false;
  private static final HoverMode DEFAULT_HOVER_MODE = HoverMode.FALSE;
  private static final DragMode DEFAULT_DRAG_MODE = DragMode.ZOOM;
  private static final int DEFAULT_HOVER_DISTANCE = 20;
  private static final BarMode DEFAULT_BAR_MODE = BarMode.GROUP;
  private static final Font DEFAULT_TITLE_FONT = Font.builder().build();
  private static final Font DEFAULT_FONT = Font.builder().build();

  private final PebbleEngine engine = TemplateUtils.getNewEngine();
  private final Scene scene;

  /** Determines the mode of hover interactions. */
  public enum HoverMode {
    X("x"),
    Y("y"),
    CLOSEST("closest"),
    FALSE("false");

    private final String value;

    HoverMode(String value) {
      this.value = value;
    }

    @Override
    public String toString() {
      return value;
    }
  }

  /**
   * Determines the display mode for bars when you have multiple bar traces. This also applies to
   * histogram bars. Group is the default.
   *
   * <p>With "stack", the bars are stacked on top of one another. With "relative", the bars are
   * stacked on top of one another, but with negative values below the axis, positive values above.
   * With "group", the bars are plotted next to one another centered around the shared location.
   * With "overlay", the bars are plotted over one another, provide an "opacity" to see through the
   * overlaid bars.
   */
  public enum BarMode {
    STACK("stack"),
    GROUP("group"),
    OVERLAY("overlay"),
    RELATIVE("relative");

    private final String value;

    BarMode(String value) {
      this.value = value;
    }

    @Override
    public String toString() {
      return value;
    }
  }

  /**
   * Determines the mode of drag interactions. "select" and "lasso" apply only to scatter traces
   * with markers or text. "orbit" and "turntable" apply only to 3D scenes.
   */
  public enum DragMode {
    ZOOM("zoom"),
    PAN("pan"),
    SELECT("select"),
    LASSO("lasso"),
    ORBIT("orbit"),
    TURNTABLE("turntable");

    private final String value;

    DragMode(String value) {
      this.value = value;
    }

    @Override
    public String toString() {
      return value;
    }
  }

  /** The global font */
  private final Font font;

  /*
   * The plot title
   */
  private final String title;

  /** Sets the title font */
  private final Font titleFont;

  /**
   * Determines whether or not a layout width or height that has been left undefined by the user is
   * initialized on each re-layout. Note that, regardless of this attribute, an undefined layout
   * width or height is always initialized on the first call to plot.
   */
  private final boolean autoSize;

  private final boolean heightSet;
  private final boolean widthSet;

  /** The width of the plot in pixels */
  private final int width;

  /** The height of the plot in pixels */
  private final int height;

  /** Sets the margins around the plot */
  private final Margin margin;

  /** Sets the color of paper where the graph is drawn. */
  private final String paperBgColor;

  /** Sets the color of plotting area in-between x and y axes. */
  private final String plotBgColor;

  /** Sets the decimal. For example, "." puts a '.' before decimals */
  private final String decimalSeparator;

  /** Sets the separator. For example, a " " puts a space between thousands. */
  private final String thousandsSeparator;

  /** Determines whether or not a legend is drawn. */
  private final Boolean showLegend;

  /** Determines the mode of hover interactions. */
  private final HoverMode hoverMode;

  /**
   * Determines the mode of drag interactions. "select" and "lasso" apply only to scatter traces
   * with markers or text. "orbit" and "turntable" apply only to 3D scenes.
   */
  private final DragMode dragMode;

  /**
   * Sets the default distance (in pixels) to look for data to add hover labels (-1 means no cutoff,
   * 0 means no looking for data). This is only a real distance for hovering on point-like objects,
   * like scatter points. For area-like objects (bars, scatter fills, etc) hovering is on inside the
   * area and off outside, but these objects will not supersede hover on point-like objects in case
   * of conflict.
   */
  private final int hoverDistance;

  private final Axis xAxis;

  private final Axis yAxis;

  private final Axis yAxis2;
  private final Axis yAxis3;
  private final Axis yAxis4;

  private final Axis zAxis;

  private final Grid grid;

  private final BarMode barMode;

  private Layout(LayoutBuilder builder) {
    this.title = builder.title;
    this.autoSize = builder.autoSize;
    this.widthSet = builder.widthSet;
    this.heightSet = builder.heightSet;
    this.decimalSeparator = builder.decimalSeparator;
    this.thousandsSeparator = builder.thousandsSeparator;
    this.dragMode = builder.dragMode;
    this.font = builder.font;
    this.titleFont = builder.titleFont;
    this.hoverDistance = builder.hoverDistance;
    this.hoverMode = builder.hoverMode;
    this.margin = builder.margin;
    this.height = builder.height;
    this.width = builder.width;
    this.xAxis = builder.xAxis;
    this.yAxis = builder.yAxis;
    this.zAxis = builder.zAxis;
    this.yAxis2 = builder.yAxis2;
    this.yAxis3 = builder.yAxis3;
    this.yAxis4 = builder.yAxis4;
    this.paperBgColor = builder.paperBgColor;
    this.plotBgColor = builder.plotBgColor;
    this.showLegend = builder.showLegend;
    this.barMode = builder.barMode;
    this.scene = builder.scene;
    this.grid = builder.grid;
  }

  public String getTitle() {
    return title;
  }

  public String asJavascript() {
    Writer writer = new StringWriter();
    PebbleTemplate compiledTemplate;
    try {
      compiledTemplate = engine.getTemplate("layout_template.html");
      compiledTemplate.evaluate(writer, getContext());
    } catch (PebbleException e) {
      throw new IllegalStateException(e);
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
    return writer.toString();
  }

  protected Map<String, Object> getContext() {
    Map<String, Object> context = new HashMap<>();
    if (!title.equals(DEFAULT_TITLE)) context.put("title", title);
    if (!titleFont.equals(DEFAULT_TITLE_FONT)) context.put("titlefont", titleFont);
    if (!font.equals(DEFAULT_FONT)) context.put("font", font);
    if (autoSize != DEFAULT_AUTO_SIZE) {
      context.put("autosize", autoSize);
      // since autosize is true, we assume the default width / height values are not wanted, not
      // serialize them, and let Plotly compute them
      if (widthSet) {
        context.put("width", width);
      }
      if (heightSet) {
        context.put("height", height);
      }
    } else {
      context.put("width", width);
      context.put("height", height);
    }
    if (hoverDistance != DEFAULT_HOVER_DISTANCE) context.put("hoverdistance", hoverDistance);
    if (!hoverMode.equals(DEFAULT_HOVER_MODE)) context.put("hoverMode", hoverMode);
    if (margin != null) {
      context.put("margin", margin);
    }
    if (!decimalSeparator.equals(DEFAULT_DECIMAL_SEPARATOR))
      context.put("decimalSeparator", decimalSeparator);
    if (!thousandsSeparator.equals(DEFAULT_THOUSANDS_SEPARATOR))
      context.put("thousandsSeparator", thousandsSeparator);
    if (!dragMode.equals(DEFAULT_DRAG_MODE)) context.put("dragmode", dragMode);
    if (showLegend != null) {
      context.put("showlegend", showLegend);
    }
    if (!plotBgColor.equals(DEFAULT_PLOT_BG_COLOR)) context.put("plotbgcolor", plotBgColor);
    if (!paperBgColor.equals(DEFAULT_PAPER_BG_COLOR)) context.put("paperbgcolor", paperBgColor);
    if (!barMode.equals(DEFAULT_BAR_MODE)) context.put("barMode", barMode);
    if (scene != null) context.put("scene", scene);

    if (xAxis != null) {
      context.put("xAxis", xAxis);
    }
    if (yAxis != null) {
      context.put("yAxis", yAxis);
    }
    if (yAxis2 != null) {
      context.put("yAxis2", yAxis2);
    }
    if (yAxis3 != null) {
      context.put("yAxis3", yAxis3);
    }
    if (yAxis4 != null) {
      context.put("yAxis4", yAxis4);
    }
    if (zAxis != null) { // TODO: remove? It's in scene for 3d scatters at least.
      context.put("zAxis", zAxis);
    }
    if (grid != null) {
      context.put("grid", grid);
    }
    return context;
  }

  public static LayoutBuilder builder() {
    return new LayoutBuilder();
  }

  public static LayoutBuilder builder(String title) {
    return Layout.builder().title(title).height(DEFAULT_HEIGHT).width(DEFAULT_WIDTH);
  }

  public static LayoutBuilder builder(String title, String xTitle) {
    return Layout.builder(title).xAxis(Axis.builder().title(xTitle).build());
  }

  public static LayoutBuilder builder(String title, String xTitle, String yTitle) {
    return Layout.builder(title, xTitle).yAxis(Axis.builder().title(yTitle).build());
  }

  public static class LayoutBuilder {

    /** The global font */
    private final Font font = DEFAULT_FONT;

    /** The plot title */
    private String title = "";

    /** Sets the title font */
    private Font titleFont = DEFAULT_TITLE_FONT;

    /**
     * Determines whether or not a layout width or height that has been left undefined by the user
     * is initialized on each relayout. Note that, regardless of this attribute, an undefined layout
     * width or height is always initialized on the first call to plot.
     */
    private boolean autoSize = false;

    private boolean widthSet = false;
    private boolean heightSet = false;

    /** The width of the plot in pixels */
    private int width = 700;

    /** The height of the plot in pixels */
    private int height = 450;

    /** Sets the margins around the plot */
    private Margin margin;

    /** Sets the color of paper where the graph is drawn. */
    private String paperBgColor = DEFAULT_PAPER_BG_COLOR;

    /** Sets the color of plotting area in-between x and y axes. */
    private String plotBgColor = DEFAULT_PLOT_BG_COLOR;

    /** Sets the decimal. For example, "." puts a '.' before decimals */
    private final String decimalSeparator = DEFAULT_DECIMAL_SEPARATOR;

    /** Sets the separator. For example, a " " puts a space between thousands. */
    private final String thousandsSeparator = DEFAULT_THOUSANDS_SEPARATOR;

    /** Determines whether or not a legend is drawn. */
    private Boolean showLegend = null;

    /** Determines the mode of hover interactions. */
    private HoverMode hoverMode = DEFAULT_HOVER_MODE;

    /**
     * Determines the mode of drag interactions. "select" and "lasso" apply only to scatter traces
     * with markers or text. "orbit" and "turntable" apply only to 3D scenes.
     */
    private final DragMode dragMode = DEFAULT_DRAG_MODE;

    /**
     * Sets the default distance (in pixels) to look for data to add hover labels (-1 means no
     * cutoff, 0 means no looking for data). This is only a real distance for hovering on point-like
     * objects, like scatter points. For area-like objects (bars, scatter fills, etc) hovering is on
     * inside the area and off outside, but these objects will not supersede hover on point-like
     * objects in case of conflict.
     */
    private int hoverDistance = DEFAULT_HOVER_DISTANCE; // greater than or equal to -1

    private Axis xAxis;

    private Axis yAxis;

    private Axis yAxis2;
    private Axis yAxis3;
    private Axis yAxis4;

    private Axis zAxis;

    private BarMode barMode = DEFAULT_BAR_MODE;

    private Scene scene;

    /** Define grid to use when creating subplots */
    private Grid grid;

    public Layout build() {
      return new Layout(this);
    }

    private LayoutBuilder() {}

    public LayoutBuilder title(String title) {
      this.title = title;
      return this;
    }

    public LayoutBuilder titleFont(Font titleFont) {
      this.titleFont = titleFont;
      return this;
    }

    public LayoutBuilder barMode(BarMode barMode) {
      this.barMode = barMode;
      return this;
    }

    public LayoutBuilder margin(Margin margin) {
      this.margin = margin;
      return this;
    }

    public LayoutBuilder scene(Scene scene) {
      this.scene = scene;
      return this;
    }

    public LayoutBuilder hoverMode(HoverMode hoverMode) {
      this.hoverMode = hoverMode;
      return this;
    }

    public LayoutBuilder hoverDistance(int distance) {
      this.hoverDistance = distance;
      return this;
    }

    public LayoutBuilder showLegend(boolean showLegend) {
      this.showLegend = showLegend;
      return this;
    }

    public LayoutBuilder height(int height) {
      this.height = height;
      this.heightSet = true;
      return this;
    }

    public LayoutBuilder width(int width) {
      this.width = width;
      this.widthSet = true;
      return this;
    }

    public LayoutBuilder autosize(boolean autosize) {
      this.autoSize = autosize;
      return this;
    }

    public LayoutBuilder xAxis(Axis axis) {
      this.xAxis = axis;
      return this;
    }

    public LayoutBuilder yAxis(Axis axis) {
      this.yAxis = axis;
      return this;
    }

    public LayoutBuilder yAxis2(Axis axis) {
      this.yAxis2 = axis;
      return this;
    }

    public LayoutBuilder yAxis3(Axis axis) {
      this.yAxis3 = axis;
      return this;
    }

    public LayoutBuilder yAxis4(Axis axis) {
      this.yAxis4 = axis;
      return this;
    }

    public LayoutBuilder zAxis(Axis axis) {
      this.zAxis = axis;
      return this;
    }

    public LayoutBuilder plotBgColor(String color) {
      this.plotBgColor = color;
      return this;
    }

    public LayoutBuilder paperBgColor(String color) {
      this.paperBgColor = color;
      return this;
    }

    public LayoutBuilder grid(Grid grid) {
      this.grid = grid;
      return this;
    }
  }
}
