/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.util;

import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.image.BufferedImage;
import java.awt.AWTException;
import javax.imageio.ImageIO;

import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.FlavorListener;
import java.awt.datatransfer.FlavorEvent;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.io.File;

/**
 * DisplayCapture.java は、画面イメージをキャプチャして、ファイルに書き出すためのクラスです。
 *
 * 基本的な使い方は、main メソッドから立ち上げて、クリップボードの状態を監視します。
 * クリップボードに、"GUI:画面ID xxxxx.jsp" 形式の文字が書き込まれると、flavorsChanged イベントが
 * 発生して、画面を、ファイルに書き出す処理が実行されます。
 * エンジンの機能と連動すれば、画面IDやファイル名をクリップボード経由でこのアプリケーションに
 * 渡すことで、画面操作を行うだけで、自動的に画面キャプチャを行うことが可能です。
 *
 * エンジンでは、jsp/indexc.jsp に、この機能が組み込まれているため、gf\BAT\displayCapture の
 * DisplayCapture.bat を起動後、アプリケーションを、jsp/indexc.jsp で呼び出せば、自動的に
 * 画面のキャプチャを開始できます。
 * キャプチャは、全画面のみですので、IEを最大に広げて操作してください。
 * また、｢Prnt Scrn｣ボタンにも対応していますので、操作中やポップアップ等の非自動化の
 * 画面キャプチャも、手動で取得できます。
 *
 * 起動時の引数に応じて、処理を制御することが可能です。
 *
 * 書き出すフォルダは、BASE_DIR で指定します。
 *  すべてのキャプチャ画像は、ベースフォルダ に集約して保存します。
 *  キャプチャ画像の情報は、出力されるファイル名に反映されます。ファイル名の形式も２種類あり、
 *  キャプチャ順と、画面ID順が指定できます。
 *  初期値は、Java起動時のフォルダになります。
 *
 * 書き出すファイル名の初期形式は、firstID の設定により異なります。
 *   firstID を gui に設定した場合
 *      ベースフォルダでフォルダを作成し、その中に、画面ID_JSPファイル名_連番.画像形式 ファイルを作成します。
 *      画面ID 単位に、画面のキャプチャを整理、使用したい場合に便利です。
 *      ファイルのタイムスタンプ(作成時刻)で並び替えを行えば、キャプチャ順に並び替えできます。
 *   firstID を seq に設定した場合
 *      ベースフォルダでフォルダを作成し、その中に、連番_画面ID_JSPファイル名.画像形式 ファイルを作成します。
 *      ファイルは、キャプチャされた順番に、画面IDも混在して作成されます。つまり、ファイル名の順番に
 *      再生すれば、リンクや他のシステムとの連携などで、画面が行き来しても、作業の順番にキャプチャできて
 *      いる事になります。
 *
 * このクラスは、これらを実現するために利用している、static メソッドをいくつか持っています。
 *   BufferedImage doCapture()
 *      画面イメージをキャプチャします。これは、全画面です。
 *   void saveImage( File saveFile, BufferedImage img, String imgType )
 *      指定のファイルに、画面イメージを書き出します。
 *      imgType に、画像の種類(png|gif|jpg)を指定します(初期値:png)。
 *   String getClipboard()
 *      現在のクリップボードの値を取り出します。ここでは、文字列のみ取り出すことが可能です。
 *      このメソッドの特徴的なところは、PrintScreenなどの文字列以外の値をクリップボードにセット
 *      した場合に、"GUI:PRINT SCREEN.img" という文字列を返すところです。つまり、その場合は、
 *      全画面のキャプチャが行われるという事です。
 *   void setClipboard( String txt )
 *      クリップボードに、文字列をセットします。
 *
 * このクラスが実装している FlavorListener は、クリップボードの"値"の更新には追従していません。
 * 内部の Transferable オブジェクトが変更された場合に、flavorsChanged メソッドが呼び出されます。
 * つまり、一度セットされた文字型データは、取り出した後、別のTransferable オブジェクトに変更して
 * おかないと、次の文字列の変更が拾えなくなります。また、この別のTransferableオブジェクトの
 * 設定で、再び、イベントが発生するので、そのままでは、無限ループになってしまいます。
 * そこで、少し、トリッキーなのですが、setClipboard( String ) すると、再びイベントが呼び出され
 * ないように、取得した文字列の先頭が、"GUI:" で始まる場合のみ、再設定するようにしています。
 *
 * Usage: java org.opengion.fukurou.util.DisplayCapture
 *                   [BASE_DIR] [firstID(seq|gui)] [imageFormat(png|gif|jpg)] [startCnt(100)]
 *
 *     args[0]  BASE_DIR    : キャプチャファイルをセーブするベースとなるディレクトリ(初期値:起動フォルダ)
 *     args[1]  firstID     : キャプチャ画像をセーブするファイル方式を指定します(初期値:seq)
 *                            gui (画面ID_JSPファイル名_連番.画像形式)
 *                            seq (連番_画面ID_JSPファイル名.画像形式)
 *     args[2]  imageFormat : 作成するイメージの形式。png|gif|jpg のどれか(初期値:png)
 *     args[3]  startCnt    : セーブファイル名をユニークにするためのカウント(初期値:100)
 *
 * この実装は同期化されません。
 *
 * @og.rev 5.1.7.0 (2010/06/01) 新規追加
 * @og.rev 5.2.1.0 (2010/10/01) 実用性を重視した改修
 *
 * @version  5.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK6.0,
 */
