package cokr.xit.fims.cmmn.pdf.print.format;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode;
import org.egovframe.rte.fdl.string.EgovStringUtil;
import org.springframework.core.io.ClassPathResource;

import cokr.xit.applib.HttpStatusCodeException;
import cokr.xit.applib.Print;
import cokr.xit.applib.Pstn;
import cokr.xit.applib.PstnAndSize;
import cokr.xit.applib.Size;
import cokr.xit.fims.cmmn.CmmnUtil;
import cokr.xit.fims.cmmn.FimsPrintOption;
import cokr.xit.fims.cmmn.pdf.print.DefaultOtptArtclStng;
import cokr.xit.fims.cmmn.pdf.print.PDFColors;
import cokr.xit.fims.cmmn.pdf.print.PDFCoordinate;
import cokr.xit.fims.cmmn.pdf.print.PDFWriter;
import cokr.xit.fims.sprt.MediaUtil;
import cokr.xit.foundation.AbstractComponent;
import cokr.xit.foundation.data.DataObject;

public abstract class PDFPrintFormat extends AbstractComponent {
	protected PDFWriter writer;

	protected Print print;
	protected FimsPrintOption printOption;
	protected List<DataObject> data;

	protected DataObject otptBscStng;
	protected List<DataObject> otptArtclStngList;
	protected String paperSeCd;
	protected float[] paperMilimeter = new float[2];
	protected List<DefaultOtptArtclStng> prototypeStngs = new ArrayList<DefaultOtptArtclStng>();

	public abstract void initDefaultStng();

	public PDFPrintFormat() {
		initDefaultStng();
	}

	public PDFPrintFormat(Print print, FimsPrintOption printOption, DataObject otptBscStng, List<DataObject> otptArtclStngList, String paperSeCd, List<DataObject> data) {
		initDefaultStng();
		this.data = data;
		this.print = print;
		this.print.setExtension("pdf");
		this.printOption = printOption;
		this.otptBscStng = otptBscStng;
		this.otptArtclStngList = otptArtclStngList;
		this.paperSeCd = paperSeCd;
		switch (paperSeCd) {
		case "01": this.paperMilimeter = new float[] {210.0f , 297.0f}; break;
		case "02": this.paperMilimeter = new float[] {216.0f , 279.0f}; break;
		default: break;
		}
	}

	public void setOtptBscStng(DataObject otptBscStng) {
		this.otptBscStng = otptBscStng;
	}

	public void setOtptArtclStngList(List<DataObject> otptArtclStngList) {
		this.otptArtclStngList = otptArtclStngList;
	}

	public List<DataObject> getOtptArtclStngList() {
		return otptArtclStngList;
	}


	public void setPaperSeCd(String paperSeCd) {
		this.paperSeCd = paperSeCd;

	}

	public void setPrint(Print print) {
		this.print = print;
	};

	/**
	 * 포맷원형을 반환한다.
	 */
	public List<DefaultOtptArtclStng> getPrototypeStngs(){
		return this.prototypeStngs;
	}

	/**
	 * 포맷원형을 반환한다.
	 */
	public List<DefaultOtptArtclStng> getPrototypeStngs(boolean unique){
		return this.prototypeStngs.stream().filter(item -> item.isUnique() == unique).toList();
	}

	/**
	 * 텍스트 출력요소를 정의한다.
	 */
	public DefaultOtptArtclStng text(String artclNm, String artclDscrp, String otptBscVl){
		return new DefaultOtptArtclStng("text", artclNm, artclDscrp, otptBscVl);
	}

	/**
	 * 이미지 출력요소 정의한다.
	 */
	public DefaultOtptArtclStng image(String artclNm, String artclDscrp, String otptBscVl){
		return new DefaultOtptArtclStng("image", artclNm, artclDscrp, otptBscVl);
	}

	/**
	 * 이미지 묶음 출력요소 정의한다.
	 */
	public DefaultOtptArtclStng images(String artclNm, String artclDscrp, String otptBscVl){
		return new DefaultOtptArtclStng("images", artclNm, artclDscrp, otptBscVl);
	}

