package dareka;

import java.awt.AWTException;
import java.awt.Color;
import java.awt.Component;
import java.awt.Desktop;
import java.awt.Font;
import java.awt.Image;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.Rectangle;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URL;
import java.util.LinkedHashMap;
import java.util.Properties;

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.text.DefaultCaret;

import dareka.common.CloseUtil;
import dareka.common.DefaultLoggerHandler;
import dareka.common.Logger;
import dareka.common.LoggerHandler;
import dareka.extensions.Extension2;
import dareka.processor.impl.Cache;
import dareka.processor.impl.NLShared;

/**
 * NicoCache_nl 用 CUI/GUI ランチャー
 * @since NicoCache_nl+110118mod
 */
public class NLMain {
    /** このパッケージ内で NLShared を使うためのインスタンス */
    static final NLShared SHARED = NLShared.getInstance();
    
    static ExtLoggerHandler extLoggerHandler;
    static GUILauncher guiLauncher;
    private static boolean debugMode;
    private static Thread mainThread;
    
    public static void main(String[] args) {
        if (isStartGUI()) {
            try {
                guiLauncher = new GUILauncher();
            } catch (Throwable t) {
                Logger.error(t);
                System.exit(-1);
            }
        }
        debugMode = Boolean.getBoolean("dareka.debug");
        mainThread = Thread.currentThread();
        
        DefaultLoggerHandler.setDebug(debugMode);
        Logger.setHandler(extLoggerHandler = new ExtLoggerHandler());
        if (guiLauncher != null) {
            guiLauncher.init();
        }
        
        Main.main(args);
        
        synchronized (NLMain.class) { // shutdownから抜けるまで待つ
            extLoggerHandler.close();
            if (guiLauncher != null) {
                guiLauncher.close();
                System.exit(0);
            }
        }
    }
    
    static boolean isStartGUI() {
        // オプションが指定されているならそちらに従う
        String dareka_gui = System.getProperty("dareka.gui");
        if (dareka_gui != null) {
            return Boolean.parseBoolean(dareka_gui);
        }
        // オプションが指定されていないならコンソールの有無で判断
        try {
            return System.console() == null;
        } catch (NoSuchMethodError e) {
            Logger.debugWithThread(e);
        }
        return false;
    }
    
    static native void setNativeWindowProc(JFrame frame, String params);
    
    static synchronized void shutdown() {
        if (guiLauncher != null) {
            guiLauncher.activatePrimaryTab();
        }
        Logger.debugWithThread("NLMain.shutdown() called");
        
        Main.stop();
        
        if (Thread.currentThread() == mainThread) {
            return;
        }
        try { // 終了するまで待つ
            long timeout = Long.getLong("shutdownTimeout", 60000L);
            long interval = 500L;
            while (!Main.isDone() && timeout > 0L) {
                Thread.sleep(interval);
                timeout -= interval;
            }
        } catch (InterruptedException e) {
            // do nothing.
        }
    }
    
    static synchronized void disconnect() {
        Logger.debugWithThread("NLMain.disconnect() called");
        Main.disconnect();
    }
    
    /**
     * デバッグモードか？
     * @return デバッグモードならtrue
     */
    public static boolean isDebugMode() {
        return debugMode;
    }
    
    /**
     * GUI起動しているか？
     * @return GUI起動しているならtrue
     */
    public static boolean isLaunchGUI() {
        return guiLauncher != null;
    }
    
    /**
     * ログウィンドウにタブを追加する。GUI 起動していなければ何もしない。
     * @see javax.swing.JTabbedPane#addTab(String, Icon, Component, String)
     * @since NicoCache_nl+110122mod
     */
    public static synchronized void addTab(
            String title, Icon icon, Component component, String tip) {
        if (guiLauncher != null) {
            guiLauncher.getTabbedPane().addTab(title, icon, component, tip);
        }
    }
    
    static class ExtLoggerHandler extends DefaultLoggerHandler {
        static PrintWriter writer;
        
        public ExtLoggerHandler() {
            if (writer == null) reset();
        }
        
