package org.apache.ibatis.migration.commands;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;

import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
import org.apache.ibatis.io.ExternalResources;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.jdbc.ScriptRunner;
import org.apache.ibatis.jdbc.SqlRunner;
import org.apache.ibatis.migration.Change;
import org.apache.ibatis.migration.MigrationException;
import org.apache.ibatis.parsing.PropertyParser;

public abstract class BaseCommand implements Command {

  private static final String DATE_FORMAT = "yyyyMMddHHmmss";
  protected PrintStream printStream = System.out;
  protected File basePath;
  protected File envPath;
  protected File scriptPath;
  protected File driverPath;
  protected String environment;
  protected String template;
  protected boolean force;
  private ClassLoader driverClassLoader;

  protected BaseCommand(File repository, String environment, boolean force) {
    this.basePath = repository;
    this.envPath = subdirectory(repository, "environments");
    this.scriptPath = subdirectory(repository, "scripts");
    this.driverPath = subdirectory(repository, "drivers");
    this.environment = environment;
    this.force = force;
  }

  protected BaseCommand(File repository, String environment, String template, boolean force) {
    this.basePath = repository;
    this.envPath = subdirectory(repository, "environments");
    this.scriptPath = subdirectory(repository, "scripts");
    this.driverPath = subdirectory(repository, "drivers");
    this.environment = environment;
    this.template = template;
    this.force = force;
  }

  public PrintStream getPrintStream() {
    return printStream;
  }

  public void setPrintStream(PrintStream printStream) {
    this.printStream = printStream;
  }

  public ClassLoader getDriverClassLoader() {
    return driverClassLoader;
  }

  public void setDriverClassLoader(ClassLoader driverClassLoader) {
    this.driverClassLoader = driverClassLoader;
  }

  protected boolean paramsEmpty(String... params) {
    return params == null || params.length < 1 || params[0] == null || params[0].length() < 1;
  }

  protected List<Change> getMigrations() {
    String[] filenames = scriptPath.list();
    if (filenames == null) {
      throw new MigrationException(scriptPath + " does not exist.");
    }
    Arrays.sort(filenames);
    List<Change> migrations = new ArrayList<Change>();
    for (String filename : filenames) {
      if (filename.endsWith(".sql") && !"bootstrap.sql".equals(filename)) {
        Change change = parseChangeFromFilename(filename);
        migrations.add(change);
      }
    }
    return migrations;
  }

  protected List<Change> getChangelog() {
    SqlRunner runner = getSqlRunner();
    try {
      List<Map<String, Object>> changelog = runner.selectAll("select ID, APPLIED_AT, DESCRIPTION from " + changelogTable() + " order by id");
      List<Change> changes = new ArrayList<Change>();
      for (Map<String, Object> change : changelog) {
        String id = change.get("ID") == null ? null : change.get("ID").toString();
        String appliedAt = change.get("APPLIED_AT") == null ? null : change.get("APPLIED_AT").toString();
        String description = change.get("DESCRIPTION") == null ? null : change.get("DESCRIPTION").toString();
        changes.add(new Change(new BigDecimal(id), appliedAt, description));
      }
      return changes;
    } catch (SQLException e) {
      throw new MigrationException("Error querying last applied migration.  Cause: " + e, e);
    } finally {
      runner.closeConnection();
    }
  }

  protected String changelogTable() {
    String changelog = environmentProperties().getProperty("changelog");
    if (changelog == null) {
      changelog = "CHANGELOG";
    }
    return changelog;
  }

  protected Change getLastAppliedChange() {
    List<Change> changelog = getChangelog();
    return changelog.get(changelog.size() - 1);
  }

  protected boolean changelogExists() {
    SqlRunner runner = getSqlRunner();
    try {
      runner.selectAll("select ID, APPLIED_AT, DESCRIPTION from " + changelogTable());
      return true;
    } catch (SQLException e) {
      return false;
    } finally {
      runner.closeConnection();
    }
  }

  protected String horizontalLine(String caption, int length) {
    StringBuilder builder = new StringBuilder();
    builder.append("==========");
    if (caption.length() > 0) {
      caption = " " + caption + " ";
      builder.append(caption);
    }
    for (int i = 0; i < length - caption.length() - 10; i++) {
      builder.append("=");
    }
    return builder.toString();
  }

