/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.wicket.protocol.http;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.TimeZone;

import jakarta.servlet.http.Cookie;

import org.apache.wicket.markup.html.pages.BrowserInfoPage;
import org.apache.wicket.request.IRequestParameters;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.util.io.IClusterable;
import org.apache.wicket.util.string.AppendingStringBuffer;


/**
 * Description of various user agent (browser) properties. To fill the properties with values from
 * the user agent you need to probe the browser using javascript and request header analysis. Wicket
 * provides a default implementation of this in {@link BrowserInfoPage}.
 * <p>
 * A convenient way of letting Wicket do a sneaky redirect to {@link BrowserInfoPage} (and back
 * again) is to put this in your Application's init method:
 *
 * <pre>
 * getRequestCycleSettings().setGatherExtendedBrowserInfo(true);
 * </pre>
 *
 * <p>
 *
 * WARNING: Be sure you think about the dangers of depending on information you pull from the client
 * too much. They may be easily spoofed or inaccurate in other ways, and properties like window and
 * browser size are all too easy to be used naively.
 *
 * @see BrowserInfoPage
 * @author Frank Bille (frankbille)
 */
public class ClientProperties implements IClusterable
{
	private static final long serialVersionUID = 1L;

	private int browserHeight = -1;
	private int browserWidth = -1;
	private boolean navigatorCookieEnabled;
	private boolean navigatorJavaEnabled;
	private String navigatorAppCodeName;
	private String navigatorAppName;
	private String navigatorAppVersion;
	private String navigatorLanguage;
	private String navigatorPlatform;
	private String navigatorUserAgent;
	private String remoteAddress;
	private int screenColorDepth = -1;
	private int screenHeight = -1;
	private int screenWidth = -1;
	private String utcDSTOffset;
	private String utcOffset;
	private String jsTimeZone;
	private String hostname;

	private boolean javaScriptEnabled;

	/** Cached timezone for repeating calls to {@link #getTimeZone()} */
	private transient TimeZone timeZone;

	/**
	 * @return The browser height at the time it was measured
	 */
	public int getBrowserHeight()
	{
		return browserHeight;
	}

	/**
	 * @return The browser width at the time it was measured
	 */
	public int getBrowserWidth()
	{
		return browserWidth;
	}

	/**
	 * @return The client's navigator.appCodeName property.
	 */
	public String getNavigatorAppCodeName()
	{
		return navigatorAppCodeName;
	}

	/**
	 * @return The client's navigator.appName property.
	 */
	public String getNavigatorAppName()
	{
		return navigatorAppName;
	}

	/**
	 * @return The client's navigator.appVersion property.
	 */
	public String getNavigatorAppVersion()
	{
		return navigatorAppVersion;
	}

	/**
	 * @return The client's navigator.language (or navigator.userLanguage) property.
	 */
	public String getNavigatorLanguage()
	{
		return navigatorLanguage;
	}

	/**
	 * @return The client's navigator.platform property.
	 */
	public String getNavigatorPlatform()
	{
		return navigatorPlatform;
	}

	/**
	 * @return The client's navigator.userAgent property.
	 */
	public String getNavigatorUserAgent()
	{
		return navigatorUserAgent;
	}

	/**
	 * @return The client's remote/ip address.
	 */
	public String getRemoteAddress()
	{
		return remoteAddress;
	}

	/**
	 * @return The clients hostname shown in the browser
	 */
	public String getHostname()
	{
		return hostname;
	}

	/**
	 * @return Color depth of the screen in bits (integer).
	 */
	public int getScreenColorDepth()
	{
		return screenColorDepth;
	}

	/**
	 * @return Height of the screen in pixels (integer).
	 */
	public int getScreenHeight()
	{
		return screenHeight;
	}

	/**
	 * @return Height of the screen in pixels (integer).
	 */
	public int getScreenWidth()
	{
		return screenWidth;
	}

