#!/usr/bin/env python3
# encoding: utf-8

import re
import itertools
import sys
import argparse

# Create SQL statement to verify dateTime64 is accepted as argument to functions taking DateTime.
FUNCTIONS = """
toTimeZone(N, 'UTC')
toYear(N, 'Asia/Istanbul')
toQuarter(N, 'Asia/Istanbul')
toMonth(N, 'Asia/Istanbul')
toDayOfYear(N, 'Asia/Istanbul')
toDayOfMonth(N, 'Asia/Istanbul')
toDayOfWeek(N, 0, 'Asia/Istanbul')
toHour(N, 'Asia/Istanbul')
toMinute(N, 'Asia/Istanbul')
toSecond(N, 'Asia/Istanbul')
toUnixTimestamp(N)
toStartOfYear(N, 'Asia/Istanbul')
toStartOfISOYear(N, 'Asia/Istanbul')
toStartOfQuarter(N, 'Asia/Istanbul')
toStartOfMonth(N, 'Asia/Istanbul')
toMonday(N, 'Asia/Istanbul')
toStartOfWeek(N, 'Asia/Istanbul')
toStartOfDay(N, 'Asia/Istanbul')
toStartOfHour(N, 'Asia/Istanbul')
toStartOfMinute(N, 'Asia/Istanbul')
toStartOfFiveMinutes(N, 'Asia/Istanbul')
toStartOfTenMinutes(N, 'Asia/Istanbul')
toStartOfFifteenMinutes(N, 'Asia/Istanbul')
toStartOfInterval(N, INTERVAL 1 year, 'Asia/Istanbul')
toStartOfInterval(N, INTERVAL 1 month, 'Asia/Istanbul')
toStartOfInterval(N, INTERVAL 1 day, 'Asia/Istanbul')
toStartOfInterval(N, INTERVAL 15 minute, 'Asia/Istanbul')
date_trunc('year', N, 'Asia/Istanbul')
date_trunc('month', N, 'Asia/Istanbul')
date_trunc('day', N, 'Asia/Istanbul')
date_trunc('minute', N, 'Asia/Istanbul')
toTime(N, 'Asia/Istanbul')
toRelativeYearNum(N, 'Asia/Istanbul')
toRelativeQuarterNum(N, 'Asia/Istanbul')
toRelativeMonthNum(N, 'Asia/Istanbul')
toRelativeWeekNum(N, 'Asia/Istanbul')
toRelativeDayNum(N, 'Asia/Istanbul')
toRelativeHourNum(N, 'Asia/Istanbul')
toRelativeMinuteNum(N, 'Asia/Istanbul')
toRelativeSecondNum(N, 'Asia/Istanbul')
toISOYear(N, 'Asia/Istanbul')
toISOWeek(N, 'Asia/Istanbul')
toWeek(N, 'Asia/Istanbul')
toYearWeek(N, 'Asia/Istanbul')
timeSlot(N, 'Asia/Istanbul')
toYYYYMM(N, 'Asia/Istanbul')
toYYYYMMDD(N, 'Asia/Istanbul')
toYYYYMMDDhhmmss(N, 'Asia/Istanbul')
addYears(N, 1, 'Asia/Istanbul')
addMonths(N, 1, 'Asia/Istanbul')
addWeeks(N, 1, 'Asia/Istanbul')
addDays(N, 1, 'Asia/Istanbul')
addHours(N, 1, 'Asia/Istanbul')
addMinutes(N, 1, 'Asia/Istanbul')
addSeconds(N, 1, 'Asia/Istanbul')
addQuarters(N, 1, 'Asia/Istanbul')
subtractYears(N, 1, 'Asia/Istanbul')
subtractMonths(N, 1, 'Asia/Istanbul')
subtractWeeks(N, 1, 'Asia/Istanbul')
subtractDays(N, 1, 'Asia/Istanbul')
subtractHours(N, 1, 'Asia/Istanbul')
subtractMinutes(N, 1, 'Asia/Istanbul')
subtractSeconds(N, 1, 'Asia/Istanbul')
subtractQuarters(N, 1, 'Asia/Istanbul')
CAST(N as DateTime('Europe/Minsk'))
CAST(N as Date)
CAST(N as UInt64)
CAST(N as DateTime64(0, 'Europe/Minsk'))
CAST(N as DateTime64(3, 'Europe/Minsk'))
CAST(N as DateTime64(6, 'Europe/Minsk'))
CAST(N as DateTime64(9, 'Europe/Minsk'))
# Casting our test values to DateTime(12) will cause an overflow and hence will fail the test under UB sanitizer.
# CAST(N as DateTime64(12, 'Asia/Istanbul'))
# DateTime64(18) will always fail due to zero precision, but it is Ok to test here:
# CAST(N as DateTime64(18, 'Asia/Istanbul'))
formatDateTime(N, '%C %d %D %e %F %H %I %j %m %i %p %R %S %T %u %V %w %y %Y %%', 'Asia/Istanbul')
""".splitlines()

