/*
 * Copyright 2003-2009, Axel Dörfler, axeld@pinc-software.de.
 * Copyright 2009-2010, Adrien Destugues, pulkomandy@gmail.com.
 * Copyright 2010-2011, Oliver Tappe <zooey@hirschkaefer.de>.
 * Distributed under the terms of the MIT License.
 */


#ifndef HoG_GENODE
#include <unicode/uversion.h>
#endif
#include <FormattingConventions.h>

#include <AutoDeleter.h>
#include <IconUtils.h>
#include <List.h>
#include <Language.h>
#include <Locale.h>
#include <LocaleRoster.h>
#include <Resources.h>
#include <String.h>
#include <UnicodeChar.h>

#ifndef HoG_GENODE
#include <unicode/datefmt.h>
#include <unicode/locid.h>
#include <unicode/smpdtfmt.h>
#include <unicode/ulocdata.h>
#include <ICUWrapper.h>
#endif  // ~HoG_GENODE

#include <iostream>
#include <map>
#include <monetary.h>
#include <new>
#include <stdarg.h>
#include <stdlib.h>


// #pragma mark - helpers


static bool
FormatUsesAmPm(const BString& format)
{
	if (format.Length() == 0)
		return false;

	bool inQuote = false;
	for (const char* s = format.String(); *s != '\0'; ++s) {
		switch (*s) {
			case '\'':
				inQuote = !inQuote;
				break;
			case 'a':
				if (!inQuote)
					return true;
				break;
		}
	}

	return false;
}


static void
CoerceFormatTo12HourClock(BString& format)
{
	char* s = format.LockBuffer(format.Length());
	if (s == NULL)
		return;

	// change format to use h instead of H, k instead of K, and append an
	// am/pm marker
	bool inQuote = false;
	for (; *s != '\0'; ++s) {
		switch (*s) {
			case '\'':
				inQuote = !inQuote;
				break;
			case 'H':
				if (!inQuote)
					*s = 'h';
				break;
			case 'K':
				if (!inQuote)
					*s = 'k';
				break;
		}
	}
	format.UnlockBuffer(format.Length());

	format.Append(" a");
}


static void
CoerceFormatTo24HourClock(BString& format)
{
	char* buffer = format.LockBuffer(format.Length());
	char* currentPos = buffer;
	if (currentPos == NULL)
		return;

	// change the format to use H instead of h, K instead of k, and determine
	// and remove the am/pm marker (including leading whitespace)
	bool inQuote = false;
	bool lastWasWhitespace = false;
	uint32 ch;
	const char* amPmStartPos = NULL;
	const char* amPmEndPos = NULL;
	const char* lastWhitespaceStart = NULL;
	for (char* previousPos = currentPos; (ch = BUnicodeChar::FromUTF8(
			(const char**)&currentPos)) != 0; previousPos = currentPos) {
		switch (ch) {
			case '\'':
				inQuote = !inQuote;
				break;
			case 'h':
				if (!inQuote)
					*previousPos = 'H';
				break;
			case 'k':
				if (!inQuote)
					*previousPos = 'K';
				break;
			case 'a':
				if (!inQuote) {
					if (lastWasWhitespace)
						amPmStartPos = lastWhitespaceStart;
					else
						amPmStartPos = previousPos;
					amPmEndPos = currentPos;
				}
				break;
			default:
				if (!inQuote && BUnicodeChar::IsWhitespace(ch)) {
					if (!lastWasWhitespace) {
						lastWhitespaceStart = previousPos;
						lastWasWhitespace = true;
					}
					continue;
				}
		}
		lastWasWhitespace = false;
	}

	format.UnlockBuffer(format.Length());
	if (amPmStartPos != NULL && amPmEndPos > amPmStartPos)
		format.Remove(amPmStartPos - buffer, amPmEndPos - amPmStartPos);
}


static void
CoerceFormatToAbbreviatedTimezone(BString& format)
{
	char* s = format.LockBuffer(format.Length());
	if (s == NULL)
		return;

	// replace a single 'z' with 'V'
	bool inQuote = false;
	bool lastWasZ = false;
	for (; *s != '\0'; ++s) {
		switch (*s) {
			case '\'':
				inQuote = !inQuote;
				break;
			case 'z':
				if (!inQuote && !lastWasZ && *(s+1) != 'z')
					*s = 'V';
				lastWasZ = true;
				continue;
		}
		lastWasZ = false;
	}
	format.UnlockBuffer(format.Length());
}


