/**
 * Copyright (C) 2007 Aelitis, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program 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 General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 * AELITIS, SAS au capital de 63.529,40 euros
 * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
 *
 */

package org.gudy.azureus2.core3.ipfilter.impl;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.ipfilter.IpRange;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.util.*;

import com.aelitis.azureus.ui.UIFunctions;
import com.aelitis.azureus.ui.UIFunctionsManager;

import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloader;
import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloaderAdapter;
import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloaderException;

import org.gudy.azureus2.pluginsimpl.local.utils.resourcedownloader.ResourceDownloaderFactoryImpl;

/**
 * @author TuxPaper
 * @created Jun 5, 2007
 *
 */
public class IpFilterAutoLoaderImpl
{
	private static final LogIDs LOGID = LogIDs.CORE;

	public static final String CFG_AUTOLOAD_LAST = "Ip Filter Autoload Last Date";
	
	public static final String CFG_AUTOLOAD_FILE = "Ip Filter Autoload File";
  

	private static AEMonitor class_mon = new AEMonitor(
			"IpFilterAutoLoaderImpl:class");

	private Object timerEventFilterReload;

	private final IpFilterImpl ipFilter;
	
	public IpFilterAutoLoaderImpl(IpFilterImpl ipFilter) {
		this.ipFilter = ipFilter;
		COConfigurationManager.setLongDefault(CFG_AUTOLOAD_LAST, 0);
		COConfigurationManager.setStringDefault(CFG_AUTOLOAD_FILE, "");
	}

	/**
	 * Load dat filter as specified at http://wiki.phoenixlabs.org/wiki/DAT_Format
	 * @param fin 
	 * @throws Exception
	 *
	 * @since 3.0.1.5
	 */
	private void loadDATFilters(InputStream fin) {
		try {
			class_mon.enter();

			List new_ipRanges = new ArrayList(1024);

			InputStreamReader streamReader = null;
			BufferedReader reader = null;
			try {
				Pattern pattern = Pattern.compile("^(.*):([0-9\\.]+)[^0-9]+([0-9\\.]+).*");
				int parseMode = -1;

				//open the file
				// TODO: test charset fallback (should fallback to ascii)
				streamReader = new InputStreamReader(fin, "utf8");
				reader = new BufferedReader(streamReader);

				int numConsecutiveUnknowns = 0;

				while (numConsecutiveUnknowns < 1000) {
					String line = reader.readLine();
					//System.out.println("line=" + line);
					if (line == null) {
						break;
					}

					line = line.trim();

					if (line.startsWith("#") || line.length() == 0) {
						continue;
					}

					String description = "";
					String startIp = null;
					String endIp = null;
					int level = 0;

					if (parseMode <= 0 || parseMode == 1) {
						Matcher matcher = pattern.matcher(line);
						if (matcher.find()) {
							if (parseMode != 1) {
								parseMode = 1;
							}
							description = matcher.group(1);
							startIp = matcher.group(2);
							endIp = matcher.group(3);
						} else {
							Logger.log(new LogEvent(LOGID, LogEvent.LT_WARNING,
									"unrecognized line while reading ip filter: " + line));
						}
					}

					if (parseMode != 1) {
						if (parseMode != 2) {
							parseMode = 2;
						}

						// spec says:
						//    1.1.1.1, 1.1.1.2, 100, moo
						// but I've seen dash format, such as
						//    1.1.1.1 - 1.1.1.2, 100, moo
						// so 	 for both
						String[] sections = line.split(" *[-,] *", 4);

						if (sections.length >= 2) {
							if (sections[0].indexOf('.') < 0 || sections[1].indexOf('.') < 0
									|| sections[0].length() > 15 || sections[1].length() > 15
									|| sections[0].length() < 7 || sections[1].length() < 7) {
								numConsecutiveUnknowns++;
								continue;
							}
						}

						if (sections.length >= 4) {
							// simple format:
							// startip, endip, level, desc
							startIp = sections[0];
							endIp = sections[1];
							description = sections[3];
							try {
								level = Integer.parseInt(sections[2]);
							} catch (NumberFormatException e) {
								description = sections[2] + " " + description;
							}
							for (int i = 4; i < sections.length; i++) {
								description += " " + sections[i];
							}
							numConsecutiveUnknowns = 0;
						} else if (sections.length == 3) {
							startIp = sections[0];
							endIp = sections[1];
							description = sections[2];
							numConsecutiveUnknowns = 0;
						} else if (sections.length == 2) {
							startIp = sections[0];
							endIp = sections[1];
							numConsecutiveUnknowns = 0;
						} else {
							numConsecutiveUnknowns++;
							continue;
						}

						if (level >= 128) {
							continue;
						}
					}

					if (startIp == null || endIp == null) {
						continue;
					}

					IpRangeImpl ipRange = new IpRangeImpl(description, startIp, endIp,
							true);

					//System.out.println(parseMode + ":" + description + ";" + ipRange.getStartIp());
					ipRange.setAddedToRangeList(true);

					new_ipRanges.add(ipRange);
				}
			} catch (IOException e) {
				Debug.out(e);
			} finally {

				if (reader != null) {
					try {
						reader.close();
					} catch (Throwable e) {
					}
				}
				if (streamReader != null) {
					try {
						streamReader.close();
					} catch (Throwable e) {
					}
				}

				Iterator it = new_ipRanges.iterator();

				while (it.hasNext()) {

					((IpRange) it.next()).checkValid();
				}

				ipFilter.markAsUpToDate();
			}
		} finally {

			class_mon.exit();
		}
	}

