//
// Copyright (c) 2009-2011 Artyom Beilis (Tonkikh)
//
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#include "boost/locale/icu/time_zone.hpp"
#include <boost/locale/hold_ptr.hpp>
#include "boost/locale/icu/icu_util.hpp"
#include <boost/predef/os.h>

//
// Bug - when ICU tries to find a file that is equivalent to /etc/localtime it finds /usr/share/zoneinfo/localtime
// that is just a symbolic link to /etc/localtime.
//
// It started in 4.0 and was fixed in version 4.6, also the fix was backported to the 4.4 branch so it should be
// available from 4.4.3... So we test if the workaround is required
//
// It is also relevant only for Linux, BSD and Apple (as I see in ICU code)
//

#if BOOST_LOCALE_ICU_VERSION >= 400 && BOOST_LOCALE_ICU_VERSION <= 406 \
  && (BOOST_LOCALE_ICU_VERSION != 404 || U_ICU_VERSION_PATCHLEVEL_NUM >= 3)
#    if BOOST_OS_LINUX || BOOST_OS_BSD_FREE || defined(__APPLE__)
#        define BOOST_LOCALE_WORKAROUND_ICU_BUG
#    endif
#endif

#ifdef BOOST_LOCALE_WORKAROUND_ICU_BUG
#    include <cstring>
#    include <dirent.h>
#    include <fstream>
#    include <memory>
#    include <pthread.h>
#    include <sys/stat.h>
#    include <sys/types.h>
#    include <unistd.h>
#endif

namespace boost { namespace locale { namespace impl_icu {

#ifndef BOOST_LOCALE_WORKAROUND_ICU_BUG

    // This is normal behavior

    icu::TimeZone* get_time_zone(const std::string& time_zone)
    {
        if(time_zone.empty())
            return icu::TimeZone::createDefault();
        else
            return icu::TimeZone::createTimeZone(time_zone.c_str());
    }

#else

    // This is a workaround for an ICU timezone detection bug.
    // It is \b very ICU specific and should not be used
    // in general. It is also designed to work only on
    // specific patforms: Linux, BSD and Apple, where this bug may actually
    // occur
    namespace {

        // Under BSD, Linux and Mac OS X dirent has normal size
        // so no issues with readdir_r

        class directory {
        public:
            directory(const char* name) : d(0), read_result(0)
            {
                d = opendir(name);
                if(!d)
                    return;
            }
            ~directory()
            {
                if(d)
                    closedir(d);
            }
            bool is_open() { return d; }
            const char* next()
            {
                if(d && readdir_r(d, &de, &read_result) == 0 && read_result != 0)
                    return de.d_name;
                return nullptr;
            }

        private:
            DIR* d;
            struct dirent de;
            struct dirent* read_result;
        };

        bool files_equal(const std::string& left, const std::string& right)
        {
            char l[256], r[256];
            std::ifstream ls(left);
            if(!ls)
                return false;
            std::ifstream rs(right);
            if(!rs)
                return false;
            do {
                ls.read(l, sizeof(l));
                rs.read(r, sizeof(r));
                size_t n;
                if((n = ls.gcount()) != size_t(rs.gcount()))
                    return false;
                if(memcmp(l, r, n) != 0)
                    return false;
            } while(!ls.eof() || !rs.eof());
            if(bool(ls.eof()) != bool(rs.eof()))
                return false;
            return true;
        }

        std::string find_file_in(const std::string& ref, size_t size, const std::string& dir)
        {
            directory d(dir.c_str());
            if(!d.is_open())
                return std::string();

            const char* name = nullptr;
            while((name = d.next()) != 0) {
                std::string file_name = name;
                if(file_name == "." || file_name == ".." || file_name == "posixrules" || file_name == "localtime")
                    continue;
                struct stat st;
                std::string path = dir + "/" + file_name;
                if(stat(path.c_str(), &st) == 0) {
                    if(S_ISDIR(st.st_mode)) {
                        std::string res = find_file_in(ref, size, path);
                        if(!res.empty())
                            return file_name + "/" + res;
                    } else {
                        if(size_t(st.st_size) == size && files_equal(path, ref))
                            return file_name;
                    }
                }
            }
            return std::string();
        }

        // This actually emulates ICU's search
        // algorithm... just it ignores localtime
        std::string detect_correct_time_zone()
        {
            const char* tz_dir = "/usr/share/zoneinfo";
            const char* tz_file = "/etc/localtime";

            struct stat st;
            if(::stat(tz_file, &st) != 0)
                return std::string();
            size_t size = st.st_size;
            std::string r = find_file_in(tz_file, size, tz_dir);
            if(r.empty())
                return r;
            if(r.compare(0, 6, "posix/") == 0 || r.compare(0, 6, "right/", 6) == 0)
                return r.substr(6);
            return r;
        }

        // Using pthread as:
        // - This bug is relevant for only Linux, BSD, Mac OS X and
        //  pthreads are native threading API
        // - The dependency on boost.thread may be removed when using
        //   more recent ICU versions (so TLS would not be needed)
        //
        // This the dependency on Boost.Thread is eliminated

        pthread_once_t init_tz = PTHREAD_ONCE_INIT;
        std::string default_time_zone_name;

        extern "C" {
        static void init_tz_proc()
        {
            try {
                default_time_zone_name = detect_correct_time_zone();
            } catch(...) {
            }
        }
        }

        std::string get_time_zone_name()
        {
            pthread_once(&init_tz, init_tz_proc);
            return default_time_zone_name;
        }

    } // namespace

    icu::TimeZone* get_time_zone(const std::string& time_zone)
    {
        if(!time_zone.empty())
            return icu::TimeZone::createTimeZone(time_zone.c_str());
        hold_ptr<icu::TimeZone> tz(icu::TimeZone::createDefault());
        icu::UnicodeString id;
        tz->getID(id);
        // Check if there is a bug?
        if(id != icu::UnicodeString("localtime"))
            return tz.release();
        // Now let's deal with the bug and run the fixed
        // search loop as that of ICU
        std::string real_id = get_time_zone_name();
        if(real_id.empty()) {
            // if we failed fallback to ICU's time zone
            return tz.release();
        }
        return icu::TimeZone::createTimeZone(real_id.c_str());
    }
#endif // bug workaround

}}} // namespace boost::locale::impl_icu