public final class DisplayCapture implements FlavorListener {
	private static final Clipboard CLIP_BOARD = Toolkit.getDefaultToolkit().getSystemClipboard();
	private static final Rectangle SCRN_SIZE  = new Rectangle( Toolkit.getDefaultToolkit().getScreenSize() );

	private File	baseDir		= new File(".");		// セーブするベースディレクトリ
	private String	firstID		= "seq";				// 保存時のファイル名の形式(seq|gui)
	private String	imgType		= "png" ;				// 画像形式(png|gif|jpg)
	private int		cnt			= 100;					// ユニーク番号(セーブファイル名に付与)

	/**
	 * キャプチャファイルをセーブするベースとなるディレクトリを設定します(初期値:java実行フォルダ)。
	 *
	 * クラスの内部的には、Java の実行フォルダ( new File(".") ) が初期値です。
	 *
	 * @param	bsDir	セーブするベースディレクトリ
	 * @throws RuntimeException セーブフォルダが作成できなかった場合
	 */
	public void setBaseDir( final String bsDir ) {
		if( bsDir != null && bsDir.length() > 0 ) {
			baseDir = new File( bsDir );

			// ベースフォルダの作成。初期値は起動フォルダなので、必ず存在する(はず)
			if( !baseDir.exists() && !baseDir.mkdirs() ) {
				String errMsg = "ERROR:セーブフォルダが作成できませんでした。" + baseDir.getAbsolutePath()  ;
				throw new RuntimeException( errMsg );
			}
		}
	}

	/**
	 * キャプチャ画像をセーブするファイル方式を指定します(初期値:seq)。
	 *
	 *    seq (連番_画面ID_JSPファイル名.画像形式)
	 *    gui (画面ID_JSPファイル名_連番.画像形式)
	 *
	 * 初期値は、seq です。
	 *
	 * @param	firstID	セーブするファイル方式(seq|gui)
	 * @throws RuntimeException ファイル方式の指定が間違っていた場合
	 */
	public void setFirstID( final String firstID ) {
		if( firstID != null ) {
			if( firstID.matches( "seq|gui" ) ) {
				this.firstID = firstID;
			}
			else {
				String errMsg = "ERROR:firstID 属性は、(seq|gui)でお願いします。firstID=[" + firstID + "]" ;
				throw new RuntimeException( errMsg );
			}
		}
	}

	/**
	 * キャプチャ画像をセーブする画像形式を指定します(初期値:png)。
	 *
	 * キャプチャされたイメージをセーブするときの画像形式を指定できます。
	 * ここでは、png , gif ,jpg を指定できます。
	 *
	 * 初期値は、png 形式です。
	 *
	 * @param	imgType	セーブする画像形式(png|gif|jpg)
	 */
	public void setImageType( final String imgType ) {
		if( imgType != null && imgType.matches( "png|gif|jpg" ) ) {
			this.imgType = imgType;
		}
	}

	/**
	 * キャプチャ画像をセーブするファイル名の先頭に付ける連番の開始数(初期値:100)。
	 *
	 * キャプチャされたイメージをセーブするとき、画面IDとJSPファイル名だけでは、前回分を
	 * 上書きしてしまうため、ファイル名の先頭に連番を付与しています。
	 * ここでは、その連番の開始番号を指定できます。
	 *
	 * 初期値は、100 です。
	 *
	 * @param	startCnt	連番の開始数(初期値:100)
	 */
	public void setStartCnt( final String startCnt ) {
		if( startCnt != null && startCnt.length() > 0 ) {
			cnt = Integer.parseInt( startCnt );
		}
	}