	private int getP2BFileVersion(InputStream is) {
		try {
			// first 4 are 255
			for (int i = 0; i < 4; i++) {
				int byteRead = is.read();
				if (byteRead != 255) {
					return -1;
				}
			}

			// next 'P2B'
			byte[] MAGIC = new byte[] {
				'P',
				'2',
				'B'
			};
			for (int i = 0; i < MAGIC.length; i++) {
				byte b = MAGIC[i];
				if (b != is.read()) {
					return -1;
				}
			}

			// next: version no
			int p2bVersion = is.read();
			Logger.log(new LogEvent(LOGID, "Log Filter: loading p2b version "
					+ p2bVersion));
			return p2bVersion;
		} catch (IOException e) {
			Debug.out(e);
		}

		return -1;
	}

	protected void loadOtherFilters(boolean allowAsyncDownloading,
			boolean loadOldWhileAsyncDownloading) {
		int p2bVersion = -1;
		try {
			class_mon.enter();

			List new_ipRanges = new ArrayList(1024);

			InputStream fin = null;
			BufferedInputStream bin = null;
			boolean isURL = false;
			try {
				//open the file
				String file = COConfigurationManager.getStringParameter(CFG_AUTOLOAD_FILE);
				Logger.log(new LogEvent(LOGID, "IP Filter file: " + file));
				File filtersFile = new File(file);
				if (filtersFile.exists()) {
					isURL = false;
				} else {
					if (!UrlUtils.isURL(file)) {
						return;
					}

					isURL = true;

					filtersFile = FileUtil.getUserFile("ipfilter.dl");
					if (filtersFile.exists()) {
						if (allowAsyncDownloading) {
							Logger.log(new LogEvent(LOGID, "Downloading " + file + "  async"));

							downloadFiltersAsync(new URL(file));

							if (!loadOldWhileAsyncDownloading) {
								return;
							}
						}
					} else {
						// no old dl, download sync now
						Logger.log(new LogEvent(LOGID, "sync Downloading " + file));
						try {
							ResourceDownloader rd = ResourceDownloaderFactoryImpl.getSingleton().create(
									new URL(file));
							fin = rd.download();
							FileUtil.copyFile(fin, filtersFile);
							setNextAutoDownload(true);
						} catch (ResourceDownloaderException e) {
							return;
						}
					}
				}

				fin = new FileInputStream(filtersFile);
				bin = new BufferedInputStream(fin, 16384);

				// extract (g)zip'd file and open that
				byte[] headerBytes = new byte[2];
				bin.mark(3);
				bin.read(headerBytes, 0, 2);
				bin.reset();

				if (headerBytes[1] == (byte) 0x8b && headerBytes[0] == 0x1f) {
					GZIPInputStream gzip = new GZIPInputStream(bin);

					filtersFile = FileUtil.getUserFile("ipfilter.ext");
					FileUtil.copyFile(gzip, filtersFile);
					fin = new FileInputStream(filtersFile);
					bin = new BufferedInputStream(fin, 16384);
				} else if (headerBytes[0] == 0x50 && headerBytes[1] == 0x4b) {
					ZipInputStream zip = new ZipInputStream(bin);

					ZipEntry zipEntry = zip.getNextEntry();
					// Skip small files
					while (zipEntry != null && zipEntry.getSize() < 1024 * 1024) {
						zipEntry = zip.getNextEntry();
					}

					if (zipEntry == null) {
						return;
					}
					filtersFile = FileUtil.getUserFile("ipfilter.ext");
					FileUtil.copyFile(zip, filtersFile);
					fin = new FileInputStream(filtersFile);
					bin = new BufferedInputStream(fin, 16384);
				}

				bin.mark(8);

				p2bVersion = getP2BFileVersion(bin);

				if (p2bVersion < 1 || p2bVersion > 3) {
					bin.reset();
					loadDATFilters(bin);
					return;
				}

				byte[] descBytes = new byte[255];
				byte[] ipBytes = new byte[4];
				String encoding = p2bVersion == 1 ? "ISO-8859-1" : "UTF-8";

				if (p2bVersion == 1 || p2bVersion == 2) {
					while (true) {
						String description = readString(bin, descBytes, encoding);

						int read = bin.read(ipBytes);
						if (read < 4) {
							break;
						}
						int startIp = ByteFormatter.byteArrayToInt(ipBytes);
						read = bin.read(ipBytes);
						if (read < 4) {
							break;
						}
						int endIp = ByteFormatter.byteArrayToInt(ipBytes);

						IpRangeImpl ipRange = new IpRangeImpl(description, startIp, endIp,
								true);

						ipRange.setAddedToRangeList(true);

						new_ipRanges.add(ipRange);
					}
				} else { // version 3
					int read = bin.read(ipBytes);
					if (read < 4) {
						return;
					}
					int numDescs = ByteFormatter.byteArrayToInt(ipBytes);
					String[] descs = new String[numDescs];
					for (int i = 0; i < numDescs; i++) {
						descs[i] = readString(bin, descBytes, encoding);
					}

					read = bin.read(ipBytes);
					if (read < 4) {
						return;
					}
					int numRanges = ByteFormatter.byteArrayToInt(ipBytes);
					for (int i = 0; i < numRanges; i++) {
						read = bin.read(ipBytes);
						if (read < 4) {
							return;
						}
						int descIdx = ByteFormatter.byteArrayToInt(ipBytes);

						read = bin.read(ipBytes);
						if (read < 4) {
							return;
						}
						int startIp = ByteFormatter.byteArrayToInt(ipBytes);

						read = bin.read(ipBytes);
						if (read < 4) {
							return;
						}
						int endIp = ByteFormatter.byteArrayToInt(ipBytes);

						String description = descIdx < descs.length && descIdx >= 0
								? descs[descIdx] : "";

						IpRangeImpl ipRange = new IpRangeImpl(description, startIp, endIp,
								true);

						ipRange.setAddedToRangeList(true);

						new_ipRanges.add(ipRange);
					}
				}
			} catch (IOException e) {
				Debug.out(e);
			} finally {

				if (bin != null) {
					try {
						bin.close();
					} catch (Throwable e) {
					}
				}
				if (fin != null) {
					try {
						fin.close();
					} catch (Throwable e) {
					}
				}

				Iterator it = new_ipRanges.iterator();

				while (it.hasNext()) {

					((IpRange) it.next()).checkValid();
				}

				ipFilter.markAsUpToDate();

				if (!isURL) {
					setFileReloadTimer();
				}
			}
		} finally {

			class_mon.exit();
		}
	}

