package charactermanaj.graphics;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import charactermanaj.graphics.filters.ColorConvertParameter;
import charactermanaj.graphics.io.ImageResource;
import charactermanaj.model.Layer;

/**
 * 各パーツの各レイヤーごとの画像を色変換したのちレイヤーの順序に従い重ね合わせ合成する。 
 * @author seraphy
 */
public class ImageBuilder {

	/**
	 * 各パーツ情報の読み取りタイムアウト
	 */
	private static final int MAX_TIMEOUT = 20; // Secs

	/**
	 * 各パーツ情報を設定するためのインターフェイス.<br>
	 * パーツ登録が完了したら、{@link #setComplite()}を呼び出す必要がある.<br>
	 * @author seraphy
	 *
	 */
	public interface ImageSourceCollector {
		
		/**
		 * 画像サイズを設定する.<br>
		 * @param size サイズ
		 */
		void setSize(Dimension size);
		
		/**
		 * 画像の背景色を設定する.<br>
		 * 画像生成処理そのものは背景色を使用しないが、画像の生成完了と同じタイミングで背景色を変えるために
		 * ホルダーとして用いることを想定している.<br>
		 * @param color
		 */
		void setImageBgColor(Color color);
		
		/**
		 * アフィン変換処理のためのパラメータを指定する.<br>
		 * 配列サイズは4または6でなければならない.<br>
		 * @param param パラメータ、変換しない場合はnull
		 */
		void setAffineTramsform(double[] param);

		/**
		 * 各パーツを登録する.<br>
		 * 複数パーツある場合は、これを繰り返し呼び出す.<br>
		 * すべて呼び出したらsetCompliteを呼び出す.<br>
		 * @param layer レイヤー
		 * @param imageResource イメージソース
		 * @param param 色変換情報
		 */
		void setImageSource(Layer layer, ImageResource imageResource, ColorConvertParameter param);

		/**
		 * パーツの登録が完了したことを通知する。
		 */
		void setComplite();
	}

	/**
	 * 合成が完了した画像を通知するインターフェイス 
	 * @author seraphy
	 */
	public interface ImageOutput {
		
		/**
		 * 画像の背景色を取得する.
		 * @return 背景色
		 */
		Color getImageBgColor();
		
		/**
		 *　画像を取得する. 
		 * @return 画像
		 */
		BufferedImage getImageOutput();
		
	}

	/**
	 * イメージを構築するためのジョブ定義.<br>
	 * イメージを構築するためのパーツを登録するハンドラと、合成されたイメージを取り出すハンドラ、および
	 * エラーハンドラからなる.<br>
	 * @author seraphy
	 */
	public interface ImageBuildJob {

		/**
		 * 合成する、各パーツを登録するためのハンドラ.<br>
		 * @param collector 登録するためのインターフェイス
		 */
		void loadParts(ImageSourceCollector collector) throws Exception;
		
		/**
		 * 合成されたイメージを取得するハンドラ
		 * @param output イメージを取得するためのインターフェイス
		 */
		void buildImage(ImageOutput output) throws Exception;

		/**
		 * 例外ハンドラ
		 * @param ex 例外
		 */
		void handleException(Exception ex);
	}

	/**
	 * イメージのローダー
	 */
	private ColorConvertedImageCachedLoader imageLoader;
	
	/**
	 * イメージのローダーを指定して構築します.<br>
	 * @param imageLoader イメージローダー
	 */
	public ImageBuilder(ColorConvertedImageCachedLoader imageLoader) {
		if (imageLoader == null) {
			throw new IllegalArgumentException();
		}
		this.imageLoader = imageLoader;
	}
	
