# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import datetime
import re
from unfurl import utils

timestamp_edge = {
    'color': {
        'color': 'blue'
    },
    'title': 'Date & Time Parsing Functions',
    'label': '🕓'
}


def trim_zero_fractional_seconds(timestamp_string, number_to_trim):
    """Timestamp formats have different levels of precision; trim off extra 0s.

    Different formats may have less precision that the microseconds datetime returns.
    Trim off the appropriate number of trailing zeros from a value to not add extra,
    incorrect precision to it.

    """
    if re.search(rf'\.\d{{{6 - number_to_trim}}}0{{{number_to_trim}}}$', timestamp_string):
        return timestamp_string[:-number_to_trim]
    return timestamp_string


def decode_epoch_seconds(seconds):
    """Decode a numeric timestamp in Epoch seconds format to a human-readable timestamp.

    An Epoch timestamp (1-10 digits) is an integer that counts the number of seconds since Jan 1 1970.

    Useful values for ranges (all Jan-1 00:00:00):
      1970: 0
      2015: 1420070400
      2025: 1735689600
      2030: 1900000000

    """
    return datetime.datetime.utcfromtimestamp(float(seconds)), 'Epoch seconds'


def decode_epoch_centiseconds(centiseconds):
    """Decode a numeric timestamp in Epoch centiseconds (10 ms) format to a human-readable timestamp.

    An Epoch centisecond timestamp (1-12 digits) is an integer that counts the number of centiseconds (10 ms)
    since Jan 1 1970.

    Useful values for ranges (all Jan-1 00:00:00):
      1970: 0
      2015: 142007040000
      2025: 173568960000
      2030: 190000000000

    """
    # Trim off the 4 trailing 0s (don't add precision that wasn't in the timestamp)
    converted_ts = trim_zero_fractional_seconds(
        str(datetime.datetime.utcfromtimestamp(float(centiseconds) / 100)), 4)
    return converted_ts, 'Epoch centiseconds'


def decode_epoch_milliseconds(milliseconds):
    """Decode a numeric timestamp in Epoch milliseconds format to a human-readable timestamp.

    An Epoch millisecond timestamp (1-13 digits) is an integer that counts the number of milliseconds since Jan 1 1970.

    Useful values for ranges (all Jan-1 00:00:00):
      1970: 0
      2015: 1420070400000
      2025: 1735689600000
      2030: 1900000000000

    """
    converted_dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(milliseconds=float(milliseconds))
    # Trim off the 3 trailing 0s (don't add precision that wasn't in the timestamp)
    converted_ts = trim_zero_fractional_seconds(str(converted_dt), 3)
    return converted_ts, 'Epoch milliseconds'


def decode_epoch_ten_microseconds(ten_microseconds):
    """Decode a numeric timestamp in Epoch ten-millisecond increments to a human-readable timestamp.

    An Epoch ten-microsecond increments timestamp (1-15 digits) is an integer that counts the number of ten-microsecond
    increments since Jan 1 1970.

    Useful values for ranges (all Jan-1 00:00:00):
      1970: 0
      2015: 142007040000000
      2025: 173568960000000
      2030: 190000000000000

    """
    # Trim off the trailing 0 (don't add precision that wasn't in the timestamp)
    converted_ts = trim_zero_fractional_seconds(
        str(datetime.datetime.utcfromtimestamp(float(ten_microseconds) / 100000)), 1)
    return converted_ts, 'Epoch ten-microsecond increments'


def decode_epoch_microseconds(microseconds):
    """Decode a numeric timestamp in Epoch microseconds format to a human-readable timestamp.

    An Epoch millisecond timestamp (1-16 digits) is an integer that counts the number of milliseconds since Jan 1 1970.

    Useful values for ranges (all Jan-1 00:00:00):
      1970: 0
      2015: 1420070400000000
      2025: 1735689600000000
      2030: 1900000000000000

    """
    converted_ts = str(datetime.datetime.utcfromtimestamp(float(microseconds) / 1000000))
    return converted_ts, 'Epoch microseconds'


def decode_webkit(microseconds):
    """Decode a numeric timestamp in Webkit format to a human-readable timestamp.

    A Webkit timestamp (17 digits) is an integer that counts the number of microseconds since 12:00AM Jan 1 1601 UTC.

    Useful values for ranges (all Jan-1 00:00:00):
      1970: 11644473600000000
      2015: 13064544000000000
      2025: 13380163200000000

    """
    return datetime.datetime.utcfromtimestamp((float(microseconds) / 1000000) - 11644473600), 'Webkit'


