package dareka;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.ServerSocketChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EventListener;
import java.util.HashSet;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import dareka.common.CloseUtil;
import dareka.common.Config;
import dareka.common.Logger;
import dareka.extensions.Extension;
import dareka.extensions.Extension2;
import dareka.extensions.ExtensionManager;
import dareka.extensions.RequestFilter;
import dareka.extensions.Rewriter;
import dareka.extensions.CompleteCache;
import dareka.extensions.SystemEventListener;
import dareka.processor.Processor;
import dareka.processor.impl.CacheDirProcessor;
import dareka.processor.impl.ConnectProcessor;
import dareka.processor.impl.DefaultRequestFilter;
import dareka.processor.impl.DmcApiProcessor;
import dareka.processor.impl.DmcCachingProcessor;
import dareka.processor.impl.EasyRewriter;
import dareka.processor.impl.ExtThumbProcessor;
import dareka.processor.impl.GetPostProcessor;
import dareka.processor.impl.GetThumbInfoProcessor;
import dareka.processor.impl.LocalDirProcessor;
import dareka.processor.impl.NicoCachingProcessor;
//import dareka.processor.impl.NicoRecordingUrlProcessor;
//import dareka.processor.impl.NicoRecordingWatchProcessor;
// [nl] Rewriterと競合するので↑は使わない (相当機能は対応するRewriterに実装)
import dareka.processor.impl.RewriterProcessor;
import dareka.processor.impl.ThumbProcessor;
import dareka.processor.impl.ThumbProcessor2;
import dareka.processor.impl.WorkaroundProcessor;

// テスト中(夏.03)
interface AddrPolicy {
	public boolean isAllowAddress(byte []ip);
}

class AddrPolicyFactory {
	public static AddrPolicy getAddrPolicy(String addr) {
		if (addr.equals("local")) {
			return null;
		} else if(addr.equals("all")) {
			return new AddrPolicy(){
				public boolean isAllowAddress(byte []ip) {
					return true;
				}
			};
		} else if (addr.equals("lan") || addr.equals("lanC")) {
			return new AddrPolicy(){
				public boolean isAllowAddress(byte []ip) {
					return ip[0] == -64/*192*/ && ip[1] == -88/*168*/;
				}
			};
		} else if (addr.equals("lanB")) {
			return new AddrPolicy(){
				public boolean isAllowAddress(byte []ip) {
					return ip[0] == -84/*172*/ && 16<= ip[1] && ip[1] < 32;
				}
			};
		} else if (addr.equals("lanA")) {
			return new AddrPolicy(){
				public boolean isAllowAddress(byte []ip) {
					return ip[0] == 10;
				}
			};
		}
		return null;
	}
}

public class Server implements Observer {
    private static final int MAX_WAITING_TIME = 10;

    private Config config;
    private ServerSocket serverSocket;
    private ExecutorService executor = Executors.newCachedThreadPool();
    private Set<ConnectionManager> liveWorkers =
            Collections.synchronizedSet(new HashSet<ConnectionManager>());
    private volatile boolean stopped = false;

    // they are able to be shared among threads.
    private Processor connectProcessor = new ConnectProcessor();
    private Processor getPostProcessor = new GetPostProcessor();
    private RewriterProcessor rewriterProcessor; // [nl]
    private WorkaroundProcessor workaroundProcessor = new WorkaroundProcessor();

    public Server(Config config) throws IOException {
        if (config == null) {
            throw new IllegalArgumentException("config must not be null");
        }

        this.config = config;

        // use channel to make it available Socket#getChannel() for non blocking
        // I/O.
        ServerSocketChannel serverCh = ServerSocketChannel.open();
        serverSocket = serverCh.socket();
        
        // [nl] 拡張Processorのロード
        rewriterProcessor = new RewriterProcessor(config);
        initClassLoader();
// UserFilterはExtensionの後に追加するように変更
        rewriterProcessor.addUserFilter();
    }

    /**
     * Receive an event which indicates completion of worker.
     * @see java.util.Observer#update(java.util.Observable, java.lang.Object)
     */
    public void update(Observable o, Object arg) {
        synchronized (liveWorkers) {
            if (liveWorkers.remove(o) == false) {
                Logger.warning("internal error: live worker mismatch");
            }
            if (stopped) {
                // This message may be printed before finalizing, but
                // decided not to make its own flag because this is
                // just looking issue.
                Logger.info("remaining worker=" + liveWorkers.size());
            }
        }
    }