	/**
	 * 포맷원형에 출력요소를 추가한다.
	 */
	public void add(DefaultOtptArtclStng... prototypeStngs){
		this.prototypeStngs.addAll(List.of(prototypeStngs));
	}

	/**
	 * 포맷원형에 우편물 출력요소를 추가한다.
	 */
	public void addForPost(DefaultOtptArtclStng... prototypeStngs){
		this.prototypeStngs.addAll(Stream.of(prototypeStngs)
			.map(setting -> setting.post()).toList()
		);
	};

	/**
	 * 출력요소에 매핑될 값을 반환한다.
	 */
	public abstract String getMappingValue(String otptArtclNm, String defaultValue, boolean forPost, DataObject dataObject, FimsPrintOption printOption, Print print);

	public abstract List<String> getMappingValues(String otptArtclNm, String defaultValue, boolean forPost, DataObject dataObject, FimsPrintOption printOption, Print print);

	/**
	 * 우편물 출력요소에 매핑될 값을 반환한다.
	 */
	public abstract String getValueForPost(String otptArtclNm, String defaultValue, DataObject dataObject, FimsPrintOption printOption, Print print);

	/**
	 * 시군구별 출력설정요소에 속성(요소 유형, 출력설명, 출력물 내 유일 요소 여부, 우편물 관련 요소 여부, 기본값)을 추가한다.
	 */
	public void appendOtptAttribute(List<DataObject> otptArtclStngList) {
		if (isEmpty(otptArtclStngList)) return;

		List<DefaultOtptArtclStng> prototypeStngs = getPrototypeStngs();

		for (DataObject otptArtclStng : otptArtclStngList) {
			String otptArtclNm = otptArtclStng.string("OTPT_ARTCL_NM");
			DefaultOtptArtclStng prototypeStng = prototypeStngs.stream()
				.filter(item -> item.getArtclNm().equals(otptArtclNm))
				.toList()
				.get(0);

			otptArtclStng.set("UNIQUE_YN", prototypeStng.isUnique() ? "Y" : "N");
			otptArtclStng.set("DSCRP", prototypeStng.getArtclDscrp());
			otptArtclStng.set("COMPONENT_TYPE", prototypeStng.getComponentType());
			otptArtclStng.set("POST_YN", prototypeStng.isForPost() ? "Y" : "N");
		}
	}

	/**
	 * 출력용 샘플 데이터를 생성한다.
	 */
	public abstract DataObject createSampleData();

	public List<DataObject> getMosSet(String imageKey, DataObject dataObject) {
		return CmmnUtil.getDataObjectListFromMap(dataObject,"MOS"+imageKey);
	}

	/**
	 * 서식에서 사용하는 출력항목 중 사용하는 항목만 추출한다.
	 */
	public List<DefaultOtptArtclStng> filterUsedArtcls() {
		List<String> otptArtclNmList = otptArtclStngList.stream()
			.map(item -> item.string("OTPT_ARTCL_NM"))
			.toList();

		return getPrototypeStngs().stream()
			.filter(item -> otptArtclNmList.contains(item.getArtclNm()))
			.toList();
	}

	/**
	 * 다운로드 이력에 남길 항목명을 추출한다.
	 */
	public List<String> filterDownloadDataNames() {
		return filterUsedArtcls().stream()
			.filter(item -> item.isDownloadData())
			.map(DefaultOtptArtclStng::getArtclDscrp)
			.toList();
	}