	/**
	 * 全画面の画像イメージ(キャプチャ画像)を取得します。
	 *
	 * java.awt.Toolkit で、全画面のスクリーンサイズを取得し、java.awt.Robot の
	 * createScreenCapture( Rectangle ) メソッドで、BufferedImage を取得しています。
	 *
	 * @return	全画面の画像イメージ
	 * @throws RuntimeException AWTException が発生した場合
	 */
	public static BufferedImage doCapture() {
		BufferedImage img = null;
		try{
			Robot robo = new Robot();
			img = robo.createScreenCapture( SCRN_SIZE );
		}
		catch( AWTException ex ) {
//			ex.printStackTrace();
			String errMsg = "ERROR:画像イメージ(キャプチャ画像)が取得できませんでした。" ;
			setClipboard( errMsg );
			throw new RuntimeException( errMsg,ex );
		}
		catch( Throwable th ) {
			String errMsg = "ERROR:" + th.getLocalizedMessage() ;
			setClipboard( errMsg );
			throw new RuntimeException( errMsg,th );
		}

		return img;
	}

	/**
	 * キャプチャ画像をファイルにセーブします。
	 *
	 * ここでは、単純に、引数そのままで、ImageIO.write( BufferedImage,String,File ) しています。
	 * saveFile のディレクトリ存在チェックや、ファイル名の拡張子(png,gif,jpgなど)の修正、
	 * imgType の形式チェックなどは、行っていません。
	 * それらの処理は、事前に、調整しておいてください。
	 *
	 * @param	img	セーブする画像イメージ
	 * @param	imgType	セーブする画像形式(png|gif|jpg)
	 * @param	saveFile	セーブする画像ファイルオブジェクト
	 * @throws RuntimeException IOException が発生した場合
	 * @see  javax.imageio.ImageIO#write( java.awt.image.RenderedImage , String , java.io.File )
	 */
	public static void saveImage( final BufferedImage img , final String imgType , final File saveFile ) {
		try{
			ImageIO.write( img,imgType,saveFile );
		}
		catch( IOException ex ) {
//			ex.printStackTrace();
			String errMsg = "ERROR:キャプチャ画像をファイルにセーブできませんでした。" + saveFile.getAbsolutePath()  ;
			setClipboard( errMsg );
			throw new RuntimeException( errMsg,ex );
		}
		catch( Throwable th ) {
			String errMsg = "ERROR:" + th.getLocalizedMessage() ;
			setClipboard( errMsg );
			throw new RuntimeException( errMsg,th );
		}
	}

	/**
	 * システムのクリップボードの文字列を取得します。
	 *
	 * Toolkit.getDefaultToolkit().getSystemClipboard() で取得された  Clipboard オブジェクトから
	 * 文字列情報(DataFlavor.stringFlavor)を取得します。
	 * 文字列情報が取得できない場合、(UnsupportedFlavorException が発生した場合) 例えば、
	 * PrntScrn ボタンが押された場合などは、文字列として、"GUI:PRINT SCREEN.img" を返します。
	 * これは、文字列が返せない場合でも、クリップボードに書き込まれたイベントで、全画面のキャプチャを
	 * 取得するための、特殊なコマンドに相当します。
	 *
	 * @return	クリップボードの文字列
	 * @throws RuntimeException IOException が発生した場合
	 * @see  java.awt.datatransfer.Clipboard#getData( DataFlavor )
	 */
	public static String getClipboard() {
		String strClip = null;

		// 方法として、Transferable を取得後、getTransferData する事もできる。
	//	Transferable data = CLIP_BOARD.getContents(null);

		try {
			// クリップボードの値を取得
	//		strClip = (String)data.getTransferData(DataFlavor.stringFlavor);
			strClip = (String)CLIP_BOARD.getData( DataFlavor.stringFlavor );
		}
		catch( UnsupportedFlavorException ex ) {
			// PrintScreen が押された場合。
			strClip = "GUI:PRINT SCREEN.img" ;		// 形式をGUI:画面ID xxxxx.jsp 形式に合わす為
		}
		catch( IOException ex ) {
//			ex.printStackTrace();
			String errMsg = "ERROR:クリップボードの値を取得できませんでした。" ;
			setClipboard( errMsg );
			throw new RuntimeException( errMsg,ex );
		}
		catch( Throwable th ) {
			String errMsg = "ERROR:" + th.getLocalizedMessage() ;
			setClipboard( errMsg );
			throw new RuntimeException( errMsg,th );
		}

		return strClip;
	}