def decode_windows_filetime(intervals):
    """Decode a numeric timestamp in Windows FileTime format to a human-readable timestamp.

    A Windows FileTime timestamp (18 digits) is a 64-bit value that represents the number of 100-nanosecond intervals
    since 12:00AM Jan 1 1601 UTC.

    Useful values for ranges (all Jan-1 00:00:00):
      1970: 116444736000000000
      2015: 130645440000000000
      2025: 133801632000000000
      2065: 146424672000000000

    """
    return datetime.datetime.utcfromtimestamp((float(intervals) / 10000000) - 11644473600), 'Windows FileTime'


def decode_datetime_ticks(ticks):
    """Decode a numeric timestamp in .Net/C# DateTime ticks format to a human-readable timestamp.

    A .Net/C# DateTime ticks timestamp (18 digits) is the number of 100-nanosecond intervals that have elapsed since
    12:00:00 midnight, January 1, 0001 (0:00:00 UTC on January 1, 0001, in the Gregorian calendar), which represents
    DateTime.MinValue. It does not include the number of ticks that are attributable to leap seconds.

    A single tick represents one hundred nanoseconds or one ten-millionth of a second. There are 10,000 ticks in a
    millisecond, or 10 million ticks in a second.

    (^ from https://docs.microsoft.com/en-us/dotnet/api/system.datetime.ticks?view=netframework-4.8)

    Useful values for ranges (all Jan-1 00:00:00):
      1970: 621355968000000000
      2015: 635556672000000000
      2025: 638712864000000000
      2038: 642815136000000000

    """
    seconds = (ticks - 621355968000000000) / 10000000
    return (datetime.datetime.fromtimestamp(seconds)), 'DateTime ticks'


def decode_mac_absolute_time(seconds):
    """Decode a numeric timestamp in Mac Absolute Time format to a human-readable timestamp.

    A Mac Absolute Time timestamp (9 digits typically) is the number of seconds since 2001-01-01. This time format is
    also known as CFAbsoluteTime, Core Data timestamp, or Cocoa Core Data timestamp. Negative values are allowed and
    denote timestamps before the reference date.

    ref: https://developer.apple.com/documentation/corefoundation/cfabsolutetime

    Useful values for ranges (all Jan-1 00:00:00):
      1970: -978307200
      2015: 441763200
      2025: 757382400
      2035: 1072915200

    """
    return datetime.datetime.utcfromtimestamp(float(seconds)+978307200), 'Mac Absolute Time / Cocoa'


def decode_epoch_hex(seconds):
    """Decode a hex string (big endian) of an Epoch seconds integer to a human-readable timestamp.

    An Epoch timestamp (1-10 digits) is an integer that counts the number of seconds since Jan 1 1970.

    Useful values for ranges (all Jan-1 00:00:00):
      2015: 54A48E00
      2025: 67748580
      2030: 713FB300

    """
    timestamp, _ = decode_epoch_seconds(int(seconds, 16))
    return timestamp, 'Epoch seconds (hex)'


def decode_windows_filetime_hex(intervals):
    """Decode a hex timestamp in Windows FileTime format to a human-readable timestamp.

    A Windows FileTime timestamp (18 digits) is a 64-bit value that represents the number of 100-nanosecond intervals
    since 12:00AM Jan 1 1601 UTC.

    Useful values for ranges (all Jan-1 00:00:00):
      1970: 19DB1DED53E8000
      2015: 1D02555E2B98000
      2025: 1DB5BE019BA4000
      2065: 2083476A0E9C000

    """
    int_right = int(intervals, 16)
    timestamp, _ = decode_windows_filetime(int_right)
    return timestamp, 'Windows FileTime (hex)'