    /**
     * Start the server. The thread which call this method is blocked until
     * stop() is called or some errors occurred.
     */
    public void start() {
        if (stopped) {
            return;
        }

        try { // ensure cleanup
            bindServerSocket();
            acceptServerSocket();
        } finally {
            Logger.info("finalizing");
            Logger.debugWithThread("stopping extensions"); // [nl]
            cleanupExtensions();
            Logger.debugWithThread("stopping accepting request");
            cleanupServerSocket();
            Logger.debugWithThread("stopping processing request");
            cleanupWorkers();
            Logger.debugWithThread("stopping threads");
            cleanupExecutor();
            Logger.info("finalized");

            if (liveWorkers.size() > 0) {
                Logger.warning("internal error: remaining live workers: "
                        + liveWorkers.size());
            }
        }
    }

    /**
     * Stop the server. Please call this method from another thread which called
     * start().
     */
    public synchronized void stop() {
        stopped = true;

        if (!serverSocket.isClosed()) {
            CloseUtil.close(serverSocket);
        }
        executor.shutdown();
    }

    private void bindServerSocket() {
		int port = Integer.getInteger("listenPort").intValue();
		String from = System.getProperty("allowFrom", "local");
		
		// [nl] 許可ホストの設定(夏.03)
		setAddrPolicy(from);
		
        try {
        	// [nl] IP制限
        	if(!from.equals("local")) {
        		serverSocket.bind(new InetSocketAddress(port));
        	} else {
            	serverSocket.bind(new InetSocketAddress(
            			InetAddress.getByName(null), port));
            }
        } catch (BindException e) {
        	Logger.info("ポート %d が既に使われているため起動できません。", port);
        	stop();
        } catch (IOException e) {
            Logger.error(e);
            stop();
        }
    }

	// [nl] デフォルトはlocalhostのみ許可
	private AddrPolicy allowFrom = null;
	public void setAddrPolicy(String param/*AddrPolicy policy*/) {
		allowFrom = AddrPolicyFactory.getAddrPolicy(param);
	}
	
	private boolean checkIpAddr(InetAddress ia) {
		if (ia.isLoopbackAddress())
			return true;
		if (allowFrom == null)
			return false;
		byte[] ip = ia.getAddress();
		return allowFrom.isAllowAddress(ip);
	}

    private void acceptServerSocket() {
        try {
            boolean timeoutSupportedOrUnknown = true;
            int timeout = Integer.getInteger("readTimeout").intValue();

            while (!stopped) {
                Socket client = serverSocket.accept();

                try { // ensure client.close() even in errors.
                    if (timeoutSupportedOrUnknown) {
                        client.setSoTimeout(timeout);
                        if (client.getSoTimeout() != timeout) {
                            Logger.warning("read timeout is not supported");
                            timeoutSupportedOrUnknown = false;
                        }
                    }

                    // [nl] lanの判定
                    if (!checkIpAddr(client.getInetAddress())) {
                    	Logger.info("Forbidden address: " +
                    			client.getInetAddress().getHostAddress());
                    	client.close();
                    	continue;
                    }

                    synchronized (this) { // avoid conflicting with stop()
                        if (stopped) {
                            break;
                        }

                        ConnectionManager worker;
                        worker = new ConnectionManager(config, client);
                        // TODO コーディングレスで登録できるようにする。

                        registerProcessor(new NicoCachingProcessor(executor),
                                worker);

                        // [nl] デフォルトフィルタの登録
                        worker.addRequestFilter(EasyRewriter.getInstance_sys());
                        worker.addRequestFilter(new DefaultRequestFilter());
                        
                        // /watch/smXXXXXX[XXX,XXX,XXX].mp4 へのアクセスを防ぐ
                        registerProcessor(workaroundProcessor, worker);
                        worker.addRequestFilter(workaroundProcessor);

                        // [nl] 拡張系の登録
                        registerExtProcessors(worker);

                        // [nl] ユーザヘッダフィルタの登録
                        worker.addRequestFilter(EasyRewriter.getInstance());

                        // [nl] Processorの登録
                        registerProcessor(new LocalDirProcessor(), worker);
                        registerProcessor(new CacheDirProcessor(), worker);
                        
                        if (Boolean.getBoolean("cacheThumbnail")) {
                        	if (!System.getProperty("thcacheMode").equals("folder")) {
                        		registerProcessor(new ThumbProcessor(), worker);
                        	} else {
                        		registerProcessor(new ThumbProcessor2(), worker);
                        	}
                        }
                        if (Boolean.getBoolean("cacheGetThumbInfo")) {
                        	registerProcessor(new GetThumbInfoProcessor(), worker);
                        }
                        if (Boolean.getBoolean("cacheExtThumb")) {
                        	registerProcessor(new ExtThumbProcessor(), worker);
                        }
                        registerProcessor(new DmcApiProcessor(), worker);
                        registerProcessor(new DmcCachingProcessor(executor), worker);
                        registerProcessor(rewriterProcessor, worker);

                        registerProcessor(getPostProcessor, worker);
                        registerProcessor(connectProcessor, worker);

                        // Observation must be prepared before call execute()
                        // to avoid loss of event in case of immediate
                        // complete
                        prepareObservation(worker);

                        executor.execute(worker);
                        // for debug
                        //new Thread(worker).start();
                    }
                } catch (Exception e) {
                    Logger.error(e);
                    CloseUtil.close(client);
                }
            }
        } catch (IOException e) {
            // stop() is called.
            // including AsynchronousCloseException (in NIO)
            Logger.debug(e);
        }
    }