	/**
	 * Get the client's time zone if that could be detected.
	 *
	 * @return The client's time zone
	 */
	public TimeZone getTimeZone()
	{
		if (timeZone == null && jsTimeZone != null)
		{
			TimeZone temptimeZone = TimeZone.getTimeZone(jsTimeZone);
			if (jsTimeZone.equals(temptimeZone.getID()))
			{
				timeZone = temptimeZone;
			}
		}
		if (timeZone == null)
		{
			String utc = getUtcOffset();
			if (utc != null)
			{
				// apparently it is platform dependent on whether you get the
				// offset in a decimal form or not. This parses the decimal
				// form of the UTC offset, taking into account several
				// possibilities
				// such as getting the format in +2.5 or -1.2

				int dotPos = utc.indexOf('.');
				if (dotPos >= 0)
				{
					String hours = utc.substring(0, dotPos);
					String hourPart = utc.substring(dotPos + 1);

					if (hours.startsWith("+"))
					{
						hours = hours.substring(1);
					}
					int offsetHours = Integer.parseInt(hours);
					int offsetMins = (int)(Double.parseDouble(hourPart) * 6);

					// construct a GMT timezone offset string from the retrieved
					// offset which can be parsed by the TimeZone class.

					AppendingStringBuffer sb = new AppendingStringBuffer("GMT");
					sb.append(offsetHours > 0 ? '+' : '-');
					sb.append(Math.abs(offsetHours));
					sb.append(':');
					if (offsetMins < 10)
					{
						sb.append('0');
					}
					sb.append(offsetMins);
					timeZone = TimeZone.getTimeZone(sb.toString());
				}
				else
				{
					int offset = Integer.parseInt(utc);
					if (offset < 0)
					{
						utc = utc.substring(1);
					}
					timeZone = TimeZone.getTimeZone("GMT" + ((offset > 0) ? '+' : '-') + utc);
				}

				String dstOffset = getUtcDSTOffset();
				if (timeZone != null && dstOffset != null)
				{
					TimeZone dstTimeZone;
					dotPos = dstOffset.indexOf('.');
					if (dotPos >= 0)
					{
						String hours = dstOffset.substring(0, dotPos);
						String hourPart = dstOffset.substring(dotPos + 1);

						if (hours.startsWith("+"))
						{
							hours = hours.substring(1);
						}
						int offsetHours = Integer.parseInt(hours);
						int offsetMins = (int)(Double.parseDouble(hourPart) * 6);

						// construct a GMT timezone offset string from the
						// retrieved
						// offset which can be parsed by the TimeZone class.

						AppendingStringBuffer sb = new AppendingStringBuffer("GMT");
						sb.append(offsetHours > 0 ? '+' : '-');
						sb.append(Math.abs(offsetHours));
						sb.append(':');
						if (offsetMins < 10)
						{
							sb.append('0');
						}
						sb.append(offsetMins);
						dstTimeZone = TimeZone.getTimeZone(sb.toString());
					}
					else
					{
						int offset = Integer.parseInt(dstOffset);
						if (offset < 0)
						{
							dstOffset = dstOffset.substring(1);
						}
						dstTimeZone = TimeZone.getTimeZone("GMT" + ((offset > 0) ? '+' : '-') +
							dstOffset);
					}
					// if the dstTimezone (1 July) has a different offset then
					// the real time zone (1 January) try to combine the 2.
					if (dstTimeZone != null &&
						dstTimeZone.getRawOffset() != timeZone.getRawOffset())
					{
						int dstSaving = Math.abs(dstTimeZone.getRawOffset() - timeZone.getRawOffset());
						String[] availableIDs = TimeZone.getAvailableIDs(dstTimeZone.getRawOffset() < timeZone.getRawOffset() ? dstTimeZone.getRawOffset() : timeZone.getRawOffset());
						for (String availableID : availableIDs)
						{
							TimeZone zone = TimeZone.getTimeZone(availableID);
							if (zone.getDSTSavings() == dstSaving)
							{
								// this is a best guess... still the start and end of the DST should
								// be needed to know to be completely correct, or better yet
								// not just the GMT offset but the TimeZone ID should be transfered
								// from the browser.
								timeZone = zone;
								break;
							}
						}
					}
				}
			}
		}

		return timeZone;
	}

	/**
	 * @return The client's time DST offset from UTC in hours (note: if you do this yourself, use
	 *         'new Date(new Date().getFullYear(), 0, 6, 0, 0, 0, 0).getTimezoneOffset() / -60'
	 *         (note the -)).
	 */
	public String getUtcDSTOffset()
	{
		return utcDSTOffset;
	}


