#!/usr/bin/python

##############################################################################
###
### generate-manifest.py
###
### A Vendor utility to generate a valid heat zip manifest file for the AT&T onboarding. 
###
### Usage:
###
###    generate-manifest.py [-f|--folder] vendor-heat-directory [-n|--name] manifest-name [-d|--description] manifet-description
###
### For example:
###
###    ./generate-manifest.py --folder ./vota --name vOTA --description "HOT template to create vOTA server"
###
### Help:
### The script is doing the following:
###  1) Split the files into different types
###     a. .env files
###     b. Network files (anything containing the string network)
###     c. Volume files (anything containing the string volume)
###     d. Top level Heat files
###     e. Other types
###  2) Match env files to heat files – looking for same name ignoring suffix and extension
###  3) Match Network childs
###     a. Look for Top level heats which name is a substring of the name of the Network heat name.
###  4) Match Volume childs
###     a. Look for Top level heats which name is a substring of the name of the Volume heat name.
###  5)      Generate the JSON file from the above
###
###
### Author: Avi Ziv
### Version 1.4 for ASDC/AT&T 1.0
### Date: 13 July 2016
###
##############################################################################

# import os,sys,getopt,json,re
import os, sys, getopt, re
from collections import OrderedDict
from json import JSONEncoder
import json

VERSION = "1.4"
ENV_EXT = ".env"
SHELL_EXT = ".sh"
YAML_EXT = [".yaml", ".yml"]
# VERSION_DELIMITER_PATTERN='_v\d{*}.\d{*}'
# VERSION_DELIMITER_PATTERN='_v*.*'
#v1.0
VERSION_DELIMITER_PATTERN = '_v\d+.\d+'
#07_12_2016
VERSION_DELIMITER_PATTERN2 = '_\d+-\d+-\d+'

# types
HEAT = "HEAT"
HEAT_BASE = "HEAT_BASE"
HEAT_NET = "HEAT_NET"
HEAT_VOL = "HEAT_VOL"
HEAT_ENV = "HEAT_ENV"
SHELL = "SHELL"
OTHER = "OTHER"

globalVolumeVal = "VOLUME"
globalNetworkVal = "NETWORK"
globalBaseVal = "BASE"


def version():
    return VERSION


def usage():
    print ('usage: ' + sys.argv[0] + ' [-f|--folder] vendor-heat-directory [-n|--name] manifest-name [-d|--description] manifet-description' )


def header():
    print ("\nASDC Vendor manifest file generator, version " + version() + "\n")


def getEnvVariables(value, defaultValue):
    try:
        eVal = os.environ[value]
        return eVal
    except KeyError:
        print ("Missing ${" + value + "} envirunment variable. Using default value: " + defaultValue)
    return defaultValue


def getF(listFiles):
    print ("Analyzing files ...")
    foundABase = False
    files = listFiles
    jsons = {}
    lOfEnvs = {}
    lOfVolumes = {}
    lOfNetworks = {}
    lOfHeats = {}
    lOfShels = {}
    lOfArtifacts = {}

    for f in files:
        fullFilename = f[1]
        fObj = ManifestFileInfo(fullFilename)
        if fObj.isEnv():
            lOfEnvs[fObj.file_name] = fObj
        elif fObj.isShell():
            lOfShels[fObj.file_name] = fObj
        elif fObj.isVolume():
            lOfVolumes[fObj.file_name] = fObj
        elif fObj.isNetwork():
            lOfNetworks[fObj.file_name] = fObj
        elif (fObj.isYaml() and not fObj.isBase()):
            lOfHeats[fObj.file_name] = fObj
        elif fObj.isArtifact():
            lOfArtifacts[fObj.file_name] = fObj
        elif (fObj.isBase() and fObj.isYaml()):
            foundABase = True
            lOfHeats[fObj.file_name] = fObj

    jsons['heats'] = lOfHeats
    jsons['envs'] = lOfEnvs
    jsons['shells'] = lOfShels
    jsons['volumes'] = lOfVolumes
    jsons['networks'] = lOfNetworks
    jsons['artifacts'] = lOfArtifacts

    if not foundABase:
        print (">>> Warning: No Base was found")
    return jsons

