root/src/preferences/time/ZoneView.cpp
/*
 * Copyright 2004-2013 Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Mike Berg, mike@berg-net.us
 *              Adrien Destugues, pulkomandy@pulkomandy.ath.cx
 *              Julun, host.haiku@gmx.de
 *              Hamish Morrison, hamish@lavabit.com
 *              Philippe Saint-Pierre, stpere@gmail.com
 *              John Scipione, jscipione@gmail.com
 *              Oliver Tappe, zooey@hirschkaefer.de
 */


#include <unicode/uversion.h>
#include "ZoneView.h"

#include <stdlib.h>
#include <syscalls.h>

#include <map>
#include <new>
#include <vector>

#include <AutoDeleter.h>
#include <Button.h>
#include <Catalog.h>
#include <Collator.h>
#include <ControlLook.h>
#include <Country.h>
#include <Directory.h>
#include <Entry.h>
#include <File.h>
#include <FindDirectory.h>
#include <ListItem.h>
#include <Locale.h>
#include <MutableLocaleRoster.h>
#include <OutlineListView.h>
#include <Path.h>
#include <RadioButton.h>
#include <ScrollView.h>
#include <StorageDefs.h>
#include <String.h>
#include <StringView.h>
#include <TimeZone.h>
#include <View.h>
#include <Window.h>

#include <unicode/datefmt.h>
#include <unicode/utmscale.h>
#include <ICUWrapper.h>

#include "TimeMessages.h"
#include "TimeZoneListItem.h"
#include "TimeZoneListView.h"
#include "TZDisplay.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Time"


using BPrivate::MutableLocaleRoster;
using BPrivate::ObjectDeleter;


struct TimeZoneItemLess {
        bool operator()(const BString& first, const BString& second) const
        {
                // sort anything starting with '<' behind anything else
                if (first.ByteAt(0) == '<') {
                        if (second.ByteAt(0) != '<')
                                return false;
                } else if (second.ByteAt(0) == '<')
                        return true;
                return fCollator.Compare(first.String(), second.String()) < 0;
        }
private:
        BCollator fCollator;
};



TimeZoneView::TimeZoneView(const char* name)
        :
        BGroupView(name, B_HORIZONTAL, B_USE_DEFAULT_SPACING),
        fGmtTime(NULL),
        fUseGmtTime(false),
        fCurrentZoneItem(NULL),
        fOldZoneItem(NULL),
        fInitialized(false)
{
        _ReadRTCSettings();
        _InitView();
}


bool
TimeZoneView::CheckCanRevert()
{
        // check GMT vs Local setting
        bool enable = fUseGmtTime != fOldUseGmtTime;

        return enable || fCurrentZoneItem != fOldZoneItem;
}


TimeZoneView::~TimeZoneView()
{
        _WriteRTCSettings();
}


void
TimeZoneView::AttachedToWindow()
{
        BView::AttachedToWindow();
        AdoptParentColors();

        if (!fInitialized) {
                fInitialized = true;

                fSetZone->SetTarget(this);
                fZoneList->SetTarget(this);
        }
}


void
TimeZoneView::DoLayout()
{
        BView::DoLayout();
        if (fCurrentZoneItem != NULL) {
                fZoneList->Select(fZoneList->IndexOf(fCurrentZoneItem));
                fCurrent->SetText(fCurrentZoneItem->Text());
                fZoneList->ScrollToSelection();
        }
}


void
TimeZoneView::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case B_OBSERVER_NOTICE_CHANGE:
                {
                        int32 change;
                        message->FindInt32(B_OBSERVE_WHAT_CHANGE, &change);
                        switch(change) {
                                case H_TM_CHANGED:
                                        _UpdateDateTime(message);
                                        break;

                                default:
                                        BView::MessageReceived(message);
                                        break;
                        }
                        break;
                }

                case H_CITY_CHANGED:
                        _UpdatePreview();
                        break;

                case H_SET_TIME_ZONE:
                {
                        _SetSystemTimeZone();
                        break;
                }

                case kMsgRevert:
                        _Revert();
                        break;

                case kRTCUpdate:
                        fUseGmtTime = fGmtTime->Value() == B_CONTROL_ON;
                        _UpdateGmtSettings();
                        _UpdateCurrent();
                        _UpdatePreview();
                        break;

                default:
                        BGroupView::MessageReceived(message);
                        break;
        }
}


