/*
 * 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.hayabusa.servlet.multipart;

import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.util.Locale ;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.ServletInputStream;

import org.opengion.fukurou.util.StringUtil;						// 6.9.0.0 (2018/01/31)
import org.opengion.fukurou.system.Closer ;

import static org.opengion.fukurou.system.HybsConst.CR ;			// 6.9.0.0 (2018/01/31)
import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

/**
 * ファイルアップロード時のマルチパート処理のパーサーです。
 *
 * @og.group その他機能
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class MultipartParser {
	private final ServletInputStream in;
	private final String boundary;
	private FilePart lastFilePart;
	private final byte[] buf = new byte[8 * 1024];
	private static final String DEFAULT_ENCODING = "MS932";
	private String encoding = DEFAULT_ENCODING;

	/**
	 * マルチパート処理のパーサーオブジェクトを構築する、コンストラクター
	 *
	 * @og.rev 5.3.7.0 (2011/07/01) 最大容量オーバー時のエラーメッセージ変更
	 * @og.rev 5.5.2.6 (2012/05/25) maxSize で、0,またはマイナスで無制限
	 * @og.rev 6.9.0.0 (2018/01/31) multipart 判定方法の変更
	 * @og.rev 7.4.2.0 (2021/04/30) Microsoft Edge は、boundary の取り方が違うので、その対応。
	 *
	 * @param	req		HttpServletRequestオブジェクト
	 * @param	maxSize	最大容量(0,またはマイナスで無制限)
	 * @throws IOException 入出力エラーが発生したとき
	 */
	public MultipartParser( final HttpServletRequest req, final int maxSize ) throws IOException {
//		String type = null;
		final String type1 = req.getHeader("Content-Type");
		final String type2 = req.getContentType();

		final String type = type1 != null && type2 != null && type1.length() < type2.length()
								? type2
								: StringUtil.nval( type1,type2 );

		// 6.9.0.0 (2018/01/31) multipart 判定方法の変更
//		if( type1 == null && type2 != null ) {
//			type = type2;
//		}
//		else if( type2 == null && type1 != null ) {
//			type = type1;
//		}
//		else if( type1 != null && type2 != null ) {
//			type = (type1.length() > type2.length() ? type1 : type2);
//		}

		// 6.9.0.0 (2018/01/31) multipart 判定方法の変更
		if( type == null || !type.toLowerCase(Locale.JAPAN).startsWith("multipart/form-data") ) {
//			throw new IOException("Posted content type isn't multipart/form-data");
			final String errMsg = "Posted content type isn't multipart/form-data" + CR
									+ "Content-Type=" + type ;
			throw new IOException( errMsg );
		}

		final int length = req.getContentLength();
		// 5.5.2.6 (2012/05/25) maxSize で、0,またはマイナスで無制限
		if( maxSize > 0 && length > maxSize ) {
			throw new IOException("登録したファイルサイズが上限(" + ( maxSize / 1024 / 1024 ) + "MB)を越えています。"
									+ " 登録ファイル=" + ( length / 1024 / 1024 ) + "MB" ); // 5.3.7.0 (2011/07/01)
		}

		// 4.0.0 (2005/01/31) The local variable "boundary" shadows an accessible field with the same name and compatible type in class org.opengion.hayabusa.servlet.multipart.MultipartParser
		// 7.4.2.0 (2021/04/30) Microsoft Edge は、boundary の取り方が違うので、その対応。
		String bound = extractBoundary(type);
//		final String bound = extractBoundary(type);
//		if( bound == null ) {
//			throw new IOException("Separation boundary was not specified");
//		}

		this.in = req.getInputStream();
	//	this.boundary = bound;					// 7.4.2.0 (2021/04/30)

		final String line = readLine();
		if( line == null ) {
			throw new IOException("Corrupt form data: premature ending");
		}
		// 7.4.2.0 (2021/04/30) Microsoft Edge は、boundary の取り方が違うので、その対応。
		else if( bound == null && line.contains( "WebKitFormBoundary" ) ) {
			bound = line;
		}

		this.boundary = bound;					// 7.4.2.0 (2021/04/30)

		if( !line.startsWith(boundary) ) {
			throw new IOException("Corrupt form data: no leading boundary: " +
														line + " != " + boundary);
		}
	}

	/**
	 * エンコードを設定します。
	 *
	 * @param  encoding エンコード
	 */
	public void setEncoding( final String encoding ) {
		 this.encoding = encoding;
	 }

	/**
	 * 次のパートを読み取ります。
	 *
	 * @og.rev 3.5.6.2 (2004/07/05) 文字列の連結にStringBuilderを使用します。
	 *
	 * @return	次のパート
	 * @throws IOException 入出力エラーが発生したとき
	 */
	public Part readNextPart() throws IOException {
		if( lastFilePart != null ) {
			Closer.ioClose( lastFilePart.getInputStream() );		// 4.0.0 (2006/01/31) close 処理時の IOException を無視
			lastFilePart = null;
		}

		String line = readLine();
		if( line == null || line.isEmpty() ) { return null; }

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );	// 6.1.0.0 (2014/12/26) refactoring
		final List<String> headers = new ArrayList<>();
		while( line != null && line.length() > 0 ) {
			String nextLine = null;
			boolean getNextLine = true;
			buf.setLength(0);									// 6.1.0.0 (2014/12/26) refactoring
			buf.append( line );
			while( getNextLine ) {
				nextLine = readLine();

				// 6.1.0.0 (2014/12/26) refactoring
				if( nextLine != null && nextLine.length() > 0 && ( nextLine.charAt(0) == ' ' || nextLine.charAt(0) == '\t' ) ) {
					buf.append( nextLine );
				}
				else {
					getNextLine = false;
				}
			}

			headers.add(buf.toString());
			line = nextLine;
		}

		if( line == null ) {
			return null;
		}

		String name		= null;
		String filename = null;
		String origname = null;
		String contentType = "text/plain";

		for( final String headerline : headers ) {
			if( headerline.toLowerCase(Locale.JAPAN).startsWith("content-disposition:") ) {
				final String[] dispInfo = extractDispositionInfo(headerline);

				name = dispInfo[1];
				filename = dispInfo[2];
				origname = dispInfo[3];
			}
			else if( headerline.toLowerCase(Locale.JAPAN).startsWith("content-type:") ) {
				final String type = extractContentType(headerline);
				if( type != null ) {
					contentType = type;
				}
			}
		}

		if( filename == null ) {
			return new ParamPart(name, in, boundary, encoding);
		}
		else {
			if( "".equals( filename ) ) {
				filename = null;
			}
			lastFilePart = new FilePart(name,in,boundary,contentType,filename,origname);
			return lastFilePart;
		}
	}

	/**
	 * ローカル変数「境界」アクセス可能なフィールドを返します。
	 *
	 * @og.rev 6.9.0.0 (2018/01/31) HttpConnect 使用時のﾌｧｲﾙ名の文字化け対策
	 *
	 * @param	line	１行
	 *
	 * @return	境界文字列
	 * @see		org.opengion.hayabusa.servlet.multipart.MultipartParser
	 */
	private String extractBoundary( final String line ) {
		// 4.0.0 (2005/01/31) The local variable "boundary" shadows an accessible field with the same name and compatible type in class org.opengion.hayabusa.servlet.multipart.MultipartParser
		int index = line.lastIndexOf("boundary=");
		if( index == -1 ) {
			return null;
		}
		String bound = line.substring(index + 9);
		if( bound.charAt(0) == '"' ) {
			index = bound.lastIndexOf('"');
			bound = bound.substring(1, index);
		}

		// 6.9.0.0 (2018/01/31) HttpConnect 使用時のﾌｧｲﾙ名の文字化け対策
		// HttpConnect で、MultipartEntityBuilder でﾌｧｲﾙをｱｯﾌﾟﾛｰﾄﾞするとき、
		// 日本語ﾌｧｲﾙ名が文字化けするため、setCharset で、UTF-8 指定しますが、
		// "; charset=UTF-8" という文字列がMIME変換文字にセットされる(バグ？)
		// のような動きをしており、強制的に削除しています。
		final int ad = bound.indexOf( "; charset=UTF-8" );
		if( ad >= 0 ) { bound=bound.substring( 0,ad ); }

		bound = "--" + bound;

		return bound;
	}

	/**
	 * コンテンツの情報を返します。
	 *
	 * @param	origline	元の行
	 *
	 * @return	コンテンツの情報配列
	 * @throws IOException 入出力エラーが発生したとき
	 */
	private String[] extractDispositionInfo( final String origline ) throws IOException {

		final String line = origline.toLowerCase(Locale.JAPAN);

		int start = line.indexOf( "content-disposition: " );
		int end = line.indexOf(';');
		if( start == -1 || end == -1 ) {
			throw new IOException( "Content disposition corrupt: " + origline );
		}
		final String disposition = line.substring( start + 21, end );
		if( !"form-data".equals(disposition) ) {
			throw new IOException("Invalid content disposition: " + disposition);
		}

		start = line.indexOf("name=\"", end);	// start at last semicolon
		end = line.indexOf( '"', start + 7);	// 6.0.2.5 (2014/10/31) refactoring skip name=\"
		if( start == -1 || end == -1 ) {
			throw new IOException("Content disposition corrupt: " + origline);
		}
		final String name = origline.substring(start + 6, end);

		String filename = null;
		String origname = null;
		start = line.indexOf("filename=\"", end + 2);	// start after name
		end = line.indexOf( '"', start + 10);			// skip filename=\"
		if( start != -1 && end != -1 ) {					// note the !=
			filename = origline.substring(start + 10, end);
			origname = filename;
			final int slash =
				Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\'));
			if( slash > -1 ) {
				filename = filename.substring(slash + 1);	// past last slash
			}
		}

		String[] retval = new String[4];	// 6.1.0.0 (2014/12/26) refactoring
		retval[0] = disposition;
		retval[1] = name;
		retval[2] = filename;
		retval[3] = origname;
		return retval;
	}

	/**
	 * コンテンツタイプの情報を返します。
	 *
	 * @param	origline	元の行
	 *
	 * @return	コンテンツタイプの情報
	 * @throws IOException 入出力エラーが発生したとき
	 */
	private String extractContentType( final String origline ) throws IOException {
		String contentType = null;

		final String line = origline.toLowerCase(Locale.JAPAN);

		if( line.startsWith("content-type") ) {
			final int start = line.indexOf(' ');
			if( start == -1 ) {
				throw new IOException("Content type corrupt: " + origline);
			}
			contentType = line.substring(start + 1);
		}
		else if( line.length() > 0 ) {	// no content type, so should be empty
			throw new IOException("Malformed line after disposition: " + origline);
		}

		return contentType;
	}

	/**
	 * 行を読み取ります。
	 *
	 * @return	読み取られた１行分
	 * @throws IOException 入出力エラーが発生したとき
	 */
	private String readLine() throws IOException {
		final StringBuilder sbuf = new StringBuilder( BUFFER_MIDDLE );
		int result;

		do {
			result = in.readLine(buf, 0, buf.length);
			if( result != -1 ) {
				sbuf.append(new String(buf, 0, result, encoding));
			}
		} while( result == buf.length );

		if( sbuf.length() == 0 ) {
			return null;
		}

		// 4.0.0 (2005/01/31) The method StringBuilder.setLength() should be avoided in favor of creating a new StringBuilder.
		String rtn = sbuf.toString();
		final int len = sbuf.length();
		if( len >= 2 && sbuf.charAt(len - 2) == '\r' ) {
			rtn = rtn.substring(0,len - 2);
		}
		else if( len >= 1 && sbuf.charAt(len - 1) == '\n' ) {
			rtn = rtn.substring(0,len - 1);
		}

		return rtn ;
	}
}