    private static void registerProcessor(Processor processor, ConnectionManager worker) {
    	if (worker == null) {
    		return;
    	}
    	
        Pattern p = processor.getSupportedURLAsPattern();
        if (p == null) {
            String url = processor.getSupportedURLAsString();
            if (url != null) {
                p = Pattern.compile(url, Pattern.LITERAL);
            }
        }

        String[] methods = processor.getSupportedMethods();
        if (methods == null) {
            return;
        }

        for (String method : methods) {
            worker.addProcessor(method, p, processor);
        }
    }

    private void prepareObservation(ConnectionManager worker) {
        liveWorkers.add(worker);
        worker.addObserver(this);
    }

    private void cleanupServerSocket() {
        if (!serverSocket.isClosed()) {
            CloseUtil.close(serverSocket);
        }
    }

    /*private*/ void cleanupWorkers() { // [nl]
        synchronized (liveWorkers) {
            for (ConnectionManager worker : liveWorkers) {
                worker.stop();
            }
        }
    }

    private void cleanupExecutor() {
        for (int i = 0; i < 10 && !executor.isTerminated(); ++i) {
            try {
                Logger.debug("waiting for terminating threads...");
                executor.shutdownNow();
                if (executor.awaitTermination(MAX_WAITING_TIME,
                        TimeUnit.SECONDS)) {
                    Logger.debug("done");
                    break;
                } else {
                    Logger.debug("timed out");
                }
            } catch (InterruptedException e) {
                Logger.warning(e.toString());
            }
        }
    }

	// [nl] 拡張Processorのテスト版
	private URLClassLoader extLoader;
	private List<Extension> procExtensions = new ArrayList<Extension>();
	private List<Extension> filtExtensions = new ArrayList<Extension>();

	private final Extension.Type supportedProcType = Extension.Type.Processor1;
	//private final Extension.Type supportedReWrType = Extension.Type.Rewriter1;
	private final Extension.Type supportedFiltType = Extension.Type.RequestFilter1;
	
	//[nl] Extension2
	private List<Extension2> newExtensions = new ArrayList<Extension2>();
	