# Expanded later to cartesian product of all arguments, using format string.
extra_ops = [
    # With same type:
    (
        ["N {op} N"],
        {
            "op": [
                "- ",  # does not work, but should it?
                "+ ",  # does not work, but should it?
                "!=",
                "==",  # equality and inequality supposed to take sub-second part in account
                "< ",
                "<=",
                "> ",
                ">=",
            ]
        },
    ),
    # With other DateTime types:
    (
        ["N {op} {arg}", "{arg} {op} N"],
        {
            "op": [
                "-",  # does not work, but should it?
                "!=",
                "==",
                # these are naturally expected to work, but they don't:
                "< ",
                "<=",
                "> ",
                ">=",
            ],
            "arg": ["DT", "D", "DT64"],
        },
    ),
    # With arithmetic types
    (
        ["N {op} {arg}", "{arg} {op} N"],
        {
            "op": ["+ ", "- ", "==", "!=", "< ", "<=", "> ", ">="],
            "arg": [
                "toUInt8(1)",
                "toInt8(-1)",
                "toUInt16(1)",
                "toInt16(-1)",
                "toUInt32(1)",
                "toInt32(-1)",
                "toUInt64(1)",
                "toInt64(-1)",
            ],
        },
    ),
]

# Expand extra_ops here
for funcs, args in extra_ops:
    args_keys = list(args.keys())
    for args_vals in itertools.product(*list(args.values())):
        for func in funcs:
            result_func = func.format(**dict(list(zip(args_keys, args_vals))))
            FUNCTIONS.append(result_func)

# filter out empty lines and commented out lines
COMMENTED_OUT_LINE_RE = re.compile(r"^\s*#")
FUNCTIONS = list(
    [f for f in FUNCTIONS if len(f) != 0 and COMMENTED_OUT_LINE_RE.match(f) == None]
)
TYPES = ["D", "DT", "DT64"]


def escape_string(s):
    if sys.version_info[0] > 2:
        return s.encode("unicode_escape").decode("utf-8").replace("'", "\\'")
    else:
        return s.encode("string-escape").decode("utf-8")


def execute_functions_for_types(functions, types):
    # NOTE: use string.Template here to allow lines with missing keys, like type, e.g. SELECT CAST(toDateTime64(1234567890), 'DateTime64')
    for func in functions:
        print(("""SELECT 'SELECT {func}';""".format(func=escape_string(func))))
        for dt in types:
            prologue = "\
WITH \
toDateTime64('2019-09-16 19:20:11.234', 3, 'Europe/Minsk') as DT64, \
toDateTime('2019-09-16 19:20:11', 'Europe/Minsk') as DT, \
toDate('2019-09-16') as D, {X} as N".format(
                X=dt
            )
            print(
                (
                    """{prologue} SELECT toTypeName(r), {func} as r FORMAT CSV;""".format(
                        prologue=prologue, func=func
                    )
                )
            )
        print("""SELECT '------------------------------------------';""")


def main():
    def parse_args():
        parser = argparse.ArgumentParser()
        parser.add_argument(
            "--functions_re",
            type=re.compile,
            help="RE to enable functions",
            default=None,
        )
        parser.add_argument(
            "--types_re",
            type=lambda s: re.compile("^(" + s + ")$"),
            help="RE to enable types, supported types: " + ",".join(TYPES),
            default=None,
        )
        parser.add_argument(
            "--list_functions",
            action="store_true",
            help="List all functions to be tested and exit",
        )
        return parser.parse_args()

    args = parse_args()

    functions = FUNCTIONS
    types = TYPES

    if args.functions_re:
        functions = list([f for f in functions if args.functions_re.search(f)])
        if len(functions) == 0:
            print("functions list is empty")
            return -1

    if args.types_re:
        types = list([t for t in types if args.types_re.match(t)])
        if len(types) == 0:
            print("types list is empty")
            return -1

    if args.list_functions:
        print(("\n".join(functions)))
        return 0

    execute_functions_for_types(functions, types)


if __name__ == "__main__":
    exit(main())