	public Map<String, Object> makeFile() {
		Map<String, Object> result = new HashMap<>();

		//폰트 로드
		Map<String, PDType0Font> fontMap = getFontMap(writer = new PDFWriter());
		//용지 크기 설정
		writer.paper("01".equals(this.paperSeCd) ? "A4" : "LETTER");
		List<DefaultOtptArtclStng> prototypeStngList = getPrototypeStngs();

		try {
			//출력요소별 속성 정보 추가
			appendOtptAttribute(otptArtclStngList);

			//출력 요소 정렬
			Comparator<DataObject> comparator1 = (item1, item2) -> {
				int int1 = item1.string("UNIQUE_YN").equals("Y") ? 0 : 1;
				int int2 = item2.string("UNIQUE_YN").equals("Y") ? 0 : 1;
				return Integer.compare(int1, int2);
			};
			Comparator<DataObject> comparator2 = (item1, item2) -> {
				int int1 = item1.number("OTPT_ARTCL_SEQ").intValue();
				int int2 = item2.number("OTPT_ARTCL_SEQ").intValue();
				return Integer.compare(int1, int2);
			};
			otptArtclStngList = otptArtclStngList.stream()
				.sorted(comparator1.thenComparing(comparator2))
				.toList();

			//대표단속사진 설정 여부 확인
			boolean rprsCrdnPhotoYn = otptArtclStngList.stream()
				.filter(item -> item.string("OTPT_ARTCL_NM").equals("rprsCrdnPhoto"))
				.count() > 0;

			for (DataObject dataObject : data) {
				writer.beginPage();

				boolean completeFoldLine = false;

				for (DataObject otptArtclStng : otptArtclStngList) {
					String otptArtclNm = otptArtclStng.string("OTPT_ARTCL_NM");

					if (!otptArtclNm.equals("background") && !completeFoldLine) {
						//접는곳 점선 그리기
						if (!otptBscStng.string("FOLD_LINE_PSTN_SE_CD").equals("")) {
							this.renderFoldLine(writer, otptBscStng.string("FOLD_LINE_PSTN_SE_CD"));
						}
						completeFoldLine = true;
					}

					DefaultOtptArtclStng prototypeStng = prototypeStngList.stream()
							.filter(item -> item.getArtclNm().equals(otptArtclNm)).toList().get(0);

					float[] pstn =  this.getPstnStng(prototypeStng, otptArtclStng, this.paperSeCd);
					float[] size = this.getSize(prototypeStng, otptArtclStng);

					boolean forPost = prototypeStng.isForPost();
					String defaultValue = prototypeStng.getOtptBscVl();

					String componentType = prototypeStng.getComponentType();
					if ("text".equals(componentType)) {
						String align = this.getAlign(prototypeStng, otptArtclStng);
						String lineChgYn = this.getLineChgYn(prototypeStng, otptArtclStng);
						PDType0Font font = this.getFontType(prototypeStng, otptArtclStng, fontMap);
						int fontSz = this.getFontSize(prototypeStng, otptArtclStng);
						RenderingMode fontStyle = this.getFontStyle(prototypeStng, otptArtclStng);
						PDColor fontColr = this.getFontColor(prototypeStng, otptArtclStng);

						String textValue = this.getMappingValue(otptArtclNm,defaultValue,forPost,dataObject,printOption, this.print);

						this.writeText(writer, textValue, pstn, size[0], align, lineChgYn
								, font, fontSz, fontStyle, fontColr);

					} else if ("image".equals(componentType)) {

						String imagePath = this.getMappingValue(otptArtclNm,defaultValue,forPost,dataObject,printOption,this.print);

						if (EgovStringUtil.null2void(imagePath).equals("")) {
							continue;
						}

						if (!imagePath.startsWith("files")) {
							imagePath = CmmnUtil.copyStaticResource(imagePath, false);
						}

						File imageFile = new File(imagePath);
						if (!imageFile.exists()) {
							if (otptArtclNm.equals("offcs"))
				                throw new HttpStatusCodeException(500, "{\"failed\":true,\"description\":\"직인 파일을 찾을 수 없습니다.\"}", true);
							if (otptArtclNm.equals("background"))
								throw new HttpStatusCodeException(500, "{\"failed\":true,\"description\":\"배경이미지 파일을 찾을 수 없습니다.\"}", true);
							if (otptArtclNm.equals("postSenderLogo"))
								throw new HttpStatusCodeException(500, "{\"failed\":true,\"description\":\"우편물 송신자 로고 파일을 찾을 수 없습니다.\"}", true);

							continue;
						}
						InputStream	is = new FileInputStream(imageFile);

						String imageKey = "";
						List<DataObject> mosSet = null;
						if (otptArtclNm.equals("rprsCrdnPhoto")) {
							imageKey = this.getMappingValue(otptArtclNm+"Key",defaultValue,forPost,dataObject,printOption,this.print);
							mosSet = this.getMosSet(imageKey, dataObject);
						}
						this.insertImage(writer, is, pstn, size);
						is.close();

						if (otptArtclNm.equals("rprsCrdnPhoto") && !imageKey.equals("") && mosSet != null) {

							File imageFile0 = new File(imagePath);
							InputStream is0 = new FileInputStream(imageFile0);

			    			List<PstnAndSize> transformedMosaicInfos = MediaUtil.getTransformedMosaic(
			    				mosSet, is0, new PstnAndSize(pstn[0],pstn[1], size[0], size[1])
			    			);
			    			is0.close();

			    			for (int k=0; k < transformedMosaicInfos.size(); k++) {
			    				PstnAndSize mosaicInfo = transformedMosaicInfos.get(k);

			    				InputStream blackImageStream = new ClassPathResource("samplefiles/black.png").getInputStream();
			    				this.insertImage(writer, blackImageStream,
			    						mosaicInfo.getPstn().to2Float(), mosaicInfo.getSize().to2Float());
			    				blackImageStream.close();
			    			}

						}


					} else if ("images".equals(componentType)) {

						List<String> imagePaths = this.getMappingValues(otptArtclNm,defaultValue,forPost,dataObject,printOption,this.print);

						List<String> imageKeys = new ArrayList<String>();
						List<List<DataObject>> mosSets = new ArrayList<List<DataObject>>();
						if (otptArtclNm.equals("crdnPhoto")) {
							imageKeys = this.getMappingValues(otptArtclNm+"Key",defaultValue,forPost,dataObject,printOption,this.print);
							if (rprsCrdnPhotoYn && imagePaths.size() > 0) {
								imageKeys.remove(0);
							}
							for (String imageKey : imageKeys) {
								mosSets.add(this.getMosSet(imageKey, dataObject));
							}

						}

						if (otptArtclNm.equals("crdnPhoto")) {
							if (rprsCrdnPhotoYn && imagePaths.size() > 0) {
								imagePaths.remove(0);
							}
						}

						int printCntSetting = otptBscStng.number("OTPT_PHOTO_CNT").intValue();
						int dataImageCnt = imagePaths.size();

						if (printCntSetting != 0 && dataImageCnt != 0) {

							List<PstnAndSize> devideInfos = this.devideImageArea(pstn, size, printCntSetting, dataImageCnt);

							for (int i=0; i< devideInfos.size(); i++){
								PstnAndSize devideInfo = devideInfos.get(i);

								if (EgovStringUtil.null2void(imagePaths.get(i)).equals("")) {
									continue;
								}

								if (!imagePaths.get(i).startsWith("files")) {
									imagePaths.set(i, CmmnUtil.copyStaticResource(imagePaths.get(i), false));
								}

								String imagePath = imagePaths.get(i);

								File imageFile = new File(imagePath);
								InputStream is = new FileInputStream(imageFile);

								this.insertImage(writer, is,
										devideInfo.getPstn().to2Float(), devideInfo.getSize().to2Float());
								is.close();

								if (otptArtclNm.equals("crdnPhoto") && !imageKeys.isEmpty() && !mosSets.isEmpty()) {

									if (mosSets.get(i) != null) {
										String imagePath0 = imagePaths.get(i);

										File imageFile0 = new File(imagePath0);
										InputStream is0 = new FileInputStream(imageFile0);

										List<PstnAndSize> transformedMosaicInfos = MediaUtil.getTransformedMosaic(
												mosSets.get(i), is0, devideInfo
										);
										is0.close();

										for (int k=0; k < transformedMosaicInfos.size(); k++) {
						    				PstnAndSize mosaicInfo = transformedMosaicInfos.get(k);
						    				InputStream blackImageStream = new ClassPathResource("samplefiles/black.png").getInputStream();
						    				this.insertImage(writer, blackImageStream,
						    						mosaicInfo.getPstn().to2Float(), mosaicInfo.getSize().to2Float());
						    				blackImageStream.close();
						    			}
									}
								}
							}
						}
					}
				}//출력항목 loop 끝

				if (!completeFoldLine) {
					if (!isEmpty(otptBscStng.string("FOLD_LINE_PSTN_SE_CD"))) {
						this.renderFoldLine(writer, otptBscStng.string("FOLD_LINE_PSTN_SE_CD"));
					}
				}

				writer.endPage();
			} //출력자료건수 loop 끝

			String downloadFileName = this.print.getFormatKorName()+"_"+this.print.getPrintRequestDt()+".pdf";
			result.put("download", writer.getDownloadable().setFilename(downloadFileName));
			result.put("downloadData", data);
			result.put("dataNames", this.filterDownloadDataNames());

		} catch(FileNotFoundException e) {
			throw new RuntimeException("파일을 찾을 수 없습니다.");
		} catch(HttpStatusCodeException e) {
			throw e;
		} catch (Exception e) {
			throw new RuntimeException("출력오류."+e);
		}

		return result;
	}