	/**
	 * イメージ構築ジョブを要求します.<br>
	 * 戻り値がtrueである場合は、ただちに完了したことを示します.<br>
	 * @param imageBuildJob イメージを構築するジョブ
	 * @return 画像がただちに得られた場合はtrue、そうでなければfalse
	 */
	public boolean requestJob(final ImageBuildJob imageBuildJob) {
		if (imageBuildJob == null) {
			throw new IllegalArgumentException();
		}
		
		// 合成する画像パーツの取得処理
		
		final ArrayList<ImageBuildPartsInfo> partsInfos = new ArrayList<ImageBuildPartsInfo>();
		final Rectangle rct = new Rectangle(0, 0, 0, 0);
		final Color[] imageBgColor = new Color[1];
		final Object[] affineParamHolder = new Object[1];
		final Semaphore compliteLock = new Semaphore(0);
		try {
			// ジョブリクエスト側に合成するイメージの情報を引き渡すように要求する.
			// loadPartsが非同期に行われる場合、すぐに制御を戻す.
			imageBuildJob.loadParts(new ImageSourceCollector() {
				// ジョブリクエスト側よりイメージサイズの設定として呼び出される
				public void setSize(Dimension size) {
					synchronized (rct) {
						rct.width = size.width;
						rct.height = size.height;
					}
				}
				public void setImageBgColor(Color color) {
					synchronized (imageBgColor) {
						imageBgColor[0] = color;
					}
				}
				public void setAffineTramsform(double[] param) {
					if (param != null && !(param.length == 4 || param.length == 6)) {
						throw new IllegalArgumentException("affineTransformParameter invalid length.");
					}
					synchronized (affineParamHolder) {
						affineParamHolder[0] = param;
					}
				}
				// ジョブリクエスト側よりパーツの登録として呼び出される
				public void setImageSource(Layer layer, ImageResource imageResource, ColorConvertParameter param) {
					synchronized (partsInfos) {
						partsInfos.add(new ImageBuildPartsInfo(partsInfos.size(), layer, imageResource, param));
					}
				}
				// ジョブリクエスト側よりイメージサイズとパーツの登録が完了したことを通知される.
				public void setComplite() {
					compliteLock.release();
				}
			});

			// ImageCollectorは非同期に呼び出されても良いことを想定している.
			// MAX_TIMEOUTを経過してもsetCompliteが呼び出されない場合、処理を放棄する.
			if (!compliteLock.tryAcquire(MAX_TIMEOUT, TimeUnit.SECONDS)) {
				throw new RuntimeException("ImageCollector Timeout.");
			}

		} catch (Exception ex) {
			// 予期せぬ例外の通知
			imageBuildJob.handleException(ex);
			return false;
		}
		
		try {
			// パーツの合成処理

			// パーツ情報を重ね合わせ順にソートする.
			Collections.sort(partsInfos);
			
			// 出力画像のカンバスを作成
			final BufferedImage canvas = new BufferedImage(rct.width, rct.height, BufferedImage.TYPE_INT_ARGB);
			Graphics2D g = (Graphics2D) canvas.getGraphics();
			try {
				int w, h;
				synchronized (rct) {
					w = rct.width;
					h = rct.height;
				}
				synchronized (partsInfos) {
					// 各パーツを重ね合わせ順にカンバスに描画する 
					for (ImageBuildPartsInfo partsInfo : partsInfos) {
	
						ImageResource imageFile = partsInfo.getFile();
						ColorConvertParameter colorConvParam = partsInfo.getColorParam();
						
						try {
							BufferedImage img = imageLoader.load(imageFile, colorConvParam);
							g.drawImage(img, 0, 0, w, h, 0, 0, w, h, null);

						} catch (Exception ex) {
							imageBuildJob.handleException(ex);
						}
					}
				}
			} finally {
				g.dispose();
			}
			
			// アフィン処理を行う.(パラメータが指定されていれば)
			final BufferedImage affineTransformedCanvas;
			double[] affineTransformParameter = (double[]) affineParamHolder[0];
			if (affineTransformParameter == null) {
				affineTransformedCanvas = canvas;
			} else {
				AffineTransform affineTransform = new AffineTransform(new double[] { -1., 0, 0, 1., rct.width, 0});
				AffineTransformOp affineTransformOp = new AffineTransformOp(affineTransform, null);
				affineTransformedCanvas = new BufferedImage(rct.width, rct.height, BufferedImage.TYPE_INT_ARGB);
				affineTransformOp.filter(canvas, affineTransformedCanvas);
			}

			// 完成したカンバスを合成結果として通知する.
			imageBuildJob.buildImage(new ImageOutput() {
				public BufferedImage getImageOutput() {
					return affineTransformedCanvas;
				}
				public Color getImageBgColor() {
					return imageBgColor[0];
				}
			});

		} catch (Exception ex) {
			// 予期せぬ例外の通知
			imageBuildJob.handleException(ex);
			return false;
		}

		// 完了
		return true;
	}
	
}

/**
 * 合成する個々のイメージ情報 .<br>
 * レイヤー順に順序づけられており、同一レイヤーであればOrder順に順序づけられます.<br>
 * @author seraphy
 */
final class ImageBuildPartsInfo implements Comparable<ImageBuildPartsInfo> {

	private int order;
	
	private Layer layer;
	
	private ImageResource imageResource;
	
	private ColorConvertParameter colorParam;

	public ImageBuildPartsInfo(int order, Layer layer, ImageResource imageResource, ColorConvertParameter colorParam) {
		this.order = order;
		this.layer = layer;
		this.imageResource = imageResource;
		this.colorParam = colorParam;
	}
	
	@Override
	public int hashCode() {
		return order ^ layer.hashCode() ^ imageResource.hashCode();
	}
	
	@Override
	public boolean equals(Object obj) {
		if (obj == this) {
			return true;
		}
		if (obj != null && obj instanceof ImageBuildPartsInfo) {
			ImageBuildPartsInfo o = (ImageBuildPartsInfo) obj;
			return order == o.order && layer.equals(o.layer)
					&& imageResource.equals(o.imageResource) && colorParam.equals(o.colorParam);
		}
		return false;
	}
	
	public int compareTo(ImageBuildPartsInfo o) {
		// レイヤー順
		int ret = layer.compareTo(o.layer);
		if (ret == 0) {
			// 同一レイヤーであれば定義順
			ret = order - o.order;
		}
		if (ret == 0) {
			// それでも同じであればイメージソースの固有の順序
			ret = imageResource.compareTo(o.imageResource);
		}
		return ret;
	}

	public int getOrder() {
		return order;
	}
	
	public Layer getLayer() {
		return layer;
	}
	
	public ColorConvertParameter getColorParam() {
		return colorParam;
	}
	
	public ImageResource getFile() {
		return imageResource;
	}
}