	/**
	 * 
	 *
	 * @since 3.0.1.5
	 */
	private void setFileReloadTimer() {
		if (timerEventFilterReload instanceof TimerEvent) {
			((TimerEvent) timerEventFilterReload).cancel();
		} else if (timerEventFilterReload instanceof TimerEventPeriodic) {
			((TimerEventPeriodic) timerEventFilterReload).cancel();
		}
		timerEventFilterReload = SimpleTimer.addPeriodicEvent("IP Filter download",
				60000, new TimerEventPerformer() {
					long lastFileModified;

					public void perform(TimerEvent event) {
						event.cancel();

						String file = COConfigurationManager.getStringParameter(CFG_AUTOLOAD_FILE);
						File filtersFile = new File(file);
						if (!filtersFile.exists()) {
							return;
						}
						long fileModified = filtersFile.lastModified();

						if (lastFileModified == 0) {
							lastFileModified = fileModified;
						} else if (lastFileModified != fileModified) {
							try {
								// reload will create a new periodic time
								ipFilter.reload();
							} catch (Exception e) {
							}
						}
					}
				});
	}

	/**
	 * @param url
	 *
	 * @since 3.0.1.5
	 */
	private void downloadFiltersAsync(URL url) {
		ResourceDownloader rd = ResourceDownloaderFactoryImpl.getSingleton().create(
				url);
		// old dl exists, load old one while new one downloads async
		rd.addListener(new ResourceDownloaderAdapter() {
			// @see org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloaderAdapter#reportPercentComplete(org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloader, int)
			public void reportPercentComplete(ResourceDownloader downloader,
					int percentage) {
			}

			public boolean completed(ResourceDownloader downloader, InputStream data) {
				try {
					setNextAutoDownload(true);

					Logger.log(new LogEvent(LOGID, "downloaded..waiting"));
					// since this is a different thread, we can use class_mon as
					// a cheap semaphore to wait until previous load completes 
					class_mon.enter();
					Logger.log(new LogEvent(LOGID, "downloaded.. copying"));

					try {
						FileUtil.copyFile(data, FileUtil.getUserFile("ipfilter.dl"));
						AEThread thread = new AEThread("reload ipfilters", true) {
							public void runSupport() {
								try {
									UIFunctions uif = UIFunctionsManager.getUIFunctions();
									if (uif != null) {
										uif.setStatusText("Reloading Filters..");
									}
									ipFilter.reload(false);
									if (uif != null) {
										uif.setStatusText(null);
									}
								} catch (Exception e) {
									Debug.out(e);
								}
							}
						};
						thread.setPriority(Thread.NORM_PRIORITY - 1);
						thread.start();
					} catch (Exception e) {
						Debug.out(e);
					}
				} finally {
					class_mon.exit();
				}

				return true;
			}
		});
		rd.asyncDownload();
	}