	/**
	 * mm단위로 입력된 값을 pdf좌표계 형식으로 변환한다.
	 */
	public PDFCoordinate toPDFCoordinate(float[] XYmm) {
		float Xmm = XYmm[0];
		float Ymm = XYmm[1];

		float Xpt = CmmnUtil.mmToPt(Xmm);
		float Ypt = CmmnUtil.mmToPt(Ymm);

		float YptMax = CmmnUtil.mmToPt(this.paperMilimeter[1]);

		return new PDFCoordinate(Xpt, Ypt, YptMax);
	}

	/**
	 * 텍스트를 pdf에 입력한다.
	 */
	public void writeText(PDFWriter writer, String allText,
			float[] XYmm, float textAreaWidth_mm, String align, String newLineYn,
			PDType0Font font, int fontSize, RenderingMode fontStyle, PDColor fontColor) {
		try {
			if (allText == null)
				allText = "";
			if (isEmpty(newLineYn))
				newLineYn = "N";

			float[] xyAbsolute = this.toPDFCoordinate(XYmm).absolute();
			float textAreaWidth_pt = CmmnUtil.mmToPt(textAreaWidth_mm);
			float allTextWidth = calcTextWidth(font, fontSize, allText);
			String textArr[];
			if (allText.equals("") || allText.length() == 1 || textAreaWidth_pt == 0
					|| (allTextWidth <= textAreaWidth_pt)
					|| newLineYn.equals("N")) {
				textArr = new String[] { allText };
			} else {
				textArr = splitByLineForLargeText(font, fontSize, allText, textAreaWidth_pt);
			}

			String firstLine = textArr[0];
			float firstLineWidth = calcTextWidth(font, fontSize, firstLine);

			writer.beginText();
			writer.setFont(font, fontSize);
			writer.setRenderingMode(fontStyle);
			writer.setNonStrokingColor(fontColor);

			float resultY = xyAbsolute[1] - fontSize;

			float firstLineAlignCorrection = 0;
			if (align.equals("right")) {
				firstLineAlignCorrection = textAreaWidth_pt - firstLineWidth;
			} else if (align.equals("center")) {
				firstLineAlignCorrection = (textAreaWidth_pt - firstLineWidth)/2.0f;
			}

			float resultX = xyAbsolute[0] + firstLineAlignCorrection;

			writer.newLineAtOffset(resultX, resultY);
			writer.showText(textArr[0]);

			if (textArr.length > 1) {
				float beforeLineWidth = firstLineWidth;

				for (int i = 1; i < textArr.length; i++) {
					String thisLine = textArr[i];
					float thisLineWidth = this.calcTextWidth(font, fontSize, thisLine);
					float alignCorrection = 0;

					if (align.equals("right")) {
						alignCorrection = beforeLineWidth - thisLineWidth;
					} if (align.equals("center")) {
						alignCorrection = (beforeLineWidth - thisLineWidth)/2.0f;
					}

					writer.newLineAtOffset(alignCorrection, -(fontSize + 1));
					writer.showText(thisLine);

					beforeLineWidth = thisLineWidth;
				}
			}

			writer.endText();
		} catch (IOException e) {
			throw new RuntimeException("PDF 파일 출력 중 오류가 발생하였습니다."+e);
		}
	}

