package logbook.gui;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;

import logbook.config.AppConfig;
import logbook.constants.AppConstants;
import logbook.gui.listener.SelectedListener;
import logbook.gui.logic.LayoutLogic;
import logbook.util.AwtUtils;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Dialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Spinner;
import org.eclipse.swt.widgets.Text;

/**
 * キャプチャダイアログ
 *
 */
public final class CaptureDialog extends Dialog {

    private static class LoggerHolder {
        /** ロガー */
        private static final Logger LOG = LogManager.getLogger(CaptureDialog.class);
    }

    private Shell shell;

    private Composite composite;
    private Text text;
    private Button capture;
    private Button interval;
    private Spinner intervalms;

    private Rectangle rectangle;
    private Timer timer;
    private boolean isAlive;

    private Font font;

    /**
     * Create the dialog.
     * @param parent
     */
    public CaptureDialog(Shell parent) {
        super(parent, SWT.CLOSE | SWT.TITLE | SWT.MIN | SWT.RESIZE);
        this.setText("キャプチャ");
    }

    /**
     * Open the dialog.
     */
    public void open() {
        try {
            this.createContents();
            this.shell.open();
            this.shell.layout();
            Display display = this.getParent().getDisplay();
            while (!this.shell.isDisposed()) {
                if (!display.readAndDispatch()) {
                    display.sleep();
                }
            }
        } finally {
            // タイマーを停止させる
            if (this.timer != null) {
                this.timer.cancel();
            }
            // フォントを開放
            if (this.font != null) {
                this.font.dispose();
            }
        }
    }

    /**
     * Create contents of the dialog.
     */
    private void createContents() {
        // シェル
        this.shell = new Shell(this.getParent(), this.getStyle());
        this.shell.setText(this.getText());
        // レイアウト
        GridLayout glShell = new GridLayout(1, false);
        glShell.horizontalSpacing = 1;
        glShell.marginHeight = 1;
        glShell.marginWidth = 1;
        glShell.verticalSpacing = 1;
        this.shell.setLayout(glShell);

        // 太字にするためのフォントデータを作成する
        FontData defaultfd = this.shell.getFont().getFontData()[0];
        FontData fd = new FontData(defaultfd.getName(), defaultfd.getHeight(), SWT.BOLD);
        this.font = new Font(Display.getDefault(), fd);

        // コンポジット
        Composite rangeComposite = new Composite(this.shell, SWT.NONE);
        rangeComposite.setLayout(new GridLayout(2, false));

        // 範囲設定
        this.text = new Text(rangeComposite, SWT.BORDER | SWT.READ_ONLY);
        GridData gdText = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1);
        gdText.widthHint = 120;
        this.text.setLayoutData(gdText);
        this.text.setText("範囲が未設定です");

        Button button = new Button(rangeComposite, SWT.NONE);
        button.setText("範囲を選択");
        button.addSelectionListener(new SelectRectangleAdapter());

