package de.visone.crawl.rules;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import org.apache.log4j.Logger;

import de.visone.ext.NetworkUtil;

/**
 * A special rule file. Behaves mostly like a {@link File}, but takes care of
 * handling the builtin rules, too: These are treated as if they were located in
 * the rule directory (see {@link #BASE}), but they only get created there if
 * they are written to.
 * 
 * @author matthias
 */
public class RuleFile {
	private static final Logger logger = Logger.getLogger(RuleFile.class);

	/** Base directory for rules. */
	private static final File BASE = NetworkUtil.VISONE_CONFIG_DIR;

	/** Package containing builtin rules, relative to {@link RuleManager}. */
	private static final String BUILTIN_RULES_SUBPACKAGE = "builtin/";

	private static final String[] STRING_ARRAY = new String[0];

	private final String name;

	private final URL builtinFile;

	private final File localFile;

	/**
	 * Creates a rule file with the given name.
	 * 
	 * @param ruleName
	 *            the rule name
	 */
	public RuleFile(final String ruleName) {
		name = ruleName;
		localFile = new File(BASE, ruleName);
		builtinFile = RuleManager.class.getResource(BUILTIN_RULES_SUBPACKAGE
				+ ruleName);
	}

	/**
	 * Checks if the file exists.
	 * 
	 * @return true if it does
	 */
	public boolean exists() {
		// local file overrides builtin file
		if (localFile.exists())
			return true;
		// if the local rule file doesn't exist but the builtin one does,
		// pretend that the rule exists locally
		if (builtinFile != null)
			return true;
		// neither local nor builtin rule
		return false;
	}

	/**
	 * Returns the name of the rule file.
	 * 
	 * @return the filename
	 */
	public String getName() {
		return name;
	}

	/**
	 * Returns an {@link InputStream} for reading from this file.
	 * 
	 * @return and input stream reading from either the local file (if it
	 *         exists) or the builtin file
	 * @throws FileNotFoundException
	 *             if neither of the two files exists
	 */
	public InputStream getInputStream() throws FileNotFoundException {
		// a local rule file overrides the builtin one, so if it exists locally,
		// use that file. this can fail with FileNotFoundException, but then
		// something's wrong because the file does indeed exist
		if (localFile.exists())
			return new FileInputStream(localFile);
		// if there is no local file, try using the builtin one instead
		try {
			if (builtinFile != null)
				return builtinFile.openStream();
		} catch (final IOException e) {
			logger.error("cannot read builtin rule file", e);
		}
		// else the file really doesn't exist
		throw new FileNotFoundException("no such file: " + name);
	}

	/**
	 * Returns an {@link OutputStream} for writing to this rule file.
	 * 
	 * @return an output stream referring to the local file
	 * @throws FileNotFoundException
	 *             if the rule file cannot be created for some reason
	 */
	public OutputStream getOutputStream() throws FileNotFoundException {
		// try creating the rule directory if it doesn't exist yet.
		if (!BASE.exists())
			if (!BASE.mkdirs())
				logger.warn("failed to create the rule directory " + BASE);

		return new FileOutputStream(localFile);
	}

	/**
	 * Compares two rule files. Only the filename is compared: If that is
	 * identical, the objects behave identically.
	 * 
	 * @param obj
	 *            the other rule file to compare to
	 * 
	 * @return true if the rule files are identical
	 */
	@Override
	public boolean equals(final Object obj) {
		if (obj instanceof RuleFile) {
			final RuleFile otherRule = (RuleFile) obj;
			return name.equals(otherRule.name);
		}
		return false;
	}

	/** the list of builtin rules. */
	private static final ArrayList<String> BUILTIN_RULES = new ArrayList<String>();

	static {

		try {

			final BufferedReader in = new BufferedReader(
					new InputStreamReader(
							RuleManager.class
									.getResourceAsStream("builtin/rules.list")));

			while (true) {
				final String line = in.readLine();
				if (line == null)
					break;
				BUILTIN_RULES.add(line);
			}
		} catch (final Exception e) {
			logger.error("cannot read list of builtin rules", e);
		}
	}

	/**
	 * Returns a list of all local and builtin rules.
	 * 
	 * @return the union of the sets of local and builtin files
	 */
	public static String[] getRules() {
		// get list of builtin rules...
		final Set<String> rules = new HashSet<String>(BUILTIN_RULES);
		// ...and add all locally existing rules
		try {
			rules.addAll(Arrays.asList(BASE.list(new FilenameFilter() {
				@Override
				public boolean accept(final File dir, final String filename) {
					return filename.endsWith(RuleManager.EXT);
				}
			})));

			// sort alphabetically (for user convenience)
			final String[] array = rules.toArray(STRING_ARRAY);
			Arrays.sort(array);
			return array;
		} catch (final Exception e) {
			logger.error("could not read rules for text crawling");
			return new String[] {};
		}
	}

	/**
	 * Check if this rule file is local, ie. refers to a locally existing rule
	 * file rather than a builtin one.
	 * 
	 * @return true if the file exists locally
	 */
	public boolean isLocal() {
		// if the local file exists, that file was used and the rule file is
		// local
		if (localFile.exists())
			return true;
		// otherwise it either refers to a builtin rule, or to a nonexistent
		// file. in either case, it isn't local
		return false;
	}

	/**
	 * Deletes the local rule file.
	 */
	public void delete() {
		localFile.delete();
	}
}