	/**
	 * システムのクリップボードに文字列を書き込みます。
	 *
	 * システムの Clipboard オブジェクトに、StringSelection を セットします。
	 * 通常であれば、単純に、クリップボード経由でデータのやり取りをするだけの機構ですが、
	 * FlavorListener を実装している関係上、flavorsChanged が発生します。
	 * このイベントについては、#flavorsChanged( FlavorEvent ) を参照ください。
	 *
	 * @param	txt	クリップボードに書き込む文字列
	 * @see  java.awt.datatransfer.StringSelection
	 * @see  java.awt.datatransfer.Clipboard#setContents( Transferable , ClipboardOwner )
	 */
	public static void setClipboard( final String txt ) {
		StringSelection strSel = new StringSelection( txt );
		CLIP_BOARD.setContents( strSel, null );
	}

	/**
	 * リスナー対象の Clipboard で使用可能な DataFlavor が変更されたときに呼び出されます。
	 *
	 * これは、FlavorListener の イベントの実装です。
	 * DataFlavor が変更されたときであり、そのデータの内容が書き換えられた場合には、イベントが
	 * 発生しません。
	 * そのため、データを取り出したあとで、Transferable を再セットする処理を行っています。
	 *
	 * クリップボードで使用可能な一連の DataFlavors の変更によるものでない、余分な通知もあります。
	 * さらに、イベントを発生させるために、Transferable をセットする処理( #setClipboard(String) )を
	 * 実行しても、同様にイベントが発生します。
	 *
	 * ここでは、取得したクリップボードの文字列が、"GUI:" の場合のみ処理しています。
	 * これにより、取得後の Transferable の再セット時の文字列は、"GUI:" を削除しています。
	 *
	 * このメソッドでは、画面キャプチャを取得し、クリップボードの文字列から、画面ID とJSPファイル名を
	 * 抜き出し、セーブする一連の処理を行っています。
	 *
	 * @param	fe	イベントソース
	 * @see  java.awt.datatransfer.FlavorListener#flavorsChanged( FlavorEvent )
	 */
	@Override
	public void flavorsChanged( final FlavorEvent fe )  {
		String txt = getClipboard();

		// クリップボードの値をクリアしたときのイベントは、拾わないため。
		if( txt != null && txt.length() > 0 && txt.startsWith( "GUI:" ) ) {
			System.out.println( cnt + ":【" + txt + "】" );
			BufferedImage img = doCapture();

			File saveFile = makeSaveFile( txt );

			saveImage( img,imgType,saveFile );

			// クリップボードのFlavorを置換します。(Windwosからセットされた時にイベントを発生させるため。)
			// 先頭の GUI: を取り除く。イベントの無限ループを防ぐ意味。
			setClipboard( txt.substring( 4 ) );
		}
	}

	/**
	 * キャプチャ画像を書き出すファイルオブジェクトを作成します。
	 *
	 * 引数は、"GUI:画面ID xxxxx.jsp" 形式を想定した文字列です。
	 *
	 * ファイル名には、２種類あります。
	 *   firstID を seq に設定した場合
	 *      ベースフォルダでフォルダを作成し、その中に、連番_画面ID_JSPファイル名.画像形式 ファイルを作成します。
	 *      ファイルは、キャプチャされた順番に、画面IDも混在して作成されます。つまり、ファイル名の順番に
	 *      再生すれば、リンクや他のシステムとの連携などで、画面が行き来しても、作業の順番にキャプチャできて
	 *      いる事になります。
	 *   firstID を gui に設定した場合
	 *      ベースフォルダでフォルダを作成し、その中に、画面ID_JSPファイル名_連番.画像形式 ファイルを作成します。
	 *      画面ID 単位に、画面のキャプチャを整理、使用したい場合に便利です。
	 *      ファイルのタイムスタンプ(作成時刻)で並び替えを行えば、キャプチャ順に並び替えできます。
	 *
	 * このメソッドで、フォルダの存在チェック、および、無ければ作成(mkdirs)も行います。
	 *
	 * @param	txt	ファイル名の元となる文字列("GUI:画面ID xxxxx.jsp" 形式)
	 *
	 * @return	書き出すファイルオブジェクト
	 */
	private File makeSaveFile( final String txt ) {
		int spc = txt.indexOf( ' ' );							// "GUI:画面ID xxxxx.jsp" をスペースで分離
		String gui = txt.substring( 4,spc );					//      画面ID の部分のみ切り出す。
		String jsp = txt.substring( spc+1,txt.length()-4 );		//             xxxxx の部分のみ切り出す。

		String saveFile = null;
		if( "seq".equalsIgnoreCase( firstID ) ) {
			saveFile = cnt++ + "_" + gui + "_" + jsp + "." + imgType ;
		}
		else if( "gui".equalsIgnoreCase( firstID ) ) {
			saveFile = gui + "_" + jsp + "_" + cnt++ + "." + imgType ;
		}
		else {		// 5.5.2.6 (2012/05/25) findbugs対応
			saveFile = cnt++ + "_" + gui + "_" + jsp + "." + imgType ;		// seqかguiしかないが、経路として、初期値(seq)を設定しておく。
		}

		File svf = new File( baseDir,saveFile );
		// セーブフォルダの作成。
		File parent = svf.getParentFile();
		if( !parent.exists() && !parent.mkdirs() ) {
			String errMsg = "ERROR:セーブフォルダが作成できませんでした。" + parent.getAbsolutePath()  ;
			setClipboard( errMsg );
			throw new RuntimeException( errMsg );
		}

		// セーブファイルの存在チェック。上書き禁止にしておきます。
		if( svf.exists() ) {
			String errMsg = "ERROR:セーブファイルがすでに作成されています。" + svf.getAbsolutePath()  ;
			setClipboard( errMsg );
			throw new RuntimeException( errMsg );
		}

		return svf;
	}

