#!/usr/bin/python3

import os, re, sys, subprocess

#---------------------------------------------------------------------------
import configparser, argparse
from argparse import Namespace
from configparser import ConfigParser
#---------------------------------------------------------------------------

def make_ast_all_h(namespace: str) -> str:
  decls = incls = ""
  nodes = subprocess.getoutput("find ast -name \\*.h -print | sed -e 's=ast/==g' | sort")

  for node in nodes.split("\n"):
    #print(f"[{node}]")
    if node == "all.h" or node == "visitor_decls.h":
        continue

    decl = incl = node

    #---- Handle declaration
    if namespace == "cdk" and decl == "literal_node.h":
        decl = "template <typename StoredType> class literal_node;"
    else:
        decl = re.sub(r'(([\w\d]|_)+)/', r'', decl)
        decl = re.sub(r'(([\w\d]|_)+?)\.h', r'class \1;', decl)
    decls = decls + f"{decl}\n"

    #---- Handle include
    if namespace == "cdk":
        incl = re.sub(r'(([\w\d]|_)+?)\.h', r'#include <cdk/ast/\1.h>', incl)
    else:
        incl = re.sub(r'^', r'#include "ast/', incl)
        incl = re.sub(r'$', r'"', incl)

    incls = incls + f"{incl}\n"
    
  #---- HACKS!!!
  cdkallinclude = ''
  if namespace != "cdk":
      cdkallinclude = "#include <cdk/ast/all.h>\n"

  #---- File "nodes/all.h" will now be produced.
  return f"""
//-- AUTOMATICALLY GENERATED BY CDK -- DO NOT EDIT --

{cdkallinclude}

#ifdef __NODE_DECLARATIONS_ONLY__

namespace {namespace} {{

{decls}

}} // namespace {namespace}

#else /* !defined(__NODE_DECLARATIONS_ONLY__) */

#ifndef __AUTOMATIC_{namespace}_NODE_ALL_NODES_H__
#define __AUTOMATIC_{namespace}_NODE_ALL_NODES_H__

{incls}

#endif /* !defined(__AUTOMATIC_{namespace}_NODE_ALL_NODES_H__) */
#endif /* !defined(__NODE_DECLARATIONS_ONLY__) */
"""

#---------------------------------------------------------------------------
#---------------------------------------------------------------------------

def make_ast_visitor_decls(namespace: str) -> str:
  decls = incls = ''

  nodes = subprocess.getoutput("find ast -name \\*.h -exec grep -H 'accept' {} \\; | grep -v basic_node | cut -d: -f1 | sed -e 's=^.*/==g' | sort")

  for node in nodes.split("\n"):
      if node == "all.h" or node == "visitor_decls.h":
          continue

      decl = incl = node

      #---- Handle declaration
      decl = re.sub(r'(([\w\d]|_)+?)\.h', r'  virtual void do_\1(@@NAMESPACE::\1 *const node, int lvl) = 0;', decl)
      decls = decls + f"{decl}\n"

      #---- Handle include
      incl = re.sub(r'(([\w\d]|_)+?)\.h', r'  void do_\1(@@NAMESPACE::\1 *const node, int lvl);', incl)
      incls = incls + f"{incl}\n"

  decls = re.sub(r'@@NAMESPACE', namespace, decls)
  incls = re.sub(r'@@NAMESPACE', namespace, incls)

  #---- HACKS!
  cdk_visitor_include = ''
  if namespace != "cdk":
      cdk_visitor_include = "#include <cdk/ast/visitor_decls.h>\n"

  #---- File "nodes/all_visitor_decls.h" will now be produced.
  return f"""
//-- AUTOMATICALLY GENERATED BY CDK -- DO NOT EDIT --
{cdk_visitor_include}
#ifdef __IN_VISITOR_HEADER__
#ifdef __PURE_VIRTUAL_DECLARATIONS_ONLY__
{decls}
#else
{incls}
#endif /* !defined(__PURE_VIRTUAL_DECLARATIONS_ONLY__) */
#endif /* __IN_VISITOR_HEADER__ */
"""

#---------------------------------------------------------------------------
#---------------------------------------------------------------------------

g_config_file_name = os.path.expanduser('~/.config/cdk.ini')

def ensure_config_exists(config_file_name: str) -> ConfigParser:
    confdir = os.path.expanduser("~/.config")
    if not os.path.isdir(confdir):
        os.makedirs(confdir)

    config = configparser.ConfigParser()
    if not os.path.isfile(config_file_name):
        config['defaults'] = {
            'cdk': os.path.expanduser('~/compiladores/root'),
            'system': 'no'
        }
        config['cvs'] = {'CVSROOT':'none'}
        with open(config_file_name, 'w') as configfile:
            config.write(configfile)
    else:
        with open(config_file_name) as configfile:
            config.read_file(configfile)
    return config

def config_load(filename: str) -> ConfigParser:
    return ensure_config_exists(filename)

def config_save(filename: str) -> None:
    config = ensure_config_exists(filename)
    with open(filename, 'w') as configfile:
          config.write(configfile)

#---------------------------------------------------------------------------

def cdk_do_ast_cdk(config: ConfigParser, args: Namespace) -> None:
    where_am_i = os.path.dirname(os.path.realpath(__file__))
    for decl in args.decls:
        if decl == 'ast':
            print(make_ast_all_h("cdk"))
        else:
            print(make_ast_visitor_decls("cdk"))