	private void initClassLoader() {
		URL[] urls = new URL[1];
		try {
			urls[0] = new File("").toURI().toURL();
		} catch (MalformedURLException e) { }
		extLoader = new URLClassLoader(urls);
		
		File extDir = new File("extensions");
		File[] files = extDir.listFiles(new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return name.endsWith(".class") && !name.contains("$") &&
// マージしたエクステンションは除外
					  !name.startsWith("nlThumbInfoRewriter") &&
					  !name.startsWith("nlSearchExtension") &&
					  !name.startsWith("dmc.");
			}
		});
		
		if (files == null || files.length == 0) {
			return;
		}
		Arrays.sort(files);
		
		// ロード時に一度だけ実行する初期化レジスタ
		ExtensionManager rRegister = new ExtensionManager() {
			public void registerProcessor(Processor p) {
				// セッション毎に登録するので初期化時は何もしない
			}
			public void registerRewriter(Rewriter r) {
				rewriterProcessor.addRewriter(r);
			}
			public void registerRequestFilter(RequestFilter f) {
				// セッション毎に登録するので初期化時は何もしない
			}
			public void registerCompleteCache(CompleteCache c) {
				NLMain.SHARED.addCompleteCache(c);
			}
			public void registerEventListener(EventListener l) {
				NLMain.SHARED.addEventListener(l);
			}
		};
		
		for (File file : files) {
			String className = file.getName().replace(".class", "");
			try {
				Object extObj = extLoader.loadClass(
								"extensions." + className).newInstance();
				//[nl] Extension2
				if (extObj instanceof Extension2) {
					Extension2 ext2 = (Extension2) extObj;
					Logger.info("Extension2: " + ext2.getVersionString());
					System.setProperty("extension." + className, "true");
					newExtensions.add(ext2);
					ext2.registerExtensions(rRegister);
					continue;
				}
				
				Extension extInterface = null;
				if (extObj instanceof Extension) {
					extInterface = (Extension) extObj;
				} else {
					continue;
				}
				
				Object obj = extInterface.queryInterface(Extension.Type.Processor1);
				if (obj instanceof Processor)
				{
					Logger.info("Processor Extension: " + extInterface.getVersionString());
					System.setProperty("extension." + className, "true");
					procExtensions.add(extInterface);
				}
				obj = extInterface.queryInterface(Extension.Type.Rewriter1);
				if (obj instanceof Rewriter)
				{
					Logger.info("Rewriter Extension: " + extInterface.getVersionString());
					System.setProperty("extension." + className, "true");
					rewriterProcessor.addRewriter((Rewriter) obj);
				}
				obj = extInterface.queryInterface(Extension.Type.RequestFilter1);
				if (obj instanceof RequestFilter)
				{
					Logger.info("RequestFilter Extension: " + extInterface.getVersionString());
					System.setProperty("extension." + className, "true");
					filtExtensions.add(extInterface);
				}
			} catch (Throwable t) {
				Logger.error(t);
				Logger.warning("skip extension: " + className);
			}
		}
		
		if (NLMain.SHARED.countSystemEventListeners() > 0) {
			new PeriodicalCaller().start();
		}
	}
	
	class PeriodicalCaller extends Thread {
		static final long INTERVAL = 60 * 1000L;
		PeriodicalCaller() {
			super("PeriodicalCaller");
			this.setPriority(MIN_PRIORITY);
		}
		public void run() {
			Logger.debugWithThread("started");
			
			long epoch = System.currentTimeMillis() / 1000L * 1000L;
			while (!stopped) {
				try {
//long start = System.currentTimeMillis();
					NLMain.SHARED.notifySystemEvent(
							SystemEventListener.PERIODIC_CALL, null, false);
//Logger.debugWithThread(String.format("%d called in %,dms",
//		start, System.currentTimeMillis() - start));
					long diff = (System.currentTimeMillis() - epoch) % INTERVAL;
					Thread.sleep(INTERVAL - diff);
				} catch (Exception e) {
					Logger.debugWithThread(e);
				}
			}
		}
	}
	
	private void cleanupExtensions() {
		for (Extension2 entry : newExtensions) {
			// リスナが実装されているかどうかで一方のみ呼び出す
			if (entry instanceof SystemEventListener) {
				((SystemEventListener) entry).onSystemEvent(
						SystemEventListener.SYSTEM_EXIT, null);
			} else {
				try { // onSystemExit() があれば呼び出す
					entry.getClass().getMethod("onSystemExit",
							new Class[0]).invoke(entry, new Object[0]);
				} catch (Exception e) {}
			}
		}
	}

    public void registerExtProcessors(ConnectionManager worker) {
    	ExtensionRegister eRegister = new ExtensionRegister(worker);
    	// Extension2
    	for (Extension2 entry : newExtensions) {
    		entry.registerExtensions(eRegister);
    	}
    	
    	// Processor
    	for (Extension entry : procExtensions) {
    		registerProcessor((Processor)
    				entry.queryInterface(supportedProcType), worker);
    	}
    	
    	// RequestFilter
    	for (Extension entry : filtExtensions) {
    		registerRequestFilter((RequestFilter)
    				entry.queryInterface(supportedFiltType), worker);
    	}
    }

    // ConnectionManagerの初期化に使用
    class ExtensionRegister implements ExtensionManager {
    	ConnectionManager worker;
    	ExtensionRegister(ConnectionManager worker) {
    		this.worker = worker;
    	}
    	public void registerProcessor(Processor p) {
    		Server.registerProcessor(p, worker);
    	}
    	public void registerRewriter(Rewriter r) { 
    		// do nothing
    	}
    	public void registerRequestFilter(RequestFilter f) {
    		worker.addRequestFilter(f);
    	}
    	public void registerCompleteCache(CompleteCache c) {
    		// do nothing
    	}
    	public void registerEventListener(EventListener e) {
    		// do nothing
    	}
    }

    private void registerRequestFilter(RequestFilter filter, ConnectionManager worker) {
    	if (worker == null) {
    		return;
    	}
    	
    	worker.addRequestFilter((RequestFilter) filter);
    }

    RewriterProcessor getRewriterProcessor() {
    	return rewriterProcessor;
    }
}