void
TimeZoneView::_UpdateDateTime(BMessage* message)
{
        // only need to update once every minute
        int32 minute;
        if (message->FindInt32("minute", &minute) == B_OK) {
                if (fLastUpdateMinute != minute) {
                        _UpdateCurrent();
                        _UpdatePreview();

                        fLastUpdateMinute = minute;
                }
        }
}


void
TimeZoneView::_InitView()
{
        fZoneList = new TimeZoneListView();
        fZoneList->SetSelectionMessage(new BMessage(H_CITY_CHANGED));
        fZoneList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE));
        _BuildZoneMenu();
        BScrollView* scrollList = new BScrollView("scrollList", fZoneList,
                B_FRAME_EVENTS | B_WILL_DRAW, false, true);
        scrollList->SetExplicitMinSize(
                BSize(200 * be_plain_font->Size() / 12.0f, 0));

        fCurrent = new TTZDisplay("currentTime", B_TRANSLATE("Current time:"));
        fPreview = new TTZDisplay("previewTime", B_TRANSLATE("Preview time:"));

        fSetZone = new BButton("setTimeZone", B_TRANSLATE("Set time zone"),
                new BMessage(H_SET_TIME_ZONE));
        fSetZone->SetEnabled(false);
        fSetZone->SetExplicitAlignment(
                BAlignment(B_ALIGN_RIGHT, B_ALIGN_BOTTOM));

        fLocalTime = new BRadioButton("localTime",
                B_TRANSLATE("Local time (Windows compatible)"),
                        new BMessage(kRTCUpdate));
        fGmtTime = new BRadioButton("greenwichMeanTime",
                B_TRANSLATE("GMT (UNIX compatible)"), new BMessage(kRTCUpdate));

        if (fUseGmtTime)
                fGmtTime->SetValue(B_CONTROL_ON);
        else
                fLocalTime->SetValue(B_CONTROL_ON);
        _ShowOrHidePreview();
        fOldUseGmtTime = fUseGmtTime;

        BLayoutBuilder::Group<>(this)
                .Add(scrollList)
                .AddGroup(B_VERTICAL, 0)
                        .Add(new BStringView("clockSetTo",
                                B_TRANSLATE("Hardware clock set to:")))
                        .AddGroup(B_VERTICAL, 0)
                                .Add(fLocalTime)
                                .Add(fGmtTime)
                                .SetInsets(B_USE_WINDOW_SPACING, 0, 0, 0)
                        .End()
                        .AddGlue()
                        .AddGroup(B_VERTICAL, B_USE_DEFAULT_SPACING)
                                .Add(fCurrent)
                                .Add(fPreview)
                        .End()
                        .Add(fSetZone)
                .End()
                .SetInsets(B_USE_WINDOW_SPACING, B_USE_WINDOW_SPACING,
                        B_USE_WINDOW_SPACING, B_USE_DEFAULT_SPACING);
}


