/*
 * CDecimalSupport.h
 * -----------------
 * Purpose: Edit field which allows negative and fractional values to be entered
 * Notes  : Alexander Uckun's original code has been modified a bit to suit our purposes.
 * Authors: OpenMPT Devs
 *          Alexander Uckun
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */


#pragma once

#include "openmpt/all/BuildSettings.hpp"

OPENMPT_NAMESPACE_BEGIN

///////////////////////////////////////////////////////////////////////////////
/// \class CDecimalSupport
/// \brief decimal number support for your control
/// \author Alexander Uckun
/// \version 1.0

// Copyright (c) 2007 - Alexander Uckun
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// 1. Redistributions of source code must retain the above copyright notice,
//    this list of conditions and the following disclaimer.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


template <class T, int limit = _CVTBUFSIZE>
class CDecimalSupport
{
protected:
	/// the locale dependant decimal separator
	TCHAR m_DecimalSeparator[5];
	/// the locale dependant negative sign
	TCHAR m_NegativeSign[6];

	bool m_allowNegative = true, m_allowFractions = true;

public:

#ifdef BEGIN_MSG_MAP
	BEGIN_MSG_MAP(CDecimalSupport)

	ALT_MSG_MAP(8)
		MESSAGE_HANDLER(WM_CHAR, OnChar)
		MESSAGE_HANDLER(WM_PASTE, OnPaste)
	END_MSG_MAP()
#endif

	/// \brief Initialize m_DecimalSeparator and m_NegativeSign
	/// \remarks calls InitDecimalSeparator and InitNegativeSign
	CDecimalSupport()
	{
		InitDecimalSeparator();
		InitNegativeSign();
	}

	/// \brief sets m_DecimalSeparator
	/// \remarks calls GetLocaleInfo with LOCALE_SDECIMAL to set m_DecimalSeparator
	/// \param[in] Locale the locale parameter (see GetLocaleInfo)
	/// \return the number of TCHARs written to the destination buffer
	int InitDecimalSeparator(LCID Locale = LOCALE_USER_DEFAULT)
	{
		return ::GetLocaleInfo(Locale, LOCALE_SDECIMAL, m_DecimalSeparator, sizeof(m_DecimalSeparator) / sizeof(TCHAR));
	}

	/// \brief sets m_NegativeSign
	/// \remarks calls GetLocaleInfo with LOCALE_SNEGATIVESIGN to set m_NegativeSign
	/// \param[in] Locale the locale parameter (see GetLocaleInfo)
	/// \return the number of TCHARs written to the destination buffer
	int InitNegativeSign(LCID Locale = LOCALE_USER_DEFAULT)
	{
		return ::GetLocaleInfo(Locale, LOCALE_SNEGATIVESIGN, m_NegativeSign, sizeof(m_NegativeSign) / sizeof(TCHAR));
	}