// #pragma mark - BFormattingConventions


enum ClockHoursState {
	CLOCK_HOURS_UNSET = 0,
	CLOCK_HOURS_24,
	CLOCK_HOURS_12
};


BFormattingConventions::BFormattingConventions(const char* id)
	:
	fCachedUse24HourClock(CLOCK_HOURS_UNSET),
	fExplicitUse24HourClock(CLOCK_HOURS_UNSET),
	fUseStringsFromPreferredLanguage(false)
#ifndef HoG_GENODE
,	fICULocale(new icu::Locale(id))
#endif  // ~HoG_GENODE
{
}


BFormattingConventions::BFormattingConventions(
	const BFormattingConventions& other)
	:
	fCachedNumericFormat(other.fCachedNumericFormat),
	fCachedMonetaryFormat(other.fCachedMonetaryFormat),
	fCachedUse24HourClock(other.fCachedUse24HourClock),
	fExplicitNumericFormat(other.fExplicitNumericFormat),
	fExplicitMonetaryFormat(other.fExplicitMonetaryFormat),
	fExplicitUse24HourClock(other.fExplicitUse24HourClock),
	fUseStringsFromPreferredLanguage(other.fUseStringsFromPreferredLanguage)
#ifndef HoG_GENODE
,	fICULocale(new icu::Locale(*other.fICULocale))
#endif  // ~HoG_GENODE
{
	for (int s = 0; s < B_DATE_FORMAT_STYLE_COUNT; ++s) {
		fCachedDateFormats[s] = other.fCachedDateFormats[s];
		fExplicitDateFormats[s] = other.fExplicitDateFormats[s];

		for (int t = 0; t < B_TIME_FORMAT_STYLE_COUNT; ++t) {
			fCachedDateTimeFormats[s][t] = other.fCachedDateFormats[s][t];
			fExplicitDateTimeFormats[s][t] = other.fExplicitDateFormats[s][t];
		}
	}
	for (int s = 0; s < B_TIME_FORMAT_STYLE_COUNT; ++s) {
		fCachedTimeFormats[s] = other.fCachedTimeFormats[s];
		fExplicitTimeFormats[s] = other.fExplicitTimeFormats[s];
	}
}


#ifndef HoG_GENODE

BFormattingConventions::BFormattingConventions(const BMessage* archive)
	:
	fCachedUse24HourClock(CLOCK_HOURS_UNSET),
	fExplicitUse24HourClock(CLOCK_HOURS_UNSET),
	fUseStringsFromPreferredLanguage(false)
{
	BString conventionsID;
	status_t status = archive->FindString("conventions", &conventionsID);
	fICULocale = new icu::Locale(conventionsID);

	for (int s = 0; s < B_DATE_FORMAT_STYLE_COUNT && status == B_OK; ++s) {
		BString format;
		status = archive->FindString("dateFormat", s, &format);
		if (status == B_OK)
			fExplicitDateFormats[s] = format;

		status = archive->FindString("timeFormat", s, &format);
		if (status == B_OK)
			fExplicitTimeFormats[s] = format;
	}

	if (status == B_OK) {
		int8 use24HourClock;
		status = archive->FindInt8("use24HourClock", &use24HourClock);
		if (status == B_OK)
			fExplicitUse24HourClock = use24HourClock;
	}
	if (status == B_OK) {
		bool useStringsFromPreferredLanguage;
		status = archive->FindBool("useStringsFromPreferredLanguage",
			&useStringsFromPreferredLanguage);
		if (status == B_OK)
			fUseStringsFromPreferredLanguage = useStringsFromPreferredLanguage;
	}
}

#endif  // ~HoG_GENODE


