/*
 * Copyright (C) 2005-2006 Kouji Sugisawa. All rights reserved.
 */

package jp.sourceforge.livez.imode;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;

import jp.sourceforge.livez.IllegalUserAgentException;
import jp.sourceforge.livez.UserAgentNotFoundException;

/**
 * ImodeUserAgent を実装したクラスです。
 * 
 * @author 杉澤 浩二
 */
final class ImodeUserAgentImpl implements ImodeUserAgent {

	private static final String WLAN_PREFIX			= "WLAN_";
	private static final String DOCOMO_PREFIX			= "DoCoMo/";

	private static final String CACHE_SIZE_PATTERN	= "c[0-9]{2,}";
	private static final String ACCESS_TYPE_PATTERN	= "T[BCDJE]";
	private static final String CHAR_SIZE_PATTERN		= "W[0-9]+H[0-9]+";

	private static final String SER_PREFIX			= "ser";
	private static final String SER_PATTERN			= "ser[0-9a-zA-Z]{11}";

	private static final String SER_FOMA_PATTERN		= "ser[0-9a-zA-Z]{15}";
	private static final String ICC_PREFIX			= "icc";
	private static final String ICC_PATTERN			= "icc[0-9a-zA-Z]{20}";

	private static final String TRUSTED_PREFIX		= "TdID";
	private static final String TRUSTED_APID_PATTERN	= "TdID[0-9a-zA-Z]{11}";

	private static final String CAR_NAVI_ID			= "ex_navi";

	/**
	 * PDC 端末の HTTPユーザエージェントパターンです。<p>
	 */
	private static final String UAERAGENT_PDC =
		"^" + DOCOMO_PREFIX + "1\\.0/[^/]+" +
			"(/" + CACHE_SIZE_PATTERN + "){0,2}" +	// F251i ではキャッシュパラメータが重複して送出されるものがある
			"(/" + ACCESS_TYPE_PATTERN + "){0,1}" +
			"(/" + CHAR_SIZE_PATTERN + "){0,1}" +
			"(/" + SER_PATTERN + "){0,1}" +
			"(/" + TRUSTED_APID_PATTERN + "){0,1}" +
		"$";

	/**
	 * FOMA 端末の HTTPユーザエージェントパターンです。<p>
	 */
	private static final String USERAGENT_FOMA =
		"^(" + WLAN_PREFIX + "){0,1}" + DOCOMO_PREFIX + "2\\.0( |　){1}[^/ (;)]+\\(" + CACHE_SIZE_PATTERN +
			"(" +
				"(;" + ACCESS_TYPE_PATTERN + "){0,1}" +
				"(;" + CHAR_SIZE_PATTERN + "){0,1}" +
				"(;" + SER_FOMA_PATTERN + ";" + ICC_PATTERN + "){0,1}" +
				"(;" + TRUSTED_APID_PATTERN + "){0,1}" +
			"){0,1}" +
		"\\)$";

	/**
	 * iモード対応カーナビの HTTPユーザエージェントパターンです。<p>
	 */
	private static final String USERAGENT_CARNAVI =
		"^" + DOCOMO_PREFIX + "1\\.0/" + CAR_NAVI_ID + "_[0-9]\\.[0-9]_.+$";

	/**
	 * iモード対応 PlayStation の HTTPユーザエージェントパターンです。<p>
	 */
	private static final String USERAGENT_PLAYSTATION =
		"^" + DOCOMO_PREFIX + "1\\.0/ex_ps[0-9]+" +
			"(/" + CACHE_SIZE_PATTERN + "){0,1}"	+
		"$";

	/**
	 * 指定された HTTP リクエスト情報からこのクラスを構築します。<p>
	 * 
	 * @param request HTTPリクエスト情報
	 * @throws UserAgentNotFoundException ユーザーエージェントが存在しない場合
	 * @throws IllegalUserAgentException ユーザエージェントの形式が不正な場合
	 */
	public ImodeUserAgentImpl(final HttpServletRequest request) throws UserAgentNotFoundException, IllegalUserAgentException {
		this(request.getHeader(HTTP_HEADER_KEY));
	}

	/**
	 * 指定された文字列からこのクラスを構築します。<p>
	 * 
	 * @param ua HTTP ユーザーエージェントを表す文字列
	 * @throws UserAgentNotFoundException ユーザーエージェントが null の場合
	 * @throws IllegalUserAgentException ユーザエージェントの形式が不正な場合
	 */
	public ImodeUserAgentImpl(final String ua) throws UserAgentNotFoundException, IllegalUserAgentException {
		if (ua == null)
			throw new UserAgentNotFoundException();

		try {
			parse(ua);
		} catch (IllegalArgumentException e) {
			throw new IllegalUserAgentException(e.getMessage(), e);
		}
	}