  protected String getNextIDAsString() {
    try {
      // Ensure that two subsequent calls are less likely to return the same value.
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      //ignore
    }
    String timezone = environmentProperties().getProperty("time_zone");
    if (timezone == null) {
      timezone = "GMT+0:00";
    }
    final SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
    final Date now = new Date();
    dateFormat.setTimeZone(TimeZone.getTimeZone(timezone));
    return dateFormat.format(now);
  }

  protected void copyResourceTo(String resource, File toFile) {
    copyResourceTo(resource, toFile, null);
  }

  protected void copyResourceTo(String resource, File toFile, Properties variables) {
    printStream.println("Creating: " + toFile.getName());
    try {
      LineNumberReader reader = new LineNumberReader(Resources.getResourceAsReader(this.getClass().getClassLoader(), resource));
      try {
        PrintWriter writer = new PrintWriter(new FileWriter(toFile));
        try {
          String line;
          while ((line = reader.readLine()) != null) {
            line = PropertyParser.parse(line, variables);
            writer.println(line);
          }
        } finally {
          writer.close();
        }
      } finally {
        reader.close();
      }
    } catch (IOException e) {
      throw new MigrationException("Error copying " + resource + " to " + toFile.getAbsolutePath() + ".  Cause: " + e, e);
    }
  }

  protected void copyExternalResourceTo(String resource, File toFile, Properties variables) {
    printStream.println("Creating: " + toFile.getName());
    try {
      File sourceFile = new File(resource);
      ExternalResources.copyExternalResource(sourceFile, toFile);
    } catch (Exception e) {
      throw new MigrationException("Error copying " + resource + " to " + toFile.getAbsolutePath() + ".  Cause: " + e, e);
    }
  }

  protected SqlRunner getSqlRunner() {
    try {
      lazyInitializeDrivers();

      Properties props = environmentProperties();
      String driver = props.getProperty("driver");
      String url = props.getProperty("url");
      String username = props.getProperty("username");
      String password = props.getProperty("password");

      UnpooledDataSource dataSource = new UnpooledDataSource(driverClassLoader, driver, url, username, password);
      dataSource.setAutoCommit(true);
      return new SqlRunner(dataSource.getConnection());
    } catch (SQLException e) {
      throw new MigrationException("Could not create SqlRunner. Cause: " + e, e);
    }
  }

  protected ScriptRunner getScriptRunner() {
    try {
      lazyInitializeDrivers();

      Properties props = environmentProperties();
      String driver = props.getProperty("driver");
      String url = props.getProperty("url");
      String username = props.getProperty("username");
      String password = props.getProperty("password");
      PrintWriter outWriter = new PrintWriter(printStream);
      UnpooledDataSource dataSource = new UnpooledDataSource(driverClassLoader, driver, url, username, password);
      dataSource.setAutoCommit(false);
      ScriptRunner scriptRunner = new ScriptRunner(dataSource.getConnection());
      scriptRunner.setStopOnError(!force);
      scriptRunner.setLogWriter(outWriter);
      scriptRunner.setErrorLogWriter(outWriter);
      setPropertiesFromFile(scriptRunner, props);
      return scriptRunner;
    } catch (Exception e) {
      throw new MigrationException("Error creating ScriptRunner.  Cause: " + e, e);
    }
  }

  private void setPropertiesFromFile(ScriptRunner scriptRunner, Properties props) {
    String delimiterString = props.getProperty("delimiter");
    scriptRunner.setAutoCommit(Boolean.valueOf(props.getProperty("auto_commit")));
    scriptRunner.setDelimiter(delimiterString == null ? ";" : delimiterString);
    scriptRunner.setFullLineDelimiter(Boolean.valueOf(props.getProperty("full_line_delimiter")));
    scriptRunner.setSendFullScript(Boolean.valueOf(props.getProperty("send_full_script")));
  }

  protected File baseFile(String fileName) {
    return new File(basePath.getAbsolutePath() + File.separator + fileName);
  }

  protected File environmentFile(String fileName) {
    return new File(envPath.getAbsolutePath() + File.separator + fileName);
  }

  protected File scriptFile(String fileName) {
    return new File(scriptPath.getAbsolutePath() + File.separator + fileName);
  }

