package jp.agentec.abook.abv.ui.viewer.foxitPdf;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;

import jp.agentec.abook.abv.bl.acms.client.json.DownloadedContentInfoJSON;
import jp.agentec.abook.abv.bl.common.CommonExecutor;
import jp.agentec.abook.abv.bl.common.exception.ABVRuntimeException;
import jp.agentec.abook.abv.bl.common.log.Logger;
import jp.agentec.abook.abv.bl.common.util.ContentFileUtil;
import jp.agentec.abook.abv.bl.download.ContentFileExtractor;
import jp.agentec.abook.abv.cl.util.BitmapUtil;
import jp.agentec.abook.abv.ui.common.vo.Size;
import jp.agentec.adf.util.FileUtil;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.PointF;
import android.os.Process;

import com.foxit.sdk.PDFException;


public class PdfImageProvider {
	protected static final String TAG = "PdfImageProvider";

	private FoxitPdfCore mFoxitPdfCore;
	private Context context;
	private String pdfPath;
	private ArrayList<PageSet> pageSetList = new ArrayList<>();
	private ArrayList<AsyncTask<Integer,Integer,Integer>> drawEntireTaskList = new ArrayList<>();
	private volatile int pauseTaskCount;
	private static Map<Long, PdfImageProvider> pdfImageProviderMap = new ConcurrentHashMap<>(); // サイネージで使用

	public PdfImageProvider(FoxitPdfCore foxitPdfCore) {
		Logger.i(TAG, "[PdfImageProvider]");
		mFoxitPdfCore = foxitPdfCore;
	}
	
	public PdfImageProvider(Context context, String pdfPath) throws FileNotFoundException {
		if (!FileUtil.exists(pdfPath)) {
			throw new FileNotFoundException(pdfPath);
		}
		this.pdfPath = pdfPath;
		this.context = context;
	}
	
	public void destory(){
		if(mFoxitPdfCore != null){
			mFoxitPdfCore.releaseDoc();
			mFoxitPdfCore = null;
		}
	}

	@SuppressWarnings("unused")
	private class PageSet {
		private int pageNumber;
		private String imagePath;
		
		private PageSet(int pageNumber, String imagePath) {
			this.pageNumber = pageNumber;
			this.imagePath = imagePath;
		}
	}

	public void createImage(long contentId, int centerPage, final Size displaySize) {
		createImage(contentId, centerPage, displaySize, false, AsyncTask.THREAD_POOL_EXECUTOR);
	}

	/**
	 * PDFイメージを作成する
	 * 
	 * @param contentId
	 * @param centerPage
	 * @param displaySize 画面サイズ（現在の回転状態で縦横が変わる）
	 * @param recreate すでにファイルがあっても再作成する
	 * @param executor
	 */
	public void createImage(final long contentId, int centerPage, final Size displaySize, final boolean recreate, final Executor executor) {
		Logger.i(TAG, "[createImage] centerPage=" + centerPage + " displaySize=" + displaySize.width + "x" + displaySize.height);
		drawEntireTaskList.clear();
		
		loadPageSet(contentId, centerPage, recreate);
		
		Logger.d(TAG, "[createImage] pageSetList.size=" + pageSetList.size());
		if (pageSetList.isEmpty()) {
			return;
		}
		
		final Size fDisplaySize = displaySize;
		for (int i = 0; i < pageSetList.size(); i++) {
			final PageSet pageSet = pageSetList.get(i);
			AsyncTask<Integer,Integer,Integer> drawEntireTask = new AsyncTask<Integer,Integer,Integer>(Process.THREAD_PRIORITY_BACKGROUND) {
				
				@Override
                protected Integer doInBackground(Integer... v) {
					if (!recreate && exists(pageSet.imagePath)) { // この時点でファイルがすでに作られている場合
						next(v[0] + 1);
						return v[0];
					}
					
					Logger.d(TAG, "pauseTaskCount=" + pauseTaskCount);
					while (pauseTaskCount > 0) { // 停止指示
						try {
							Thread.sleep(1000);
						} catch (InterruptedException e) { // interruptに対してはbreakする
							break;
						}
					}

					try {
						Thread.sleep(1000); // 他のスレッドに実行権を渡す
					} catch (InterruptedException e) {}
					
					createPdfImage(contentId, fDisplaySize, pageSet.pageNumber, pageSet.imagePath, true, false, true);
					if (!FileUtil.exists(pageSet.imagePath)) {
						Logger.e(TAG, "create pdf image failed. Cancel all tasks. path=%s", pageSet.imagePath);
						cancelAll(true);
						return null;
					}
					
					next(v[0] + 1);
					
					return v[0];
				}

				protected void next(int nextIndex) {
					for (; nextIndex < drawEntireTaskList.size(); nextIndex++) {
						AsyncTask<Integer,Integer,Integer> nextDrawEntireTask = drawEntireTaskList.get(nextIndex);
						synchronized (nextDrawEntireTask) {
							if (nextDrawEntireTask.getStatus() == Status.PENDING) { // キャンセルされている場合は飛ばす
								Logger.v(TAG, "nextIndex=%d", nextIndex);
								nextDrawEntireTask.executeOnExecutor(executor, nextIndex); // 次の描画を実行
								break;
							}
						}
					}
				}
			};
			drawEntireTaskList.add(drawEntireTask);
		}
		
		drawEntireTaskList.get(0).executeOnExecutor(executor , 0);
	}