void
TimeZoneView::_BuildZoneMenu()
{
        BTimeZone defaultTimeZone;
        BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone);

        BLanguage language;
        BLocale::Default()->GetLanguage(&language);

        // Group timezones by regions, but filter out unwanted (duplicate) regions
        // and add an additional region with generic GMT-offset timezones at the end
        typedef std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap;
        ZoneItemMap zoneItemMap;
        const char* kOtherRegion = B_TRANSLATE_MARK("<Other>");
        const char* kSupportedRegions[] = {
                B_TRANSLATE_MARK("Africa"),             B_TRANSLATE_MARK("America"),
                B_TRANSLATE_MARK("Antarctica"), B_TRANSLATE_MARK("Arctic"),
                B_TRANSLATE_MARK("Asia"),               B_TRANSLATE_MARK("Atlantic"),
                B_TRANSLATE_MARK("Australia"),  B_TRANSLATE_MARK("Europe"),
                B_TRANSLATE_MARK("Indian"),             B_TRANSLATE_MARK("Pacific"),
                kOtherRegion,
                NULL
        };

        // Since the zone-map contains translated country-names (we get those from
        // ICU), we need to use translated region names in the zone-map, too:
        typedef std::map<BString, BString> TranslatedRegionMap;
        TranslatedRegionMap regionMap;
        for (const char** region = kSupportedRegions; *region != NULL; ++region) {
                BString translatedRegion = B_TRANSLATE_NOCOLLECT(*region);
                regionMap[*region] = translatedRegion;

                TimeZoneListItem* regionItem
                        = new TimeZoneListItem(translatedRegion, NULL, NULL);
                regionItem->SetOutlineLevel(0);
                zoneItemMap[translatedRegion] = regionItem;
        }

        // Get all time zones
        BMessage zoneList;
        BLocaleRoster::Default()->GetAvailableTimeZonesWithRegionInfo(&zoneList);

        typedef std::map<BString, std::vector<const char*> > ZonesByCountyMap;
        ZonesByCountyMap zonesByCountryMap;
        const char* zoneID;
        BString timeZoneCode;
        for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK
                        && zoneList.FindString("region", tz, &timeZoneCode) == B_OK; tz++) {
                // From the global ("001") timezones, we only accept the generic GMT
                // timezones, as all the other world-zones are duplicates of others.
                if (timeZoneCode == "001" && strncmp(zoneID, "Etc/GMT", 7) != 0)
                        continue;
                zonesByCountryMap[timeZoneCode].push_back(zoneID);
        }

        ZonesByCountyMap::const_iterator countryIter = zonesByCountryMap.begin();
        for (; countryIter != zonesByCountryMap.end(); ++countryIter) {
                const char* countryCode = countryIter->first.String();
                if (countryCode == NULL)
                        continue;

                size_t zoneCountInCountry = countryIter->second.size();
                for (size_t tz = 0; tz < zoneCountInCountry; tz++) {
                        BString zoneID(countryIter->second[tz]);
                        BTimeZone* timeZone
                                = new(std::nothrow) BTimeZone(zoneID, &language);
                        if (timeZone == NULL)
                                continue;

                        int32 slashPos = zoneID.FindFirst('/');
                        BString region(zoneID, slashPos);
                        if (region == "Etc")
                                region = kOtherRegion;

                        // just accept timezones from our supported regions, others are
                        // aliases and would just make the list even longer
                        TranslatedRegionMap::iterator regionIter = regionMap.find(region);
                        if (regionIter == regionMap.end())
                                continue;

                        BString fullCountryID = regionIter->second;
                        BCountry* country = new(std::nothrow) BCountry(countryCode);
                        if (country == NULL)
                                continue;

                        BString countryName;
                        country->GetName(countryName);
                        bool hasUsedCountry = false;
                        bool countryIsRegion = countryName == regionIter->second
                                || region == kOtherRegion;
                        if (!countryIsRegion)
                                fullCountryID << "/" << countryName;

                        BString timeZoneName;
                        BString fullZoneID = fullCountryID;
                        if (zoneCountInCountry > 1) {
                                // we can't use the country name as timezone name, since there
                                // are more than one timezones in this country - fetch the
                                // localized name of the timezone and use that
                                timeZoneName = timeZone->Name();
                                int32 openParenthesisPos = timeZoneName.FindFirst('(');
                                if (openParenthesisPos >= 0) {
                                        timeZoneName.Remove(0, openParenthesisPos + 1);
                                        int32 closeParenthesisPos = timeZoneName.FindLast(')');
                                        if (closeParenthesisPos >= 0)
                                                timeZoneName.Truncate(closeParenthesisPos);
                                }
                                fullZoneID << "/" << timeZoneName;
                        } else {
                                timeZoneName = countryName;
                                fullZoneID << "/" << zoneID;
                        }

                        // skip duplicates
                        ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID);
                        if (zoneIter != zoneItemMap.end()) {
                                delete timeZone;
                                continue;
                        }

                        TimeZoneListItem* countryItem = NULL;
                        TimeZoneListItem* zoneItem = NULL;
                        if (zoneCountInCountry > 1) {
                                ZoneItemMap::iterator countryIter
                                        = zoneItemMap.find(fullCountryID);
                                if (countryIter == zoneItemMap.end()) {
                                        countryItem = new TimeZoneListItem(countryName.String(),
                                                country, NULL);
                                        countryItem->SetOutlineLevel(1);
                                        zoneItemMap[fullCountryID] = countryItem;
                                        hasUsedCountry = true;
                                } else
                                        countryItem = countryIter->second;

                                zoneItem = new TimeZoneListItem(timeZoneName.String(),
                                        NULL, timeZone);
                                zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2);
                        } else {
                                zoneItem = new TimeZoneListItem(timeZoneName.String(),
                                        country, timeZone);
                                zoneItem->SetOutlineLevel(1);
                                hasUsedCountry = true;
                        }
                        zoneItemMap[fullZoneID] = zoneItem;

                        if (timeZone->ID() == defaultTimeZone.ID()) {
                                fCurrentZoneItem = zoneItem;
                                if (countryItem != NULL)
                                        countryItem->SetExpanded(true);

                                ZoneItemMap::iterator regionItemIter
                                        = zoneItemMap.find(regionIter->second);
                                if (regionItemIter != zoneItemMap.end())
                                        regionItemIter->second->SetExpanded(true);
                        }

                        if (!hasUsedCountry)
                                delete country;
                }
        }

        fOldZoneItem = fCurrentZoneItem;

        ZoneItemMap::iterator zoneIter;
        bool lastWasCountryItem = false;
        TimeZoneListItem* currentItem = NULL;
        for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end();
                        ++zoneIter) {
                if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) {
                        // Some countries (e.g. Spain and Chile) have their timezones
                        // spread across different regions. As a result, there might still
                        // be country items with only one timezone below them. We manually
                        // filter those country items here.
                        ZoneItemMap::iterator next = zoneIter;
                        ++next;
                        if (next != zoneItemMap.end()
                                && next->second->OutlineLevel() != 2) {
                                fZoneList->RemoveItem(currentItem);
                                zoneIter->second->SetText(currentItem->Text());
                                zoneIter->second->SetCountry(currentItem->HasCountry()
                                        ? new(std::nothrow) BCountry(currentItem->Country())
                                        : NULL);
                                if (currentItem->HasTimeZone()) {
                                        zoneIter->second->SetTimeZone(new(std::nothrow)
                                                BTimeZone(currentItem->TimeZone()));
                                }
                                zoneIter->second->SetOutlineLevel(1);
                                delete currentItem;
                        }
                }

                fZoneList->AddItem(zoneIter->second);
                if (zoneIter->second->OutlineLevel() == 1) {
                        lastWasCountryItem = true;
                        currentItem = zoneIter->second;
                } else
                        lastWasCountryItem = false;
        }
}


