package com.github.cosysoft.device.android.impl;

import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.Client;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.RawImage;
import com.android.ddmlib.TimeoutException;
import com.android.ddmlib.logcat.LogCatListener;
import com.android.ddmlib.logcat.LogCatReceiverTask;
import com.github.cosysoft.device.android.AndroidApp;
import com.github.cosysoft.device.android.AndroidDevice;
import com.github.cosysoft.device.android.AndroidDeviceBrand;
import com.github.cosysoft.device.android.DeviceTargetPlatform;
import com.github.cosysoft.device.exception.AndroidDeviceException;
import com.github.cosysoft.device.exception.DeviceUnlockException;
import com.github.cosysoft.device.exception.NestedException;
import com.github.cosysoft.device.image.ImageUtils;
import com.github.cosysoft.device.model.ClientDataInfo;
import com.github.cosysoft.device.model.DeviceInfo;
import com.github.cosysoft.device.shell.AndroidSdk;
import com.github.cosysoft.device.shell.AndroidSdkException;
import com.github.cosysoft.device.shell.ShellCommand;
import com.github.cosysoft.device.shell.ShellCommandException;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.profiler.Profiler;

public abstract class AbstractDevice implements AndroidDevice {
  private static final Logger log = LoggerFactory
      .getLogger(AbstractDevice.class);
  protected String serial = null;
  protected IDevice device;
  private static final Integer COMMAND_TIMEOUT = 20000;
  private AndroidDeviceBrand brand = null;

  /**
   * Constructor meant to be used with Android Emulators because a reference to the {@link IDevice}
   * will become available if the emulator will be started. Please make sure that #setIDevice is
   * called on the emulator.
   */
  public AbstractDevice(String serial) {
    this.serial = serial;
  }

  /**
   * Constructor mean to be used with Android Hardware devices because a reference to the {@link
   * IDevice} will be available immediately after they are connected.
   */
  public AbstractDevice(IDevice device) {
    this.device = device;
    this.serial = device.getSerialNumber();
  }

  @Override
  public IDevice getDevice() {
    return device;
  }

  protected AbstractDevice() {
  }

  protected boolean isSerialConfigured() {
    return serial != null && !serial.isEmpty();
  }

  @Override
  public boolean isDeviceReady() {
    CommandLine command = adbCommand("shell", "getprop init.svc.bootanim");
    String bootAnimDisplayed = null;
    try {
      bootAnimDisplayed = ShellCommand.exec(command);
    } catch (ShellCommandException e) {
      log.info("Could not get property init.svc.bootanim", e);
    }
    return bootAnimDisplayed != null
        && bootAnimDisplayed.contains("stopped");
  }

  /**
   * ugly implementation
   */
  @Override
  public boolean isScreenOn() {
    CommandLine command = adbCommand("shell", "dumpsys power");
    try {
      String powerState = ShellCommand.exec(command).toLowerCase();
      if (powerState.indexOf("mscreenon=true") > -1
          || powerState.indexOf("mpowerstate=0") == -1) {
        return true;
      }
    } catch (ShellCommandException e) {
      log.info("Could not get property init.svc.bootanim", e);
    }
    return false;
  }

  ;

  @Override
  public String currentActivity() {

    CommandLine command = adbCommand("shell", "dumpsys activity top");

    String out = executeCommandQuietly(command);
    if (out.indexOf("ACTIVITY") > -1) {
      try {
        List<String> lines = IOUtils.readLines(new StringReader(out));
        for (String line : lines) {
          if (line.contains("ACTIVITY")) {
            String[] tokens = StringUtils.split(line, " ");
            return tokens[1];
          }
        }
      } catch (IOException e) {
        log.debug("currentActivity {}", out);
      }
    }
    throw new NestedException("Can't get currentActivity");
  }

  @Override
  public void unlock() {
    String unlockPackage = "ctrip.cap.mi";
    String activity = ".CapMI";

    if (this.isInstalled(unlockPackage)) {
      innerUnlock(unlockPackage, activity);
      return;
    }

    unlockPackage = "io.appium.unlock";
    activity = ".Unlock";

    if (!this.isInstalled(unlockPackage)) {
      throw new DeviceUnlockException(
          "UnLock app not installed on your device,Please install it manully.in windows You can try to execute"
              + System.lineSeparator()
              + "adb install "
              + System.getProperty("user.home")
              + "\\AppData\\Roaming\\npm\\node_modules\\appium\\build\\unlock_apk\\unlock_apk-debug.apk");
    }
    innerUnlock(unlockPackage, activity);
  }

  private void innerUnlock(String unlockPackage, String activity) {
    CommandLine command = adbCommand("shell", "am", "start", "-a",
        "android.intent.action.MAIN", "-n", unlockPackage + "/"
            + activity);

    String out = executeCommandQuietly(command);
    try {
      // give it a second to recover from the activity start
      Thread.sleep(1000);
    } catch (InterruptedException ie) {
      log.warn("unlock", ie);
    }
    log.debug("unlock {}", out);
  }

  ;

  @Override
  public boolean isInstalled(String appBasePackage)
      throws AndroidSdkException {
    CommandLine command = adbCommand("shell", "pm", "list", "packages");

    command.addArgument(appBasePackage, false);
    String result = null;
    try {
      result = ShellCommand.exec(command);
    } catch (ShellCommandException e) {
    }

    return result != null && result.contains("package:" + appBasePackage);
  }

  @Override
  public boolean isInstalled(AndroidApp app) {
    return isInstalled(app.getBasePackage());
  }

  @Override
  public void install(AndroidApp app) {
    // Reinstall if already installed, Install otherwise
    CommandLine command = adbCommand("install", "-r", app.getAbsolutePath());

    String out = executeCommandQuietly(command, COMMAND_TIMEOUT * 6);
    try {
      // give it a second to recover from the install
      Thread.sleep(1000);
    } catch (InterruptedException ie) {
      throw new RuntimeException(ie);
    }
    if (!out.contains("Success")) {
      throw new AndroidSdkException("APK installation failed. Output:\n"
          + out);
    }
  }

  public boolean start(AndroidApp app) {
    if (!isInstalled(app)) {
      install(app);
    }

    String mainActivity = app.getMainActivity().replace(
        app.getBasePackage(), "");
    CommandLine command = adbCommand("shell", "am", "start", "-a",
        "android.intent.action.MAIN", "-n", app.getBasePackage() + "/"
            + mainActivity);

    String out = executeCommandQuietly(command);
    try {
      // give it a second to recover from the activity start
      Thread.sleep(1000);
    } catch (InterruptedException ie) {
      throw new RuntimeException(ie);
    }
    return out.contains("Starting: Intent");
  }

  protected String executeCommandQuietly(CommandLine command) {
    return executeCommandQuietly(command, COMMAND_TIMEOUT);
  }

  protected String executeCommandQuietly(CommandLine command, long timeout) {
    try {
      return ShellCommand.exec(command, timeout);
    } catch (ShellCommandException e) {
      String logMessage = String.format("Could not execute command: %s",
          command);
      log.warn(logMessage, e);
      return "";
    }
  }

  @Override
  public void uninstall(String appBasePackage) {
    CommandLine command = adbCommand("uninstall", appBasePackage);

    executeCommandQuietly(command);
    try {
      // give it a second to recover from the uninstall
      Thread.sleep(1000);
    } catch (InterruptedException ie) {
      throw new RuntimeException(ie);
    }
  }

  @Override
  public void uninstall(AndroidApp app) {
    uninstall(app.getBasePackage());
  }

  @Override
  public void clearUserData(String appBasePackage) {
    CommandLine command = adbCommand("shell", "pm", "clear", appBasePackage);
    executeCommandQuietly(command);
  }

  @Override
  public void clearUserData(AndroidApp app) {
    clearUserData(app.getBasePackage());
  }

  @Override
  public void kill(String appBasePackage) {
    try {
      CommandLine command = adbCommand("shell", "am", "force-stop",
          appBasePackage);
      executeCommandQuietly(command);
    } finally {
    }
  }