	private void parse(final String ua) throws IllegalUserAgentException, IllegalArgumentException {
		this.value = ua;
		this.type = getImodeType(ua);

		if (this.type == ImodeType.FOMA) {
			if (ua.startsWith(WLAN_PREFIX))
				this.networkType = NetworkType.WLAN;

			String[] s = ua.split("[/ (;)]");
	
			this.id = s[2];
			// 端末IDを補正して端末名を設定します
			if ("MST_v_SH2101V".equals(this.id))
				this.name = "SH2101V";
			else
				this.name = this.id;

			parseName(this.name);

			for (int i = 3; i < s.length; i++) {
				if (s[i].matches(CACHE_SIZE_PATTERN)) {
					this.cacheSize = Integer.parseInt(s[i].substring(1));
				} else if (s[i].matches(ACCESS_TYPE_PATTERN)) {
					this.accessType = AccessType.getAccessType(s[i]);
				} else if (s[i].matches(CHAR_SIZE_PATTERN)) {
					String[] numbers = s[i].split("(W|H)");
					this.width	= Integer.parseInt(numbers[1]);
					this.height	= Integer.parseInt(numbers[2]);
				} else if (s[i].matches(SER_FOMA_PATTERN)) {
					this.ser = s[i].substring(SER_PREFIX.length());
				} else if (s[i].matches(ICC_PATTERN)) {
					this.icc = s[i].substring(ICC_PREFIX.length());
				} else if (s[i].matches(TRUSTED_APID_PATTERN)) {
					this.trustedId = s[i].substring(TRUSTED_PREFIX.length());
				}
			}
		} else if (this.type == ImodeType.CAR_NAVI) {
			this.id = CAR_NAVI_ID;
			this.name = CAR_NAVI_ID;
			final int start = (DOCOMO_PREFIX + "1.0/" + CAR_NAVI_ID).length() + 1;
			final int i = ua.indexOf('_', start);
			this.htmlVersion = new Float(ua.substring(start, i));
			this.browserName = ua.substring(i + 1);
		} else {
			String[] s = ua.split("/");

			this.id = s[2];
			if ("P209is".equals(this.id))
				this.name = "P209iS";
			else
				this.name = this.id;

			parseName(name);

			for (int i = 3; i < s.length; i++) {
				if (s[i].matches(CACHE_SIZE_PATTERN)) {
					this.cacheSize = Integer.parseInt(s[i].substring(1));
				} else if (s[i].matches(ACCESS_TYPE_PATTERN)) {
					this.accessType = AccessType.getAccessType(s[i]);
				} else if (s[i].matches(CHAR_SIZE_PATTERN)) {
					String[] numbers = s[i].split("(W|H)");
					this.width	= Integer.parseInt(numbers[1]);
					this.height	= Integer.parseInt(numbers[2]);
				} else if (s[i].matches(SER_PATTERN)) {
					this.ser = s[i].substring(SER_PREFIX.length());
				} else if (s[i].matches(TRUSTED_APID_PATTERN)) {
					this.trustedId = s[i].substring(TRUSTED_PREFIX.length());
				}
			}

			if (this.type == ImodeType.PDC && this.cacheSize <= 0)
				this.cacheSize = 5;
			if (this.type == ImodeType.PLAYSTATION)
				this.htmlVersion = new Float("2.0");
		}
	}

	/**
	 * 指定されたHTTPユーザエージェントから適切なiモード端末タイプを返します。<p>
	 * 
	 * @param ua HTTPユーザエージェント
	 * @return iモード端末タイプ
	 * @throws IllegalUserAgentException HTTPユーザエージェントが不正な場合
	 */
	private static ImodeType getImodeType(final String ua) throws IllegalUserAgentException {
		if (ua.matches(USERAGENT_FOMA))
			return ImodeType.FOMA;
		else if (ua.matches(USERAGENT_CARNAVI))
			return ImodeType.CAR_NAVI;
		else if (ua.matches(USERAGENT_PLAYSTATION))
			return ImodeType.PLAYSTATION;
		else if (ua.matches(UAERAGENT_PDC))
			return ImodeType.PDC;
		else
			throw new IllegalUserAgentException();
	}

	private void parseName(final String name) {
		if (name == null || !name.matches("^[A-Z]+[0-9]{3,}.*$"))
			return;

		// 数字で始まる位置を取得します
		int start = -1;
		for (int i = 0; i < name.length(); i++) {
			final char c = name.charAt(i);
			if (c >= '0' && c <= '9') {
				start = i;
				break;
			}
		}

		this.vendorId = name.substring(0, start);

		// 終了位置を取得します
		int end = -1;
		for (int i = start + 1; i < name.length(); i++) {
			final char c = name.charAt(i);
			if (c < '0' || c > '9') {
				end = i;
				if (c == 'i' || c == 'V')
					end = i + 1;
				break;
			}
		}

		// ベースとなるシリーズ名を取得します
		if (end == -1)
			this.seriesName = name.substring(start);
		else
			this.seriesName = name.substring(start, end);
	}