	private void loadPageSet(long contentId, int centerPage, boolean recreate) {
		// キャッシュがない場合追加
		pageSetList.clear();
		
		if (mFoxitPdfCore == null) {
			try {
				mFoxitPdfCore = createFoxitPdfCore(contentId);
			} catch (FileNotFoundException e) {
				Logger.e(TAG, "createPdfImage failed. pdf file not found. " + pdfPath, e);
			} catch (Exception e) {
				throw new ABVRuntimeException("mFoxitPdfCore failed. filepath=" + pdfPath);
			}
		}
		long totalPage = 0;
		try {
			totalPage = mFoxitPdfCore.countPages();
		} catch(PDFException e) {
			Logger.e(TAG, "mFoxitPdfCore.countPages failed." + pdfPath, e);
		}
		if(totalPage == 0) {
			throw new ABVRuntimeException("mFoxitPdfCore failed. totalPage is 0. filepath=" + pdfPath);
		}
		for (int i = 0; i < Math.max(totalPage - centerPage, centerPage); i++) {
			for (int targetPage : new int[]{centerPage + i, centerPage - 1 - i}) { // 中心ページからだんだん外に開いていく
				if (targetPage < 0 || targetPage >= totalPage) {
					continue;
				}
				String imagePath = ContentFileUtil.getPdfImagePath(contentId, targetPage);
				if (recreate || !exists(imagePath)) {
					pageSetList.add(new PageSet(targetPage, imagePath));
				}
			}
		}
	}

	private FoxitPdfCore createFoxitPdfCore(long contentId) throws Exception {
		FoxitPdfCore foxitPdfCore = new FoxitPdfCore(context, pdfPath);
		boolean isExistPassWord = foxitPdfCore.loadPDFDoc(null);
		if(isExistPassWord) {
			// PDFパスワードあり
			DownloadedContentInfoJSON json = ContentFileUtil.getDownloadedContentInfoJSON(contentId);
			if (json.pdfPassword == null) {
				//エラー
				throw new ABVRuntimeException("authenticatePassword failed. contentId=" + contentId);
			}
			//TODO パスワードの認証失敗のエラーキャッチ
			foxitPdfCore.loadPDFDoc(json.pdfPassword.getBytes());
		}
		return foxitPdfCore;
	}

	/**
	 * ファイルが存在しかつ1000byte以上か
	 */
	private boolean exists(String imagePath) {
		File file = new File(imagePath);
		return file.exists() && file.length() > 1000;
	}