BFormattingConventions&
BFormattingConventions::operator=(const BFormattingConventions& other)
{
	if (this == &other)
		return *this;

	for (int s = 0; s < B_DATE_FORMAT_STYLE_COUNT; ++s) {
		fCachedDateFormats[s] = other.fCachedDateFormats[s];
		fExplicitDateFormats[s] = other.fExplicitDateFormats[s];
		for (int t = 0; t < B_TIME_FORMAT_STYLE_COUNT; ++t) {
			fCachedDateTimeFormats[s][t] = other.fCachedDateTimeFormats[s][t];
			fExplicitDateTimeFormats[s][t]
				= other.fExplicitDateTimeFormats[s][t];
		}
	}
	for (int s = 0; s < B_TIME_FORMAT_STYLE_COUNT; ++s) {
		fCachedTimeFormats[s] = other.fCachedTimeFormats[s];
		fExplicitTimeFormats[s] = other.fExplicitTimeFormats[s];
	}
	fCachedNumericFormat = other.fCachedNumericFormat;
	fCachedMonetaryFormat = other.fCachedMonetaryFormat;
	fCachedUse24HourClock = other.fCachedUse24HourClock;

	fExplicitNumericFormat = other.fExplicitNumericFormat;
	fExplicitMonetaryFormat = other.fExplicitMonetaryFormat;
	fExplicitUse24HourClock = other.fExplicitUse24HourClock;

	fUseStringsFromPreferredLanguage = other.fUseStringsFromPreferredLanguage;

#ifndef HoG_GENODE
	*fICULocale = *other.fICULocale;
#endif  // ~HoG_GENODE

	return *this;
}


BFormattingConventions::~BFormattingConventions()
{
#ifndef HoG_GENODE
	delete fICULocale;
#endif  // ~HoG_GENODE
}


bool
BFormattingConventions::operator==(const BFormattingConventions& other) const
{
	if (this == &other)
		return true;

	for (int s = 0; s < B_DATE_FORMAT_STYLE_COUNT; ++s) {
		if (fExplicitDateFormats[s] != other.fExplicitDateFormats[s])
			return false;
	}
	for (int s = 0; s < B_TIME_FORMAT_STYLE_COUNT; ++s) {
		if (fExplicitTimeFormats[s] != other.fExplicitTimeFormats[s])
			return false;
	}

	return fExplicitNumericFormat == other.fExplicitNumericFormat
		&& fExplicitMonetaryFormat == other.fExplicitMonetaryFormat
		&& fExplicitUse24HourClock == other.fExplicitUse24HourClock
		&& fUseStringsFromPreferredLanguage
			== other.fUseStringsFromPreferredLanguage
#ifndef HoG_GENODE
		&& *fICULocale == *other.fICULocale;
#else
		;
#endif  // ~HoG_GENODE
}


#ifndef HoG_GENODE

bool
BFormattingConventions::operator!=(const BFormattingConventions& other) const
{
	return !(*this == other);
}


const char*
BFormattingConventions::ID() const
{
	return fICULocale->getName();
}


const char*
BFormattingConventions::LanguageCode() const
{
	return fICULocale->getLanguage();
}


const char*
BFormattingConventions::CountryCode() const
{
	const char* country = fICULocale->getCountry();
	if (country == NULL || country[0] == '\0')
		return NULL;

	return country;
}


bool
BFormattingConventions::AreCountrySpecific() const
{
	return CountryCode() != NULL;
}


status_t
BFormattingConventions::GetNativeName(BString& name) const
{
	UnicodeString string;
	fICULocale->getDisplayName(*fICULocale, string);
	string.toTitle(NULL, *fICULocale);

	name.Truncate(0);
	BStringByteSink converter(&name);
	string.toUTF8(converter);

	return B_OK;
}


status_t
BFormattingConventions::GetName(BString& name,
	const BLanguage* displayLanguage) const
{
	BString displayLanguageID;
	if (displayLanguage == NULL) {
		BLanguage defaultLanguage;
		BLocale::Default()->GetLanguage(&defaultLanguage);
		displayLanguageID = defaultLanguage.Code();
	} else {
		displayLanguageID = displayLanguage->Code();
	}

	UnicodeString uString;
	fICULocale->getDisplayName(Locale(displayLanguageID.String()), uString);
	name.Truncate(0);
	BStringByteSink stringConverter(&name);
	uString.toUTF8(stringConverter);

	return B_OK;
}


BMeasurementKind
BFormattingConventions::MeasurementKind() const
{
	UErrorCode error = U_ZERO_ERROR;
	switch (ulocdata_getMeasurementSystem(ID(), &error)) {
		case UMS_US:
			return B_US;
		case UMS_SI:
		default:
			return B_METRIC;
	}
}