  /**
   * get current android page's dump file
   */
  public String getDump() {
    pushAutomator2Device();
    runtest();
    String path = pullDump2PC();
    String xml = "";
    try {
      FileInputStream fileInputStream = new FileInputStream(path);
      @SuppressWarnings("resource")
      BufferedReader in = new BufferedReader(
          new InputStreamReader(fileInputStream));
      StringBuffer buffer = new StringBuffer();
      String line = "";
      while ((line = in.readLine()) != null) {
        buffer.append(line);
      }
      xml = buffer.toString();
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
    return xml;
  }

  /**
   * try to click GPS Popup window
   */
  public boolean handlePopBox(String deviceBrand) {
    pushHandleGps2Device();
    CommandLine exeCommand = null;
    if (deviceBrand.contains("HTC")) {

      exeCommand = adbCommand("shell", "uiautomator", "runtest",
          "/data/local/tmp/handlePopBox.jar", "-c", "com.test.device.gps.HTCGPSTest");
    } else if (deviceBrand.contains("Meizu")) {

      exeCommand = adbCommand("shell", "uiautomator", "runtest",
          "/data/local/tmp/handlePopBox.jar", "-c", "com.test.device.gps.MeizuGPSTest");
    }

    String output = executeCommandQuietly(exeCommand);
    log.debug("run test {}", output);

    try {
      // give it a second to recover from the activity start
      Thread.sleep(1000);
    } catch (InterruptedException ie) {
      throw new RuntimeException(ie);
    }
    return output.contains("OK");
  }

  /**
   * Push handlePopBox.jar to android tmp folder
   *
   * @return push device successful or not
   */
  private boolean pushHandleGps2Device() {

    InputStream io = AbstractDevice.class.getResourceAsStream("handlePopBox.jar");
    File dest = new File(FileUtils.getTempDirectory(), "handlePopBox.jar");

    try {
      FileUtils.copyInputStreamToFile(io, dest);
    } catch (IOException e) {
      e.printStackTrace();
    }

    CommandLine pushcommand = adbCommand("push ", dest.getAbsolutePath(), "/data/local/tmp/");
    String outputPush = executeCommandQuietly(pushcommand);
    log.debug("Push automator.jar to device {}", outputPush);

    try {
      // give it a second to recover from the activity start
      Thread.sleep(1000);
    } catch (InterruptedException ie) {
      throw new RuntimeException(ie);
    }
    return outputPush.contains("KB/s");
  }

  /**
   * Push automator.jar to android tmp folder
   *
   * @return push device successful or not
   */
  public boolean pushAutomator2Device() {
    InputStream io = AbstractDevice.class.getResourceAsStream("automator.jar");
    File dest = new File(FileUtils.getTempDirectory(), "automator.jar");

    try {
      FileUtils.copyInputStreamToFile(io, dest);
    } catch (IOException e) {
      e.printStackTrace();
    }

    CommandLine pushcommand = adbCommand("push ", dest.getAbsolutePath(), "/data/local/tmp/");
    String outputPush = executeCommandQuietly(pushcommand);
    log.debug("Push automator.jar to device {}", outputPush);

    try {
      // give it a second to recover from the activity start
      Thread.sleep(1000);
    } catch (InterruptedException ie) {
      throw new RuntimeException(ie);
    }
    return outputPush.contains("KB/s");
  }

  /**
   * clean file dump.xml, qian.xml, uidump.xml in tmp folder
   */
  public void cleanTemp() {
    CommandLine dumpcommand = adbCommand("shell", "rm", "-r",
        "/data/local/tmp/local/tmp/dump.xml");
    executeCommandQuietly(dumpcommand);
    try {
      // give it a second to recover from the activity start
      Thread.sleep(1000);
    } catch (InterruptedException ie) {
      throw new RuntimeException(ie);
    }

    CommandLine qiancommand = adbCommand("shell", "rm", "-r",
        "/data/local/tmp/local/tmp/qian.xml");
    String output = executeCommandQuietly(qiancommand);
    log.debug("Delete file qian.xml: {}", output);
    try {
      // give it a second to recover from the activity start
      Thread.sleep(1000);
    } catch (InterruptedException ie) {
      throw new RuntimeException(ie);
    }

    CommandLine command = adbCommand("shell", "rm", "-r",
        "/data/local/tmp/uidump.xml");
    executeCommandQuietly(command);
    try {
      // give it a second to recover from the activity start
      Thread.sleep(1000);
    } catch (InterruptedException ie) {
      throw new RuntimeException(ie);
    }
  }

  /**
   * run command to get dump file
   */
  public boolean runtest() {
    cleanTemp();
    CommandLine command = adbCommand("shell", "uiautomator", "runtest",
        "/data/local/tmp/automator.jar", "-c", "com.uia.example.my.test");
    String output = executeCommandQuietly(command);
    log.debug("run test {}", output);

    try {
      // give it a second to recover from the activity start
      Thread.sleep(1000);
    } catch (InterruptedException ie) {
      throw new RuntimeException(ie);
    }
    return output.contains("OK");
  }

  /**
   * pull dump file from android device to pc
   *
   * @return pc dump file path
   */
  public String pullDump2PC() {

    String serial = device.getSerialNumber();
    File dest = new File(FileUtils.getTempDirectory(), serial
        + ".xml");
    String path = dest.getPath();
    log.debug("pull dump file to pc's path {}", path);

    CommandLine commandpull = adbCommand("pull", "/data/local/tmp/local/tmp/qian.xml", path);
    String out = executeCommandQuietly(commandpull);
    log.debug("pull dump file to pc's result {}", out);
    return path;
  }

  @Override
  public void kill(AndroidApp aut) {
    kill(aut.getBasePackage());
  }

  public void removeForwardPort(int port) {
    CommandLine command = adbCommand("forward", "--remove", "tcp:" + port);
    try {
      ShellCommand.exec(command, COMMAND_TIMEOUT);
    } catch (ShellCommandException e) {
      log.warn("Could not free Selendroid port", e);
    }
  }

  public void forwardPort(int local, int remote) {
    CommandLine command = adbCommand("forward", "tcp:" + local, "tcp:"
        + remote);
    try {
      ShellCommand.exec(command, COMMAND_TIMEOUT);
    } catch (ShellCommandException forwardException) {
      String debugForwardList;
      try {
        debugForwardList = ShellCommand.exec(
            adbCommand("forward", "--list"), COMMAND_TIMEOUT);
      } catch (ShellCommandException listException) {
        debugForwardList = "Could not get list of forwarded ports.";
      }

      throw new RuntimeException("Could not forward port: " + command
          + "\nList of forwarded ports:\n" + debugForwardList,
          forwardException);
    }
  }

  protected String getProp(String key) {
    CommandLine command = adbCommand("shell", "getprop", key);
    String prop = executeCommandQuietly(command);

    return prop == null ? "" : prop.replace("\r", "").replace("\n", "");
  }

  protected static String extractValue(String regex, String output) {
    Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE);
    Matcher matcher = pattern.matcher(output);
    if (matcher.find()) {
      return matcher.group(1);
    }

    return "";
  }

  public String runAdbCommand(String parameter) {
    if (parameter == null || parameter.isEmpty()) {
      return null;
    }
    CommandLine command = adbCommand();

    String[] params = parameter.split(" ");
    for (String param : params) {
      command.addArgument(param, false);
    }

    String commandOutput = executeCommandQuietly(command);
    return commandOutput.trim();
  }

  public BufferedImage takeScreenshot() {
    if (device == null) {
      throw new AndroidDeviceException(
          "Device not accessible via ddmlib.");
    }
    RawImage rawImage;
    try {
      Profiler profiler = new Profiler("native screen");
      profiler.start("start");
      rawImage = device.getScreenshot();

      profiler.stop();
      profiler.print();
    } catch (IOException ioe) {
      throw new AndroidDeviceException("Unable to get frame buffer: "
          + ioe.getMessage());
    } catch (TimeoutException e) {
      throw new AndroidDeviceException(e.getMessage());
    } catch (AdbCommandRejectedException e) {
      throw new AndroidDeviceException(e.getMessage());
    }

    BufferedImage image = ImageUtils.convertImage(rawImage);

    return image;
  }

  @Override
  public void takeScreenshot(String fileUrl) {
    BufferedImage image = takeScreenshot();
    ImageUtils.writeToFile(image, fileUrl);
  }

  /**
   * Use adb to send a keyevent to the device. <p> Full list of keys available here:
   * http://developer.android.com/reference/android/view/KeyEvent.html
   *
   * @param value - Key to be sent to 'adb shell input keyevent'
   */
  public void inputKeyevent(int value) {
    executeCommandQuietly(adbCommand("shell", "input", "keyevent", ""
        + value));
    // need to wait a beat for the UI to respond
    try {
      Thread.sleep(500);
    } catch (InterruptedException e) {
      log.warn("", e);
    }
  }

  public void invokeActivity(String activity) {
    executeCommandQuietly(adbCommand("shell", "am", "start", "-a", activity));
    // need to wait a beat for the UI to respond
    try {
      Thread.sleep(500);
    } catch (InterruptedException e) {
      log.warn("", e);
    }
  }

  public void restartADB() {
    executeCommandQuietly(adbCommand("kill-server"));
    try {
      Thread.sleep(500);
    } catch (InterruptedException e) {
      log.warn("", e);
    }
    // make sure it's backup again
    executeCommandQuietly(adbCommand("devices"));
  }