        void reset() {
            close();
            String logfile = System.getProperty("dareka.logfile");
            if (logfile != null && logfile.length() > 0) {
                try {
                    writer = new PrintWriter(new BufferedWriter(
                            new FileWriter(logfile)), true);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        
        void close() {
            if (writer != null) {
                writer.close(); writer = null;
            }
        }
        
        @Override
        public void debug(String message) {
            super.debug(message);
            if (NLMain.isDebugMode()) {
                String debugMes = "DEBUG: " + message;
                if (writer != null) writer.println(debugMes);
                GUILauncher.appendDebug(debugMes);
            }
        }
        
        @Override
        public void info(String message) {
            this.info(message, true);
        }
        
        protected void info(String message, boolean appendGUI) {
            super.info(message);
            if (writer != null) writer.println(message);
            if (appendGUI) GUILauncher.append(message);
        }
        
        @Override
        public void warning(String message) {
            super.warning(message);
            if (writer != null) writer.println(message);
            GUILauncher.append(message);
        }
        
        @Override
        public void error(Throwable t) {
            this.info(getStackTraceString(t));
        }
    }
    
    static class ExtLogger extends ExtLoggerHandler {
        String prefix, debugKey;
        boolean guiOnly;
        GUILauncher.LogPane extPane;
        
        public ExtLogger(String prefix, String debugKey, String tip, boolean guiOnly) {
            if (guiLauncher != null) {
                String title = prefix;
                if (title == null) {
                    if (debugKey != null && debugKey.endsWith("Debug")) {
                        title = debugKey.substring(0, debugKey.length() - 5);
                    } else {
                        title = tip.split("[\\s\\+_-]+")[0];
                    }
                } else if (title.startsWith("TABONLY:")) {
                    title = title.substring(8);
                    prefix = null;
                }
                extPane = guiLauncher.addExtPane(title, tip);
            }
            if (prefix != null) {
                this.prefix = prefix + ": ";
            } else {
                this.prefix = "";
            }
            this.debugKey = debugKey;
            this.guiOnly = guiOnly;
        }
        
        @Override
        public void debug(String message) {
            if (Boolean.getBoolean(debugKey)) {
                String debugMes = "DEBUG: " + prefix + message;
                if (!guiOnly) super.info(debugMes, false);
                if (extPane != null) {
                    // GUIの場合はdebugPaneのみに出力する
                    GUILauncher.appendDebug(debugMes);
                    extPane.append("DEBUG: " + message);
                }
            }
        }
        
        @Override
        public void info(String message) {
            if (!guiOnly) super.info(prefix + message);
            if (extPane != null) extPane.append(message);
        }
        
        @Override
        public void warning(String message) {
            if (!guiOnly) super.warning(prefix + message);
            if (extPane != null) extPane.append(message);
        }
        
        @Override
        public void error(Throwable t) {
            this.info(getStackTraceString(t));
        }
    }
    
    /**
     * 拡張ロガーのインスタンスを取得する。GUI 起動の場合は専用タブを追加する。
     * なお、デバッグモードの出力は {@linkplain LoggerHandler#info(String)}
     * を使って通常ログとして出力するので注意。
     * 
     * @param extension 呼び出し元の Extension
     * @param prefix ログに付加するプレフィックス(GUI 起動の場合はタブ名も兼ねる)。
     * プレフィックスが "TABONLY:" から始まる場合はタブ名としてのみ扱う。
     * また、null を指定した場合はログにプレフィックスは付加しない。
     * その場合のタブ名は debugKey から末尾の "Debug" を除いた文字列となる。
     * debugKey も null の場合は Extension のバージョン文字列を用いる。
     * @param debugKey Extension 固有のデバッグモードを判断するプロパティキー。
     * nullを指定した場合は prefix + "Debug" を使用する
     * @param guiOnly true なら GUI の専用タブのみに出力する
     * @return 拡張ロガーのインスタンス
     * @since NicoCache_nl+110125mod
     */
    public static LoggerHandler getExtLogger(
            Extension2 extension, String prefix, String debugKey, boolean guiOnly) {
        if (debugKey == null && prefix != null) {
            debugKey = prefix + "Debug";
        }
        return new ExtLogger(prefix, debugKey, extension.getVersionString(), guiOnly);
    }
    
    /**
     * guiOnly = false
     * @see #getExtLogger(Extension2, String, String, boolean)
     */
    public static LoggerHandler getExtLogger(
            Extension2 extension, String prefix, String debugKey) {
        return getExtLogger(extension, prefix, debugKey, false);
    }
}

class GUILauncher {
    static File propFile    = new File("NicoCacheGUI.property");
    static File extIconFile = new File("NicoCacheGUI_Icon.gif");
    static ConfigGUI config = new ConfigGUI();
    static Image iconImage;
    static {
        if (extIconFile.exists()) {
            ImageIcon icon = new ImageIcon(extIconFile.getPath());
            if (icon.getIconWidth() > 0) {
                iconImage = icon.getImage();
            }
        }
        if (iconImage == null) {
            URL url = GUILauncher.class.getResource("GUILauncherIcon.gif");
            iconImage = new ImageIcon(url).getImage();
        }
    }
    
    static LauncherTray tray;
    static LogWindow logWindow;
    static Rectangle logWindowRect;
    
    public GUILauncher() {
        boolean debugMode = config.getBoolean("DebugMode");
        if (System.getProperty("dareka.debug") != null) { // コマンドライン優先
            debugMode = Boolean.getBoolean("dareka.debug");
        }
        if (debugMode) {
            System.setProperty("dareka.debug", "true");
            if (System.getProperty("dareka.logfile") == null) {
                System.setProperty("dareka.logfile", config.getProperty("DebugLog"));
            }
        }
        try {
            if (SystemTray.isSupported()) {
                tray = new LauncherTray();
            }
        } catch (NoClassDefFoundError e) {
            Logger.debugWithThread(e);
        }
        logWindow = new LogWindow(debugMode);
        logWindowRect = logWindow.frame.getBounds();
    }
    
    void init() {
        Logger.debug("launching NicoCacheGUI mode");
        
        // Windowsの場合は一度ウィンドウを表示してEventQueueThreadを走らせる
        // また、シャットダウン対策のためJNIを使ってWM_ENDSESSIONを処理する
        if (System.getProperty("os.name").startsWith("Windows")) {
            logWindow.frame.setVisible(true);
            
            String libname = "NicoCacheGUI_native";
            if (System.getProperty("os.arch").equals("amd64")) {
                libname += "64";
            }
            if (new File(libname + ".dll").exists()) {
                try {
                    System.loadLibrary(libname);
                    String params = config.getKeyValue("DisableSuspend");
                    NLMain.setNativeWindowProc(logWindow.frame, params);
                    Logger.debug(libname + " loaded");
                } catch (UnsatisfiedLinkError e) {
                    Logger.error(e);
                }
            }
        }
        logWindow.frame.setVisible(
                tray == null || !config.getBoolean("HideWindow"));
        if (tray != null) {
            Logger.debug("trayIconSize=" + tray.trayIcon.getSize());
        }
    }
    
    void activatePrimaryTab() {
        if (logWindow.debugPane != null) {
            getTabbedPane().setSelectedIndex(1);
            logWindow.debugPane.setBackLog(false);
        } else {
            getTabbedPane().setSelectedIndex(0);
            logWindow.mainPane.setBackLog(false);
        }
    }
    
    void close() {
        if (tray != null) {
            tray.close(); tray = null;
        }
        if (logWindow.frame != null) {
            Rectangle rect = logWindow.frame.getBounds();
            if (!rect.equals(logWindowRect)) {
                config.setInteger("LogWindowX", rect.x);
                config.setInteger("LogWindowY", rect.y);
                config.setInteger("LogWindowW", rect.width);
                config.setInteger("LogWindowH", rect.height);
            }
            logWindow.frame.repaint();
            logWindow.frame.dispose();
            logWindow.frame = null;
        }
        config.save();
    }
    
    JTabbedPane getTabbedPane() {
        return logWindow.tabbedPane;
    }
    
    LogPane addExtPane(String title, String tip) {
        return logWindow.addTab(title, tip, config.getInteger("MaxLines") / 2);
    }
    
    static void append(String log) {
        if (logWindow != null) {
            logWindow.mainPane.append(log);
            appendDebug(log);
        }
    }
    
    static void appendDebug(String log) {
        if (logWindow != null && logWindow.debugPane != null) {
            logWindow.debugPane.append(log);
        }
    }
    
    static class ConfigGUI {
        Properties defaults, properties;
        boolean changed = true;
        
        ConfigGUI() {
            defaults = new Properties();
            defaults.setProperty("LogWindowX", "16");
            defaults.setProperty("LogWindowY", "16");
            defaults.setProperty("LogWindowW", "640");
            defaults.setProperty("LogWindowH", "480");
            defaults.setProperty("LogWindowAlwaysOnTop", "true");
            defaults.setProperty("LogWindowLineWrap", "true");
            defaults.setProperty("DebugMode", "falae");
            defaults.setProperty("DebugLog", "debug.log");
            defaults.setProperty("ExitOnClose", "false");
            defaults.setProperty("FlipColor", "false");
            defaults.setProperty("FontName", Font.MONOSPACED);
            defaults.setProperty("FontSize", "14");
            defaults.setProperty("HideWindow", "false");
            defaults.setProperty("MaxLines", "1000");
            defaults.setProperty("MaxLinesHard", "30000");
            
            // 全ての値を書き出す必要は無いのでデフォルト値指定はしない
            properties = new Properties();
            for (Object o : defaults.keySet()) {
                String key = (String) o;
                properties.setProperty(key, defaults.getProperty(key));
            }
            if (propFile.exists()) {
                load();
            }
        }
        
        void load() {
            FileInputStream in = null;
            try {
                in = new FileInputStream(propFile);
                properties.load(in);
                changed = false;
            } catch (IOException e) {
                Logger.error(e);
            } finally {
                CloseUtil.close(in);
            }
        }
        
        void save() {
            if (!changed) return;
            
            FileOutputStream out = null;
            try {
                out = new FileOutputStream(propFile);
                properties.store(out, "NicoCache_nl GUI Properties");
                changed = false;
            } catch (IOException e) {
                Logger.error(e);
            } finally {
                CloseUtil.close(out);
            }
        }
        
        String getProperty(String key) {
            return properties.getProperty(key);
        }
        
        Object setProperty(String key, String value) {
            changed = true;
            return properties.setProperty(key, value);
        }
        
        boolean getBoolean(String key) {
            return Boolean.parseBoolean(properties.getProperty(key));
        }
        
        void setBoolean(String key, boolean value) {
            changed = true;
            setProperty(key, String.valueOf(value));
        }
        
        int getInteger(String key) {
            try {
                return Integer.parseInt(properties.getProperty(key));
            } catch (Exception e) {
                Logger.error(e);
            }
            return Integer.parseInt(defaults.getProperty(key));
        }
        
        void setInteger(String key, int value) {
            setProperty(key, String.valueOf(value));
        }
        
        String getKeyValue(String key) {
            String value = getProperty(key);
            if (value == null) {
                return "";
            }
            return key + "=" + value + ";";
        }
    }
    
    static class LauncherTray {
        TrayIcon trayIcon;
        
        LauncherTray() {
            PopupMenu popup = new PopupMenu();
            
            addMenuItem("ニコニコ動画を開く", new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    openBrowser("http://www.nicovideo.jp/");
                }
            }, popup);
            
            addMenuItem("キャッシュページを開く", new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    openBrowser("http://www.nicovideo.jp/cache/");
                }
            }, popup);
            