	/**
	 * ユーザエージェントの生値を保持します。
	 */
	private String value = null;

	/*
	 * @see jp.sourceforge.livez.UserAgent#getValue()
	 */
	public String getValue() {
		return value;
	}

	/**
	 * 端末 ID を保持します。
	 */
	private String id = null;

	/*
	 * @see jp.sourceforge.livez.UserAgent#getId()
	 */
	public String getId() {
		return id;
	}

	/**
	 * 通信の種類を保持します。
	 */
	private NetworkType networkType = NetworkType.IMODE;

	/*
	 * @see jp.sourceforge.livez.imode.ImodeUserAgent#getNetworkType()
	 */
	public NetworkType getNetworkType() {
		return networkType;
	}

	/**
	 * iモード端末タイプを保持します。
	 */
	private ImodeType type = ImodeType.PDC;

	/*
	 * @see jp.sourceforge.livez.imode.ImodeUserAgent#getType()
	 */
	public ImodeType getType() {
		return type;
	}

	/**
	 * 端末名を保持します。
	 */
	private String name = null;

	/*
	 * @see jp.sourceforge.livez.imode.ImodeUserAgent#getName()
	 */
	public String getName() {
		return name;
	}

	/**
	 * ベンダーIDを保持します。
	 */
	private String vendorId = null;

	/*
	 * @see jp.sourceforge.livez.imode.ImodeUserAgent#getVendorId()
	 */
	public String getVendorId() {
		return vendorId;
	}

	/**
	 * シリーズ名を保持します。
	 */
	private String seriesName = null;

	/*
	 * @see jp.sourceforge.livez.imode.ImodeUserAgent#getSeriesName()
	 */
	public String getSeriesName() {
		return seriesName;
	}

	/**
	 * キャッシュサイズ(キロバイト単位)を保持します。
	 */
	private int cacheSize = 0;

	/*
	 * @see jp.sourceforge.livez.imode.ImodeUserAgent#getCacheSize()
	 */
	public int getCacheSize() {
		return cacheSize;
	}

	/**
	 * 通信状態コードを保持します。
	 */
	private AccessType accessType = null;

	/*
	 * @see jp.sourceforge.livez.imode.ImodeUserAgent#getAccessType()
	 */
	public AccessType getAccessType() {
		return accessType;
	}

	/**
	 * 横表示可能バイト数を保持します。
	 */
	private int width = 0;

	/*
	 * @see jp.sourceforge.livez.imode.ImodeUserAgent#getWidth()
	 */
	public int getWidth() {
		return width;
	}

	/**
	 * 縦表示可能バイト数を保持します。
	 */
	private int height = 0;

	/*
	 * @see jp.sourceforge.livez.imode.ImodeUserAgent#getHeight()
	 */
	public int getHeight() {
		return height;
	}

	/**
	 * 製造番号を保持します。
	 */
	private String ser = null;

	/*
	 * @see jp.sourceforge.livez.imode.ImodeUserAgent#getSer()
	 */
	public String getSer() {
		return ser;
	}

	/**
	 * FOMA カード識別番号を保持します。
	 */
	private String icc = null;

	/*
	 * @see jp.sourceforge.livez.imode.ImodeUserAgent#getIcc()
	 */
	public String getIcc() {
		return icc;
	}

	/**
	 * トラステッドAPID を保持します。
	 */
	private String trustedId = null;

	/*
	 * @see jp.sourceforge.livez.imode.ImodeUserAgent#getTrustedId()
	 */
	public String getTrustedId() {
		return trustedId;
	}

	/**
	 * iモード対応カーナビ端末や PlayStation での対応HTMLバージョン情報を保持します。
	 */
	private Float htmlVersion = null;

	/*
	 * @see jp.sourceforge.livez.imode.ImodeUserAgent#getHtmlVersion()
	 */
	public Float getHtmlVersion() {
		return htmlVersion;
	}

	/**
	 * iモード対応カーナビ端末でのブラウザ名を保持します。
	 */
	private String browserName = null;

	/*
	 * @see jp.sourceforge.livez.imode.ImodeUserAgent#getBrowserName()
	 */
	public String getBrowserName() {
		return browserName;
	}

	@Override
	public int hashCode() {
		return HashCodeBuilder.reflectionHashCode(this);
	}

	@Override
	public boolean equals(final Object obj) {
		return EqualsBuilder.reflectionEquals(this, obj);
	}

	@Override
	public String toString() {
		return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
	}

}