  private CommandLine adbCommand() {
    CommandLine command = new CommandLine(AndroidSdk.adb());
    if (isSerialConfigured()) {
      command.addArgument("-s", false);
      command.addArgument(serial, false);
    }
    return command;
  }

  private CommandLine adbCommand(String... args) {
    CommandLine command = adbCommand();
    for (String arg : args) {
      command.addArgument(arg, false);
    }
    return command;
  }

  public String getExternalStoragePath() {
    return runAdbCommand("shell echo $EXTERNAL_STORAGE");
  }

  /**
   * Get crash log from AUT
   *
   * @return empty string if there is no crash log on the device, otherwise returns the stack trace
   * caused by the crash of the AUT
   */
  public String getCrashLog() {
    String crashLogFileName = null;
    File crashLogFile = new File(getExternalStoragePath(), crashLogFileName);

    // the "test" utility doesn't exist on all devices so we'll check the
    // output of ls.
    CommandLine directoryListCommand = adbCommand("shell", "ls",
        crashLogFile.getParentFile().getAbsolutePath());
    String directoryList = executeCommandQuietly(directoryListCommand);
    if (directoryList.contains(crashLogFileName)) {
      return executeCommandQuietly(adbCommand("shell", "cat",
          crashLogFile.getAbsolutePath()));
    }

    return "";
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass() || device == null) {
      return false;
    }

    AbstractDevice that = (AbstractDevice) o;

    return device.equals(that.device);
  }

  @Override
  public void tap(int x, int y) {
    CommandLine command = adbCommand("shell", "input", "tap",
        String.valueOf(x), String.valueOf(y));

    executeCommandQuietly(command, COMMAND_TIMEOUT * 6);
    try {
      Thread.sleep(1000);
    } catch (InterruptedException ie) {
      throw new RuntimeException(ie);
    }
  }

  ;

  @Override
  public void swipe(int x1, int y1, int x2, int y2) {
    CommandLine command = adbCommand("shell", "input", "swipe",
        String.valueOf(x1), String.valueOf(y1), String.valueOf(x2),
        String.valueOf(y2));

    executeCommandQuietly(command, COMMAND_TIMEOUT * 6);
    try {
      Thread.sleep(1000);
    } catch (InterruptedException ie) {
      throw new RuntimeException(ie);
    }
  }

  @Override
  public boolean isWifiOff() {
    CommandLine command = adbCommand("shell", "settings", "get", "global",
        "wifi_on");
    String commandOutput = executeCommandQuietly(command);
    String result = commandOutput.trim();

    return "1".equals(result) ? false : true;
  }

  @Override
  public String getName() {
    return device.getName();
  }

  @Override
  public AndroidDeviceBrand getBrand() {
    if (brand != null) {
      return brand;
    }
    String name = getName();
    String manufacture = StringUtils.substringBefore(name, "-");
    String model = StringUtils.substringBetween(name, "-", "-");
    brand = AndroidDeviceBrand.from(manufacture, model);
    return brand;
  }

  ;

  @Override
  public DeviceInfo getDeviceInfo() {

    DeviceInfo deviceInfo = new DeviceInfo();
    deviceInfo.setName(this.getName());
    deviceInfo.setSerial(this.getSerialNumber());
    deviceInfo.setDensity(device.getDensity());
    DeviceTargetPlatform tf = this.getTargetPlatform();
    deviceInfo.setOsName(tf.formatedName());

    try {
      deviceInfo.setKernel(device.getSystemProperty("ro.build.kernel.id")
          .get());
      deviceInfo.setBattery(device.getBattery().get());
    } catch (InterruptedException | ExecutionException e) {
      log.warn("getDeviceInfo", e);
    }

    return deviceInfo;
  }

  ;

  private LogCatReceiverTask logCatReceiverTask;
  private final Set<LogCatListener> logCatListeners = new HashSet<>();
  ;
  private boolean logCatRunning = false;

  @Override
  public synchronized void addLogCatListener(LogCatListener logCatListener) {
    if (logCatReceiverTask == null) {
      logCatReceiverTask = new LogCatReceiverTask(device);
    }
    logCatReceiverTask.addLogCatListener(logCatListener);
    logCatListeners.add(logCatListener);
    if (!logCatRunning) {
      logCatReceiverTask.run();
      logCatRunning = true;
    }
  }

  @Override
  public synchronized void removeLogCatListener(LogCatListener logCatListener) {
    if (logCatReceiverTask == null) {
      logCatReceiverTask = new LogCatReceiverTask(device);
    }
    logCatReceiverTask.removeLogCatListener(logCatListener);
    logCatListeners.remove(logCatListener);
    if (logCatListeners.size() < 1) {
      logCatReceiverTask.stop();
      logCatRunning = false;
    }
  }

  @Override
  public List<ClientDataInfo> getClientDatasInfo() {
    List<ClientDataInfo> dataInfos = new ArrayList<ClientDataInfo>();
    Client clients[] = this.device.getClients();
    for (int i = 0; i < clients.length; i++) {
      dataInfos.add(new ClientDataInfo(clients[i]));
    }
    return dataInfos;
  }

  @Override
  public Client[] getAllClient() {
    return this.device.getClients();
  }

  @Override
  public Client getClientByAppName(String appName) {
    return this.device.getClient(appName);
  }

  @Override
  public int hashCode() {
    return device.hashCode();
  }
}


/*
Copyright (c) 2005, Pete Bevin.
<http://markdownj.petebevin.com>

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.

* Neither the name "Markdown" nor the names of its contributors may
  be used to endorse or promote products derived from this software
  without specific prior written permission.

This software is provided by the copyright holders and contributors "as
is" and any express or implied warranties, including, but not limited
to, the implied warranties of merchantability and fitness for a
particular purpose are disclaimed. In no event shall the copyright owner
or contributors be liable for any direct, indirect, incidental, special,
exemplary, or consequential damages (including, but not limited to,
procurement of substitute goods or services; loss of use, data, or
profits; or business interruption) however caused and on any theory of
liability, whether in contract, strict liability, or tort (including
negligence or otherwise) arising in any way out of the use of this
software, even if advised of the possibility of such damage.

*/

package org.markdownj;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Mutable String with common operations used in Markdown processing.
 */
public class TextEditor {
    private StringBuilder text;

    /**
     * Create a new TextEditor based on the contents of a String or
     * StringBuffer.
     *
     * @param text
     */
    public TextEditor(CharSequence text) {
        this.text = new StringBuilder(text);
    }

    /**
     * Give up the contents of the TextEditor.
     * @return
     */
    @Override
    public String toString() {
        return text.toString();
    }

    /**
     * Replace all occurrences of the regular expression with the replacement.  The replacement string
     * can contain $1, $2 etc. referring to matched groups in the regular expression.
     *
     * @param regex
     * @param replacement
     * @return
     */
    public TextEditor replaceAll(String regex, String replacement) {
        if (text.length() > 0) {
            final String r = replacement;
            Pattern p = Pattern.compile(regex, Pattern.MULTILINE);
            Matcher m = p.matcher(text);
            StringBuffer sb = new StringBuffer();
            while (m.find()) {
                m.appendReplacement(sb, r);
            }
            m.appendTail(sb);
            text = new StringBuilder(sb.toString());
        }
        return this;
    }

    /**
     * Same as replaceAll(String, String), but does not interpret
     * $1, $2 etc. in the replacement string.
     * @param regex
     * @param replacement
     * @return
     */
    public TextEditor replaceAllLiteral(String regex, final String replacement) {
        return replaceAll(Pattern.compile(regex, Pattern.MULTILINE), new Replacement() {
            public String replacement(Matcher m) {
                return replacement;
            }
        });
    }

    /**
     * Replace all occurrences of the Pattern.  The Replacement object's replace() method is
     * called on each match, and it provides a replacement, which is placed literally
     * (i.e., without interpreting $1, $2 etc.)
     *
     * @param pattern
     * @param replacement
     * @return
     */
    public TextEditor replaceAll(Pattern pattern, Replacement replacement) {
        Matcher m = pattern.matcher(text);
        int lastIndex = 0;
        StringBuilder sb = new StringBuilder();
        while (m.find()) {
            sb.append(text.subSequence(lastIndex, m.start()));
            sb.append(replacement.replacement(m));
            lastIndex = m.end();
        }
        sb.append(text.subSequence(lastIndex, text.length()));
        text = sb;
        return this;
    }

    /**
     * Remove all occurrences of the given regex pattern, replacing them
     * with the empty string.
     *
     * @param pattern Regular expression
     * @return
     * @see java.util.regex.Pattern
     */
    public TextEditor deleteAll(String pattern) {
        return replaceAll(pattern, "");
    }

    /**
     * Convert tabs to spaces given the default tab width of 4 spaces.
     * @return
     */
    public TextEditor detabify() {
        return detabify(4);
    }

