/* LanguageTool, a natural language style checker
 * Copyright (C) 2012 Daniel Naber (http://www.danielnaber.de)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
 * USA
 */
package org.languagetool.gui;

import java.awt.Cursor;
import java.awt.Desktop;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import java.util.Set;

import javax.swing.JTextPane;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.html.HTMLDocument;

import org.apache.commons.lang3.StringUtils;
import org.languagetool.Language;
import org.languagetool.LanguageMaintainedState;
import org.languagetool.rules.ITSIssueType;
import org.languagetool.rules.Rule;
import org.languagetool.rules.RuleMatch;
import org.languagetool.tools.ContextTools;

/**
 * Helper for the JTextPane where the result of text checking is displayed.
 */
class ResultAreaHelper implements LanguageToolListener, HyperlinkListener {

  private static final String KEY = "org.languagetool.gui.ResultAreaHelper";
  private static final String EMPTY_PARA = "<p class=\"small\"></p>";
  private static final String HEADER = "header";
  private static final String MAIN = "maincontent";
  private static final String TEMPLATE = "<html>\n"
          + "  <head>\n"
          + "     <style type=\"text/css\">\n"
          + "       #" + HEADER + " {  }\n"
          + "       #" + MAIN + " { }\n"
          + "       p { font-family: Arial,Helvetica; padding: 1px; margin: 1px }\n"
          + "       p.small { font-size: 1px; }\n"
          + "       p.grayed { font-family: Arial,Helvetica; color: #666666 }\n"
          + "     </style>\n"
          + "  </head>\n"
          + "  <body>\n"
          + "    <div id=\"" + HEADER + "\">\n"
          + "    </div>\n"
          + "    <div id=\"" + MAIN + "\">\n"
          + "    </div>\n"
          + "  </body>\n"
          + "</html>";
  private static final String DEACTIVATE_URL = "http://languagetool.org/deactivate/";
  private static final String REACTIVATE_URL = "http://languagetool.org/reactivate/";
  private static final String LT_ERROR_MARKER_START = "<b><font bgcolor=\"#d7d7ff\">";
  private static final String SPELL_ERROR_MARKER_START = "<b><font bgcolor=\"#ffd7d7\">";

  private final ResourceBundle messages;
  private final JTextPane statusPane;
  private final LanguageToolSupport ltSupport;

  private long runTime;
  private final Object lock = new Object();
  private boolean enabled = false;

  static void install(ResourceBundle messages, LanguageToolSupport ltSupport, JTextPane pane) {
    Object prev = pane.getClientProperty(KEY);
    if (prev instanceof ResultAreaHelper) {
      enable(pane);
      return;
    }
    ResultAreaHelper helper = new ResultAreaHelper(messages, ltSupport, pane);
    pane.putClientProperty(KEY, helper);
  }

  static void enable(JTextPane pane) {
    Object helper = pane.getClientProperty(KEY);
    if (helper instanceof ResultAreaHelper) {
      ((ResultAreaHelper) helper).enable();
    }
  }

  static void disable(JTextPane pane) {
    Object helper = pane.getClientProperty(KEY);
    if (helper instanceof ResultAreaHelper) {
      ((ResultAreaHelper) helper).disable();
    }
  }

  private ResultAreaHelper(ResourceBundle messages, LanguageToolSupport ltSupport, JTextPane statusPane) {
    this.messages = messages;
    this.ltSupport = ltSupport;
    this.statusPane = statusPane;
    statusPane.setContentType("text/html");
    statusPane.setEditable(false);
    statusPane.setTransferHandler(new RetainLineBreakTransferHandler());
    enable();
  }

  private void enable() {
    synchronized (lock) {
      if (enabled) {
        return;
      }
      enabled = true;
      statusPane.setText(TEMPLATE);
      setHeader(messages.getString("resultAreaText"));
      statusPane.addHyperlinkListener(this);
      ltSupport.addLanguageToolListener(this);
    }
  }

  private void disable() {
    synchronized (lock) {
      if (!enabled) {
        return;
      }
      enabled = false;
      statusPane.setText(TEMPLATE);
      statusPane.removeHyperlinkListener(this);
      ltSupport.removeLanguageToolListener(this);
    }
  }