	public void setNextAutoDownload(boolean updateLastDownloadedDate) {
		long now = SystemTime.getCurrentTime();
		long lastDL;

		if (updateLastDownloadedDate) {
			COConfigurationManager.setParameter(CFG_AUTOLOAD_LAST, now);
			lastDL = now;
		} else {
			lastDL = COConfigurationManager.getLongParameter(CFG_AUTOLOAD_LAST);
			if (lastDL > now) {
				lastDL = now;
				COConfigurationManager.setParameter(CFG_AUTOLOAD_LAST, now);
			}
		}

		long nextDL = lastDL + (86400000L * 7);

		if (timerEventFilterReload instanceof TimerEvent) {
			((TimerEvent) timerEventFilterReload).cancel();
		} else if (timerEventFilterReload instanceof TimerEventPeriodic) {
			((TimerEventPeriodic) timerEventFilterReload).cancel();
		}
		timerEventFilterReload = SimpleTimer.addEvent("IP Filter download", nextDL,
				new TimerEventPerformer() {
					public void perform(TimerEvent event) {
						String file = COConfigurationManager.getStringParameter(CFG_AUTOLOAD_FILE);
						try {
							downloadFiltersAsync(new URL(file));
						} catch (MalformedURLException e) {
						}
					}
				});
	}

	/**
	 * @param bin
	 * @param descBytes
	 * @param encoding
	 * @return
	 *
	 * @since 3.0.1.5
	 */
	private String readString(BufferedInputStream bin, byte[] descBytes,
			String encoding) {
		int pos = 0;
		try {
			while (true) {
				int byteRead = bin.read();
				if (byteRead < 0) {
					break;
				}
				if (pos < descBytes.length) {
					descBytes[pos] = (byte) byteRead;
					pos++;
				}
				if (byteRead == 0) {
					break;
				}
			}
		} catch (IOException e) {
		}

		if (pos > 1) {
			try {
				return new String(descBytes, 0, pos - 1, encoding);
			} catch (UnsupportedEncodingException e) {
			}
		}

		return "";
	}
}
