package org.basex.core;

import static org.basex.core.Text.*;

import java.io.*;
import java.util.*;

import org.basex.core.cmd.*;
import org.basex.core.parse.*;
import org.basex.data.*;
import org.basex.io.out.*;
import org.basex.util.*;
import org.basex.util.list.*;
import org.xml.sax.*;

/**
 * This class provides the architecture for all internal command
 * implementations. It evaluates queries that are sent by the GUI, the client or
 * the standalone version.
 *
 * @author BaseX Team 2005-14, BSD License
 * @author Christian Gruen
 */
public abstract class Command extends Proc {
  /** Command arguments. */
  public final String[] args;
  /** Permission required to execute this command. */
  public final Perm perm;
  /** Indicates if the command requires an opened database. */
  public final boolean openDB;
  /** Exception, resulting from command execution. */
  public Exception cause;

  /** Performance measurements. */
  protected Performance perf;
  /** Database context. */
  protected Context context;
  /** Output stream. */
  protected PrintOutput out;
  /** Optional input source. */
  protected InputSource in;
  /** Database options. */
  protected MainOptions options;
  /** Global options. */
  protected GlobalOptions goptions;

  /** Container for query information. */
  private final TokenBuilder info = new TokenBuilder();

  /**
   * Constructor for commands requiring no opened database.
   * @param p required permission
   * @param arg arguments
   */
  protected Command(final Perm p, final String... arg) {
    this(p, false, arg);
  }

  /**
   * Constructor.
   * @param p required permission
   * @param d requires opened database
   * @param arg arguments
   */
  protected Command(final Perm p, final boolean d, final String... arg) {
    perm = p;
    openDB = d;
    args = arg;
  }

  /**
   * Executes the command and prints the result to the specified output
   * stream. If an exception occurs, a {@link BaseXException} is thrown.
   * @param ctx database context
   * @param os output stream reference
   * @throws BaseXException command exception
   */
  public final void execute(final Context ctx, final OutputStream os) throws BaseXException {
    // check if data reference is available
    final Data dt = ctx.data();
    if(dt == null && openDB) throw new BaseXException(NO_DB_OPENED);

    // check permissions
    if(!ctx.perm(perm, dt != null ? dt.meta : null))
      throw new BaseXException(PERM_REQUIRED_X, perm);

    // set updating flag
    updating = updating(ctx);

    try {
      // register process
      ctx.register(this);
      // run command and return success flag
      if(!run(ctx, os)) {
        final BaseXException ex = new BaseXException(info());
        ex.initCause(cause);
        throw ex;
      }
    } catch(final RuntimeException th) {
      Util.stack(th);
      throw th;
    } finally {
      // guarantee that process will be unregistered
      ctx.unregister(this);
    }
  }

  /**
   * Executes the command and returns the result as string.
   * If an exception occurs, a {@link BaseXException} is thrown.
   * @param ctx database context
   * @return string result
   * @throws BaseXException command exception
   */
  public final String execute(final Context ctx) throws BaseXException {
    final ArrayOutput ao = new ArrayOutput();
    execute(ctx, ao);
    return ao.toString();
  }

  /**
   * Attaches an input stream.
   * @param is input stream
   */
  public void setInput(final InputStream is) {
    in = new InputSource(is);
  }

  /**
   * Attaches an input source.
   * @param is input source
   */
  public void setInput(final InputSource is) {
    in = is;
  }

  /**
   * Runs the command without permission, data and concurrency checks.
   * Should be called with care, and only by other database commands.
   * @param ctx database context
   * @return result of check
   */
  public final boolean run(final Context ctx) {
    return run(ctx, new NullOutput());
  }

  /**
   * Returns command information.
   * @return info string
   */
  public final String info() {
    return info.toString();
  }

  /**
   * Returns the result set, generated by a query command. Will only yield results if
   * {@link MainOptions#CACHEQUERY} is set, and can only be called once.
   * @return result set
   */
  public Result result() {
    return null;
  }

  /**
   * Checks if the command performs updates/write operations.
   * @param ctx database context
   * @return result of check
   */
  @SuppressWarnings("unused")
  public boolean updating(final Context ctx) {
    return perm == Perm.CREATE || perm == Perm.WRITE;
  }

  /**
   * Checks if the command has updated any data.
   * @param ctx database context
   * @return result of check
   */
  public boolean updated(final Context ctx) {
    return updating(ctx);
  }