def run(unfurl, node):
    new_timestamp = (None, 'unknown')

    # If the node is explicitly classified as a raw timestamp, use that
    # type for the conversion
    if node.data_type == 'epoch-seconds':
        new_timestamp = decode_epoch_seconds(node.value)

    elif node.data_type == 'epoch-centiseconds':
        new_timestamp = decode_epoch_centiseconds(node.value)

    elif node.data_type == 'epoch-milliseconds':
        new_timestamp = decode_epoch_milliseconds(node.value)

    elif node.data_type == 'epoch-ten-microseconds':
        new_timestamp = decode_epoch_ten_microseconds(node.value)

    elif node.data_type == 'epoch-microseconds':
        new_timestamp = decode_epoch_microseconds(node.value)

    elif node.data_type == 'windows-filetime':
        new_timestamp = decode_windows_filetime(node.value)

    elif node.data_type == 'webkit':
        new_timestamp = decode_webkit(node.value)

    elif node.data_type == 'datetime-ticks':
        new_timestamp = decode_datetime_ticks(node.value)

    elif node.data_type == 'mac-absolute-time':
        new_timestamp = decode_mac_absolute_time(node.value)

    elif node.data_type == 'epoch-hex-seconds':
        new_timestamp = decode_epoch_hex(node.value)

    # Otherwise, examine the value of the node and see if we can detect a reasonable timestamp
    else:
        # There are some known cases where we want to suppress a timestamp conversion; put them here.
        if node.data_type == 'google.ei':
            return

        matches_digits = utils.digits_re.fullmatch(str(node.value))
        matches_float = utils.float_re.fullmatch(str(node.value))
        matches_hex = utils.hex_re.fullmatch(str(node.value))

        if matches_digits:
            timestamp = int(node.value)

            # Windows FileTime (18 digits)
            if 130645440000000000 <= timestamp <= 133801632000000000:  # 2015 <= ts <= 2025
                new_timestamp = decode_windows_filetime(timestamp)

            # .Net/C# DateTime ticks (18 digits)
            elif 635556672000000000 <= timestamp <= 638712864000000000:  # 2015 <= ts <= 2025
                new_timestamp = decode_datetime_ticks(timestamp)

            # WebKit (17 digits)
            elif 13064544000000000 <= timestamp <= 13380163200000000:  # 2015 <= ts <= 2025
                new_timestamp = decode_webkit(timestamp)

            # Epoch microseconds (16 digits)
            elif 1420070400000000 <= timestamp <= 1735689600000000:  # 2015 <= ts <= 2025
                new_timestamp = decode_epoch_microseconds(timestamp)

            # Epoch ten microsecond increments (15 digits)
            elif 142007040000000 <= timestamp <= 173568960000000:  # 2015 <= ts <= 2025
                new_timestamp = decode_epoch_microseconds(timestamp)

            # Epoch milliseconds (13 digits)
            elif 1420070400000 <= timestamp <= 1735689600000:  # 2015 <= ts <= 2025
                new_timestamp = decode_epoch_milliseconds(timestamp)

            # Epoch seconds (10 digits)
            elif 1420070400 <= timestamp <= 1735689600:  # 2015 <= ts <= 2025
                new_timestamp = decode_epoch_seconds(timestamp)

            # Mac Absolute Time (9 digits)
            elif 441763200 <= timestamp <= 757382400:  # 2015 <= ts <= 2025
                new_timestamp = decode_mac_absolute_time(timestamp)

        elif matches_float:
            timestamp = float(node.value)

            # Epoch seconds (10 digits)
            if 1420070400.0 <= timestamp <= 1735689600.0:  # 2015 <= ts <= 2025
                new_timestamp = decode_epoch_seconds(timestamp)

            # Mac Absolute Time (9 digits)
            elif 441763200.0 <= timestamp <= 757382400.0:  # 2015 <= ts <= 2025
                new_timestamp = decode_mac_absolute_time(timestamp)

        elif matches_hex:
            timestamp = node.value.replace(':', '')

            # Epoch hex seconds (8 hex chars)
            if 1420070400 <= int(timestamp, 16) <= 1735689600:  # 2015 <= ts <= 2025
                new_timestamp = decode_epoch_hex(timestamp)

            # Windows FileTime hex (16 hex digits)
            elif 130645440000000000 <= int(timestamp, 16) <= 133801632000000000:  # 2015 <= ts <= 2025
                new_timestamp = decode_windows_filetime_hex(timestamp)

    if new_timestamp != (None, 'unknown'):
        unfurl.add_to_queue(
            data_type=new_timestamp[1], key=None, value=new_timestamp[0],
            hover=f'Converted as {new_timestamp[1]}', parent_id=node.node_id,
            incoming_edge_config=timestamp_edge)