	/**
	 * @return The client's time offset from UTC in hours (note: if you do this yourself, use 'new
	 *         Date(new Date().getFullYear(), 0, 1, 0, 0, 0, 0).getTimezoneOffset() / -60' (note the
	 *         -)).
	 */
	public String getUtcOffset()
	{
		return utcOffset;
	}

	/**
	 * Flag indicating support of JavaScript in the browser.
	 *
	 * @return True if JavaScript is enabled
	 */
	public boolean isJavaScriptEnabled() {
		return javaScriptEnabled;
	}

	/**
	 *
	 *
	 * @return The client's navigator.cookieEnabled property.
	 */
	public boolean isNavigatorCookieEnabled()
	{
		if (!navigatorCookieEnabled && RequestCycle.get() != null)
		{
			Collection<Cookie> cookies = ((WebRequest)RequestCycle.get().getRequest()).getCookies();
			navigatorCookieEnabled = cookies != null && cookies.size() > 0;
		}
		return navigatorCookieEnabled;
	}

	/**
	 * @return The client's navigator.javaEnabled property.
	 */
	public boolean isNavigatorJavaEnabled()
	{
		return navigatorJavaEnabled;
	}

	/**
	 * @param browserHeight
	 *            The height of the browser
	 */
	public void setBrowserHeight(int browserHeight)
	{
		this.browserHeight = browserHeight;
	}

	/**
	 * @param browserWidth
	 *            The browser width
	 */
	public void setBrowserWidth(int browserWidth)
	{
		this.browserWidth = browserWidth;
	}

	/**
	 * @param cookiesEnabled
	 *            The client's navigator.cookieEnabled property.
	 */
	public void setNavigatorCookieEnabled(boolean cookiesEnabled)
	{
		this.navigatorCookieEnabled = cookiesEnabled;
	}

	/**
	 * @param navigatorJavaEnabled
	 *            The client's navigator.javaEnabled property.
	 */
	public void setNavigatorJavaEnabled(boolean navigatorJavaEnabled)
	{
		this.navigatorJavaEnabled = navigatorJavaEnabled;
	}

	/**
	 * @param navigatorAppCodeName
	 *            The client's navigator.appCodeName property.
	 */
	public void setNavigatorAppCodeName(String navigatorAppCodeName)
	{
		this.navigatorAppCodeName = navigatorAppCodeName;
	}

	/**
	 * @param navigatorAppName
	 *            The client's navigator.appName property.
	 */
	public void setNavigatorAppName(String navigatorAppName)
	{
		this.navigatorAppName = navigatorAppName;
	}

	/**
	 * @param navigatorAppVersion
	 *            The client's navigator.appVersion property.
	 */
	public void setNavigatorAppVersion(String navigatorAppVersion)
	{
		this.navigatorAppVersion = navigatorAppVersion;
	}

	/**
	 * @param navigatorLanguage
	 *            The client's navigator.language (or navigator.userLanguage) property.
	 */
	public void setNavigatorLanguage(String navigatorLanguage)
	{
		this.navigatorLanguage = navigatorLanguage;
	}

	/**
	 * @param navigatorPlatform
	 *            The client's navigator.platform property.
	 */
	public void setNavigatorPlatform(String navigatorPlatform)
	{
		this.navigatorPlatform = navigatorPlatform;
	}

	/**
	 * @param navigatorUserAgent
	 *            The client's navigator.userAgent property.
	 */
	public void setNavigatorUserAgent(String navigatorUserAgent)
	{
		this.navigatorUserAgent = navigatorUserAgent;
	}

	/**
	 * @param remoteAddress
	 *            The client's remote/ip address.
	 */
	public void setRemoteAddress(String remoteAddress)
	{
		this.remoteAddress = remoteAddress;
	}

	/**
	 * @param hostname
	 *            the hostname shown in the browser.
	 */
	public void setHostname(String hostname)
	{
		this.hostname = hostname;
	}

	/**
	 * @param screenColorDepth
	 *            Color depth of the screen in bits (integer).
	 */
	public void setScreenColorDepth(int screenColorDepth)
	{
		this.screenColorDepth = screenColorDepth;
	}

	/**
	 * @param screenHeight
	 *            Height of the screen in pixels (integer).
	 */
	public void setScreenHeight(int screenHeight)
	{
		this.screenHeight = screenHeight;
	}