status_t
BFormattingConventions::GetDateFormat(BDateFormatStyle style,
	BString& outFormat) const
{
	if (style < 0 || style >= B_DATE_FORMAT_STYLE_COUNT)
		return B_BAD_VALUE;

	outFormat = fExplicitDateFormats[style].Length()
		? fExplicitDateFormats[style]
		: fCachedDateFormats[style];

	if (outFormat.Length() > 0)
		return B_OK;

	ObjectDeleter<DateFormat> dateFormatter(
		DateFormat::createDateInstance((DateFormat::EStyle)style, *fICULocale));
	if (dateFormatter.Get() == NULL)
		return B_NO_MEMORY;

	SimpleDateFormat* dateFormatterImpl
		= static_cast<SimpleDateFormat*>(dateFormatter.Get());

	UnicodeString icuString;
	dateFormatterImpl->toPattern(icuString);
	BStringByteSink stringConverter(&outFormat);
	icuString.toUTF8(stringConverter);

	fCachedDateFormats[style] = outFormat;

	return B_OK;
}


status_t
BFormattingConventions::GetTimeFormat(BTimeFormatStyle style,
	BString& outFormat) const
{
	if (style < 0 || style >= B_TIME_FORMAT_STYLE_COUNT)
		return B_BAD_VALUE;

	outFormat = fExplicitTimeFormats[style].Length()
		? fExplicitTimeFormats[style]
		: fCachedTimeFormats[style];

	if (outFormat.Length() > 0)
		return B_OK;

	ObjectDeleter<DateFormat> timeFormatter(
		DateFormat::createTimeInstance((DateFormat::EStyle)style, *fICULocale));
	if (timeFormatter.Get() == NULL)
		return B_NO_MEMORY;

	SimpleDateFormat* timeFormatterImpl
		= static_cast<SimpleDateFormat*>(timeFormatter.Get());

	UnicodeString icuString;
	timeFormatterImpl->toPattern(icuString);
	BStringByteSink stringConverter(&outFormat);
	icuString.toUTF8(stringConverter);

	CoerceFormatForClock(outFormat);

	if (style != B_FULL_TIME_FORMAT) {
		// use abbreviated timezone in short timezone format
		CoerceFormatToAbbreviatedTimezone(outFormat);
	}

	fCachedTimeFormats[style] = outFormat;

	return B_OK;
}


status_t
BFormattingConventions::GetDateTimeFormat(BDateFormatStyle dateStyle,
	BTimeFormatStyle timeStyle, BString& outFormat) const
{
	if (dateStyle < 0 || dateStyle >= B_DATE_FORMAT_STYLE_COUNT)
		return B_BAD_VALUE;

	if (timeStyle < 0 || timeStyle >= B_TIME_FORMAT_STYLE_COUNT)
		return B_BAD_VALUE;

	outFormat = fExplicitDateTimeFormats[dateStyle][timeStyle].Length()
		? fExplicitDateTimeFormats[dateStyle][timeStyle]
		: fCachedDateTimeFormats[dateStyle][timeStyle];

	if (outFormat.Length() > 0)
		return B_OK;

	ObjectDeleter<DateFormat> dateFormatter(
		DateFormat::createDateTimeInstance((DateFormat::EStyle)dateStyle,
			(DateFormat::EStyle)timeStyle, *fICULocale));
	if (dateFormatter.Get() == NULL)
		return B_NO_MEMORY;

	SimpleDateFormat* dateFormatterImpl
		= static_cast<SimpleDateFormat*>(dateFormatter.Get());

	UnicodeString icuString;
	dateFormatterImpl->toPattern(icuString);
	BStringByteSink stringConverter(&outFormat);
	icuString.toUTF8(stringConverter);

	CoerceFormatForClock(outFormat);

	if (dateStyle != B_FULL_DATE_FORMAT) {
		// use abbreviated timezone in short timezone format
		CoerceFormatToAbbreviatedTimezone(outFormat);
	}

	fCachedDateTimeFormats[dateStyle][timeStyle] = outFormat;

	return B_OK;
}

#endif  // ~HoG_GENODE


status_t
BFormattingConventions::GetNumericFormat(BString& outFormat) const
{
	// TODO!
	return B_UNSUPPORTED;
}


status_t
BFormattingConventions::GetMonetaryFormat(BString& outFormat) const
{
	// TODO!
	return B_UNSUPPORTED;
}


