package sample.textdetection;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringJoiner;

import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;

import sample.textdetection.mapper.GCPBlock;
import sample.textdetection.mapper.GCPBoundingBox;
import sample.textdetection.mapper.GCPFeature;
import sample.textdetection.mapper.GCPFullTextAnnotation;
import sample.textdetection.mapper.GCPImage;
import sample.textdetection.mapper.GCPPage;
import sample.textdetection.mapper.GCPRequest;
import sample.textdetection.mapper.GCPResponse;
import sample.textdetection.mapper.GCPResultInfo;
import sample.textdetection.mapper.GCPSendInfo;

public class TextDetection {

	/* Google Cloud Vision API のサービスエンドポイント（URL） */
	public static final String uri = "https://vision.googleapis.com/v1/images:annotate";
	/* Google Cloud Vision APIキー */
	 public static final String key = "作成したAPIキー";
	/* Excelは、インデックスが0始まり */
	public static final int OFFSET = 1;

	/**
	 * @param args
	 * @throws EncryptedDocumentException
	 * @throws InvalidFormatException
	 */
	public static void main(String[] args) throws EncryptedDocumentException, InvalidFormatException {
		if (args.length != 2) {
			System.err.println("引数にはフォルダパスを指定してください。");
			return;
		}
		String folderPath = args[0];
		String filePath = args[1];
		TextDetection detection = new TextDetection();
		try {
			detection.run(folderPath, filePath);
		} catch (IOException e) {
			e.printStackTrace();
		} catch (URISyntaxException e) {
			e.printStackTrace();
		}
	}

	/**
	 * テスト採点処理
	 * @param folderPath テスト画像を格納した任意のフォルダパス
	 * @param filePath 採点結果記入用のExcelファイルパス
	 * @throws IOException
	 * @throws URISyntaxException
	 * @throws InvalidFormatException
	 * @throws EncryptedDocumentException
	 */
	public void run(String folderPath, String filePath)
			throws IOException, URISyntaxException, EncryptedDocumentException, InvalidFormatException {
		/* 指定フォルダ内のファイルをFileオブジェクトに変換する */
		List<File> files = load(folderPath);
		/* HTTP通信用のクライアントを作成する */
		HttpClient httpclient = createClient();
		/* Requestを作成する */
		HttpPost request = createRequest();

		/* DTOクラスを作成する */
		GCPSendInfo sendInfo = createSendDTO(files);
		/* DTOクラスからJSON文字列へ変換する */
		ObjectMapper mapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
		String body = mapper.writeValueAsString(sendInfo);

		/* リクエスト本文にJSON文字列を設定する */
		StringEntity reqEntity = new StringEntity(body);
		request.setEntity(reqEntity);
		/* 画像解析依頼(リクエスト)を実行する */
		HttpResponse response = httpclient.execute(request);
		/* 処理結果(レスポンス)からデータを取得する */
		HttpEntity entity = response.getEntity();
		/* JSONの文字列として取得 */
		String jsonString = EntityUtils.toString(entity).trim();

		/* JSONの文字列をJavaオブジェクトにマッピング */
		GCPResultInfo resultInfo = mapper.readValue(jsonString, GCPResultInfo.class);

		List<DetectResult> results = createDetectResults(resultInfo);

		writeMarkResult(filePath, results);
	}

	/**
	 * 答案用紙から検出した文字列をExcelに書き出す
	 * @param templatePath
	 * @param results
	 * @throws EncryptedDocumentException
	 * @throws InvalidFormatException
	 * @throws IOException
	 */
	public void writeMarkResult(String templatePath, List<DetectResult> results)
			throws EncryptedDocumentException, InvalidFormatException, IOException {
		/* 第一引数のテンプレートパスを読み込む */
		FileInputStream in = new FileInputStream(templatePath);
		/* 書き込み用ストリームを定義 */
		FileOutputStream out = null;
		XSSFWorkbook srcWb = null;
		try {
			/* ExcelのWorkbookを作成 */
			srcWb = (XSSFWorkbook) WorkbookFactory.create(in);
			/* WorkbookからSheet1を取得する */
			XSSFSheet sheet = srcWb.getSheet("Sheet1");
			/* 値の書き込まれている最終行を取得する。データはその次の行以降に挿入する */
			int lastRow = sheet.getLastRowNum();
			/* 値の書き込まれている最終列を取得する */
			XSSFRow srcRow = sheet.getRow(0);
			int lastCol = srcRow.getLastCellNum();
			/* 人数分の解答を書き込む */
			for (int num = 0; num < results.size(); num++) {
				DetectResult result = results.get(num);
				/* 値が入力されている行の1つ下に新しい行を挿入する */
				XSSFRow dstRow = sheet.createRow(++lastRow);
				/* 氏名と解答及び式をセルに書き込む */
				for (int i = 0; i < lastCol; i++) {
					/* セルを作成 */
					XSSFCell cell = dstRow.createCell(i);
					if (i != 0 && (i % 2) == 0) {
						int colIdx = cell.getColumnIndex() - OFFSET;
						int rowIdx = cell.getRowIndex() + OFFSET;
						String col = String.valueOf((char) ('A' + colIdx));
						String formula = String.format("%s$2=%s%d", col, col, rowIdx);
						cell.setCellFormula(formula);
					} else {
						if (i == (lastCol - 1)) {
							int rowIdx = cell.getRowIndex() + OFFSET;
							String formula = String.format("COUNTIF(B%d:G%d, TRUE)", rowIdx, rowIdx);
							cell.setCellFormula(formula);
						} else {
							if (i == 0) {
								cell.setCellValue(result.getName());
							} else {
								cell.setCellValue(Integer.parseInt(result.getAnswers().get(i / 2)));
							}
						}
					}
				}
			}
			out = new FileOutputStream(templatePath);
			srcWb.write(out);
		} finally {
			if (srcWb != null)
				srcWb.close();
			if (out != null)
				out.close();
		}
	}