	/**
	 * @param screenWidth
	 *            Height of the screen in pixels (integer).
	 */
	public void setScreenWidth(int screenWidth)
	{
		this.screenWidth = screenWidth;
	}

	/**
	 * Sets time zone.
	 *
	 * @param timeZone
	 */
	public void setTimeZone(TimeZone timeZone)
	{
		this.timeZone = timeZone;
	}

	/**
	 * @param utcDSTOffset
	 */
	public void setUtcDSTOffset(String utcDSTOffset)
	{
		this.utcDSTOffset = utcDSTOffset;
	}

	/**
	 * @param utcOffset
	 *            The client's time offset from UTC in minutes (note: if you do this yourself, use
	 *            'new Date().getTimezoneOffset() / -60' (note the -)).
	 */
	public void setUtcOffset(String utcOffset)
	{
		this.utcOffset = utcOffset;
	}

	/**
	 * @param jsTimeZone
	 */
	public void setJsTimeZone(String jsTimeZone)
	{
		this.jsTimeZone = jsTimeZone;
	}

	/**
	 * @param javaScriptEnabled
	 *            is JavaScript supported in the browser
	 */
	public void setJavaScriptEnabled(boolean javaScriptEnabled) {
		this.javaScriptEnabled = javaScriptEnabled;
	}

	@Override
	public String toString()
	{
		StringBuilder b = new StringBuilder();

		Class<?> clazz = getClass();
		while (clazz != Object.class) {
			Field[] fields = clazz.getDeclaredFields();

			for (Field field : fields)
			{
				// Ignore these fields
				if (Modifier.isStatic(field.getModifiers()) ||
					Modifier.isTransient(field.getModifiers())  ||
					field.isSynthetic())
				{
					continue;
				}

				field.setAccessible(true);

				Object value;
				try
				{
					value = field.get(this);
				}
				catch (IllegalArgumentException e)
				{
					throw new RuntimeException(e);
				}
				catch (IllegalAccessException e)
				{
					throw new RuntimeException(e);
				}

				if (field.getType().equals(Integer.TYPE))
				{
					if (Integer.valueOf(-1).equals(value))
					{
						value = null;
					}
				}

				if (value != null)
				{
					b.append(field.getName());
					b.append('=');
					b.append(value);
					b.append('\n');
				}
			}

			clazz = clazz.getSuperclass();
		}
		return b.toString();
	}

	/**
	 * Read parameters.
	 *
	 * @param parameters
	 *            parameters sent from browser
	 */
	public void read(IRequestParameters parameters)
	{
		setNavigatorAppCodeName(parameters.getParameterValue("navigatorAppCodeName").toString("N/A"));
		setNavigatorAppName(parameters.getParameterValue("navigatorAppName").toString("N/A"));
		setNavigatorAppVersion(parameters.getParameterValue("navigatorAppVersion").toString("N/A"));
		setNavigatorCookieEnabled(parameters.getParameterValue("navigatorCookieEnabled").toBoolean(false));
		setNavigatorJavaEnabled(parameters.getParameterValue("navigatorJavaEnabled").toBoolean(false));
		setNavigatorLanguage(parameters.getParameterValue("navigatorLanguage").toString("N/A"));
		setNavigatorPlatform(parameters.getParameterValue("navigatorPlatform").toString("N/A"));
		setNavigatorUserAgent(parameters.getParameterValue("navigatorUserAgent").toString("N/A"));
		setScreenWidth(parameters.getParameterValue("screenWidth").toInt(-1));
		setScreenHeight(parameters.getParameterValue("screenHeight").toInt(-1));
		setScreenColorDepth(parameters.getParameterValue("screenColorDepth").toInt(-1));
		setUtcOffset(parameters.getParameterValue("utcOffset").toString(null));
		setUtcDSTOffset(parameters.getParameterValue("utcDSTOffset").toString(null));
		setJsTimeZone(parameters.getParameterValue("jsTimeZone").toString(null));
		setBrowserWidth(parameters.getParameterValue("browserWidth").toInt(-1));
		setBrowserHeight(parameters.getParameterValue("browserHeight").toInt(-1));
		setHostname(parameters.getParameterValue("hostname").toString("N/A"));
	}
}