    /**
     * Convert tabs to spaces.
     *
     * @param tabWidth  Number of spaces per tab.
     * @return
     */
    public TextEditor detabify(final int tabWidth) {
        replaceAll(Pattern.compile("(.*?)\\t"), new Replacement() {
            public String replacement(Matcher m) {
                String lineSoFar = m.group(1);
                int width = lineSoFar.length();
                StringBuilder replacement = new StringBuilder(lineSoFar);
                do {
                    replacement.append(' ');
                    ++width;
                } while (width % tabWidth != 0);
                return replacement.toString();
            }
        });
        return this;
    }

    /**
     * Remove a number of spaces at the start of each line.
     * @param spaces
     * @return
     */
    public TextEditor outdent(int spaces) {
        return deleteAll("^(\\t|[ ]{1," + spaces + "})");
    }

    /**
     * Remove one tab width (4 spaces) from the start of each line.
     * @return
     */
    public TextEditor outdent() {
        return outdent(4);
    }

    /**
     * Remove leading and trailing space from the start and end of the buffer.  Intermediate
     * lines are not affected.
     * @return
     */
    public TextEditor trim() {
        text = new StringBuilder(text.toString().trim());
        return this;
    }

    /**
     * Introduce a number of spaces at the start of each line.
     * @param spaces
     * @return
     */
    public TextEditor indent(int spaces) {
        StringBuilder sb = new StringBuilder(spaces);
        for (int i = 0; i < spaces; i++) {
            sb.append(' ');
        }
        return replaceAll("^", sb.toString());
    }

    /**
     * Add a string to the end of the buffer.
     * @param s
     */
    public void append(CharSequence s) {
        text.append(s);
    }

    /**
     * Parse HTML tags, returning a Collection of HTMLToken objects.
     * @return
     */
    public Collection<HTMLToken> tokenizeHTML() {
        List<HTMLToken> tokens = new ArrayList<HTMLToken>();
        String nestedTags = nestedTagsRegex(6);

        Pattern p = Pattern.compile("" +
                "(?s:<!(--.*?--\\s*)+>)" +
                "|" +
                "(?s:<\\?.*?\\?>)" +
                "|" +
                nestedTags +
                "", Pattern.CASE_INSENSITIVE);

        Matcher m = p.matcher(text);
        int lastPos = 0;
        while (m.find()) {
            if (lastPos < m.start()) {
                tokens.add(HTMLToken.text(text.substring(lastPos, m.start())));
            }
            tokens.add(HTMLToken.tag(text.substring(m.start(), m.end())));
            lastPos = m.end();
        }
        if (lastPos < text.length()) {
            tokens.add(HTMLToken.text(text.substring(lastPos, text.length())));
        }

        return tokens;
    }

    /**
     * Regex to match a tag, possibly with nested tags such as <a href="<MTFoo>">.
     *
     * @param depth - How many levels of tags-within-tags to allow.  The example <a href="<MTFoo>"> has depth 2.
     */
    private String nestedTagsRegex(int depth) {
        if (depth == 0) {
            return "";
        } else {
            return "(?:<[a-z/!$](?:[^<>]|" + nestedTagsRegex(depth - 1) + ")*>)";
        }
    }

    /**
     * Add a string to the start of the first line of the buffer.
     * @param s
     */
    public void prepend(CharSequence s) {
        text.insert(0, s);
    }

    /**
     * Find out whether the buffer is empty.
     * @return
     */
    public boolean isEmpty() {
        return text.length() == 0;
    }
}


/*
Copyright (c) 2005, Martian Software
Authors: Pete Bevin, John Mutchek
http://www.martiansoftware.com/markdownj

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.

* Neither the name "Markdown" nor the names of its contributors may
  be used to endorse or promote products derived from this software
  without specific prior written permission.

This software is provided by the copyright holders and contributors "as
is" and any express or implied warranties, including, but not limited
to, the implied warranties of merchantability and fitness for a
particular purpose are disclaimed. In no event shall the copyright owner
or contributors be liable for any direct, indirect, incidental, special,
exemplary, or consequential damages (including, but not limited to,
procurement of substitute goods or services; loss of use, data, or
profits; or business interruption) however caused and on any theory of
liability, whether in contract, strict liability, or tort (including
negligence or otherwise) arising in any way out of the use of this
software, even if advised of the possibility of such damage.

*/

package org.markdownj;

import java.util.Collection;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Convert Markdown text into HTML, as per http://daringfireball.net/projects/markdown/ .
 * Usage:
 *

 *     MarkdownProcessor markdown = new MarkdownProcessor();
 *     String html = markdown.markdown("*italic*   **bold**\n_italic_   __bold__");
 *

 */
public class MarkdownProcessor {
    private Random rnd = new Random();
    private Map<String, LinkDefinition> linkDefinitions = new TreeMap<String, LinkDefinition>();
    private static final CharacterProtector HTML_PROTECTOR = new CharacterProtector();
    private static final CharacterProtector CHAR_PROTECTOR = new CharacterProtector();
    private int listLevel;
    private int tabWidth = 4;

    /**
     * Creates a new Markdown processor.
     */
    public MarkdownProcessor() {
        listLevel = 0;
    }

    /**
     * Perform the conversion from Markdown to HTML.
     *
     * @param txt - input in markdown format
     * @return HTML block corresponding to txt passed in.
     */
    public String markdown(String txt) {
        if (txt == null) {
            txt = "";
        }
        TextEditor text = new TextEditor(txt);

        // Standardize line endings:
        text.replaceAll("\\r\\n", "\n"); 	// DOS to Unix
        text.replaceAll("\\r", "\n");    	// Mac to Unix
        text.replaceAll("^[ \\t]+$", "");

        // Make sure $text ends with a couple of newlines:
        text.append("\n\n");

        text.detabify();
        text.deleteAll("^[ ]+$");
        hashHTMLBlocks(text);
        stripLinkDefinitions(text);
        text = runBlockGamut(text);
        unEscapeSpecialChars(text);

        text.append("\n");
        return text.toString();
    }

    private TextEditor encodeBackslashEscapes(TextEditor text) {
        char[] normalChars = "`_>!".toCharArray();
        char[] escapedChars = "*{}[]()#+-.".toCharArray();

        // Two backslashes in a row
        text.replaceAllLiteral("\\\\\\\\", CHAR_PROTECTOR.encode("\\"));

        // Normal characters don't require a backslash in the regular expression
        encodeEscapes(text, normalChars, "\\\\");
        encodeEscapes(text, escapedChars, "\\\\\\");

        return text;
    }

    private TextEditor encodeEscapes(TextEditor text, char[] chars, String slashes) {
        for (char ch : chars) {
            String regex = slashes + ch;
            text.replaceAllLiteral(regex, CHAR_PROTECTOR.encode(String.valueOf(ch)));
        }
        return text;
    }