	/// callback for the WM_PASTE message
	/// validates the input
	/// \param uMsg
	/// \param wParam
	/// \param lParam
	/// \param[out] bHandled true, if the text is a valid number
	/// \return 0
	LRESULT OnPaste(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, bool &bHandled)
	{
		bHandled = false;
		int neg_sign = 0;
		int dec_point = 0;

		T* pT = static_cast<T*>(this);
		int nStartChar;
		int nEndChar;
		pT->GetSel(nStartChar, nEndChar);
		TCHAR buffer[limit];
		pT->GetWindowText(buffer, limit);

		// Check if the text already contains a decimal point
		for (TCHAR* x = buffer; *x; ++x)
		{
			if (x - buffer == nStartChar) x += nEndChar - nStartChar;
			if (*x == m_DecimalSeparator[0] || *x == _T('.')) ++dec_point;
			if (*x == m_NegativeSign[0] || *x == _T('-')) ++neg_sign;
		}

#ifdef _UNICODE
		if (!IsClipboardFormatAvailable(CF_UNICODETEXT)) return 0;
#else
		if (!IsClipboardFormatAvailable(CF_TEXT)) return 0;
#endif

		if (!OpenClipboard((HWND) *pT)) return 0;

#ifdef _UNICODE
		HGLOBAL hglb = GetClipboardData(CF_UNICODETEXT);
#else
		HGLOBAL hglb = ::GetClipboardData(CF_TEXT);
#endif
		if (hglb != NULL)
		{
			TCHAR *lptstr = static_cast<TCHAR *>(GlobalLock(hglb));
			if (lptstr != nullptr)
			{
				bHandled = true;
				for (TCHAR* s = lptstr; *s; ++s)
				{
					if ((*s == m_NegativeSign[0] ||*s == _T('-')) && m_allowNegative)
					{

						for (TCHAR* t = m_NegativeSign + 1; *t; ++t, ++s)
						{
							if (*t != *(s+1)) ++neg_sign;
						}

						if (neg_sign || nStartChar > 0)
						{
							bHandled = false;
							break;
						}

						++neg_sign;
						continue;
					}
					if ((*s == m_DecimalSeparator[0] || *s == _T('.')) && m_allowFractions)
					{
						for (TCHAR* t = m_DecimalSeparator + 1; *t ; ++t, ++s)
						{
							if (*t != *(s+1)) ++dec_point;
						}

						if (dec_point)
						{
							bHandled = false;
							break;
						}
						++dec_point;
						continue;
					}

					if (*s == _T('\r'))
					{
						// Stop at new line
						*s = 0;
						break;
					}
					if (*s < _T('0') || *s > _T('9'))
					{
						bHandled = false;
						break;
					}

				}
				if(bHandled) pT->ReplaceSel(lptstr, true);

				GlobalUnlock(hglb);

			}
		}
		CloseClipboard();
		return 0;
	}


	/// callback for the WM_CHAR message
	/// handles the decimal point and the negative sign keys
	/// \param uMsg
	/// \param[in] wParam contains the pressed key
	/// \param lParam
	/// \param[out] bHandled true, if the key press was handled in this function
	/// \return 0
	LRESULT OnChar(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled)
	{
		bHandled = false;
		if ((static_cast<TCHAR>(wParam) == m_DecimalSeparator[0] || wParam == _T('.')) && m_allowFractions)
		{
			T* pT = static_cast<T*>(this);
			int nStartChar;
			int nEndChar;
			pT->GetSel(nStartChar, nEndChar);
			TCHAR buffer[limit];
			pT->GetWindowText(buffer, limit);
			//Verify that the control doesn't already contain a decimal point
			for (TCHAR* x = buffer; *x; ++x)
			{
				if (x - buffer == nStartChar) x += nEndChar - nStartChar;
				if (*x == m_DecimalSeparator[0]) return 0;
			}

			pT->ReplaceSel(m_DecimalSeparator, true);
			bHandled = true;
		}

		if ((static_cast<TCHAR>(wParam) == m_NegativeSign[0] || wParam == _T('-')) && m_allowNegative)
		{
			T* pT = static_cast<T*>(this);
			int nStartChar;
			int nEndChar;
			pT->GetSel(nStartChar, nEndChar);
			if (nStartChar) return 0;

			TCHAR buffer[limit];
			pT->GetWindowText(buffer, limit);
			//Verify that the control doesn't already contain a negative sign
			if (nEndChar == 0 && buffer[0] == m_NegativeSign[0]) return 0;
			pT->ReplaceSel(m_NegativeSign, true);
			bHandled = true;
		}
		return 0;
	}

	/// converts the controls text to double
	/// \param[out] d the converted value
	/// \return true on success
	bool GetDecimalValue(double& d) const
	{
		TCHAR szBuff[limit];
		static_cast<const T*>(this)->GetWindowText(szBuff, limit);
		return TextToDouble(szBuff, d);
	}