	/**
	 * DisplayCapture.java は、画面イメージをキャプチャする、メインメソッドです。
	 *
	 * Javaアプリケーションとして実行すると、無限処理に入ります。
	 * 内部的には、flavorsChanged イベント によるクリップボードの監視を行います。
	 * クリップボードに、"GUI:画面ID xxxxx.jsp" 形式の文字が書き込まれると、画面キャプチャを、
	 * ファイルに書き出す処理が実行されます。
	 * 書き出すファイル名の初期形式は、firstID の設定により異なります。
	 *   firstID を seq に設定した場合
	 *      ベースフォルダでフォルダを作成し、その中に、連番_画面ID_JSPファイル名.画像形式 ファイルを作成します。
	 *      ファイルは、キャプチャされた順番に、画面IDも混在して作成されます。つまり、ファイル名の順番に
	 *      再生すれば、リンクや他のシステムとの連携などで、画面が行き来しても、作業の順番にキャプチャできて
	 *      いる事になります。
	 *   firstID を gui に設定した場合
	 *      ベースフォルダでフォルダを作成し、その中に、画面ID_JSPファイル名_連番.画像形式 ファイルを作成します。
	 *      画面ID 単位に、画面のキャプチャを整理、使用したい場合に便利です。
	 *      ファイルのタイムスタンプ(作成時刻)で並び替えを行えば、キャプチャ順に並び替えできます。
	 *
	 * Usage: java org.opengion.fukurou.util.DisplayCapture
	 *                   [BASE_DIR] [firstID(seq|gui)] [imageFormat(png|gif|jpg)] [startCnt(100)]
	 *
	 *     args[0]  BASE_DIR    : キャプチャファイルをセーブするベースとなるディレクトリ(初期値:起動フォルダ)
	 *     args[1]  firstID     : キャプチャ画像をセーブするファイル方式を指定します(初期値:seq)
	 *                            seq (連番_画面ID_JSPファイル名.画像形式)
	 *                            gui (画面ID_JSPファイル名_連番.画像形式)
	 *     args[2]  imageFormat : 作成するイメージの形式。png|gif|jpg のどれか(初期値:png)
	 *     args[3]  startCnt    : セーブファイル名をユニークにするためのカウント(初期値:100)
	 *
	 * @param	args	引数 [BASE_DIR] [firstID(seq|gui)] [imageFormat(png|gif|jpg)] [startCnt(100)]
	 */
	public static void main( final String[] args ) {
		System.out.println( "DisplayCapture を起動しました。" );

		DisplayCapture dispCap = new DisplayCapture();

		if( args.length > 0 ) { dispCap.setBaseDir(   args[0] ); }
		if( args.length > 1 ) { dispCap.setFirstID(   args[1] ); }
		if( args.length > 2 ) { dispCap.setImageType( args[2] ); }
		if( args.length > 3 ) { dispCap.setStartCnt(  args[3] ); }

		// クリップボードの値をクリア(FlavorEvent を起こさせるため)
		DisplayCapture.setClipboard( null );

		// FlavorListener を登録します。(自分自身のオブジェクト)
		CLIP_BOARD.addFlavorListener( dispCap );

		// FlavorEvent で処理させるので、ずっとスレッドをSleepさせておけばよい。
		while( true ) {
			try {
				Thread.sleep( 100000 );
			}
			catch( InterruptedException ex ) {}
		}
	}
}