    private void stripLinkDefinitions(TextEditor text) {
        Pattern p = Pattern.compile("^[ ]{0,3}\\[(.+)\\]:" + // ID = $1
                "[ \\t]*\\n?[ \\t]*" + // Space
                "<?(\\S+?)>?" + // URL = $2
                "[ \\t]*\\n?[ \\t]*" + // Space
                "(?:[\"(](.+?)[\")][ \\t]*)?" + // Optional title = $3
                "(?:\\n+|\\Z)",
                Pattern.MULTILINE);

        text.replaceAll(p, new Replacement() {
            public String replacement(Matcher m) {
                String id = m.group(1).toLowerCase();
                String url = encodeAmpsAndAngles(new TextEditor(m.group(2))).toString();
                String title = m.group(3);

                if (title == null) {
                    title = "";
                }
                title = replaceAll(title, "\"", """);
                linkDefinitions.put(id, new LinkDefinition(url, title));
                return "";
            }
        });
    }

    public TextEditor runBlockGamut(TextEditor text) {
        doHeaders(text);
        doHorizontalRules(text);
        doLists(text);
        doCodeBlocks(text);
        doBlockQuotes(text);

        hashHTMLBlocks(text);

        return formParagraphs(text);
    }

    private void doHorizontalRules(TextEditor text) {
        String[] hrDelimiters = {"\\*", "-", "_"};
        for (String hrDelimiter : hrDelimiters) {
            text.replaceAll("^[ ]{0,2}([ ]?" + hrDelimiter + "[ ]?){3,}[ ]*$", "
");
        }
    }

    private void hashHTMLBlocks(TextEditor text) {
        // Hashify HTML blocks:
        // We only want to do this for block-level HTML tags, such as headers,
        // lists, and tables. That's because we still want to wrap
s around
        // "paragraphs" that are wrapped in non-block-level tags, such as anchors,
        // phrase emphasis, and spans. The list of tags we're looking for is
        // hard-coded:

        String[] tagsA = {
            "p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "pre", "table",
            "dl", "ol", "ul", "script", "noscript", "form", "fieldset", "iframe", "math"
        };
        String[] tagsB = {"ins", "del"};

        String alternationA = join("|", tagsA);
        String alternationB = alternationA + "|" + join("|", tagsB);

        int less_than_tab = tabWidth - 1;

        // First, look for nested blocks, e.g.:
        //

        //

        //       tags for inner block must be indented.
        //

        //

        //
        // The outermost tags must start at the left margin for this to match, and
        // the inner nested divs must be indented.
        // We need to do this before the next, more liberal match, because the next
        // match will start at the first `<div>` and stop at the first `</div>`.
        Pattern p1 = Pattern.compile("(" +
                "^<(" + alternationA + ")" +
                "\\b" +
                "(.*\\n)*?" +
                "</\\2>" +
                "[ ]*" +
                "(?=\\n+|\\Z))", Pattern.MULTILINE |  Pattern.CASE_INSENSITIVE);

        Replacement protectHTML = new Replacement() {
            public String replacement(Matcher m) {
                String literal = m.group();
                return "\n\n" + HTML_PROTECTOR.encode(literal) + "\n\n";
            }
        };
        text.replaceAll(p1, protectHTML);

        // Now match more liberally, simply from `\n<tag>` to `</tag>\n`
        Pattern p2 = Pattern.compile("(" +
                "^" +
                "<(" + alternationB + ")" +
                "\\b" +
                "(.*\\n)*?" +
                ".*</\\2>" +
                "[ ]*" +
                "(?=\\n+|\\Z))", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
        text.replaceAll(p2, protectHTML);

        // Special case for

        Pattern p3 = Pattern.compile("(?:" +
                "(?<=\\n\\n)" +
                "|" +
                "\\A\\n?" +
                ")" +
                "(" +
                "[ ]{0," + less_than_tab + "}" +
                "<(hr)" +
                "\\b" +
                "([^<>])*?" +
                "/?>" +
                "[ ]*" +
                "(?=\\n{2,}|\\Z))", Pattern.CASE_INSENSITIVE);
        text.replaceAll(p3, protectHTML);

        // Special case for standalone HTML comments:
        Pattern p4 = Pattern.compile("(?:" +
                "(?<=\\n\\n)" +
                "|" +
                "\\A\\n?" +
                ")" +
                "(" +
                "[ ]{0," + less_than_tab + "}" +
                "(?s:" +
                "<!" +
                "(--.*?--\\s*)+" +
                ">" +
                ")" +
                "[ ]*" +
                "(?=\\n{2,}|\\Z)" +
                ")");
        text.replaceAll(p4, protectHTML);
    }

    private TextEditor formParagraphs(TextEditor markup) {
        markup.deleteAll("\\A\\n+");
        markup.deleteAll("\\n+\\z");

        String[] paragraphs;
        if (markup.isEmpty()) {
            paragraphs = new String[0];
        } else {
            paragraphs = Pattern.compile("\\n{2,}").split(markup.toString());
        }
        for (int i = 0; i < paragraphs.length; i++) {
            String paragraph = paragraphs[i];
            String decoded = HTML_PROTECTOR.decode(paragraph);
            if (decoded != null) {
                paragraphs[i] = decoded;
            } else {
                paragraph = runSpanGamut(new TextEditor(paragraph)).toString();
                paragraphs[i] = "
" + paragraph + "
";
            }
        }
        return new TextEditor(join("\n\n", paragraphs));
    }

    private TextEditor doAutoLinks(TextEditor markup) {
        markup.replaceAll("<((https?|ftp):[^'\">\\s]+)>", "$1");
        Pattern email = Pattern.compile("<([-.\\w]+\\@[-a-z0-9]+(\\.[-a-z0-9]+)*\\.[a-z]+)>");
        markup.replaceAll(email, new Replacement() {
            public String replacement(Matcher m) {
                String address = m.group(1);
                TextEditor ed = new TextEditor(address);
                unEscapeSpecialChars(ed);
                String addr = encodeEmail(ed.toString());
                String url = encodeEmail("mailto:" + ed.toString());
                return "" + addr + "";
            }
        });
        return markup;
    }

    private void unEscapeSpecialChars(TextEditor ed) {
        for (String hash : CHAR_PROTECTOR.getAllEncodedTokens()) {
            String plaintext = CHAR_PROTECTOR.decode(hash);
            ed.replaceAllLiteral(hash, plaintext);
        }
    }

    private String encodeEmail(String s) {
        StringBuilder sb = new StringBuilder();
        char[] email = s.toCharArray();
        for (char ch : email) {
            double r = rnd.nextDouble();
            if (r < 0.45) {      // Decimal
                sb.append("&#");
                sb.append((int) ch);
                sb.append(';');
            } else if (r < 0.9) {  // Hex
                sb.append("&#x");
                sb.append(Integer.toString((int) ch, 16));
                sb.append(';');
            } else {
                sb.append(ch);
            }
        }
        return sb.toString();
    }

    private TextEditor doBlockQuotes(TextEditor markup) {
        Pattern p = Pattern.compile("(" +
                "(" +
                "^[ \t]*>[ \t]?" + // > at the start of a line
                ".+\\n" + // rest of the first line
                "(.+\\n)*" + // subsequent consecutive lines
                "\\n*" + // blanks
                ")+" +
                ")", Pattern.MULTILINE);
        return markup.replaceAll(p, new Replacement() {
            public String replacement(Matcher m) {
                TextEditor blockQuote = new TextEditor(m.group(1));
                blockQuote.deleteAll("^[ \t]*>[ \t]?");
                blockQuote.deleteAll("^[ \t]+$");
                blockQuote = runBlockGamut(blockQuote);
                blockQuote.replaceAll("^", "  ");

                Pattern p1 = Pattern.compile("(\\s*
.*?
)", Pattern.DOTALL);
                blockQuote = blockQuote.replaceAll(p1, new Replacement() {
                    public String replacement(Matcher m1) {
                        String pre = m1.group(1);
                        return deleteAll(pre, "^  ");
                    }
                });
                return "
\n" + blockQuote + "\n
\n\n";
            }
        });
    }

    private TextEditor doCodeBlocks(TextEditor markup) {
        Pattern p = Pattern.compile("" +
                "(?:\\n\\n|\\A)" +
                "((?:" +
                "(?:[ ]{4})" +
                ".*\\n+" +
                ")+" +
                ")" +
                "((?=^[ ]{0,4}\\S)|\\Z)", Pattern.MULTILINE);
        return markup.replaceAll(p, new Replacement() {
        			private static final String LANG_IDENTIFIER = "lang:";
                    public String replacement(Matcher m) {
                        String codeBlock = m.group(1);
                        TextEditor ed = new TextEditor(codeBlock);
                        ed.outdent();
                        encodeCode(ed);
                        ed.detabify().deleteAll("\\A\\n+").deleteAll("\\s+\\z");
                        String text = ed.toString();
                        String out;
                        String firstLine = firstLine(text);
                        if (isLanguageIdentifier(firstLine)) {
                          out = languageBlock(firstLine, text);
                        } else {
                          out = genericCodeBlock(text);
                        }
                        return out;
                    }

                    public String firstLine(String text)
                    {
                        if (text == null) {
                            return "";
                        }
                        String[] splitted = text.split("\\n");
                        return splitted[0];
                    }

                    public boolean isLanguageIdentifier(String line)
                    {
                        if (line == null) {
                            return false;
                        }
                        String lang = "";
                        if (line.startsWith(LANG_IDENTIFIER)) {
                        	lang = line.replaceFirst(LANG_IDENTIFIER, "").trim();
                        }
                        return lang.length() > 0;
                    }

                    public String languageBlock(String firstLine, String text)
                    {
                        // dont'use %n in format string (markdown aspect every new line char as "\n")
                    	//String codeBlockTemplate = "
%n%s%n
"; // http://alexgorbatchev.com/wiki/SyntaxHighlighter
                        String codeBlockTemplate = "\n\n
\n%s\n
\n\n"; // http://shjs.sourceforge.net/doc/documentation.html
                        String lang = firstLine.replaceFirst(LANG_IDENTIFIER, "").trim();
                        String block = text.replaceFirst( firstLine+"\n", "");
                        return String.format(codeBlockTemplate, lang, block);
                    }
                    public String genericCodeBlock(String text)
                    {
                        // dont'use %n in format string (markdown aspect every new line char as "\n")
                    	String codeBlockTemplate = "\n\n
%s\n
\n\n";
                        return String.format(codeBlockTemplate, text);
                    }
                });
    }

    private void encodeCode(TextEditor ed) {
        ed.replaceAll("&", "&");
        ed.replaceAll("<", "<");
        ed.replaceAll(">", ">");
        ed.replaceAll("\\*", CHAR_PROTECTOR.encode("*"));
        ed.replaceAll("_", CHAR_PROTECTOR.encode("_"));
        ed.replaceAll("\\{", CHAR_PROTECTOR.encode("{"));
        ed.replaceAll("\\}", CHAR_PROTECTOR.encode("}"));
        ed.replaceAll("\\[", CHAR_PROTECTOR.encode("["));
        ed.replaceAll("\\]", CHAR_PROTECTOR.encode("]"));
        ed.replaceAll("\\\\", CHAR_PROTECTOR.encode("\\"));
    }

    private TextEditor doLists(TextEditor text) {
        int lessThanTab = tabWidth - 1;

        String wholeList =
                "(" +
                "(" +
                "[ ]{0," + lessThanTab + "}" +
                "((?:[-+*]|\\d+[.]))" + // $3 is first list item marker
                "[ ]+" +
                ")" +
                "(?s:.+?)" +
                "(" +
                "\\z" + // End of input is OK
                "|" +
                "\\n{2,}" +
                "(?=\\S)" + // If not end of input, then a new para
                "(?![ ]*" +
                "(?:[-+*]|\\d+[.])" +
                "[ ]+" +
                ")" + // negative lookahead for another list marker
                ")" +
                ")";

        if (listLevel > 0) {
            Replacement replacer = new Replacement() {
                public String replacement(Matcher m) {
                    String list = m.group(1);
                    String listStart = m.group(3);
                    String listType;

                    if (listStart.matches("[*+-]")) {
                        listType = "ul";
                    } else {
                        listType = "ol";
                    }

                    // Turn double returns into triple returns, so that we can make a
                    // paragraph for the last item in a list, if necessary:
                    list = replaceAll(list, "\\n{2,}", "\n\n\n");

                    String result = processListItems(list);

                    // Trim any trailing whitespace, to put the closing `</ol>` or `</ul>`
                    // up on the preceding line, to get it past the current stupid
                    // HTML block parser. This is a hack to work around the terrible
                    // hack that is the HTML block parser.
                    result = result.replaceAll("\\s+$", "");

                    String html;
                    if ("ul".equals(listType)) {
                        html = "
" + result + "
\n";
                    } else {
                        html = "
" + result + "
\n";
                    }
                    return html;
                }
            };
            Pattern matchStartOfLine = Pattern.compile("^" + wholeList, Pattern.MULTILINE);
            text.replaceAll(matchStartOfLine, replacer);
        } else {
            Replacement replacer = new Replacement() {
                public String replacement(Matcher m) {
                    String list = m.group(1);
                    String listStart = m.group(3);
                    String listType;

                    if (listStart.matches("[*+-]")) {
                     listType = "ul";
                    } else {
                     listType = "ol";
                    }

                    // Turn double returns into triple returns, so that we can make a
                    // paragraph for the last item in a list, if necessary:
                    list = replaceAll(list, "\n{2,}", "\n\n\n");

                    String result = processListItems(list);

                    String html;
                    if ("ul".equals(listType)) {
                        html = "
\n" + result + "
\n";
                    } else {
                        html = "
\n" + result + "
\n";
                    }
                    return html;
                }
            };
            Pattern matchStartOfLine = Pattern.compile("(?:(?<=\\n\\n)|\\A\\n?)" + wholeList, Pattern.MULTILINE);
            text.replaceAll(matchStartOfLine, replacer);

        }

        return text;
    }

    private String processListItems(String list) {
        // The listLevel variable keeps track of when we're inside a list.
        // Each time we enter a list, we increment it; when we leave a list,
        // we decrement. If it's zero, we're not in a list anymore.
        //
        // We do this because when we're not inside a list, we want to treat
        // something like this:
        //
        //       I recommend upgrading to version
        //       8. Oops, now this line is treated
        //       as a sub-list.
        //
        // As a single paragraph, despite the fact that the second line starts
        // with a digit-period-space sequence.
        //
        // Whereas when we're inside a list (or sub-list), that line will be
        // treated as the start of a sub-list. What a kludge, huh? This is
        // an aspect of Markdown's syntax that's hard to parse perfectly
        // without resorting to mind-reading. Perhaps the solution is to
        // change the syntax rules such that sub-lists must start with a
        // starting cardinal number; e.g. "1." or "a.".
        listLevel++;

        // Trim trailing blank lines:
        list = replaceAll(list, "\\n{2,}\\z", "\n");

        Pattern p = Pattern.compile("(\\n)?" +
                "^([ \\t]*)([-+*]|\\d+[.])[ ]+" +
                "((?s:.+?)(\\n{1,2}))" +
                "(?=\\n*(\\z|\\2([-+\\*]|\\d+[.])[ \\t]+))",
                Pattern.MULTILINE);
        list = replaceAll(list, p, new Replacement() {
            public String replacement(Matcher m) {
                String text = m.group(4);
                TextEditor item = new TextEditor(text);
                String leadingLine = m.group(1);
                if (!isEmptyString(leadingLine) || hasParagraphBreak(item)) {
                    item = runBlockGamut(item.outdent());
                } else {
                    // Recurse sub-lists
                    item = doLists(item.outdent());
                    item = runSpanGamut(item);
                }
                return "
" + item.trim().toString() + "
\n";
            }
        });
        listLevel--;
        return list;
    }

    private boolean hasParagraphBreak(TextEditor item) {
        return item.toString().indexOf("\n\n") != -1;
    }

    private boolean isEmptyString(String leadingLine) {
        return leadingLine == null || leadingLine.equals("");
    }

    private TextEditor doHeaders(TextEditor markup) {
        // setext-style headers
        markup.replaceAll("^(.*)\n====+$", "
$1
");
        markup.replaceAll("^(.*)\n----+$", "
$1
");

        // atx-style headers - e.g., "#### heading 4 ####"
        Pattern p = Pattern.compile("^(#{1,6})\\s*(.*?)\\s*\\1?$", Pattern.MULTILINE);
        markup.replaceAll(p, new Replacement() {
            public String replacement(Matcher m) {
                String marker = m.group(1);
                String heading = m.group(2);
                int level = marker.length();
                String tag = "h" + level;
                return "<" + tag + ">" + heading + "</" + tag + ">\n";
            }
        });
        return markup;
    }

    private String join(String separator, String[] strings) {
        int length = strings.length;
        StringBuilder buf = new StringBuilder();
        if (length > 0) {
            buf.append(strings[0]);
            for (int i = 1; i < length; i++) {
                buf.append(separator).append(strings[i]);
            }
        }
        return buf.toString();
    }

    public TextEditor runSpanGamut(TextEditor text) {
        text = escapeSpecialCharsWithinTagAttributes(text);
        text = doCodeSpans(text);
        text = encodeBackslashEscapes(text);

        doImages(text);
        doAnchors(text);
        doAutoLinks(text);

        // Fix for BUG #1357582
        // We must call escapeSpecialCharsWithinTagAttributes() a second time to
        // escape the contents of any attributes generated by the prior methods.
        // - Nathan Winant, nw@exegetic.net, 8/29/2006
        text = escapeSpecialCharsWithinTagAttributes(text);

        encodeAmpsAndAngles(text);
        doItalicsAndBold(text);

        // Manual line breaks
        text.replaceAll(" {2,}\n", "
\n");
        return text;
    }

    /**
     * escape special characters
     *
     * Within tags -- meaning between < and > -- encode [\ ` * _] so they
     * don't conflict with their use in Markdown for code, italics and strong.
     * We're replacing each such character with its corresponding random string
     * value; this is likely overkill, but it should prevent us from colliding
     * with the escape values by accident.
     *
     * @param text
     * @return
     */
    private TextEditor escapeSpecialCharsWithinTagAttributes(TextEditor text) {
        Collection<HTMLToken> tokens = text.tokenizeHTML();
        TextEditor newText = new TextEditor("");

        for (HTMLToken token : tokens) {
            String value = token.getText();
            if (token.isTag()) {
                value = value.replaceAll("\\\\", CHAR_PROTECTOR.encode("\\"));
                value = value.replaceAll("`", CHAR_PROTECTOR.encode("`"));
                value = value.replaceAll("\\*", CHAR_PROTECTOR.encode("*"));
                value = value.replaceAll("_", CHAR_PROTECTOR.encode("_"));
            }
            newText.append(value);
        }

        return newText;
    }

    private void doImages(TextEditor text) {
        // Inline image syntax
    	text.replaceAll("!\\[(.*)\\]\\((.*) \"(.*)\"\\)", "");
    	text.replaceAll("!\\[(.*)\\]\\((.*)\\)", "");

        // Reference-style image syntax
    	Pattern imageLink = Pattern.compile("(" +
            	"[!]\\[(.*?)\\]" + // alt text = $2
            	"[ ]?(?:\\n[ ]*)?" +
            	"\\[(.*?)\\]" + // ID = $3
            	")");
    	text.replaceAll(imageLink, new Replacement() {
        	public String replacement(Matcher m) {
            	String replacementText;
            	String wholeMatch = m.group(1);
            	String altText = m.group(2);
            	String id = m.group(3).toLowerCase();
            	if ("".equals(id)) {
                	id = altText.toLowerCase();
            	}

            	// imageDefinition is the same as linkDefinition
            	LinkDefinition defn = linkDefinitions.get(id);
            	if (defn != null) {
                	String url = defn.getUrl();
                	url = url.replaceAll("\\*", CHAR_PROTECTOR.encode("*"));
                	url = url.replaceAll("_", CHAR_PROTECTOR.encode("_"));
                	String title = defn.getTitle();
                	String titleTag = "";
                	if (title != null && !title.equals("")) {
                    	title = title.replaceAll("\\*", CHAR_PROTECTOR.encode("*"));
                    	title = title.replaceAll("_", CHAR_PROTECTOR.encode("_"));
                    	titleTag = " alt=\"" + altText + "\" title=\"" + title + "\"";
                	}
                	replacementText = "";
            	} else {
                	replacementText = wholeMatch;
            	}
            	return replacementText;
        	}
    	});
	}

    private TextEditor doAnchors(TextEditor markup) {
        // Internal references: [link text] [id]
        Pattern internalLink = Pattern.compile("(" +
                "\\[(.*?)\\]" + // Link text = $2
                "[ ]?(?:\\n[ ]*)?" +
                "\\[(.*?)\\]" + // ID = $3
                ")");
        markup.replaceAll(internalLink, new Replacement() {
            public String replacement(Matcher m) {
                String replacementText;
                String wholeMatch = m.group(1);
                String linkText = m.group(2);
                String id = m.group(3).toLowerCase();
                if ("".equals(id)) { // for shortcut links like [this][]
                    id = linkText.toLowerCase();
                }

                LinkDefinition defn = linkDefinitions.get(id);
                if (defn != null) {
                    String url = defn.getUrl();
                    // protect emphasis (* and _) within urls
                    url = url.replaceAll("\\*", CHAR_PROTECTOR.encode("*"));
                    url = url.replaceAll("_", CHAR_PROTECTOR.encode("_"));
                    String title = defn.getTitle();
                    String titleTag = "";
                    if (title != null && !title.equals("")) {
                        // protect emphasis (* and _) within urls
                        title = title.replaceAll("\\*", CHAR_PROTECTOR.encode("*"));
                        title = title.replaceAll("_", CHAR_PROTECTOR.encode("_"));
                        titleTag = " title=\"" + title + "\"";
                    }
                    replacementText = "" + linkText + "";
                } else {
                    replacementText = wholeMatch;
                }
                return replacementText;
            }
        });

        // Inline-style links: [link text](url "optional title")
        Pattern inlineLink = Pattern.compile("(" + // Whole match = $1
                "\\[(.*?)\\]" + // Link text = $2
                "\\(" +
                "[ \\t]*" +
                "<?(.*?)>?" + // href = $3
                "[ \\t]*" +
                "(" +
                "(['\"])" + // Quote character = $5
                "(.*?)" + // Title = $6
                "\\5" +
                ")?" +
                "\\)" +
                ")", Pattern.DOTALL);
        markup.replaceAll(inlineLink, new Replacement() {
            public String replacement(Matcher m) {
                String linkText = m.group(2);
                String url = m.group(3);
                String title = m.group(6);
                // protect emphasis (* and _) within urls
                url = url.replaceAll("\\*", CHAR_PROTECTOR.encode("*"));
                url = url.replaceAll("_", CHAR_PROTECTOR.encode("_"));
                StringBuilder result = new StringBuilder();
                result.append("<a href=\"").append(url).append("\"");
                if (title != null) {
                    // protect emphasis (* and _) within urls
                    title = title.replaceAll("\\*", CHAR_PROTECTOR.encode("*"));
                    title = title.replaceAll("_", CHAR_PROTECTOR.encode("_"));
                    title = replaceAll(title, "\"", """);
                    result.append(" title=\"");
                    result.append(title);
                    result.append("\"");
                }
                result.append(">").append(linkText);
                result.append("");
                return result.toString();
            }
        });