def cdk_do_ast_language(config: ConfigParser, args: Namespace) -> None:
    where_am_i = os.path.dirname(os.path.realpath(__file__))
    for decl in args.decls:
        if decl == 'ast':
            print(make_ast_all_h(args.language))
        else:
            print(make_ast_visitor_decls(args.language))

def cdk_do_target(config: ConfigParser, args: Namespace) -> None:
    where_am_i = os.path.dirname(os.path.realpath(__file__))
    for target in args.target:
        print(cdk_mk_visitor_skel(config, args.language, target))

#---------------------------------------------------------------------------

def cdk_mk_visitor_skel(config: ConfigParser, language: str, visitor: str) -> str:
    namespace = language

    if not os.path.isfile(f"targets/{visitor}.cpp"):
      print(f"File 'targets/{visitor}.cpp' does not exist. Exiting.")
      exit(1)

    cdk_location = '' # initial value
    #DEBUG print(config['defaults']['cdk'] + '/usr/include/cdk/ast')
    if os.path.isdir(config['defaults']['cdk'] + '/usr/include/cdk/ast'):
        cdk_location = config['defaults']['cdk'] + '/usr/include/cdk/ast'
    else:
        cdk_location = '/usr/include/cdk/ast'

    code =        mk_code(visitor, language, 'cdk', cdk_location)
    code = code + mk_code(visitor, language, namespace, 'ast')
    code = re.sub(r'^\s+|\s+$', r'', code)

    if code == "":
        print(f"File 'targets/{visitor}.cpp' contains all methods. Exiting.\n", file=sys.stderr)
        return ""

    return f"""
//---- AUTOMATICALLY GENERATED BY CDK
//---- Edit these methods to fit your specs and add them to "targets/{visitor}.cpp"

{code}
"""

#---------------------------------------------------------------------------
#---------------------------------------------------------------------------

#---- check whether the visitor contains an implementation for the given node

def implementation_missing(visitor: str, node: str) -> bool:
  result = subprocess.getoutput(f"grep ::do_{node} targets/{visitor}.cpp")
  #DEBUG print(f"### FOUND ### [{visitor}][{node}] = {result} ###")
  return result == ""

#---- generate missing visitor code for nodes in the given AST directory
def mk_code(visitor: str, language: str, namespace: str, directory: str) -> str:
  code = ''
  #DEBUG print(f"{visitor}-{language}-{namespace}-{dir}")
  nodes = subprocess.getoutput(f"find {directory}" + " -name \\*.h -exec grep -H 'accept' {} \\; | grep -v basic_node | cut -d: -f1 | sed -e 's=^.*/==g' | sed -e 's=\\.h==g' | sort")

  for node in nodes.split("\n"):
    if node == "all" or node == "visitor_decls":
        continue

    incl = node
    if implementation_missing(visitor, incl):
        incl = re.sub(r'([\w_][\w\d_]+)', r'void @@LANGUAGE@@::@@VISITOR@@::do_\1(@@NAMESPACE@@::\1 *const node, int lvl) {\n  /* EMPTY */\n}', incl)
        incl = re.sub(r'@@LANGUAGE@@', language, incl)
        incl = re.sub(r'@@VISITOR@@', visitor, incl)
        incl = re.sub(r'@@NAMESPACE@@', namespace, incl)
        code = code + f"{incl}\n"
  
  return code

#---------------------------------------------------------------------------
#---------------------------------------------------------------------------
#---------------------------------------------------------------------------

g_config = config_load(g_config_file_name)
#DEBUG print("G_CONFIG" + str(g_config.sections()))

#---------------------------------------------------------------------------

g_parser = argparse.ArgumentParser(prog="cdk", description='CDK command line interface.')

subparsers = g_parser.add_subparsers(help='CDK sub-commands.', dest="subcommand")

# create the parser for the "ast" command
parser_ast = subparsers.add_parser('ast', help='AST commands.')

required_a = parser_ast.add_argument_group('required arguments')
required_a.add_argument('--decls', action='append', choices=['ast', 'target'], help='generate declarations (multiple choice).', required=True)
group_ast = required_a.add_mutually_exclusive_group(required=True)
group_ast.add_argument('--cdk',      dest='in_cdk',   action='store_true', help="generate code for the CDK")
group_ast.add_argument('--language', dest='language', action='store',      help="generate code for given language")

# create the parser for the "target" command
parser_t = subparsers.add_parser('target', help='Generation target (visitor) commands.')
required_t = parser_t.add_argument_group('required arguments')
required_t.add_argument('--language', type=str, dest='language', action="store", help='Check LANGUAGE target', required=True)
required_t.add_argument('--target', type=str, dest='target', action="append", help='Which TARGET(s) to check (may be repeated)', required=True)

#---------------------------------------------------------------------------
### DO THINGS

g_args = g_parser.parse_args()

if g_args.subcommand == 'ast':
    if g_args.in_cdk:
        cdk_do_ast_cdk(g_config, g_args)
    else:
        cdk_do_ast_language(g_config, g_args)
elif g_args.subcommand == 'target':
    cdk_do_target(g_config, g_args)
else:
    g_parser.print_help()