void
TimeZoneView::_Revert()
{
        fCurrentZoneItem = fOldZoneItem;

        if (fCurrentZoneItem != NULL) {
                int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem);
                fZoneList->Select(currentZoneIndex);
        } else
                fZoneList->DeselectAll();
        fZoneList->ScrollToSelection();

        fUseGmtTime = fOldUseGmtTime;
        if (fUseGmtTime)
                fGmtTime->SetValue(B_CONTROL_ON);
        else
                fLocalTime->SetValue(B_CONTROL_ON);
        _ShowOrHidePreview();

        _UpdateGmtSettings();
        _SetSystemTimeZone();
        _UpdatePreview();
        _UpdateCurrent();
}


void
TimeZoneView::_UpdatePreview()
{
        int32 selection = fZoneList->CurrentSelection();
        TimeZoneListItem* item
                = selection < 0
                        ? NULL
                        : static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection));

        if (item == NULL || !item->HasTimeZone()) {
                fPreview->SetText("");
                fPreview->SetTime("");
                return;
        }

        BString timeString = _FormatTime(item->TimeZone());
        fPreview->SetText(item->Text());
        fPreview->SetTime(timeString.String());

        fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0));
}


void
TimeZoneView::_UpdateCurrent()
{
        if (fCurrentZoneItem == NULL)
                return;

        BString timeString = _FormatTime(fCurrentZoneItem->TimeZone());
        fCurrent->SetText(fCurrentZoneItem->Text());
        fCurrent->SetTime(timeString.String());
}