        // Last, handle reference-style shortcuts: [link text]
        // These must come last in case you've also got [link test][1]
        // or [link test](/foo)
        Pattern referenceShortcut = Pattern.compile("(" + // wrap whole match in $1
                                                        "\\[" +
                                                        "([^\\[\\]]+)" + // link text = $2; can't contain '[' or ']'
                                                        "\\]" +
                                                    ")", Pattern.DOTALL);
        markup.replaceAll(referenceShortcut, new Replacement() {
            public String replacement(Matcher m) {
                String replacementText;
                String wholeMatch = m.group(1);
                String linkText = m.group(2);
                String id = m.group(2).toLowerCase(); // link id should be lowercase
                id = id.replaceAll("[ ]?\\n", " "); // change embedded newlines into spaces

                LinkDefinition defn = linkDefinitions.get(id.toLowerCase());
                if (defn != null) {
                    String url = defn.getUrl();
                    // protect emphasis (* and _) within urls
                    url = url.replaceAll("\\*", CHAR_PROTECTOR.encode("*"));
                    url = url.replaceAll("_", CHAR_PROTECTOR.encode("_"));
                    String title = defn.getTitle();
                    String titleTag = "";
                    if (title != null && !title.equals("")) {
                        // protect emphasis (* and _) within urls
                        title = title.replaceAll("\\*", CHAR_PROTECTOR.encode("*"));
                        title = title.replaceAll("_", CHAR_PROTECTOR.encode("_"));
                        titleTag = " title=\"" + title + "\"";
                    }
                    replacementText = "" + linkText + "";
                } else {
                    replacementText = wholeMatch;
                }
                return replacementText;
         }
        });