	/**
	 * 글꼴모양,폰트크기 등에 따른 텍스트의 가로 길이(pt)를 계산한다.
	 */
	public float calcTextWidth(PDType0Font font, int fontSize, String text) {
		try {
			return (font.getStringWidth(text) / 1000.0f) * fontSize;
		} catch (IOException e) {
			throw new RuntimeException("PDF 파일 출력 중 오류가 발생하였습니다.");
		}
	}

	/**
	 * 출력할 텍스트가 지정한 영역 안에 들어가지 않을 경우 줄바꿈 처리를 위해 텍스트를 나누어 배열로 반환한다.
	 */
	public String[] splitByLineForLargeText(PDType0Font font, int fontSize, String allText, float textAreaWidth_pt) {
		float allTextWidth = calcTextWidth(font, fontSize, allText);

		if (allText.equals("") || allText.length() == 1 || textAreaWidth_pt == 0
				|| (allTextWidth <= textAreaWidth_pt)
			) {
			return new String[] { allText };
		}

		ArrayList<String> textList = new ArrayList<String>();
		while(!allText.equals("")) {
			int lengthForLine = this.getLengthForLine(font, fontSize, allText, textAreaWidth_pt);
			String newLine = allText.substring(0,lengthForLine);
			textList.add(newLine);
			allText = allText.substring(lengthForLine);
		}

		return textList.toArray(new String[textList.size()]);
	}