  @Override
  public void languageToolEventOccurred(LanguageToolEvent event) {
    if (event.getType() == LanguageToolEvent.Type.CHECKING_STARTED) {
      Language lang = ltSupport.getLanguage();
      String langName;
      if (lang.isExternal()) {
        langName = lang.getTranslatedName(messages) + Main.EXTERNAL_LANGUAGE_SUFFIX;
      } else {
        langName = lang.getTranslatedName(messages);
      }
      String msg = org.languagetool.tools.Tools.i18n(
              messages, "startChecking", langName) + "...";
      setHeader(msg);
      setMain(EMPTY_PARA);
      if (event.getCaller() == this) {
        statusPane.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
      }
    } else if (event.getType() == LanguageToolEvent.Type.CHECKING_FINISHED) {
      setRunTime(event.getElapsedTime());
      String inputText = event.getSource().getTextComponent().getText();
      displayResult(inputText, event.getSource().getMatches());
      if (event.getCaller() == this) {
        statusPane.setCursor(Cursor.getDefaultCursor());
      }
    } else if (event.getType() == LanguageToolEvent.Type.RULE_DISABLED
            || event.getType() == LanguageToolEvent.Type.RULE_ENABLED) {
      String inputText = event.getSource().getTextComponent().getText();
      displayResult(inputText, event.getSource().getMatches());
    }
  }

  private void setHeader(String txt) {
    HTMLDocument d = (HTMLDocument) statusPane.getDocument();
    Element e = d.getElement(HEADER);
    try {
      d.setInnerHTML(e, "<p class=\"grayed\">" + txt + "</p>");
    } catch (BadLocationException | IOException ex) {
      Tools.showError(ex);
    }
  }

  private void setMain(String html) {
    HTMLDocument d = (HTMLDocument) statusPane.getDocument();
    Element e = d.getElement(MAIN);
    try {
      d.setInnerHTML(e, html);
    } catch (BadLocationException | IOException ex) {
      Tools.showError(ex);
    }
  }

  private void appendMain(String html) {
    HTMLDocument d = (HTMLDocument) statusPane.getDocument();
    Element e = d.getElement(MAIN);
    try {
      d.insertBeforeEnd(e, html);
    } catch (BadLocationException | IOException ex) {
      Tools.showError(ex);
    }
  }

  private void getRuleMatchHtml(List<RuleMatch> ruleMatches, String text) {
    ContextTools contextTools = new ContextTools();
    StringBuilder sb = new StringBuilder(200);
    if (ltSupport.getLanguage().getMaintainedState() != LanguageMaintainedState.ActivelyMaintained) {
      sb.append("<p><b>").append(messages.getString("unsupportedWarning"))
              .append("</b></p>\n");
    } else {
      sb.append(EMPTY_PARA);
    }
    setMain(sb.toString());
    sb.setLength(0);
    int i = 0;
    for (RuleMatch match : ruleMatches) {
      sb.append("<p>");
      String output = org.languagetool.tools.Tools.i18n(messages, "result1", i + 1, match.getLine() + 1, match.getColumn());
      sb.append(output);
      String msg = match.getMessage()
          .replaceAll("<suggestion>", "<b>").replaceAll("</suggestion>", "</b>")
          .replaceAll("<old>", "<b>").replaceAll("</old>", "</b>");
      sb.append("<b>").append(messages.getString("errorMessage")).append("</b> ");
      sb.append(msg);
      RuleLink ruleLink = RuleLink.buildDeactivationLink(match.getRule());
      sb.append(" <a href=\"").append(ruleLink).append("\">").append(messages.getString("deactivateRule")).append("</a><br>\n");
      if (match.getSuggestedReplacements().size() > 0) {
        String replacement = String.join("; ", match.getSuggestedReplacements());
        sb.append("<b>").append(messages.getString("correctionMessage")).append("</b> ").append(replacement).append("<br>\n");
      }
      if (ITSIssueType.Misspelling == match.getRule().getLocQualityIssueType()) {
        contextTools.setErrorMarkerStart(SPELL_ERROR_MARKER_START);
      } else {
        contextTools.setErrorMarkerStart(LT_ERROR_MARKER_START);
      }
      String context = contextTools.getContext(match.getFromPos(), match.getToPos(), text);
      sb.append("<b>").append(messages.getString("errorContext")).append("</b> ").append(context);
      if ((match.getRule().getUrl() != null || match.getUrl() != null) && Desktop.isDesktopSupported()) {
        sb.append("<br>\n");
        sb.append("<b>").append(messages.getString("moreInfo")).append("</b> <a href=\"");
        String url;
        if(match.getUrl() != null) {
          url = match.getUrl().toString();
        } else {
          url = match.getRule().getUrl().toString();
        }
        sb.append(url);
        String shortUrl = StringUtils.abbreviate(url, 60);
        sb.append("\">").append(shortUrl).append("</a>\n");
      }
      sb.append("</p>");
      i++;
      appendMain(sb.toString());
      sb.setLength(0);
    }
    sb.append("<p class=\"grayed\">");
    sb.append(getDisabledRulesHtml());
    String checkDone = org.languagetool.tools.Tools.i18n(messages, "checkDone",
            ruleMatches.size(), runTime);
    sb.append("<br>\n").append(checkDone);
    sb.append("</p>");
    appendMain(sb.toString());
  }

