package org.cyclopsgroup.jmxterm.cmd;

import java.io.IOException;
import java.text.FieldPosition;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.management.JMException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import org.apache.commons.lang3.Validate;
import org.cyclopsgroup.jcli.annotation.Argument;
import org.cyclopsgroup.jcli.annotation.Cli;
import org.cyclopsgroup.jcli.annotation.MultiValue;
import org.cyclopsgroup.jcli.annotation.Option;
import org.cyclopsgroup.jmxterm.Command;
import org.cyclopsgroup.jmxterm.Session;
import org.cyclopsgroup.jmxterm.io.CommandOutput;
import org.cyclopsgroup.jmxterm.io.JlineCommandInput;
import org.jline.reader.impl.LineReaderImpl;

/**
 * Command to watch an MBean attribute TODO Consider the use case for CSV file backend generation
 *
 * @author <a href="mailto:jiaqi.guo@gmail.com">Jiaqi Guo</a>
 */
@Cli(
    name = "watch",
    description = "Watch the value of one MBean attribute constantly",
    note = "DO NOT call this command in a script and expect decent output")
public class WatchCommand extends Command {
  private static class ConsoleOutput extends Output {
    private final LineReaderImpl console;

    private ConsoleOutput(Session session) {
      if (!(session.getInput() instanceof JlineCommandInput)) {
        throw new IllegalStateException("Under current context, watch command can't execute.");
      }
      this.console = ((JlineCommandInput) session.getInput()).getConsole();
    }

    void printLine(String line) throws IOException {
      console.redrawLine();
      console.getTerminal().writer().print(line);
      console.flush();
    }
  }

  private abstract static class Output {
    abstract void printLine(String line) throws IOException;
  }

  private static class ReportOutput extends Output {
    private final CommandOutput out;

    private ReportOutput(Session session) {
      this.out = session.output;
    }

    @Override
    void printLine(String line) {
      out.println(line);
    }
  }

  private static final String BUILDING_ATTRIBUTE_NOW = "%now";

  private static final int DEFAULT_REFRESH_INTERVAL = 1;

  private List<String> attributes = new ArrayList<String>();

  private String outputFormat;

  private int refreshInterval = DEFAULT_REFRESH_INTERVAL;

  private boolean report;

  private int stopAfter;

  @Override
  public List<String> doSuggestArgument() throws IOException, JMException {
    if (getSession().getBean() != null) {
      MBeanServerConnection con = getSession().getConnection().getServerConnection();
      MBeanAttributeInfo[] ais =
          con.getMBeanInfo(new ObjectName(getSession().getBean())).getAttributes();
      List<String> results = new ArrayList<String>(ais.length);
      for (MBeanAttributeInfo ai : ais) {
        results.add(ai.getName());
      }
      results.add(BUILDING_ATTRIBUTE_NOW);
      return results;
    }
    return null;
  }

  @Override
  public void execute() throws IOException, JMException {
    if (report && stopAfter == 0) {
      throw new IllegalArgumentException(
          "When --report is sepcified, --stopafter(-s) must be specificed");
    }
    Session session = getSession();
    String domain = DomainCommand.getDomainName(null, session);
    if (domain == null) {
      throw new IllegalStateException("Please specify a domain using domain command first.");
    }
    String beanName = BeanCommand.getBeanName(null, domain, session);
    if (beanName == null) {
      throw new IllegalStateException("Please specify a bean using bean command first.");
    }

    final ObjectName name = new ObjectName(beanName);
    final MBeanServerConnection con = session.getConnection().getServerConnection();
    final Output output;
    if (report) {
      output = new ReportOutput(session);
    } else {
      output = new ConsoleOutput(session);
      getSession().output.printMessage("press any key to stop. DO NOT press Ctrl+C !!!");
    }

    final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    executor.scheduleWithFixedDelay(
        new Runnable() {
          public void run() {
            try {
              printValues(name, con, output);
            } catch (IOException e) {
              getSession().output.printError(e);
            }
          }
        },
        0,
        refreshInterval,
        TimeUnit.SECONDS);
    if (stopAfter > 0) {
      executor.schedule(
          new Runnable() {
            public void run() {
              executor.shutdownNow();
            }
          },
          stopAfter,
          TimeUnit.SECONDS);
    }
    if (!report) {
      System.in.read();
      System.out.println();
      executor.shutdownNow();
    }

    session.output.println("");
  }

  private Object getAttributeValue(
      ObjectName beanName, String attributeName, MBeanServerConnection connection)
      throws IOException {
    // $now is a reserved keyword for current java.util.Date
    if (attributeName.equals(BUILDING_ATTRIBUTE_NOW)) {
      return new Date();
    }
    try {
      return connection.getAttribute(beanName, attributeName);
    } catch (JMException e) {
      return e.getClass().getSimpleName();
    }
  }

  private void printValues(ObjectName beanName, MBeanServerConnection connection, Output output)
      throws IOException {
    StringBuffer result = new StringBuffer();
    if (outputFormat == null) {
      boolean first = true;
      for (String attributeName : attributes) {
        if (first) {
          first = false;
        } else {
          result.append(", ");
        }
        result.append(getAttributeValue(beanName, attributeName, connection));
      }
    } else {
      Object[] values = new Object[attributes.size()];
      int i = 0;
      for (String attributeNamne : attributes) {
        values[i++] = getAttributeValue(beanName, attributeNamne, connection);
      }
      MessageFormat format = new MessageFormat(outputFormat);
      format.format(values, result, new FieldPosition(0));
    }
    output.printLine(result.toString());
  }

  /** @param attributes Name of attributes to watch */
  @MultiValue(listType = ArrayList.class, minValues = 1)
  @Argument(displayName = "attr", description = "Name of attributes to watch")
  public final void setAttributes(List<String> attributes) {
    this.attributes = attributes;
  }

  /** @param outputFormat Pattern used in {@link MessageFormat} */
  @Option(
      name = "f",
      longName = "format",
      displayName = "expr",
      description = "Java pattern(java.text.MessageFormat) to print attribute values")
  public final void setOutputFormat(String outputFormat) {
    this.outputFormat = outputFormat;
  }

  /** @param refreshInterval Refreshing interval in seconds */
  @Option(
      name = "i",
      longName = "interval",
      displayName = "sec",
      description = "Optional number of seconds between consecutive poll, default is 1 second",
      defaultValue = "1")
  public final void setRefreshInterval(int refreshInterval) {
    Validate.isTrue(refreshInterval > 0, "Invalid interval value " + refreshInterval);
    this.refreshInterval = refreshInterval;
  }

  /** @param report True to output result line by line as report */
  @Option(name = "r", longName = "report", description = "Output result line by line as report")
  public final void setReport(boolean report) {
    this.report = report;
  }

  /** @param stopAfter After this number of seconds, stop watching */
  @Option(
      name = "s",
      longName = "stopafter",
      displayName = "sec",
      description = "Stop after watching a number of seconds")
  public final void setStopAfter(int stopAfter) {
    Validate.isTrue(stopAfter >= 0, "Invalid stop after argument " + stopAfter);
    this.stopAfter = stopAfter;
  }
}