	/**
	 * 텍스트를 줄바꿈 처리할 경우 첫 라인에 출력될 글자 수를 반환한다.
	 */
	private int getLengthForLine(PDType0Font font, int fontSize, String allText, float textAreaWidth_pt) {
		int forLine = allText.length();
		if (forLine <= 1)
			return forLine;

		for ( ; ; forLine--) {
			if (forLine == 1)
				break;

			boolean hit = ( this.calcTextWidth(font, fontSize, allText.substring(0, forLine)) <= textAreaWidth_pt );
			if (hit)
				break;
		}

		return forLine;
	}

	/**
	 * pdf에 이미지를 삽입한다.
	 */
	public void insertImage(PDFWriter writer, InputStream imageStream, float[] XYmm, float[] SIZEmm) {
		float[] xyAbsolute = this.toPDFCoordinate(XYmm).absolute();
		float[] size = new float[] {CmmnUtil.mmToPt(SIZEmm[0]),CmmnUtil.mmToPt(SIZEmm[1])};

		try {
			writer.image(imageStream, xyAbsolute[0], xyAbsolute[1]-size[1], size[0], size[1]);
		} catch (IOException e) {
			throw new RuntimeException("PDF 파일 출력 중 오류가 발생하였습니다."+e);
		}
	}

	public List<PstnAndSize> devideImageArea(float[] totAreaPstn, float[] totAreaSize, int imageCntSetting, int dataImageCnt) {
		List<PstnAndSize> result = new ArrayList<PstnAndSize>();

		int printable = imageCntSetting <= dataImageCnt ? imageCntSetting : dataImageCnt;

		if (printable <= 1) {
			result.add(new PstnAndSize(totAreaPstn[0], totAreaPstn[1], totAreaSize[0], totAreaSize[1]));
			return result;
		}

		boolean longerThenWidth = totAreaSize[1] > totAreaSize[0];

		int xLengthOfD2 = 0;
		int yLengthOfD2 = 0;

		if (printable == 2){
			if (longerThenWidth) {
				xLengthOfD2 = 1;
				yLengthOfD2 = 2;
			} else {
				xLengthOfD2 = 2;
				yLengthOfD2 = 1;
			}
		} else if (printable == 3 || printable == 4){
			xLengthOfD2 = 2;
			yLengthOfD2 = 2;
		} else if (printable == 5 || printable == 6){
			if (longerThenWidth) {
				xLengthOfD2 = 2;
				yLengthOfD2 = 3;
			} else {
				xLengthOfD2 = 3;
				yLengthOfD2 = 2;
			}
		} else {
			throw new RuntimeException("PDF 파일 출력 중 오류가 발생하였습니다.");
		}

		PstnAndSize[][] d2Array = new PstnAndSize[xLengthOfD2][yLengthOfD2];
		Size cellSize = new Size(totAreaSize[0] / (xLengthOfD2 * 1.0f), totAreaSize[1] / (yLengthOfD2 * 1.0f));

		d2Array[0][0] = new PstnAndSize(totAreaPstn[0], totAreaPstn[1], cellSize.getWidth(), cellSize.getHeight());
		d2Array = this.fillImageCells(d2Array);

		//출력영역 크기 병합
		if (printable == 3 || printable == 5) {

			boolean longerThenWidth_cell = cellSize.getHeight() > cellSize.getWidth();
			if (longerThenWidth_cell) {
				d2Array = this.mergeLastImageCell(d2Array, "left");
			} else {
				d2Array = this.mergeLastImageCell(d2Array, "up");
			}

		}
		for (int y=0; y < yLengthOfD2; y++) {
			for (int x=0; x < xLengthOfD2; x++) {
				if (d2Array[x][y] != null) {
					result.add(d2Array[x][y]);
				}
			}
		}

		return result;
	}