def loadFilesToList(folder):
    print ("Analyzing files in folder: << " + folder + " >>")
    files = os.listdir(folder)
    listOfFiles = []
    for f in files:
        if os.path.isdir(os.path.join(folder, f)):
            ConsoleLogger.warning("Sub folders are ignored by this script, you may want to remove it before archiving")
            continue

        filename, file_extension = os.path.splitext(f)
        if filename == 'MANIFEST':
            ConsoleLogger.warning("Your folder already contains a manifest file that will be overridden")
            continue
        listOfFiles.append([filename, f])
    return listOfFiles


def make(files):
    flist = []
    dEnvs = {}
    dEnvs = files['envs']
    dHeats = files['heats']
    dNetworks = files['networks']
    dVolumes = files['volumes']
    dArtifacts = files['artifacts']
    dShells = files['shells']

    env_items = dEnvs.items()
    for fileItem in env_items:
        env_name = fileItem[1].file_name
        env_base = fileItem[1].base_file_name
        if env_name in dHeats:
            dHeats[env_name].add(fileItem[1])
            continue

        if env_name in dNetworks.items():
            dNetworks[env_name].add(fileItem[1])
            continue

        if env_name in dVolumes.items():
            dVolumes[env_name[0]].add(env_name[1])
            continue

        for fName in dHeats:
            heat_base = dHeats[fName].base_file_name
            if env_base in heat_base:
                dHeats[fName].add(dEnvs[env_name])
                break
        else:
            for fName in dNetworks:
                net_base = dNetworks[fName].base_file_name
                if env_base in net_base:
                    dNetworks[fName].add(dEnvs[env_name])
                    break
            else:
                for fName in dVolumes:
                    vol_base = dVolumes[fName].base_file_name
                    if env_base in vol_base:
                        dVolumes[fName].add(dEnvs[env_name])
                        break

                else:
                    flist.append(dEnvs[env_name])

    for fName in dVolumes:
        vol_base = dVolumes[fName].base_file_name
        for hfName in dHeats:
            heat_base = dHeats[hfName].base_file_name
            if heat_base in vol_base:
                dHeats[hfName].add(dVolumes[fName])
                break
        else:
            flist.append(dVolumes[fName])

    for fName in dNetworks:
        net_base = dNetworks[fName].base_file_name
        for hfName in dHeats:
            heat_base = dHeats[hfName].base_file_name
            if heat_base in net_base:
                dHeats[hfName].add(dNetworks[fName])
                break
        else:
            flist.append(dNetworks[fName])

    for fName in dHeats:
        flist.append(dHeats[fName])
    for fName in dShells:
        flist.append(dShells[fName])
    for fName in dArtifacts:
        flist.append(dArtifacts[fName])

    print ("\n------------------------------------------------------------\n")
    return flist


def generate(folder, name, description):
    print ("Checking envirunment variables ...")
    global globalVolumeVal
    globalVolumeVal = getEnvVariables("VOLUME", globalVolumeVal)

    global globalNetworkVal
    globalNetworkVal = getEnvVariables("NETWORK", globalNetworkVal)

    global globalBaseVal
    globalBaseVal = getEnvVariables("BASE", globalBaseVal)

    YamlTabCleaner(folder).cleanYamlTabs()

    print ("Generating manifest file ...")
    jsons = getF(loadFilesToList(folder))

    lFiles = make(jsons)
    manifest = Manifest(name, description, '1.0', lFiles)
    output_json = json.dumps(manifest, default=jdefault, indent=4, sort_keys=False)

    f = open(os.path.join(folder, 'MANIFEST.json'), 'w')
    f.write(output_json)
    print("MANIFEST file created")


################

def jdefault(obj):
    if hasattr(obj, '__json__'):
        return obj.__json__()
    else:
        return obj.__dict__


