import os
import socket
import json
import hashlib
import filecmp
import re

from urllib.parse import urlparse, parse_qs
from distutils.version import LooseVersion

# Searches the current working directory for the directory named with the
# highest version number as per LooseVersion.
def get_latest_version():
    latest = None
    for d in os.listdir('.'):
        print(d+" - "+str(latest))
        if os.path.isfile(d):
            continue
        if not d[0].isdigit():
            continue
        if latest is None or LooseVersion(latest) < LooseVersion(d):
            latest = d
    return latest


# Returns a list of all files found relative to `path`.
# Parameters:
#   path - The directory that will be traversed, results will be relative to
#          this path.
#   Ignore - A list of file names which to ignore
def get_all_paths(path, ignore=[]):
    ignore = set(ignore)
    paths = []
    for entry in os.walk(path):
        d, _, files = entry
        files = set(files).difference(ignore)
        paths += [os.path.join(d, f) for f in files]
    out = [d.replace('{}{}'.format(path, os.path.sep), '') for d in paths]
    return set(out)


# Returns a tuple containing three lists: deleted files, new_file, changed
# files.
# Parameters
#    left - The original directory
#    right - The directory with updates
#    ignore - A list o file name which to ignore
def get_diff_list(left, right, ignore=['.DS_Store', 'pymakr.conf']):
    left_paths = get_all_paths(left, ignore=ignore)
    right_paths = get_all_paths(right, ignore=ignore)
    new_files = right_paths.difference(left_paths)
    to_delete = left_paths.difference(right_paths)
    common = left_paths.intersection(right_paths)

    to_update = []
    for f in common:
        if not filecmp.cmp(os.path.join(left, f),
                           os.path.join(right, f),
                           shallow=False):
            to_update.append(f)

    return (to_delete, new_files, (to_update))


# Searches the current working directory for a file starting with "firmware_"
# followed by a version number higher than `current_ver` as per LooseVersion.
# Returns None if such a file does not exist.
# Parameters
#    path - the path to the directory to be searched
#    current_ver - the result must be higher than this version
#
def get_new_firmware(path, current_ver):
    latest = None
    for f in os.listdir(path):
        # Ignore directories
        if not os.path.isfile(f):
            continue

        try:
            m = re.search(r'firmware_([0-9a-zA-Z.]+)(?=.bin|hex)', f)
            version = m.group(1)
            if LooseVersion(current_ver) < LooseVersion(version):
                latest = f
        except AttributeError:
            # file does not match firmware naming scheme
            pass
    return latest


# Returns a dict containing a manifest entry which contains the files
# destination path, download URL and SHA1 hash.
# Parameters
#    path - The relative path to the file
#    version - The version number of the file
#    host - The server address, used in URL formatting
def generate_manifest_entry(host, path, version):
    path = "/".join(path.split(os.path.sep))
    entry = {}
    entry["dst_path"] = "/{}".format(path)
    entry["URL"] = "http://{}/{}/{}".format(host, version, path)
    data = open(os.path.join('.', version, path), 'rb').read()
    hasher = hashlib.sha1(data)
    entry["hash"] = hasher.hexdigest()
    return entry


# Returns the update manivest as a dictionary with the following entries:
#    delete - List of files that are no longer needed
#    new - A list of manifest entries for new files to be downloaded
#    update - A list of manifest entries for files that require Updating
#    version - The version that this manifest will bring the client up to
#    firmware(optional) - A manifest entry for the new firmware, if one is
#                         available.
def generate_manifest(current_ver, host):
    latest = get_latest_version()
    # If the current version is already the latest, there is nothing to do
    if latest == current_ver:
        return None

    # Get lists of difference between versions
    to_delete, new_files, to_update = get_diff_list(current_ver, latest)

    manifest = {
      "delete": list(to_delete),
      "new": [generate_manifest_entry(host, f, latest) for f in new_files],
      "update": [generate_manifest_entry(host, f, latest) for f in to_update],
      "version": latest
    }

    # If there is a newer firmware version add it to the manifest
    new_firmware = get_new_firmware('.', current_ver)
    if new_firmware is not None:
        entry = {}
        entry["URL"] = "http://{}/{}".format(host, new_firmware)
        data = open(os.path.join('.', new_firmware), 'rb').read()
        hasher = hashlib.sha1(data)
        entry["hash"] = hasher.hexdigest()
        manifest["firmware"] = entry

    return manifest

version_list = list(filter(lambda x: x[0].isdigit(), [x[1] for x in os.walk('./')][0]))
for current_ver in version_list:
    print("Generating a manifest from version: {}".format(current_ver))
    manifest = generate_manifest(current_ver, 'airmonitor-utils.terrasls.com/softwareupdatebeta')
    j = json.dumps(manifest,sort_keys=True, indent=4, separators=(',', ': '))
    f = open("./manifest_{}.json".format(current_ver), "w")
    f.write(j)
    f.close()