	private PstnAndSize[][] fillImageCells(PstnAndSize[][] d2) {
		int xLengthOfD2 = d2.length;
		int yLengthOfD2 = d2[0].length;

		PstnAndSize firstCell = d2[0][0];

		Size cellSize = firstCell.getSize();
		float cellWidth = cellSize.getWidth();
		float cellHeight = cellSize.getHeight();

		Pstn startPoint = firstCell.getPstn();
		float spX = startPoint.getLeft();
		float spY = startPoint.getTop();

		for (int x=0; x < xLengthOfD2; x++) {
			for (int y=0; y < yLengthOfD2; y++) {
				if (x != 0 || y != 0) {
					d2[x][y] = new PstnAndSize(spX + (cellWidth * (x * 1.0f)), spY + (cellHeight * (y * 1.0f)), cellSize);
				}
			}
		}

		return d2;
	}

	public PstnAndSize[][] mergeLastImageCell(PstnAndSize[][] d2, String mergeDirection) {
		if (!"up".equals(mergeDirection) && !"left".equals(mergeDirection))
			throw new RuntimeException("PDF 파일 출력 중 오류가 발생하였습니다.");

		int xLengthOfD2 = d2.length;
		int yLengthOfD2 = d2[0].length;

		int lastCellIndexX = xLengthOfD2-1;
		int lastCellIndexY = yLengthOfD2-1;

		int mergeTargetCellIndexX = 0;
		int mergeTargetCellIndexY = 0;

		if (mergeDirection.equals("up")) {
			mergeTargetCellIndexX = lastCellIndexX;
			mergeTargetCellIndexY = lastCellIndexY-1;
		}
		if (mergeDirection.equals("left")) {
			mergeTargetCellIndexX = lastCellIndexX-1;
			mergeTargetCellIndexY = lastCellIndexY;
		}

		//병합대상셀
		if (mergeDirection.equals("up")) {
			d2[mergeTargetCellIndexX][mergeTargetCellIndexY] = (d2[mergeTargetCellIndexX][mergeTargetCellIndexY]).x2Height();
		}
		if (mergeDirection.equals("left")) {
			d2[mergeTargetCellIndexX][mergeTargetCellIndexY] = (d2[mergeTargetCellIndexX][mergeTargetCellIndexY]).x2Width();
		}

		//마지막셀
		d2[lastCellIndexX][lastCellIndexY] = null;

		return d2;
	}