class ManifestFileInfo(object):
    def __init__(self, filename):
        self.name = filename
        self.base = 'false'
        self.data = []
        self.file_name, self.file_extension = os.path.splitext(filename)
        self.base_file_name = re.sub(VERSION_DELIMITER_PATTERN, '', self.file_name)
        self.base_file_name = re.sub(VERSION_DELIMITER_PATTERN2, '', self.base_file_name)

        if self.isEnv():
            self.heat_type = Types.ENV
        elif self.isShell():
            self.heat_type = Types.SHELL
        elif self.isVolume():
            self.heat_type = Types.VOL
        elif self.isNetwork():
            self.heat_type = Types.NET
        elif self.isYaml() and not self.isBase():
            self.heat_type = Types.HEAT
        elif self.isArtifact():
            self.heat_type = Types.OTHER
        elif (self.isBase() and self.isYaml()):
            self.heat_type = Types.HEAT
            self.base = 'true'

    def set(self, data):
        self.data = data

    def add(self, item):
        self.data.append(item)

    def isYaml(self):
        return any(val in self.file_extension.lower() for val in YAML_EXT)

    def isEnv(self):
        return self.file_extension.lower() == ENV_EXT.lower()

    def isShell(self):
        return self.file_extension.lower() == SHELL_EXT.lower()

    def isVolume(self):
        res = globalVolumeVal.lower() in self.file_name.lower()
        return res

    def isNetwork(self):
        res = globalNetworkVal.lower() in self.file_name.lower()
        return res

    def isBase(self):
        res = globalBaseVal.lower() in self.file_name.lower()
        return res

    def isArtifact(self):
        return (not self.isBase() and not self.isVolume() and not self.isNetwork() and not self.isEnv())

    def isHEAT(self):
        return ((self.heat_type == Types.HEAT) | (self.heat_type == Types.BASE) | (self.heat_type == Types.NET) | (
            self.heat_type == Types.VOL))

    def __json__(self):
        dict = OrderedDict(
            [('file', self.name), ('type', self.heat_type)])
        if self.isHEAT():
            dict['isBase'] = self.base
            if self.data != []:
                dict['data'] = self.data

        return dict


class Manifest(object):
    def __init__(self, name, description, version, data):
        self.name = name
        self.description = description
        self.version = version
        self.data = data

    def add(self, data):
        self.data.append(data)

    def __json__(self):
        return OrderedDict([('name', self.name), ('description', self.description), ('data', self.data)])


class YamlTabCleaner(object):
    def __init__(self, folder):
        self.folder = folder

    def replaceTabs(self, sourceFile, targetFile):
        with open(sourceFile, "rt") as fin:
            if '\t' in fin.read():
                print("\'tab\' character was found in the file: " + sourceFile + "\na clean version of the file can be found under \'clean\' folder")
                target = os.path.dirname(targetFile)
                if not os.path.exists(target):
                    os.makedirs(target)
                fin.seek(0)
                with open(targetFile, "wt") as fout:
                    for line in fin:
                        fout.write(line.replace('\t', ' '))

    def cleanYamlTabs(self):
        included_extenstions = ['yml', 'yaml']
        files = [fn for fn in os.listdir(self.folder)
                 if any(fn.endswith(ext) for ext in included_extenstions)]
        target = os.path.join(self.folder, "clean")
        for file in files:
            self.replaceTabs(os.path.join(self.folder, file), os.path.join(target, file))

class ConsoleLogger(object):
    @classmethod
    def error(cls, message):
        print(">>> Error: " + message)

    @classmethod
    def warning(cls, message):
        print(">>> Warning: " + message)


    @classmethod
    def info(cls, message):
        print(">>> Info: " + message)


def enum(**named_values):
    return type('Enum', (), named_values)


################

def main(argv):
    action = ''
    folderName = '.'
    name = ''
    description = ''
    version = ''

    try:
        opts, args = getopt.getopt(argv, "h:f:n:d", ["folder=", "name=", "description=", ])
    except getopt.GetoptError as err:
        # print help information and exit:
        print ('>>>>' + str(err))
        usage()
        sys.exit(2)
    for opt, arg in opts:
        if opt == '-h':
            usage()
            sys.exit()
        elif opt in ('-f', '--folder'):
            action = 'generate'
            if not arg:
                print ("Error: missing heat files directory")
                usage()
                sys.exit(2)
            else:
                folderName = arg
        elif opt in ('-n', '--name'):
            name = arg
        elif opt in ('-d', '--description'):
            description = arg
        else:
            usage()

    if action == 'generate':
        generate(folderName, name, description)
        sys.exit()
    else:
        usage()


if __name__ == "__main__":
    header()
    Types = enum(HEAT='HEAT', BASE='HEAT_BASE', NET='HEAT_NET', VOL='HEAT_VOL', ENV='HEAT_ENV', SHELL='SHELL',
                 OTHER='OTHER')
    main(sys.argv[1:])