  private String getDisabledRulesHtml() {
    StringBuilder sb = new StringBuilder(40);
    sb.append(messages.getString("deactivatedRulesText"));
    int i = 0;
    int deactivatedRuleCount = 0;
    for (String ruleId : ltSupport.getConfig().getDisabledRuleIds()) {
      if (ruleId.trim().isEmpty()) {
        continue;
      }
      Rule rule = ltSupport.getRuleForId(ruleId);
      if (rule == null || rule.isDefaultOff()) {
        continue;
      }
      if (i++ > 0) {
        sb.append(',');
      }
      RuleLink reactivationLink = RuleLink.buildReactivationLink(rule);
      sb.append(" <a href=\"").append(reactivationLink).append("\">")
              .append(rule.getDescription()).append("</a>");
      deactivatedRuleCount++;
    }
    sb.append("<br>");
    if (deactivatedRuleCount == 0) {
      return "";
    } else {
      return sb.toString();
    }
  }

  private void setRunTime(long runTime) {
    this.runTime = runTime;
  }

  private void displayResult(String inputText, List<RuleMatch> matches) {
    List<RuleMatch> filtered = filterRuleMatches(matches);
    getRuleMatchHtml(filtered, inputText);
    statusPane.setCaretPosition(0);
  }

  private List<RuleMatch> filterRuleMatches(List<RuleMatch> matches) {
    List<RuleMatch> filtered = new ArrayList<>();
    Set<String> disabledRuleIds = ltSupport.getConfig().getDisabledRuleIds();
    for (RuleMatch ruleMatch : matches) {
      if (!disabledRuleIds.contains(ruleMatch.getRule().getId())) {
        filtered.add(ruleMatch);
      }
    }
    return filtered;
  }

  @Override
  public void hyperlinkUpdate(HyperlinkEvent e) {
    if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
      URL url = e.getURL();
      try {
        String uri = url.toURI().toString();
        if (uri.startsWith(DEACTIVATE_URL) || uri.startsWith(REACTIVATE_URL)) {
          handleRuleLinkClick(uri);
        } else {
          Tools.openURL(url);
        }
      } catch (Exception ex) {
        throw new RuntimeException("Could not handle URL click: " + url, ex);
      }
    }
  }

  private void handleRuleLinkClick(String uri) throws IOException {
    RuleLink ruleLink = RuleLink.getFromString(uri);
    String ruleId = ruleLink.getId();
    if (uri.startsWith(DEACTIVATE_URL)) {
      ltSupport.disableRule(ruleId);
    } else {
      ltSupport.enableRule(ruleId);
    }
    ltSupport.getConfig().saveConfiguration(ltSupport.getLanguage());
    ltSupport.checkImmediately(this);
  }

}