	/// converts a string to double
	/// \remarks the decimal separator and the negative sign may change in the string
	/// \param[in, out] szBuff the string to convert
	/// \param[out] d the converted value
	/// \return true on success
	bool TextToDouble(TCHAR* szBuff, double& d) const
	{
		//replace the locale dependant separator with .
		if (m_DecimalSeparator[0] != _T('.'))
		{
			for (TCHAR* x = szBuff; *x; ++x)
			{
				if (*x == m_DecimalSeparator[0])
				{
					*x = _T('.');
					break;
				}
			}

		}

		TCHAR* endPtr;
		//replace the negative sign with -
		if (szBuff[0] == m_NegativeSign[0]) szBuff[0] = _T('-');
		d = _tcstod(szBuff, &endPtr);
		return *endPtr == _T('\0');
	}

	/// sets a number as the controls text
	/// \param[in] d the value
	/// \param[in] count digits after the decimal point
	void SetFixedValue(double d, int count)
	{
		int decimal_pos;
		int sign;
		char digits[limit];
		_fcvt_s(digits, d, count, &decimal_pos, &sign);
		return DisplayDecimalValue(digits, decimal_pos, sign);
	}

	/// sets a number as the controls text
	/// \param[in] d the value
	/// \param[in] count total number of digits

	void SetDecimalValue(double d, int count)
	{
		int decimal_pos;
		int sign;
		char digits[limit];
		_ecvt_s(digits, d, count, &decimal_pos, &sign);
		DisplayDecimalValue(digits, decimal_pos, sign);
	}
	/// sets a number as the controls text
	/// \param[in] d the value
	/// \remarks the total number of digits is calculated using the GetLimitText function

	void SetDecimalValue(double d)
	{
		SetDecimalValue(d, std::min(limit, static_cast<int>(static_cast<const T*>(this)->GetLimitText())) - 2);
	}

	/// sets the controls text
	/// \param[in] digits array containing the digits
	/// \param[in] decimal_pos the position of the decimal point
	/// \param[in] sign 1 if negative

	void DisplayDecimalValue(const char* digits, int decimal_pos, int sign)
	{
		TCHAR szBuff[limit];
		DecimalToText(szBuff, limit, digits, decimal_pos, sign);
		static_cast<T*>(this)->SetWindowText(szBuff);
	}

	/// convert a digit array to string
	/// \param[out] szBuff target buffer for output
	/// \param[in] buflen maximum characters in output buffer
	/// \param[in] digits array containing the digits
	/// \param[in] decimal_pos the position of the decimal point
	/// \param[in] sign 1 if negative

	void DecimalToText(TCHAR* szBuff, size_t buflen, const char* digits, int decimal_pos, int sign) const
	{
		int i = 0;
		size_t pos = 0;
		if (sign)
		{
			for (const TCHAR *x = m_NegativeSign; *x ; ++x, ++pos) szBuff[pos] = *x;
		}

		for (; pos < buflen && digits[i] && i < decimal_pos ; ++i, ++pos) szBuff[pos] = digits[i];

		if (decimal_pos < 1) szBuff[pos++] = _T('0');
		size_t last_nonzero = pos;

		for (const TCHAR *x = m_DecimalSeparator; *x ; ++x, ++pos) szBuff[pos] = *x;
		for (; pos < buflen && decimal_pos < 0; ++decimal_pos, ++pos) szBuff[pos] = _T('0');

		for (; pos < buflen && digits[i]; ++i, ++pos)
		{
			szBuff[pos] = digits[i];
			if (digits[i] != '0') last_nonzero = pos+1;
		}
		szBuff[std::min(buflen - 1, last_nonzero)] = _T('\0');
	}

	void AllowNegative(bool allow)
	{
		m_allowNegative = allow;
	}

	void AllowFractions(bool allow)
	{
		m_allowFractions = allow;
	}

};


class CNumberEdit : public CEdit, public CDecimalSupport<CNumberEdit>
{
public:
	void SetTempoValue(const TEMPO &t);
	TEMPO GetTempoValue();

protected:
	afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);
	afx_msg LPARAM OnPaste(WPARAM wParam, LPARAM lParam);
	DECLARE_MESSAGE_MAP()
};

OPENMPT_NAMESPACE_END