  protected File driverFile(String fileName) {
    return new File(getCustomDriverPath().getAbsolutePath() + File.separator + fileName);
  }

  protected File environmentFile() {
    return environmentFile(environment + ".properties");
  }

  protected File existingEnvironmentFile() {
    File envFile = environmentFile();
    if (!envFile.exists()) {
      throw new MigrationException("Environment file missing: " + envFile.getAbsolutePath());
    }
    return envFile;
  }

  private void lazyInitializeDrivers() {
    try {
      File localDriverPath = getCustomDriverPath();
      if (driverClassLoader == null && localDriverPath.exists()) {
        List<URL> urlList = new ArrayList<URL>();
        for (File file : localDriverPath.listFiles()) {
          String filename = file.getCanonicalPath();
          if (!filename.startsWith("/")) {
            filename = "/" + filename;
          }
          urlList.add(new URL("jar:file:" + filename + "!/"));
          urlList.add(new URL("file:" + filename));
        }
        URL[] urls = urlList.toArray(new URL[urlList.size()]);
        driverClassLoader = new URLClassLoader(urls);
      }
    } catch (IOException e) {
      throw new MigrationException("Error loading JDBC drivers. Cause: " + e, e);
    }
  }

  protected Properties environmentProperties() {
    FileInputStream fileInputStream = null;
    try {
      File file = existingEnvironmentFile();
      Properties props = new Properties();
      fileInputStream = new FileInputStream(file);
      props.load(fileInputStream);
      return props;
    } catch (IOException e) {
      throw new MigrationException("Error loading environment properties.  Cause: " + e, e);
    } finally {
      if (fileInputStream != null) {
        try {
          fileInputStream.close();
        } catch (IOException e) {
          //Nothing to do here
        }
      }
    }
  }

  protected void insertChangelog(Change change) {
    SqlRunner runner = getSqlRunner();
    change.setAppliedTimestamp(generateAppliedTimeStampAsString());
    try {
      runner.insert("insert into " + changelogTable() + " (ID, APPLIED_AT, DESCRIPTION) values (?,?,?)", change.getId(), change.getAppliedTimestamp(), change.getDescription());
    } catch (SQLException e) {
      throw new MigrationException("Error querying last applied migration.  Cause: " + e, e);
    } finally {
      runner.closeConnection();
    }
  }

  protected String generateAppliedTimeStampAsString() {
    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.sql.Date(System.currentTimeMillis()));
  }

  protected int getStepCountParameter(int defaultSteps, String... params) {
    final String stringParam = params.length > 0 ? params[0] : null;
    if (stringParam == null || "".equals(stringParam)) {
      return defaultSteps;
    } else {
      try {
        return Integer.parseInt(stringParam);
      } catch (NumberFormatException e) {
        throw new MigrationException("Invalid parameter passed to command: " + params[0]);
      }
    }
  }

  private File getCustomDriverPath() {
    String customDriverPath = environmentProperties().getProperty("driver_path");
    if (customDriverPath != null && customDriverPath.length() > 0) {
      return new File(customDriverPath);
    } else {
      return driverPath;
    }
  }

  private File subdirectory(File base, String sub) {
    return new File(base.getAbsoluteFile() + File.separator + sub);
  }

  private Change parseChangeFromFilename(String filename) {
    try {
      Change change = new Change();
      String[] parts = filename.split("\\.")[0].split("_");
      change.setId(new BigDecimal(parts[0]));
      StringBuilder builder = new StringBuilder();
      for (int i = 1; i < parts.length; i++) {
        if (i > 1) {
          builder.append(" ");
        }
        builder.append(parts[i]);
      }
      change.setDescription(builder.toString());
      change.setFilename(filename);
      return change;
    } catch (Exception e) {
      throw new MigrationException("Error parsing change from file.  Cause: " + e, e);
    }
  }

  protected Reader scriptFileReader(File scriptFile) throws FileNotFoundException, UnsupportedEncodingException {
    InputStream inputStream = new FileInputStream(scriptFile);
    String charset = environmentProperties().getProperty("script_char_set");
    if (charset == null || charset.length() == 0) {
      return new InputStreamReader(inputStream);
    } else {
      return new InputStreamReader(inputStream, charset);
    }
  }
}