	/**
	 * 指定したフォルダパス内のファイルを読み込む
	 * @param path 任意のフォルダパス
	 * @return
	 */
	public List<File> load(String path) {
		List<File> files = new ArrayList<>();
		/* フォルダパスをFileオブジェクトに変換する */
		File folder = new File(path);
		if (folder.exists()) {
			/* フォルダからファイルを取得する */
			for (File file : folder.listFiles()) {
				files.add(file);
			}
		}
		return files;
	}

	/**
	 * VisionAPIのリクエストデータに設定するためにファイルをBase64のStringに変換
	 * @param file 画像認識対象のファイル
	 * @return
	 * @throws IOException
	 */
	public String encodeBase64String(File file) throws IOException {
		FileInputStream fis = new FileInputStream(file);
		byte[] bytes = new byte[(int) file.length()];
		fis.read(bytes);
		fis.close();
		return Base64.encodeBase64String(bytes);
	}

	/**
	 * HTTP Client を作成
	 * @return
	 */
	public HttpClient createClient() {
		/* タイムアウト時間の設定（ミリ秒） */
		int socketTimeout = 5000;
		int connectionTimeout = 5000;
		RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(connectionTimeout)
				.setSocketTimeout(socketTimeout).build();

		HttpClient httpclient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).build();
		return httpclient;
	}

	/**
	 * Requestを作成
	 * @return
	 * @throws URISyntaxException
	 */
	public HttpPost createRequest() throws URISyntaxException {
		/* Google Cloud Vision API のURLにPOSTリクエストを送る */
		URIBuilder builder = new URIBuilder(uri);
		/* パラメータ名→key, パラメータ値→{APIキー} */
		builder.setParameter("key", key);
		URI uri = builder.build();
		HttpPost request = new HttpPost(uri);
		/* JSON形式であることを明示 */
		request.setHeader("Content-Type", "application/json");
		return request;
	}

	/**
	 * リクエストボディを作成する
	 * @param files
	 * @return
	 * @throws IOException
	 */
	public GCPSendInfo createSendDTO(List<File> files) throws IOException {
		GCPSendInfo sendInfo = new GCPSendInfo();
		sendInfo.requests = new GCPRequest[files.size()];
		int i = 0;
		for (File file : files) {
			/* ファイルごとにリクエストを作成 */
			sendInfo.requests[i] = new GCPRequest();
			sendInfo.requests[i].image = new GCPImage();
			sendInfo.requests[i].image.content = encodeBase64String(file);
			sendInfo.requests[i].features = new GCPFeature[1];
			sendInfo.requests[i].features[0] = new GCPFeature();
			sendInfo.requests[i].features[0].type = "TEXT_DETECTION";
			i++;
		}
		return sendInfo;
	}

	/**
	 * 画像認識の結果から名前、解答を抽出する
	 * @param resultInfo
	 * @return
	 */
	public List<DetectResult> createDetectResults(GCPResultInfo resultInfo) {
		List<DetectResult> results = new ArrayList<>();
		for (GCPResponse res : resultInfo.responses) {
			int c = 0;
			DetectResult result = new DetectResult();
			GCPFullTextAnnotation anno = res.fullTextAnnotation;
			for (GCPPage page : anno.pages) {
				for (GCPBlock block : page.blocks) {
					GCPBoundingBox box = block.boundingBox;
					int x = box.vertices[0].x;
					int y = box.vertices[0].y;
					/* 閾値を固定値とする。氏名、解答のみを解析対象となるように設定 */
					if (x < 800 || y > 1800)
						continue;
					StringJoiner sj = new StringJoiner("");
					Arrays.stream(block.paragraphs).flatMap(para -> Arrays.stream(para.words))
							.flatMap(word -> Arrays.stream(word.symbols)).forEach(symbol -> {
								sj.add(symbol.text);
							});
					String text = sj.toString();
					if (c > 0) {
						result.addAnswer(text);
					} else {
						result.setName(text);
					}
					c++;
				}
			}
			results.add(result);
		}
		return results;
	}

	public static class DetectResult {

		/* 氏名 */
		private String name;

		/* 解答の一覧 */
		private List<String> answers;

		public DetectResult() {
			this.answers = new ArrayList<String>();
		}

		public String getName() {
			return name;
		}

		public void setName(String name) {
			this.name = name;
		}

		public List<String> getAnswers() {
			return answers;
		}

		public void addAnswer(String answer) {
			this.answers.add(answer);
		}

		@Override
		public String toString() {
			return getName() + ":" + getAnswers();
		}
	}
}