  /**
   * Closes an open data reference and returns {@code true} if this command will change
   * the {@link Context#data()} reference. This method is required by the progress dialog
   * in the frontend.
   * @param ctx database context
   * @return result of check
   */
  @SuppressWarnings("unused")
  public boolean newData(final Context ctx) {
    return false;
  }

  /**
   * Returns true if this command returns a progress value.
   * This method is required by the progress dialog in the frontend.
   * @return result of check
   */
  public boolean supportsProg() {
    return false;
  }

  /**
   * Returns true if this command can be stopped.
   * This method is required by the progress dialog in the frontend.
   * @return result of check
   */
  public boolean stoppable() {
    return false;
  }

  @Override
  public final String toString() {
    final CmdBuilder cb = new CmdBuilder(this);
    build(cb);
    return cb.toString();
  }

  // PROTECTED METHODS ========================================================

  /**
   * Executes the command and serializes the result (internal call).
   * @return success of operation
   * @throws IOException I/O exception
   */
  protected abstract boolean run() throws IOException;

  /**
   * Builds a string representation from the command. This string must be
   * correctly built, as commands are sent to the server as strings.
   * @param cb command builder
   */
  protected void build(final CmdBuilder cb) {
    cb.init().args();
  }

  /**
   * Adds the error message to the message buffer {@link #info}.
   * @param msg error message
   * @param ext error extension
   * @return {@code false}
   */
  protected final boolean error(final String msg, final Object... ext) {
    info.reset();
    info.addExt(msg == null ? "" : msg, ext);
    return false;
  }

  /**
   * Adds information on command execution.
   * @param str information to be added
   * @param ext extended info
   * @return {@code true}
   */
  protected final boolean info(final String str, final Object... ext) {
    if(!str.isEmpty()) info.addExt(str, ext).add(Prop.NL);
    return true;
  }

  /**
   * Returns the specified command option.
   * @param typ options enumeration
   * @param <E> token type
   * @return option
   */
  protected final <E extends Enum<E>> E getOption(final Class<E> typ) {
    final E e = getOption(args[0], typ);
    if(e == null) error(UNKNOWN_TRY_X, args[0]);
    return e;
  }

  /**
   * Adds the names of the database that has been addressed by the argument index.
   * No databases will be added if the argument uses glob syntax.
   * @param db databases
   * @param a argument index
   * @return {@code false} if database cannot be determined due to glob syntax
   */
  protected final boolean databases(final StringList db, final int a) {
    // return true if the addressed database argument does not exists
    if(args.length <= a || args[a] == null) return true;
    final boolean noglob = !args[a].matches(".*[\\?\\*,].*");
    if(noglob) db.add(args[a]);
    return noglob;
  }

  /**
   * Returns the specified command option.
   * @param s string to be found
   * @param typ options enumeration
   * @param <E> token type
   * @return option
   */
  protected static <E extends Enum<E>> E getOption(final String s, final Class<E> typ) {
    try {
      return Enum.valueOf(typ, s.toUpperCase(Locale.ENGLISH));
    } catch(final Exception ex) {
      return null;
    }
  }

  /**
   * Closes the specified database if it is currently opened and only pinned once.
   * @param ctx database context
   * @param db database to be closed
   * @return closed flag
   */
  protected static boolean close(final Context ctx, final String db) {
    final boolean close = ctx.data() != null &&
        db.equals(ctx.data().meta.name) && ctx.dbs.pins(db) == 1;
    return close && new Close().run(ctx);
  }

  /**
   * Runs the command without permission, data and concurrency checks.
   * @param ctx database context
   * @param os output stream
   * @return result of check
   */
  public boolean run(final Context ctx, final OutputStream os) {
    perf = new Performance();
    context = ctx;
    options = ctx.options;
    goptions = ctx.globalopts;
    out = PrintOutput.get(os);

    try {
      return run();
    } catch(final ProcException ex) {
      // process was interrupted by the user or server
      abort();
      return error(INTERRUPTED);
    } catch(final Throwable ex) {
      // unexpected error
      Performance.gc(2);
      abort();
      if(ex instanceof OutOfMemoryError) {
        Util.debug(ex);
        return error(OUT_OF_MEM + (perm == Perm.CREATE ? H_OUT_OF_MEM : ""));
      }
      return error(Util.bug(ex) + Prop.NL + info);
    } finally {
      // flushes the output
      try { if(out != null) out.flush(); } catch(final IOException ignored) { }
    }
  }
}
