package de.visone.crawl.gui;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.URL;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;

import org.apache.log4j.Logger;

import de.visone.base.Mediator;
import de.visone.crawl.Crawler;
import de.visone.crawl.Settings;
import de.visone.crawl.gui.editor.RuleEditorDialog;
import de.visone.crawl.out.CrawlListener;
import de.visone.crawl.rules.BlacklistFilter;
import de.visone.crawl.rules.RuleManager;
import de.visone.crawl.sys.Helper4Crawler;
import de.visone.crawl.sys.UserAgents;
import de.visone.crawl.xml.ProgressProducer;
import de.visone.gui.tabs.AbstractAlgorithmCard;
import de.visone.gui.tabs.option.DropdownOptionItem;
import de.visone.gui.tabs.option.DummyVisoneOptionItem;
import de.visone.gui.tabs.option.TextOptionItem;
import de.visone.gui.tabs.option.URLOptionItem;
import de.visone.nlpext.gui.AbstractNLPCard;

/**
 * Class holding common code for the two crawler functions.
 * 
 * @author Joschi
 * @author matthias
 */
public abstract class AbstractCrawlCard extends AbstractNLPCard {
	private static final Logger logger = Logger
			.getLogger(AbstractAlgorithmCard.class);

	private static final String SPECIFIED = "specified";
	private static final String DOMAIN_SPECIFIC = "domain specific";
	private static final String STANDARD = "standard";
	private static final String APPLY_BUTTON_LABEL = "crawl!";
	private static final int PROGRESS_DELAY = 100;
	public static final Dimension DIM = new Dimension(10, 10);

	private TextOptionItem urlField;
	private DropdownOptionItem<String> browser;
	private DropdownOptionItem<String> domainSpecific;
	private RulesOptionItem rules;
	private JButton edit;

	private TextOptionItem cookieField;

	/**
	 * Creates the crawl card.
	 * 
	 * @param cardName
	 *            name of the card (used in the dropdown list)
	 * @param mediator
	 *            the {@link Mediator}
	 */
	protected AbstractCrawlCard(final String cardName, final Mediator mediator) {
		super(cardName, mediator);
	}

	@Override
	public String getApplyText() {
		return APPLY_BUTTON_LABEL;
	}

	@Override
	public final boolean workOnCopyDefault() {
		return false;
	}

	@Override
	public boolean doRunAlgorithm() {
		final String url = urlField.getValue();
		final Settings s = createSettings(url);
		final CrawlListener listener = getCrawlListener(s);
		if (listener == null)
			return false;
		final Crawler c = new Crawler(s, url, listener);
		showProgress(c);
		c.start();
		// best we can do. if it does fail, it does so asynchronously
		return true;
	}

	/** {@inheritDoc} */
	@Override
	protected void fillParameterPanel() {
		addHeading("main options");
		fillImportantOptions();
		addHeading("expert options");
		fillObscureOptions();
	}

	private void fillImportantOptions() {
		urlField = new URLOptionItem();
		urlField.setValue("http://example.com");
		affectsOthers(urlField);
		addOptionItem(urlField, "url");

		browser = new DropdownOptionItem<String>(true, UserAgents.getNames());
		// browser.setItemFactory(new StringItemFactory());
		addOptionItem(browser, "browser");

		fillAdditionalImportantOptions();
	}

	protected abstract void fillAdditionalImportantOptions();

	private void fillObscureOptions() {
		addCrawlRuleOptions();

		cookieField = new TextOptionItem(true);
		cookieField.setToolTip("the value sent in the \"Cookie:\" HTTP header");
		addOptionItem(cookieField, "cookies");

		fillAdditionalObscureOptions();
	}

	@Override
	protected void toggleOptionItemsEnabled() {
		setCookie();
		rules.setEnabled(domainSpecific.getValue() == SPECIFIED);
		if (domainSpecific.getValue() == DOMAIN_SPECIFIC) {
			final URL url = Helper4Crawler.getURL(urlField.getValue());
			if (url != null) {
				final String rule = RuleManager.getRuleForURL(url);
				rules.setValue(rule);
			}
		}
		enableEdit();
	}

