import struct
import locale
from collections import namedtuple
from datetime import datetime
import subprocess
from distutils.version import StrictVersion
import subprocess
import os

#locale.setlocale(locale.LC_ALL, 'en_US')

# sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t)
APP_DESC_OFFSET = 32

# sizeof(esp_app_desc_t)
APP_DESC_SIZE = 256

APP_DESC_STRUCT = "<II2I32s32s16s16s32s32B20I"
AppDesc = namedtuple('AppDesc',
                     [
                         "secure_version",
                         "version",
                         "project_name",
                         "time",
                         "date",
                         "idf_ver",
                         "app_elf_sha256",
                     ]
                     )

def version_number_from_git(tag_prefix='release/', sha_length=10, version_format="{version}.dev{commits}+{sha}"):
    def get_released_versions():
        tags = sorted(subprocess.getoutput('git tag').split('\n'))
        versions = [t[len(tag_prefix):]
                    for t in tags if t.startswith(tag_prefix)]
        return versions

    def tag_from_version(v):
        return tag_prefix + v

    def increment_version(v):
        parsed_version = [int(i) for i in v.split('.')]
        parsed_version[-1] += 1
        return '.'.join(str(i) for i in parsed_version)

    version_strings = get_released_versions()
    version_strings.sort(key=StrictVersion)
    latest_release = version_strings[-1]
    commits_since_tag = subprocess.getoutput(
        'git rev-list {}..HEAD --count'.format(tag_from_version(latest_release)))
    sha = subprocess.getoutput('git rev-parse HEAD')[:sha_length]
    is_dirty = len(subprocess.getoutput(
        "git status --untracked-files=no -s")) > 0

    if int(commits_since_tag) == 0:
        version_string = latest_release
    else:
        next_version = increment_version(latest_release)
        version_string = version_format.format(
            version=next_version, commits=commits_since_tag, sha=sha)

    if is_dirty:
        version_string += ".dirty"
    return version_string


def read_app_description(file_name):
    def process_bytes(b):
        if not isinstance(b, bytes):
            return b
        s = b.decode()
        return s[:s.find("\x00")]

    with open(file_name, 'rb') as f:
        f.seek(APP_DESC_OFFSET, 0)
        raw_app_desc = f.read(APP_DESC_SIZE)
        unpacked = struct.unpack(APP_DESC_STRUCT, raw_app_desc)
        unpacked = tuple(process_bytes(e) for e in unpacked)
        magic_word, secure_version, _, _, version, project_name, time, date, idf_ver, app_elf_sha256, *_ = unpacked
        assert magic_word == 0xABCD5432
        return AppDesc(secure_version, version, project_name, time, date, idf_ver, app_elf_sha256)


def patch_app_description(file_name, version, project_name, time, date):
    assert len(version) < 32
    assert len(project_name) < 32
    assert len(time) < 16
    assert len(date) < 16

    def fill_zeros(s, total_length):
        s += "\x00" * (total_length - len(s))
        s = s.encode()
        assert len(s) == total_length
        return s

    with open(file_name, 'r+b') as f:
        f.seek(APP_DESC_OFFSET, 0)
        raw_app_desc = f.read(APP_DESC_SIZE)
        unpacked = list(struct.unpack(APP_DESC_STRUCT, raw_app_desc))
        unpacked[4] = fill_zeros(version, 32)
        unpacked[5] = fill_zeros(project_name, 32)
        unpacked[6] = fill_zeros(time, 16)
        unpacked[7] = fill_zeros(date, 16)
        packed = struct.pack(APP_DESC_STRUCT, *unpacked)
        f.seek(APP_DESC_OFFSET, 0)
        f.write(packed)


def add_info_to_firmware(firmware_file, version):
    now = datetime.now()
    date = now.strftime("%b %d %Y")
    time = now.strftime("%H:%M:%S")
    patch_app_description(firmware_file, version=version,
                          project_name="swimtracker.bauer.tech", time=time, date=date)


if __name__ == "__main__":
    firmware_file = ".pio/build/esp32/firmware.bin"
    version_file_name = "VERSION"

    version = version_number_from_git()
    add_info_to_firmware(firmware_file, version)
    print(read_app_description(firmware_file))
    with open(version_file_name, "w") as version_file:
      print(version, file=version_file)
    subprocess.run(["scp", firmware_file, "root@server:/volumes/swimtracker-update"])
    subprocess.run(["scp", version_file_name, "root@server:/volumes/swimtracker-update"])

    os.unlink(firmware_file)
    os.unlink(version_file_name)
    