	/**
	 * 指定ページのTaskをキャンセルする
	 * 
	 * @param pageNumber
	 * @param currentTask
	 * @return
	 */
	public boolean cancelTask(int pageNumber, boolean currentTask) {
		Logger.i(TAG, "cancelTask pageNumber=" + pageNumber + " currentTask=" + currentTask);
		for (int i=0; i < pageSetList.size(); i++) {
			PageSet pageSet = pageSetList.get(i);
			if (pageSet.pageNumber == pageNumber && i < drawEntireTaskList.size()) {
				AsyncTask<Integer,Integer,Integer> drawEntireTask = drawEntireTaskList.get(i);
				synchronized (drawEntireTask) {
					if (drawEntireTask.getStatus() == AsyncTask.Status.PENDING) {
						return drawEntireTask.cancel(currentTask);
					}
					else { // 実行中、完了はfalseで返す
						return false;
					}
				}
			}
		}
		return true;
	}
	
	/**
	 * すべてのTaskをキャンセルする
	 * 
	 * @param currentTask 現在実行中もキャンセルするか
	 */
	public void cancelAll(boolean currentTask) {
		Logger.i(TAG, "cancelAll currentTask=" + currentTask);
		for (AsyncTask<Integer,Integer,Integer> drawEntireTask : drawEntireTaskList) {
			synchronized (drawEntireTask) {
				if (drawEntireTask.getStatus() != AsyncTask.Status.FINISHED) {
					drawEntireTask.cancel(currentTask);
				}
			}
		}
		pauseTaskCount = 0;
		drawEntireTaskList.clear();
	}
	
	/**
	 * Taskをpauseもしくは再開する
	 * （ただし実行中は止めない）
	 * 
	 * @param pauseTask
	 */
	public synchronized void setPauseTask(boolean pauseTask) {
		if (Logger.isDebugEnabled()) {
			StackTraceElement ste = new Throwable().getStackTrace()[1];
			Logger.d(TAG, "setPauseTask " + ste.getClassName() + "#" + ste.getMethodName() + " " + pauseTask);
		}
		else {
			Logger.i(TAG, "setPauseTask " + pauseTask);
		}
		if (pauseTask) {
			pauseTaskCount++;
		}
		else {
			pauseTaskCount--;
		}
	}

	public Bitmap createPdfImage(long contentId, Size displaySize, int pageNumber, final String imagePath, boolean isOutput, boolean outputAsync, final boolean recycle) {
		if (mFoxitPdfCore == null) {
			try {
				mFoxitPdfCore = createFoxitPdfCore(contentId);
				mFoxitPdfCore.countPages();
			} catch (FileNotFoundException e) {
				Logger.e(TAG, "createPdfImage failed. pdf file not found. " + pdfPath, e);
				return null;
			} catch (Exception e) {
				Logger.e(TAG, "mFoxitPdfCore failed. filepath=" + pdfPath, e);
				return null;
			}
		}

		PointF size = null;
		try {
			size = mFoxitPdfCore.getPageSize(pageNumber);
		} catch (PDFException e) {
			Logger.e(TAG, "mFoxitPdfCore getPageSize failed. filepath=" + pdfPath, e);
			return null;
		}
		if (size == null) {
			return null;
		}
		Logger.v(TAG, "size.y=%s size.x=%s", size.y, size.x);
		if ((displaySize.width > displaySize.height) != (size.x > size.y)) {
			displaySize = displaySize.getSwapSize();
		}
		
		float pageScale = Math.min((float)displaySize.height / size.y,  (float)displaySize.width / size.x);
		int width = (int) (size.x * pageScale);
		int height = (int) (size.y * pageScale);

		Logger.i(TAG, "[drawPage]: pageNumber=" + pageNumber + " width=" + width + " height=" + height + " imagePath=" + imagePath);
		Bitmap bm = null;
		try {
			bm = mFoxitPdfCore.drawPage(pageNumber, width, height, 0, 0, width, height);
		} catch (OutOfMemoryError e) {
            Logger.e(TAG, "mFoxitPdfCore.drawPage error." + e.toString());
			if (bm != null) {
				bm.recycle();
			}
			System.gc();
			try {
				bm = mFoxitPdfCore.drawPage(pageNumber, width, height, 0, 0, width, height);
			} catch (OutOfMemoryError e2) {
                Logger.e(TAG, "mFoxitPdfCore.drawPage error." + e2.toString());
				if (bm != null) {
					bm.recycle();
				}
			}
		}
		
		if (bm != null) {

			synchronized (bm) {
				if (!bm.isRecycled() && isOutput) {
					final Bitmap finalBm = bm;
					if (outputAsync) {
						CommonExecutor.execute(new Runnable() {
							@Override
                            public void run() {
								BitmapUtil.outputFile(finalBm, imagePath, recycle);
							}
                        });
					} else {
						BitmapUtil.outputFile(finalBm, imagePath, recycle);
					}
				}
			}
		}
		
		return bm;
	}