        return markup;
    }

    private TextEditor doItalicsAndBold(TextEditor markup) {
        markup.replaceAll("(\\*\\*|__)(?=\\S)(.+?[*_]*)(?<=\\S)\\1", "$2");
        markup.replaceAll("(\\*|_)(?=\\S)(.+?)(?<=\\S)\\1", "$2");
        return markup;
    }

    private TextEditor encodeAmpsAndAngles(TextEditor markup) {
        // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
        // http://bumppo.net/projects/amputator/
        markup.replaceAll("&(?!#?[xX]?(?:[0-9a-fA-F]+|\\w+);)", "&");
        markup.replaceAll("<(?![a-zA-Z/?\\$!])", "<");
        return markup;
    }

    private TextEditor doCodeSpans(TextEditor markup) {
            return markup.replaceAll(Pattern.compile("(?<!\\\\)(`+)(.+?)(?<!`)\\1(?!`)"), new Replacement() {
                    public String replacement(Matcher m) {
                        String code = m.group(2);
                        TextEditor subEditor = new TextEditor(code);
                        subEditor.deleteAll("^[ \\t]+").deleteAll("[ \\t]+$");
                        encodeCode(subEditor);
                        return "" + subEditor.toString() + "";
                    }
                });
    }

    private String deleteAll(String text, String regex) {
        return replaceAll(text, regex, "");
    }

    private String replaceAll(String text, String regex, String replacement) {
        TextEditor ed = new TextEditor(text);
        ed.replaceAll(regex, replacement);
        return ed.toString();
    }

    private String replaceAll(String markup, Pattern pattern, Replacement replacement) {
        TextEditor ed = new TextEditor(markup);
        ed.replaceAll(pattern, replacement);
        return ed.toString();
    }

    @Override
    public String toString() {
        return "Markdown Processor for Java 0.4.0 (compatible with Markdown 1.0.2b2)";
    }

    public static void main(String[] args) {
        StringBuilder buf = new StringBuilder();
        char[] cbuf = new char[1024];
        java.io.Reader in = new java.io.InputStreamReader(System.in);
        try {
            int charsRead = in.read(cbuf);
            while (charsRead >= 0) {
                buf.append(cbuf, 0, charsRead);
                charsRead = in.read(cbuf);
            }
            System.out.println(new MarkdownProcessor().markdown(buf.toString()));
        } catch (java.io.IOException e) {
            System.err.println("Error reading input: " + e.getMessage());
            System.exit(1);
        }
    }
}