void
BFormattingConventions::SetExplicitDateFormat(BDateFormatStyle style,
	const BString& format)
{
	fExplicitDateFormats[style] = format;
}


void
BFormattingConventions::SetExplicitTimeFormat(BTimeFormatStyle style,
	const BString& format)
{
	fExplicitTimeFormats[style] = format;
}


void
BFormattingConventions::SetExplicitDateTimeFormat(BDateFormatStyle dateStyle,
	BTimeFormatStyle timeStyle, const BString& format)
{
	fExplicitDateTimeFormats[dateStyle][timeStyle] = format;
}


void
BFormattingConventions::SetExplicitNumericFormat(const BString& format)
{
	fExplicitNumericFormat = format;
}


void
BFormattingConventions::SetExplicitMonetaryFormat(const BString& format)
{
	fExplicitMonetaryFormat = format;
}


bool
BFormattingConventions::UseStringsFromPreferredLanguage() const
{
	return fUseStringsFromPreferredLanguage;
}


void
BFormattingConventions::SetUseStringsFromPreferredLanguage(bool value)
{
	fUseStringsFromPreferredLanguage = value;
}


#ifndef HoG_GENODE

bool
BFormattingConventions::Use24HourClock() const
{
	int8 use24HourClock = fExplicitUse24HourClock != CLOCK_HOURS_UNSET
		?  fExplicitUse24HourClock : fCachedUse24HourClock;

	if (use24HourClock == CLOCK_HOURS_UNSET) {
		BString format;
		GetTimeFormat(B_MEDIUM_TIME_FORMAT, format);
		fCachedUse24HourClock
			= FormatUsesAmPm(format) ? CLOCK_HOURS_12 : CLOCK_HOURS_24;
		return fCachedUse24HourClock == CLOCK_HOURS_24;
	}

	return fExplicitUse24HourClock == CLOCK_HOURS_24;
}

#endif  // ~HoG_GENODE


void
BFormattingConventions::SetExplicitUse24HourClock(bool value)
{
	int8 newUse24HourClock = value ? CLOCK_HOURS_24 : CLOCK_HOURS_12;
	if (fExplicitUse24HourClock == newUse24HourClock)
		return;

	fExplicitUse24HourClock = newUse24HourClock;

	for (int s = 0; s < B_TIME_FORMAT_STYLE_COUNT; ++s)
		fCachedTimeFormats[s].Truncate(0);
}


void
BFormattingConventions::UnsetExplicitUse24HourClock()
{
	fExplicitUse24HourClock = CLOCK_HOURS_UNSET;

	for (int s = 0; s < B_TIME_FORMAT_STYLE_COUNT; ++s)
		fCachedTimeFormats[s].Truncate(0);
}


status_t
BFormattingConventions::Archive(BMessage* archive, bool deep) const
{
#ifndef HoG_GENODE
	status_t status = archive->AddString("conventions", fICULocale->getName());
#else
	status_t status = 0;
#endif  // ~HoG_GENODE
	for (int s = 0; s < B_DATE_FORMAT_STYLE_COUNT && status == B_OK; ++s) {
		status = archive->AddString("dateFormat", fExplicitDateFormats[s]);
		if (status == B_OK)
			status = archive->AddString("timeFormat", fExplicitTimeFormats[s]);
	}
	if (status == B_OK)
		status = archive->AddInt8("use24HourClock", fExplicitUse24HourClock);
	if (status == B_OK) {
		status = archive->AddBool("useStringsFromPreferredLanguage",
			fUseStringsFromPreferredLanguage);
	}

	return status;
}


#ifndef HoG_GENODE

void
BFormattingConventions::CoerceFormatForClock(BString& outFormat) const
{
	int8 use24HourClock = fExplicitUse24HourClock != CLOCK_HOURS_UNSET
		? fExplicitUse24HourClock : fCachedUse24HourClock;
	if (use24HourClock != CLOCK_HOURS_UNSET) {
		// adjust to 12/24-hour clock as requested
		bool localeUses24HourClock = !FormatUsesAmPm(outFormat);
		if (localeUses24HourClock) {
			if (use24HourClock == CLOCK_HOURS_12)
				CoerceFormatTo12HourClock(outFormat);
		} else {
			if (use24HourClock == CLOCK_HOURS_24)
				CoerceFormatTo24HourClock(outFormat);
		}
	}
}

#endif  // ~HoG_GENODE