	/**
	 * PDFページイメージを返却する
	 * （ない場合は作成し、PDFが存在しない場合はnullを返す）
	 *
	 * @param displaySize 画面サイズ
	 * @param contentId コンテンツID
	 * @param page ページ番号
	 * @return bm
	 * @throws IOException
	 */
	public Bitmap getPdfImage(Size displaySize, long contentId, int page) throws IOException {
		Logger.d(TAG, "getPdfImage contentId=%s, page=%s", contentId, page);
		Bitmap bm = readFromFile(displaySize, contentId, page);
		if (bm == null) {
			String imagePath = ContentFileUtil.getPdfImagePath(contentId, page);
			bm = createPdfImage(contentId, displaySize, page, imagePath, true, true, false);
		}
		return bm;
	}

	/**
	 * ファイルがあれば、ファイルから読み込む
	 * 
	 * @param displaySize
	 * @param contentId
	 * @param page
	 * @return
	 */
	public static Bitmap readFromFile(Size displaySize, long contentId, int page) {
		String imagePath = ContentFileUtil.getPdfImagePath(contentId, page);
		File file = new File(imagePath);
		if (file.exists()) {
			if (file.length() > displaySize.width * displaySize.height / 55) {
				return BitmapFactory.decodeFile(imagePath, null);
			}
			else { // displaySize.width * displaySize.height / 55以下は無効とみなして削除後再作成(本来描画する画像サイズが望ましい) 本当に真っ白な場合は毎回作成する
				file.delete();
			}
		}
		return null;
	}
	
	/**
	 * Mapに登録（サイネージでのみ使用）
	 * 
	 * @param contentId
	 * @param pdfImageProvider
	 */
	public static void putToMap(long contentId, PdfImageProvider pdfImageProvider) {
		pdfImageProviderMap.put(contentId, pdfImageProvider);
	}
	
	/**
	 * 指定コンテンツIDのタスクをキャンセルし、Mapから削除（サイネージでのみ使用）
	 * 
	 * @param contentId
	 */
	public static void cancel(long contentId) {
		PdfImageProvider pdfImageProvider = pdfImageProviderMap.get(contentId);
		if (pdfImageProvider != null) {
			pdfImageProvider.cancelAll(true);
			pdfImageProviderMap.remove(contentId);
		}
	}
	
	/**
	 * Mapから削除（サイネージでのみ使用）
	 * 
	 * @param contentId
	 */
	public static void removeFromMap(long contentId) {
		pdfImageProviderMap.remove(contentId);
	}
	
	/**
	 * PDFのイメージを作成する
	 * 
	 * @param context
	 * @param displaySize
	 * @param contentId
	 * @throws IOException
	 * @throws FileNotFoundException
	 */
	public static void createPdfImage(Context context, Size displaySize, long contentId) throws IOException {
		String pdfPath = ContentFileExtractor.getInstance().getPdfPath(contentId);
		if (contentId == 0) {
			pdfPath =  ContentFileExtractor.getInstance().getGuidePDFFilePath(contentId);
		}

		if (pdfPath != null) {
			PdfImageProvider pdfImageProvider = new PdfImageProvider(context, pdfPath);
			Logger.i(TAG, "start create pdf image. contentId=%s", contentId);
			cancel(contentId);
			pdfImageProvider.createImage(contentId, 0, displaySize, true, AsyncTask.SERIAL_EXECUTOR); // DLに比べてこの処理が遅い場合を考慮し、シングルで実行する
			putToMap(contentId, pdfImageProvider);
		}
	}

}