	/**
	 * pdf에 봉합선을 넣는다.
	 */
	public void renderFoldLine(PDFWriter writer, String foldCode) {
		if (foldCode.equals("") && !foldCode.equals("01") && !foldCode.equals("02")) {
			return;
		}

		float paperWidth = writer.getPaperSize().getWidth();

		float partHeight;
		if (foldCode.equals("01")) {
			if (PDRectangle.A4.equals(writer.getPaperSize())) {
				partHeight = 99;
			} else {
				partHeight = 93;
			}
		} else {
			partHeight = 93;
		}

		float margin = 0;
		if (foldCode.equals("02")) {
			margin = 18;
		}

		float bottomFold = margin + partHeight; //하단접는곳
		float topFold = margin + (partHeight * 2); //상단접는곳

		float bottomFold_pt = CmmnUtil.mmToPt(bottomFold);
		float topFold_pt  = CmmnUtil.mmToPt(topFold);

		try {
			writer.setLineDashPattern(new float[]{3,1}, 0);
			writer.moveTo(0, topFold_pt);
			writer.lineTo(paperWidth, topFold_pt);
			writer.stroke();
			writer.moveTo(0, bottomFold_pt);
			writer.lineTo(paperWidth, bottomFold_pt);
			writer.stroke();
			writer.setLineDashPattern(new float[]{}, 0);
		} catch (IOException e) {
			throw new RuntimeException("PDF 파일 출력 중 오류가 발생하였습니다."+e);
		}
	}

	public float[] getPstnStng(DefaultOtptArtclStng prototypeStng, DataObject sggStng, String paperSeCd) {
		if (!isEmpty(sggStng.get("LEFT_PSTN")) && !isEmpty(sggStng.get("TOP_PSTN"))) {
			return new float[] {
				toFloat(sggStng.get("LEFT_PSTN")),
				toFloat(sggStng.get("TOP_PSTN"))
			};
		} else {
			return new float[] {
				prototypeStng.getLeftPstn(paperSeCd),
				prototypeStng.getTopPstn(paperSeCd)
			};
		}
	}

	public PDType0Font getFontType(DefaultOtptArtclStng prototypeStng, DataObject sggStng, Map<String, PDType0Font> fontMap) {
		String fontName = sggStng != null ? sggStng.string("FONT_NM") : "";
		return fontMap.get(ifEmpty(fontName, prototypeStng::getFontNm));
	}

	public int getFontSize(DefaultOtptArtclStng prototypeStng, DataObject sggStng) {
		int fontSize = toInt(sggStng.get("FONT_SZ"));
		return fontSize > 0 ? fontSize : prototypeStng.getFontSz();
	}

	public RenderingMode getFontStyle(DefaultOtptArtclStng prototypeStng, DataObject sggStng) {
		return RenderingMode.valueOf(ifEmpty(sggStng.string("FONT_STYLE"), prototypeStng::getFontStyle));
	}

	public PDColor getFontColor(DefaultOtptArtclStng prototypeStng, DataObject sggStng) {
		return PDFColors.getColor(ifEmpty(sggStng.string("FONT_COLR"), prototypeStng::getFontColr));
	}

	public float[] getSize(DefaultOtptArtclStng prototypeStng, DataObject sggStng) {
		if (!isEmpty(sggStng.get("WIDTH_SZ"))) {
			return new float[] {
				toFloat(sggStng.get("WIDTH_SZ")),
				toFloat(sggStng.get("HEIGHT_SZ"))
			};
		} else {
			return new float[] {
				prototypeStng.getWidthSz(),
				prototypeStng.getHeightSz()
			};
		}
	}

	public String getAlign(DefaultOtptArtclStng prototypeStng, DataObject sggStng) {
		return ifEmpty(sggStng.string("TEXT_SORT"), prototypeStng::getTextSort);
	}

	public String getLineChgYn(DefaultOtptArtclStng prototypeStng, DataObject sggStng) {
		return ifEmpty(sggStng.string("LINE_CHG_YN"), prototypeStng::getLineChgYn);
	}

	public Map<String, PDType0Font> getFontMap(PDFWriter writer) {
		try {
			return Map.of(
				"gulim", writer.font("fonts/gulim.ttf"),
				"gulimche", writer.font("fonts/gulimche.ttf"),
				"batang", writer.font("fonts/batang.ttf"),
				"batangche", writer.font("fonts/batangche.ttf"),
				"dotum", writer.font("fonts/dotum.ttf"),
				"dotumche", writer.font("fonts/dotumche.ttf"),
				"gungsuh", writer.font("fonts/gungsuh.ttf"),
				"gungsuhche", writer.font("fonts/gungsuhche.ttf")
			);
		} catch (IOException e) {
			throw new RuntimeException("글꼴 로드 오류"+e);
		}
	}
}