            addMenuItem("キャッシュフォルダを開く", new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    openFolder(new File(Cache.getCacheDir()));
                }
            }, popup);
            
            popup.addSeparator();
            
            addMenuItem("ログウインドウを表示", new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    logWindow.frame.setVisible(true);
                }
            }, popup);
            
            addMenuItem("プロキシ接続を全切断", new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    NLMain.disconnect();
                }
            }, popup);
            
            popup.addSeparator();
            
            addMenuItem("NicoCache_nl を終了", new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    NLMain.shutdown();
                }
            }, popup);
            
            trayIcon = new TrayIcon(iconImage, Main.getVersion(), popup);
            trayIcon.setImageAutoSize(true);
            trayIcon.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    logWindow.frame.setVisible(true); // ダブルクリックでログを表示
                }
            });
            try {
                SystemTray.getSystemTray().add(trayIcon);
            } catch (AWTException e) {
                Logger.error(e);
            }
        }
        
        void close() {
            SystemTray.getSystemTray().remove(trayIcon);
        }
        
        void addMenuItem(String label, ActionListener listener, PopupMenu popup) {
            MenuItem menuItem = new MenuItem(label);
            menuItem.addActionListener(listener);
            popup.add(menuItem);
        }
        
        void openBrowser(String url) {
            Desktop desk = Desktop.getDesktop();
            try {
                desk.browse(new URL(url).toURI());
            } catch (Exception e) {
                Logger.error(e);
            }
        }
        
        void openFolder(File file) {
            Desktop desk = Desktop.getDesktop();
            try {
                desk.open(file);
            } catch (Exception e) {
                Logger.error(e);
            }
        }
    }
    
    static class LogWindow {
        JFrame frame = new JFrame();
        JTabbedPane tabbedPane = new JTabbedPane();
        LinkedHashMap<Integer, LogPane> tabs = new LinkedHashMap<Integer, LogPane>();
        LogPane mainPane, debugPane;
        String windowTitle = "NicoCache_nl";
        
        LogWindow(boolean debugMode) {
            int maxLines = config.getInteger("MaxLines");
            mainPane = addTab("main", "通常ログ", maxLines);
            if (debugMode) {
                debugPane = addTab("debug", "デバッグログ", maxLines * 10);
                windowTitle += "：デバッグモード";
            }
            tabbedPane.addChangeListener(new ChangeListener() {
                public void stateChanged(ChangeEvent e) {
                    LogPane pane = tabs.get(tabbedPane.getSelectedIndex());
                    if (pane != null) {
                        pane.updateLogWindowTitle();
                    } else {
                        appendTitle(null); // Extension独自タブ
                    }
                }
            });
            frame.add(tabbedPane);
            frame.addWindowListener(new WindowAdapter() {
                public void windowClosing(WindowEvent e) {
                    if (tray == null || config.getBoolean("ExitOnClose")) {
                        NLMain.shutdown();
                    } else {
                        frame.setVisible(false);
                    }
                }
            });
            frame.setAlwaysOnTop(
                    config.getBoolean("LogWindowAlwaysOnTop"));
            frame.setBounds(
                    config.getInteger("LogWindowX"),
                    config.getInteger("LogWindowY"),
                    config.getInteger("LogWindowW"),
                    config.getInteger("LogWindowH"));
            frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
            frame.setIconImage(iconImage);
            frame.setTitle(windowTitle);
        }
        
        void appendTitle(String append) {
            if (append != null) {
                frame.setTitle(windowTitle + " (" + append + ")");
            } else {
                frame.setTitle(windowTitle);
            }
        }
        
        LogPane addTab(String title, String tip, int maxLines) {
            final LogPane pane = new LogPane(title, tip, maxLines, this);
            if ("main".equals(title)) {
                pane.popup.addSeparator();
                
                addCheckBoxMenuItem("右端で折り返す",
                        "LogWindowLineWrap", new ChangeListener() {
                    public void stateChanged(ChangeEvent e) {
                        boolean value = isSelected(e);
                        for (LogPane pane : tabs.values()) {
                            pane.textArea.setLineWrap(value);
                        }
                        config.setBoolean("LogWindowLineWrap", value);
                    }
                }, pane.popup);
                
                addCheckBoxMenuItem("常に最前面に表示",
                        "LogWindowAlwaysOnTop", new ChangeListener() {
                    public void stateChanged(ChangeEvent e) {
                        boolean value = isSelected(e);
                        frame.setAlwaysOnTop(value);
                        config.setBoolean("LogWindowAlwaysOnTop", value);
                    }
                }, pane.popup);
                
                if (tray != null) {
                    pane.popup.addSeparator();
                    
                    addCheckBoxMenuItem("起動時に隠す",
                            "HideWindow", pane.popup);
                    addCheckBoxMenuItem("「閉じる」で終了",
                            "ExitOnClose",pane.popup);
                } else {
                    Logger.warning("SystemTray not supported.");
                }
            }
            tabs.put(tabbedPane.getTabCount(), pane);
            tabbedPane.addTab(title, null, pane.scrollPane, tip);
            
            return pane;
        }
        
        void addCheckBoxMenuItem(String label, String key, JPopupMenu popup) {
            final String configKey = key;
            addCheckBoxMenuItem(label, configKey, new ChangeListener() {
                public void stateChanged(ChangeEvent e) {
                    config.setBoolean(configKey, isSelected(e));
                }
            }, popup);
        }
        
        void addCheckBoxMenuItem(String label, String key,
                ChangeListener listener, JPopupMenu popup) {
            boolean checked = config.getBoolean(key);
            JCheckBoxMenuItem menuItem = new JCheckBoxMenuItem(label, checked);
            menuItem.addChangeListener(listener);
            popup.add(menuItem);
        }
        
        boolean isSelected(ChangeEvent e) {
            return ((JCheckBoxMenuItem)e.getSource()).isSelected();
        }
    }
    
    static class LogPane {
        static final Font FONT = new Font(
                config.getProperty("FontName"),
                Font.PLAIN,
                config.getInteger("FontSize"));
        JTextArea textArea = new JTextArea();
        JPopupMenu popup = new JPopupMenu();
        JScrollPane scrollPane = new JScrollPane(textArea,
                JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
                JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        int maxLines;
        int maxLinesInBacklog;
        LogWindow logWindow;
        
        LogPane(String title, String tip,
                int maxLines, LogWindow logWindow) {
            this.maxLines = Math.min(maxLines, config.getInteger("MaxLinesHard"));
            this.logWindow = logWindow;
            this.maxLinesInBacklog = config.getInteger("MaxLinesHard");
            setupTextArea();
            setupPopupMenu();
            setupScrollPane();
        }
        
        void setupTextArea() {
            textArea.setEditable(false);
            textArea.setFont(new Font(
                    config.getProperty("FontName"),
                    Font.PLAIN,
                    config.getInteger("FontSize")));
            textArea.setLineWrap(
                    config.getBoolean("LogWindowLineWrap"));
            textArea.addFocusListener(new FocusAdapter() {
                public void focusGained(FocusEvent e) {
                    updateLogWindowTitle();
                }
            });
            if (config.getBoolean("FlipColor")) {
                Color bg = textArea.getBackground();
                Color fg = textArea.getForeground();
                textArea.setBackground(fg);
                textArea.setForeground(bg);
            }
        }
        
        void setupPopupMenu() {
            JMenuItem menuItem;
            
            menuItem = new JMenuItem("コピー");
            menuItem.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    textArea.copy();
                }
            });
            popup.add(menuItem);
            
            menuItem = new JMenuItem("全選択");
            menuItem.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    textArea.selectAll();
                }
            });
            popup.add(menuItem);
            
            // ログウインドウ内の右クリックメニュー
            textArea.addMouseListener(new MouseAdapter() {
                public void mousePressed(MouseEvent e) {
                    // 右クリックの場合はポップアップを表示
                    if (e.getButton() == MouseEvent.BUTTON3) {
                        popup.show((Component)(e.getSource()), e.getX(), e.getY());
                    }
                }
            });
        }
        
        void setupScrollPane() {
            scrollPane.addMouseWheelListener(backLogListener);
            scrollPane.getVerticalScrollBar().addMouseListener(backLogListener);
            scrollPane.getViewport().addChangeListener(new ChangeListener() {
                boolean changedByMyself;
                int lastHeight;
                public void stateChanged(ChangeEvent e) {
                    if (changedByMyself) {
                        changedByMyself = false;
                        return;
                    }
                    int height =  textArea.getHeight();
                    if (lastHeight != height && !isBackLog()) {
                        // Viewportを一番下に移動
                        Rectangle rect = scrollPane.getViewport().getViewRect();
                        rect.setLocation(rect.x, height - rect.height);
                        changedByMyself = true; // scrollRectToVisibleは時々stateChangedを発生させる
                        textArea.scrollRectToVisible(rect);
                        changedByMyself = true;
                    }
                    lastHeight = height;
                }
            });
        }
        
        DefaultCaret caret = new DefaultCaret();
        MouseAdapter backLogListener = new MouseAdapter() {
            JScrollBar scrollBar = scrollPane.getVerticalScrollBar();
            {
                caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
                textArea.setCaret(caret);
            }
            public void mouseReleased(MouseEvent e) {
                updateBackLog(e);
            }
            public void mouseWheelMoved(MouseWheelEvent e) {
                updateBackLog(e);
            }
            private void updateBackLog(MouseEvent e) {
                int max = scrollBar.getMaximum() - scrollBar.getVisibleAmount();
                setBackLog(scrollBar.getValue() < max);
                updateLogWindowTitle();
            }
        };
        
        boolean isBackLog() {
            return caret.getUpdatePolicy() == DefaultCaret.NEVER_UPDATE;
        }
        
        void setBackLog(boolean backlog) {
            if (backlog) {
                caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
            } else {
                caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
                caret.setDot(textArea.getDocument().getLength());
            }
        }
        
        void updateLogWindowTitle() {
            logWindow.appendTitle(isBackLog() ? "バックログ" : null);
        }
        
        void append(String log) {
            if (textArea == null) {
                return;
            }
            synchronized (textArea) {
                textArea.append(log + "\n");
                
                // 最大ログ行数を超えたら先頭から順に削除
                int threashold = isBackLog() ? maxLinesInBacklog : maxLines;
                if (textArea.getLineCount() > threashold) {
                    try {
                        int offset = textArea.getLineEndOffset(
                                textArea.getLineCount() - threashold - 1);
                        textArea.replaceRange(null, 0, offset);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        
    }
}