	private void addCrawlRuleOptions() {
		domainSpecific = new DropdownOptionItem<String>(STANDARD,
				DOMAIN_SPECIFIC, SPECIFIED);
		domainSpecific.setValue(DOMAIN_SPECIFIC);
		affectsOthers(domainSpecific);
		addOptionItem(domainSpecific, "crawl rule selection");

		rules = new RulesOptionItem();
		affectsOthers(rules);
		edit = new JButton("edit rule");
		edit.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(final ActionEvent e) {
				showRuleEditor(false);
			}
		});
		final JButton create = new JButton("create rule");
		create.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(final ActionEvent e) {
				showRuleEditor(true);
			}
		});
		addOptionItem(rules, "crawl rule");

		final DummyVisoneOptionItem ruleBox = new DummyVisoneOptionItem(
				new GridBagLayout());
		final GridBagConstraints gbc = new GridBagConstraints();
		gbc.insets = new Insets(0, 3, 0, 3);
		gbc.gridy = 0;
		gbc.weightx = 1;
		ruleBox.add(new JLabel(), gbc);
		gbc.weightx = 0;
		ruleBox.add(edit, gbc);
		ruleBox.add(create, gbc);
		addDummyOptionItem(ruleBox);
	}

	protected abstract void fillAdditionalObscureOptions();

	/**
	 * Yields the {@link CrawlListener} to use.
	 * 
	 * @param settings
	 *            the {@link Settings} object used by the crawler
	 * 
	 * @return the listener, or null if an error occured and crawling cannot
	 *         start
	 */
	protected abstract CrawlListener getCrawlListener(Settings settings);

	private void showProgress(final Crawler c) {
		final Thread t = new Thread("Delayed CrawlProgress-starter") {
			@Override
			public void run() {
				try {
					synchronized (this) {
						wait(PROGRESS_DELAY);
					}
					final ProgressProducer pp = c.getProgressProducer();
					if (pp.isCrawling()) {
						final JFrame mainFrame = mediator.getWindow();
						final CrawlerProgress cp = new CrawlerProgress(
								mainFrame, pp);
						if (cp.isActive())
							cp.setVisible(true);
					}
				} catch (final InterruptedException e) {
					interrupt();
				}
			}
		};
		t.setDaemon(true);
		t.start();
	}

	private void setCookie() {
		final String cookie = cookieField.getValue().trim();
		if (!cookie.isEmpty())
			// overwrite cookie-field
			Helper4Crawler.addCustomHeader("Cookie:", cookie);
	}

	private void enableEdit() {
		edit.setEnabled(domainSpecific.getValue() != STANDARD
				&& rules.hasValue());
	}

	private void showRuleEditor(final boolean create) {
		String t = urlField.getValue();
		if (t.equals("http://"))
			t = "";
		final URL url = Helper4Crawler.getURL(t);
		final String rule = create ? null : rules.getValue();
		final JFrame mainFrame = mediator.getWindow();
		final String r = RuleEditorDialog.showRuleEditor(mainFrame, rule, url,
				!wantsIntro());
		if (r != null)
			rules.updateRules(r);
	}

	protected abstract boolean wantsIntro();

	protected Settings createSettings(final String url) {
		final Settings s = new Settings();
		s.userAgent = UserAgents.getUserAgent(browser.getValue());
		s.domainSpecific = domainSpecific.getValue() == DOMAIN_SPECIFIC;
		if (domainSpecific.getValue() == SPECIFIED) {
			final BlacklistFilter blf = new BlacklistFilter(null);
			RuleManager.addRule(rules.getValue(), blf, s.query);
			s.customAccepter.add(blf);
		}
		s.haltOnError = getHaltOnError();
		setCookie();
		return s;
	}

	protected abstract boolean getHaltOnError();

	/**
	 * Reports a crawling error to the user.
	 * 
	 * @param url
	 *            the url that caused the error
	 * @param e
	 *            the error
	 * @param parent
	 *            the parent component (for
	 *            {@link JOptionPane#showMessageDialog(Component, Object)})
	 */
	public static void printErrorDialog(final String url, final Exception e,
			final Component parent) {
		logger.error("error on page \"" + url + "\":" + e.getMessage(), e);
		final String msg = "<html>an error halted the crawling process while scanning<br>"
				+ url;
		JOptionPane.showMessageDialog(parent, msg, "error while crawling",
				JOptionPane.ERROR_MESSAGE);
	}

	private class RulesOptionItem extends DropdownOptionItem<String> {
		private RulesOptionItem() {
			super(RuleManager.getRules());
		}

		private void updateRules(final String newRule) {
			// need to rebuild because it may have replaced a builtin rule...
			model.removeAllElements();
			for (final String item : RuleManager.getRules())
				model.addElement(item);
			model.setSelectedItem(newRule);
		}
	}
}