/**
 * Copyright 2014 Simeon Malchev
 *
 * Licensed 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.vibur.dbcp.pool;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.vibur.dbcp.ViburDBCPDataSource;
import org.vibur.dbcp.ViburDBCPException;
import org.vibur.dbcp.pool.HookHolder.ConnHooksAccessor;
import org.vibur.objectpool.PoolService;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLTimeoutException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;

import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.vibur.dbcp.ViburConfig.*;
import static org.vibur.dbcp.proxy.Proxy.newProxyConnection;
import static org.vibur.dbcp.util.JdbcUtils.chainSQLException;
import static org.vibur.dbcp.util.ViburUtils.getPoolName;

/**
 * The facade class through which the {@link ConnectionFactory} and {@link PoolService} functions are accessed.
 * Essentially, these are the operations that allow us to get and restore a JDBC Connection from the pool
 * as well as to process the SQLExceptions that might have occurred on a taken JDBC Connection.
 *
 * @author Simeon Malchev
 */
public class PoolOperations {

    private static final Logger logger = LoggerFactory.getLogger(PoolOperations.class);

    private static final long[] NO_WAIT = {};
    private static final Pattern whitespaces = Pattern.compile("\\s");

    private final ViburDBCPDataSource dataSource;
    private final long connectionTimeoutInNanos;
    private final ViburObjectFactory connectionFactory;
    private final PoolService<ConnHolder> poolService;

    private final Set<String> criticalSQLStates;

    /**
     * Instantiates the PoolOperations facade.
     *
     * @param dataSource the Vibur dataSource on which we will operate
     * @param connectionFactory the Vibur connection factory
     * @param poolService the Vibur pool service
     */
    public PoolOperations(ViburDBCPDataSource dataSource, ViburObjectFactory connectionFactory, PoolService<ConnHolder> poolService) {
        this.dataSource = dataSource;
        this.connectionTimeoutInNanos = MILLISECONDS.toNanos(dataSource.getConnectionTimeoutInMs());
        this.connectionFactory = connectionFactory;
        this.poolService = poolService;
        this.criticalSQLStates = new HashSet<>(Arrays.asList(
                whitespaces.matcher(dataSource.getCriticalSQLStates()).replaceAll("").split(",")));
    }

    ////////////// getProxyConnection(...) //////////////

    public Connection getProxyConnection(long timeoutMs) throws SQLException {
        int attempt = 1;
        ConnHolder connHolder = null;
        SQLException sqlException = null;
        long startNanoTime = System.nanoTime();

        while (connHolder == null) {
            try {
                connHolder = getConnHolder(timeoutMs);

            } catch (ViburDBCPException e) { // thrown only if we can retry the operation, see getConnHolder(...)
                sqlException = chainSQLException(e.unwrapSQLException(), sqlException);

                if (attempt++ > dataSource.getAcquireRetryAttempts()) { // check the max retries limit
                    throw sqlException;
                }
                if (timeoutMs > 0) { // check the time limit if applicable
                    timeoutMs = NANOSECONDS.toMillis(connectionTimeoutInNanos - (System.nanoTime() - startNanoTime))
                            - dataSource.getAcquireRetryDelayInMs(); // calculates the remaining timeout
                    if (timeoutMs <= 0) {
                        throw sqlException;
                    }
                }

                try {
                    MILLISECONDS.sleep(dataSource.getAcquireRetryDelayInMs());
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw chainSQLException(new SQLException(ie), sqlException);
                }
            }
        }

        if (logger.isTraceEnabled()) {
            logger.trace("Taking rawConnection {}", connHolder.rawConnection());
        }

        Connection proxy = newProxyConnection(connHolder, this, dataSource);
        if (dataSource.isPoolEnableConnectionTracking()) {
            connHolder.setProxyConnection(proxy);
        }
        return proxy;
    }

    /**
     * Tries to take a {@code ConnHolder} object from the underlying object pool. If successful, never returns
     * {@code null}.
     *
     * @param timeoutMs timeout in millis to pass to the underlying object pool {@code take} methods
     * @throws SQLException to indicate a generic non-recoverable error that cannot be retried
     * @throws SQLTimeoutException to indicate a non-recoverable error due to timeout that cannot be retried
     * @throws ViburDBCPException to indicate a recoverable error that can be retried
     */
    private ConnHolder getConnHolder(long timeoutMs) throws SQLException, ViburDBCPException {
        Hook.GetConnection[] onGet = ((ConnHooksAccessor) dataSource.getConnHooks()).onGet();
        ConnHolder connHolder = null;
        long[] waitedNanos = NO_WAIT;
        SQLException sqlException = null;
        ViburDBCPException viburException = null;

        try {
            if (onGet.length > 0) {
                waitedNanos = new long[1];
                connHolder = timeoutMs > 0 ? poolService.tryTake(timeoutMs, MILLISECONDS, waitedNanos) : poolService.take(waitedNanos);
            }
            else {
                connHolder = timeoutMs > 0 ? poolService.tryTake(timeoutMs, MILLISECONDS) : poolService.take();
            }

            if (connHolder == null) { // we were *not* able to obtain a connection from the pool
                sqlException = createSQLException(onGet.length > 0 ? waitedNanos[0] : MILLISECONDS.toNanos(timeoutMs));
            }
        } catch (ViburDBCPException e) { // thrown (indirectly) by the ConnectionFactory.create() methods
            viburException = e;
            sqlException = e.unwrapSQLException(); // currently all such errors are treated as recoverable, i.e., can be retried

        } finally {
            Connection rawConnection = connHolder != null ? connHolder.rawConnection() : null;
            try {
                for (Hook.GetConnection hook : onGet) {
                    hook.on(rawConnection, waitedNanos[0]);
                }
            } catch (SQLException e) {
                sqlException = chainSQLException(sqlException, e);
            }
        }

        if (viburException != null) {
            throw viburException; // a recoverable error
        }
        if (sqlException != null) {
            throw sqlException; // a non-recoverable error
        }

        return connHolder; // never null if we reach this point
    }

    private SQLException createSQLException(long takenNanos) {
        String poolName = getPoolName(dataSource);
        if (poolService.isTerminated()) {
            return new SQLException(format("Pool %s, the poolService is terminated.", poolName),
                    SQLSTATE_POOL_CLOSED_ERROR);
        }

        Hook.GetConnectionTimeout[] onTimeout = ((ConnHooksAccessor) dataSource.getConnHooks()).onTimeout();
        boolean isInterrupted = Thread.currentThread().isInterrupted(); // someone else has interrupted us, so we do not clear the flag
        if (!isInterrupted && onTimeout.length > 0) {
            TakenConnection[] takenConnections = dataSource.getTakenConnections();
            for (Hook.GetConnectionTimeout hook : onTimeout) {
                hook.on(takenConnections, takenNanos);
            }
        }

        double takenMs = takenNanos * 0.000_001;
        int intTakenMs = (int) Math.round(takenMs);
        return !isInterrupted ?
                new SQLTimeoutException(format("Pool %s, couldn't obtain SQL connection within %.3f ms.",
                        poolName, takenMs), SQLSTATE_TIMEOUT_ERROR, intTakenMs) :
                new SQLException(format("Pool %s, interrupted while getting SQL connection, waited for %.3f ms.",
                        poolName, takenMs), SQLSTATE_INTERRUPTED_ERROR, intTakenMs);
    }

    ////////////// restore(...) //////////////

    public void restore(ConnHolder connHolder, boolean valid, SQLException[] exceptions) {
        if (logger.isTraceEnabled()) {
            logger.trace("Restoring rawConnection {}", connHolder.rawConnection());
        }
        boolean reusable = valid && exceptions.length == 0 && connHolder.version() == connectionFactory.version();
        poolService.restore(connHolder, reusable);
        processSQLExceptions(connHolder, exceptions);
    }

    /**
     * Processes SQL exceptions that have occurred on the given JDBC Connection (wrapped in a {@code ConnHolder}).
     *
     * @param connHolder the given connection
     * @param exceptions the list of SQL exceptions that have occurred on the connection; might be an empty list but not a {@code null}
     */
    private void processSQLExceptions(ConnHolder connHolder, SQLException[] exceptions) {
        int connVersion = connHolder.version();
        SQLException criticalException = getCriticalSQLException(exceptions);
        if (criticalException != null && connectionFactory.compareAndSetVersion(connVersion, connVersion + 1)) {
            int destroyed = poolService.drainCreated(); // destroys all connections in the pool
            logger.error("Critical SQLState {} occurred, destroyed {} connections from pool {}, current connection version is {}.",
                    criticalException.getSQLState(), destroyed, getPoolName(dataSource), connectionFactory.version(), criticalException);
        }
    }

    private SQLException getCriticalSQLException(SQLException[] exceptions) {
        for (SQLException exception : exceptions) {
            if (isCriticalSQLException(exception)) {
                return exception;
            }
        }
        return null;
    }

    private boolean isCriticalSQLException(SQLException exception) {
        if (exception == null) {
            return false;
        }
        if (criticalSQLStates.contains(exception.getSQLState())) {
            return true;
        }
        return isCriticalSQLException(exception.getNextException());
    }
}