import os
import sys
import glob
import string

import yaml
import jsonschema
import munch

THIS_SCRIPT_DIR      = os.path.dirname( os.path.abspath( __file__ ) )
SCHEMA_DIR           = os.path.join( THIS_SCRIPT_DIR, 'schema' )
TEMPLATE_DIR         = os.path.join( THIS_SCRIPT_DIR, 'templates' )
DEFAULT_OUTPUT_DIR   = 'out'
CONFIG_SCHEMA_FILE   = os.path.join( SCHEMA_DIR, 'config.yaml' )
TEMPLATE_SCHEMA_FILE = os.path.join( SCHEMA_DIR, 'template_info.yaml' )

PRINT_INDENT         = '  '

def print_indent( text, indent_depth = 0 ):
    print( "{indent}{text}".format(
        indent = PRINT_INDENT * indent_depth,
        text   = text
    ))

def create_munch( yaml_body, none_altanative = '' ):
    if type( yaml_body ) is list:
        result = []
        for x in yaml_body:
            result.append( create_munch( x, none_altanative ) )
        return result

    result = munch.DefaultMunch( '', yaml_body )
    for k in yaml_body.keys():
        if result[ k ] is None:
            result[ k ] = none_altanative

    return result

def validate_yaml( source, schema ):
    try:
        jsonschema.validate( source, schema )
    except Exception:
        raise Exception( 'Yaml Validation Failed!' )

def load_text( filename, encoding = 'utf8' ):
    with open( filename, mode = 'r', encoding = encoding ) as fp:
        return fp.read()

def preprocess_yaml( filename ):
    text = load_text( filename )
    yaml_body = create_munch( yaml.safe_load( text ) )
    if len( yaml_body.variables ) == 0:
        return yaml_body

    vars_table = {}
    for x in yaml_body.variables:
        vars_table.update(x)

    template = string.Template( text )
    text = template.substitute( vars_table )
    result = create_munch( yaml.safe_load( text ) )
    return result

def load_yaml( filename ) :
    return preprocess_yaml( filename )

def load_template( filename ):
    return string.Template( load_text( filename ) )

def load_template_table():
    template_table  = create_munch( {} )
    template_schema = load_yaml( TEMPLATE_SCHEMA_FILE )

    for x in glob.glob( os.path.join( TEMPLATE_DIR, '**', '*.yaml' ), recursive = True ):
        template_infos = load_yaml( x )
        validate_yaml( template_infos, template_schema )

        for t in template_infos.templates:
            template_table[ t[ 'name' ] ] = munch.DefaultMunch( '', t )

    return template_table

def generate_code( template, parameter_dict = {} ):
    return template.safe_substitute( parameter_dict )

def generate_fully_classname( class_info, template_info, template_meta ):
    prefix = template_info.prefix
    suffix = template_info.suffix

    if len( template_meta.prefix ) > 0:
        prefix = template_meta.prefix

    if len( template_meta.suffix ) > 0:
        suffix = template_meta.suffix

    return (
        "{name_prefix}{name}{name_suffix}".format(
            name_prefix = prefix,
            name        = class_info.name,
            name_suffix = suffix ),
        prefix,
        suffix
    )

def process_output( code_text, config_data, class_info, template_meta ):

    template_info = config_data.template_table[ template_meta.name ]

    file_name = generate_fully_classname( class_info, template_info, template_meta )[0]

    output_filename = "{name}{suffix}".format(
        name        = file_name,
        suffix      = config_data.suffix
    )

    output_dir = DEFAULT_OUTPUT_DIR
    if len( template_meta.output_dir ) > 0:
        output_dir = template_meta.output_dir
    elif len( config_data.output_dir ) > 0:
        output_dir = config_data.output_dir

    output_path = os.path.join( output_dir, output_filename )

    print_indent( file_name, 2 )
    print_indent( output_path, 3 )

    os.makedirs( output_dir, exist_ok = True )

    with open( output_path, 'w', encoding = 'utf8' ) as fp:
        fp.write( code_text )

def generate_replace_variables( config_data, class_info, template_meta, template_info ):
    names  = generate_fully_classname( class_info, template_info, template_meta )
    values = create_munch( {} )
    values.classname    = names[ 0 ]
    values.prefix       = names[ 1 ]
    values.suffix       = names[ 2 ]
    values.name         = class_info.name
    values.description  = class_info.description

    if len( template_meta.namespace ) > 0 :
        values.namespace = template_meta.namespace
    elif len( class_info.namespace ) > 0 :
        values.namespace = class_info.namespace
    else:
        values.namespace = config_data.namespace

    if type( class_info.user_variables ) is dict:
        values.update( class_info.user_variables )

    return values

def process_template( config_data, class_info, template_meta ):

    if not template_meta.name in config_data.template_table:
        print_indent( "  template: `{name}` is not found.".format( name = template_meta.name ), 1 )
        return

    template_info = config_data.template_table[ template_meta.name ]
    template_text = load_template( os.path.join( TEMPLATE_DIR, template_info.path ) )

    values = generate_replace_variables( config_data, class_info, template_meta, template_info )
    # print_indent( values, 1 )

    code_text = generate_code( template_text, values )
    process_output( code_text, config_data, class_info, template_meta )

def process_class( config_data, class_info ):
    print_indent( class_info.name, 1 )
    for x in class_info.templates:
        process_template( config_data, class_info, create_munch( x ) )

def main( argv ):
    for input_file in argv:
        print_indent( input_file )

        schema_data = load_yaml( CONFIG_SCHEMA_FILE )
        config_data = load_yaml( input_file )
        validate_yaml( config_data, schema_data )

        config_data.template_table = load_template_table()

        for x in config_data.classes:
            process_class( config_data, create_munch( x ) )

def usage():
    print( "python {script} <config file>".format( script=os.path.basename( __file__ ) ) )
    print( "see config file schema : {dir}/schema/config.yaml".format( dir=THIS_SCRIPT_DIR ) )

if __name__ == '__main__':
    if len( sys.argv ) > 1:
        main( sys.argv[ 1: ] )
    else:
        usage()