void
TimeZoneView::_SetSystemTimeZone()
{
        /*      Set system timezone for all different API levels. How to do this?
         *      1) tell locale-roster about new default timezone
         *      2) tell kernel about new timezone offset
         */

        int32 selection = fZoneList->CurrentSelection();
        if (selection < 0)
                return;

        TimeZoneListItem* item
                = static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection));
        if (item == NULL || !item->HasTimeZone())
                return;

        fCurrentZoneItem = item;
        const BTimeZone& timeZone = item->TimeZone();

        MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone);

        _kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(),
                timeZone.ID().Length());

        fSetZone->SetEnabled(false);
        fLastUpdateMinute = -1;
                // just to trigger updating immediately
}


BString
TimeZoneView::_FormatTime(const BTimeZone& timeZone)
{
        BString result;

        time_t now = time(NULL);
        bool rtcIsGMT;
        _kern_get_real_time_clock_is_gmt(&rtcIsGMT);
        if (!rtcIsGMT) {
                int32 currentOffset
                        = fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone()
                                ? fCurrentZoneItem->OffsetFromGMT()
                                : 0;
                now -= timeZone.OffsetFromGMT() - currentOffset;
        }
        fTimeFormat.Format(result, now, B_SHORT_TIME_FORMAT, &timeZone);

        return result;
}


void
TimeZoneView::_ReadRTCSettings()
{
        BPath path;
        if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
                return;

        path.Append("RTC_time_settings");

        BEntry entry(path.Path());
        if (entry.Exists()) {
                BFile file(&entry, B_READ_ONLY);
                if (file.InitCheck() == B_OK) {
                        char buffer[6];
                        file.Read(buffer, 6);
                        if (strncmp(buffer, "gmt", 3) == 0)
                                fUseGmtTime = true;
                }
        }
}


void
TimeZoneView::_WriteRTCSettings()
{
        BPath path;
        if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, true) != B_OK)
                return;

        path.Append("RTC_time_settings");

        BFile file(path.Path(), B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY);
        if (file.InitCheck() == B_OK) {
                if (fUseGmtTime)
                        file.Write("gmt", 3);
                else
                        file.Write("local", 5);
        }
}


void
TimeZoneView::_UpdateGmtSettings()
{
        _WriteRTCSettings();

        _ShowOrHidePreview();

        _kern_set_real_time_clock_is_gmt(fUseGmtTime);
}


void
TimeZoneView::_ShowOrHidePreview()
{
        if (fUseGmtTime) {
                // Hardware clock uses GMT time, changing timezone will adjust the
                // offset and we need to display a preview
                fCurrent->Show();
                fPreview->Show();
        } else {
                // Hardware clock uses local time, changing timezone will adjust the
                // clock and there is no offset to manage, thus, no preview.
                fCurrent->Hide();
                fPreview->Hide();
        }
}