        // コンポジット
        this.composite = new Composite(this.shell, SWT.NONE);
        GridLayout loglayout = new GridLayout(3, false);
        this.composite.setLayout(loglayout);
        this.composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.FILL_VERTICAL));

        // 周期設定
        this.interval = new Button(this.composite, SWT.CHECK);
        this.interval.addSelectionListener((SelectedListener) e -> {
            this.capture.setText(getCaptureButtonText(false, this.interval.getSelection()));
        });
        this.interval.setText("周期");

        this.intervalms = new Spinner(this.composite, SWT.BORDER);
        this.intervalms.setMaximum(60000);
        this.intervalms.setMinimum(100);
        this.intervalms.setSelection(1000);
        this.intervalms.setIncrement(100);

        Label label = new Label(this.composite, SWT.NONE);
        label.setText("ミリ秒");

        this.capture = new Button(this.shell, SWT.NONE);
        this.capture.setFont(this.font);
        GridData gdCapture = new GridData(GridData.FILL_HORIZONTAL | GridData.FILL_VERTICAL);
        gdCapture.horizontalSpan = 3;
        gdCapture.heightHint = 64;
        this.capture.setLayoutData(gdCapture);
        this.capture.setEnabled(false);
        this.capture.setText(getCaptureButtonText(false, this.interval.getSelection()));
        this.capture.addSelectionListener(new CaptureStartAdapter());

        this.shell.pack();
    }

    /**
     * キャプチャボタンの文字を取得します
     *
     * @param isrunning
     * @param interval
     * @return
     */
    private static String getCaptureButtonText(boolean isrunning, boolean interval) {
        if (isrunning && interval) {
            return "停 止";
        } else if (interval) {
            return "開 始";
        } else {
            return "キャプチャ";
        }
    }

    /**
     * 範囲の選択を押した時
     *
     */
    public final class SelectRectangleAdapter extends SelectionAdapter {
        /** ダイアログが完全に消えるまで待つ時間 */
        private static final int WAIT = 250;

        @Override
        public void widgetSelected(SelectionEvent paramSelectionEvent) {
            try {
                Display display = Display.getDefault();
                // ダイアログを非表示にする
                CaptureDialog.this.shell.setVisible(false);
                // 消えるまで待つ
                Thread.sleep(WAIT);
                // ディスプレイに対してGraphics Contextを取得する(フルスクリーンキャプチャ)
                GC gc = new GC(display);
                Image image = new Image(display, display.getBounds());
                gc.copyArea(image, 0, 0);
                gc.dispose();

                try {
                    // 範囲を取得する
                    Rectangle rectangle = new FullScreenDialog(CaptureDialog.this.shell, image,
                            CaptureDialog.this.shell.getMonitor())
                                    .open();

                    if ((rectangle != null) && (rectangle.width > 1) && (rectangle.height > 1)) {
                        CaptureDialog.this.rectangle = rectangle;
                        CaptureDialog.this.text.setText("(" + rectangle.x + "," + rectangle.y + ")-("
                                + (rectangle.x + rectangle.width) + "," + (rectangle.y + rectangle.height) + ")");
                        CaptureDialog.this.capture.setEnabled(true);
                    }
                } finally {
                    image.dispose();
                }
                CaptureDialog.this.shell.setVisible(true);
                CaptureDialog.this.shell.setActive();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * キャプチャボタンを押した時
     *
     */
    public final class CaptureStartAdapter extends SelectionAdapter {

        @Override
        public void widgetSelected(SelectionEvent e) {
            Timer timer = CaptureDialog.this.timer;

            Rectangle rectangle = CaptureDialog.this.rectangle;
            boolean interval = CaptureDialog.this.interval.getSelection();
            int intervalms = CaptureDialog.this.intervalms.getSelection();

            if (timer != null) {
                // タイマーを停止させる
                timer.cancel();
                timer = null;
            }

            if (CaptureDialog.this.isAlive) {

                CaptureDialog.this.capture.setText(getCaptureButtonText(false, interval));
                LayoutLogic.enable(CaptureDialog.this.composite, true);
                CaptureDialog.this.isAlive = false;
            } else {
                timer = new Timer(true);
                if (interval) {
                    // 固定レートで周期キャプチャ
                    timer.scheduleAtFixedRate(new CaptureTask(rectangle), 0, intervalms);
                    CaptureDialog.this.isAlive = true;
                } else {
                    // 一回だけキャプチャ
                    timer.schedule(new CaptureTask(rectangle), 0);
                }

                CaptureDialog.this.capture.setText(getCaptureButtonText(true, interval));

                if (interval) {
                    LayoutLogic.enable(CaptureDialog.this.composite, false);
                }
            }
            CaptureDialog.this.timer = timer;
        }
    }

    /**
     * 画面キャプチャスレッド
     *
     */
    public static final class CaptureTask extends TimerTask {
        /** Jpeg品質 */
        private static final float QUALITY = 0.9f;
        /** 日付フォーマット(ファイル名) */
        private SimpleDateFormat fileNameFormat;
        /** キャプチャ範囲 */
        private final Rectangle rectangle;
        /** トリム範囲 */
        private java.awt.Rectangle trimRect;

        public CaptureTask(Rectangle rectangle) {
            this.rectangle = rectangle;
            try {
                this.fileNameFormat = new SimpleDateFormat(AppConfig.get().getImageNameFormat());
            } catch (IllegalArgumentException e) {
                this.fileNameFormat = new SimpleDateFormat(AppConstants.DATE_LONG_FORMAT);
            }
        }

        @Override
        public void run() {
            try {
                // 時刻からファイル名を作成
                Date now = Calendar.getInstance().getTime();

                // 範囲をキャプチャする
                BufferedImage image = AwtUtils.getCapture(this.rectangle);

                String dir = AppConfig.get().getCapturePath();
                String name = this.fileNameFormat.format(now) + "." + AppConfig.get().getImageFormat();
                Path path = Paths.get(dir, name);

                File file = path.toFile();

                if (file.exists()) {
                    if (file.isDirectory()) {
                        throw new IOException("File '" + file + "' exists but is a directory");
                    }
                    if (!(file.canWrite()))
                        throw new IOException("File '" + file + "' cannot be written to");
                } else {
                    File parent = file.getParentFile();
                    if ((parent != null) &&
                            (!(parent.mkdirs())) && (!(parent.isDirectory()))) {
                        throw new IOException("Directory '" + parent + "' could not be created");
                    }
                }

                if (image != null) {

                    try (ImageOutputStream ios = ImageIO.createImageOutputStream(file)) {
                        ImageWriter writer = ImageIO.getImageWritersByFormatName(AppConfig.get().getImageFormat())
                                .next();
                        try {
                            ImageWriteParam iwp = writer.getDefaultWriteParam();
                            if (iwp.canWriteCompressed()) {
                                iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                                iwp.setCompressionQuality(QUALITY);
                            }
                            writer.setOutput(ios);

                            if (this.trimRect == null) {
                                this.trimRect = AwtUtils.getTrimSize(image);
                            }

                            writer.write(null, new IIOImage(AwtUtils.trim(image, this.trimRect), null, null), iwp);
                        } finally {
                            writer.dispose();
                        }
                    }
                }
            } catch (Exception e) {
                LoggerHolder.LOG.warn("キャプチャ中に例外が発生しました", e);
            }
        }
    }
}