#!/usr/bin/env python3
#
# Mini-Kconfig parser
#
# Copyright (c) 2015 Red Hat Inc.
#
# Authors:
#  Paolo Bonzini <pbonzini@redhat.com>
#
# This work is licensed under the terms of the GNU GPL, version 2
# or, at your option, any later version.  See the COPYING file in
# the top-level directory.

import os
import sys
import re
import random

__all__ = [ 'KconfigDataError', 'KconfigParserError',
            'KconfigData', 'KconfigParser' ,
            'defconfig', 'allyesconfig', 'allnoconfig', 'randconfig' ]

def debug_print(*args):
    #print('# ' + (' '.join(str(x) for x in args)))
    pass

# -------------------------------------------
# KconfigData implements the Kconfig semantics.  For now it can only
# detect undefined symbols, i.e. symbols that were referenced in
# assignments or dependencies but were not declared with "config FOO".
#
# Semantic actions are represented by methods called do_*.  The do_var
# method return the semantic value of a variable (which right now is
# just its name).
# -------------------------------------------

class KconfigDataError(Exception):
    def __init__(self, msg):
        self.msg = msg

    def __str__(self):
        return self.msg

allyesconfig = lambda x: True
allnoconfig = lambda x: False
defconfig = lambda x: x
randconfig = lambda x: random.randint(0, 1) == 1

class KconfigData:
    class Expr:
        def __and__(self, rhs):
            return KconfigData.AND(self, rhs)
        def __or__(self, rhs):
            return KconfigData.OR(self, rhs)
        def __invert__(self):
            return KconfigData.NOT(self)

        # Abstract methods
        def add_edges_to(self, var):
            pass
        def evaluate(self):
            assert False

    class AND(Expr):
        def __init__(self, lhs, rhs):
            self.lhs = lhs
            self.rhs = rhs
        def __str__(self):
            return "(%s && %s)" % (self.lhs, self.rhs)

        def add_edges_to(self, var):
            self.lhs.add_edges_to(var)
            self.rhs.add_edges_to(var)
        def evaluate(self):
            return self.lhs.evaluate() and self.rhs.evaluate()

    class OR(Expr):
        def __init__(self, lhs, rhs):
            self.lhs = lhs
            self.rhs = rhs
        def __str__(self):
            return "(%s || %s)" % (self.lhs, self.rhs)

        def add_edges_to(self, var):
            self.lhs.add_edges_to(var)
            self.rhs.add_edges_to(var)
        def evaluate(self):
            return self.lhs.evaluate() or self.rhs.evaluate()

    class NOT(Expr):
        def __init__(self, lhs):
            self.lhs = lhs
        def __str__(self):
            return "!%s" % (self.lhs)

        def add_edges_to(self, var):
            self.lhs.add_edges_to(var)
        def evaluate(self):
            return not self.lhs.evaluate()

    class Var(Expr):
        def __init__(self, name):
            self.name = name
            self.value = None
            self.outgoing = set()
            self.clauses_for_var = list()
        def __str__(self):
            return self.name

        def has_value(self):
            return not (self.value is None)
        def set_value(self, val, clause):
            self.clauses_for_var.append(clause)
            if self.has_value() and self.value != val:
                print("The following clauses were found for " + self.name)
                for i in self.clauses_for_var:
                    print("    " + str(i), file=sys.stderr)
                raise KconfigDataError('contradiction between clauses when setting %s' % self)
            debug_print("=> %s is now %s" % (self.name, val))
            self.value = val

        # depth first search of the dependency graph
        def dfs(self, visited, f):
            if self in visited:
                return
            visited.add(self)
            for v in self.outgoing:
                v.dfs(visited, f)
            f(self)

        def add_edges_to(self, var):
            self.outgoing.add(var)
        def evaluate(self):
            if not self.has_value():
                raise KconfigDataError('cycle found including %s' % self)
            return self.value

    class Clause:
        def __init__(self, dest):
            self.dest = dest
        def priority(self):
            return 0
        def process(self):
            pass

    class AssignmentClause(Clause):
        def __init__(self, dest, value):
            KconfigData.Clause.__init__(self, dest)
            self.value = value
        def __str__(self):
            return "CONFIG_%s=%s" % (self.dest, 'y' if self.value else 'n')

        def process(self):
            self.dest.set_value(self.value, self)

    class DefaultClause(Clause):
        def __init__(self, dest, value, cond=None):
            KconfigData.Clause.__init__(self, dest)
            self.value = value
            self.cond = cond
            if not (self.cond is None):
                self.cond.add_edges_to(self.dest)
        def __str__(self):
            value = 'y' if self.value else 'n'
            if self.cond is None:
                return "config %s default %s" % (self.dest, value)
            else:
                return "config %s default %s if %s" % (self.dest, value, self.cond)

        def priority(self):
            # Defaults are processed just before leaving the variable
            return -1
        def process(self):
            if not self.dest.has_value() and \
                    (self.cond is None or self.cond.evaluate()):
                self.dest.set_value(self.value, self)

    class DependsOnClause(Clause):
        def __init__(self, dest, expr):
            KconfigData.Clause.__init__(self, dest)
            self.expr = expr
            self.expr.add_edges_to(self.dest)
        def __str__(self):
            return "config %s depends on %s" % (self.dest, self.expr)

        def process(self):
            if not self.expr.evaluate():
                self.dest.set_value(False, self)

    class SelectClause(Clause):
        def __init__(self, dest, cond):
            KconfigData.Clause.__init__(self, dest)
            self.cond = cond
            self.cond.add_edges_to(self.dest)
        def __str__(self):
            return "select %s if %s" % (self.dest, self.cond)

        def process(self):
            if self.cond.evaluate():
                self.dest.set_value(True, self)

    def __init__(self, value_mangler=defconfig):
        self.value_mangler = value_mangler
        self.previously_included = []
        self.incl_info = None
        self.defined_vars = set()
        self.referenced_vars = dict()
        self.clauses = list()

    # semantic analysis -------------

    def check_undefined(self):
        undef = False
        for i in self.referenced_vars:
            if not (i in self.defined_vars):
                print("undefined symbol %s" % (i), file=sys.stderr)
                undef = True
        return undef

    def compute_config(self):
        if self.check_undefined():
            raise KconfigDataError("there were undefined symbols")
            return None

        debug_print("Input:")
        for clause in self.clauses:
            debug_print(clause)

        debug_print("\nDependency graph:")
        for i in self.referenced_vars:
            debug_print(i, "->", [str(x) for x in self.referenced_vars[i].outgoing])

        # The reverse of the depth-first order is the topological sort
        dfo = dict()
        visited = set()
        debug_print("\n")
        def visit_fn(var):
            debug_print(var, "has DFS number", len(dfo))
            dfo[var] = len(dfo)

        for name, v in self.referenced_vars.items():
            self.do_default(v, False)
            v.dfs(visited, visit_fn)

        # Put higher DFS numbers and higher priorities first.  This
        # places the clauses in topological order and places defaults
        # after assignments and dependencies.
        self.clauses.sort(key=lambda x: (-dfo[x.dest], -x.priority()))

        debug_print("\nSorted clauses:")
        for clause in self.clauses:
            debug_print(clause)
            clause.process()

        debug_print("")
        values = dict()
        for name, v in self.referenced_vars.items():
            debug_print("Evaluating", name)
            values[name] = v.evaluate()

        return values

    # semantic actions -------------

    def do_declaration(self, var):
        if (var in self.defined_vars):
            raise KconfigDataError('variable "' + var + '" defined twice')

        self.defined_vars.add(var.name)

    # var is a string with the variable's name.
    def do_var(self, var):
        if (var in self.referenced_vars):
            return self.referenced_vars[var]

        var_obj = self.referenced_vars[var] = KconfigData.Var(var)
        return var_obj

    def do_assignment(self, var, val):
        self.clauses.append(KconfigData.AssignmentClause(var, val))

    def do_default(self, var, val, cond=None):
        val = self.value_mangler(val)
        self.clauses.append(KconfigData.DefaultClause(var, val, cond))

    def do_depends_on(self, var, expr):
        self.clauses.append(KconfigData.DependsOnClause(var, expr))

    def do_select(self, var, symbol, cond=None):
        cond = (cond & var) if cond is not None else var
        self.clauses.append(KconfigData.SelectClause(symbol, cond))

    def do_imply(self, var, symbol, cond=None):
        # "config X imply Y [if COND]" is the same as
        # "config Y default y if X [&& COND]"
        cond = (cond & var) if cond is not None else var
        self.do_default(symbol, True, cond)

# -------------------------------------------
# KconfigParser implements a recursive descent parser for (simplified)
# Kconfig syntax.
# -------------------------------------------

# tokens table
TOKENS = {}
TOK_NONE = -1
TOK_LPAREN = 0;   TOKENS[TOK_LPAREN] = '"("';
TOK_RPAREN = 1;   TOKENS[TOK_RPAREN] = '")"';
TOK_EQUAL = 2;    TOKENS[TOK_EQUAL] = '"="';
TOK_AND = 3;      TOKENS[TOK_AND] = '"&&"';
TOK_OR = 4;       TOKENS[TOK_OR] = '"||"';
TOK_NOT = 5;      TOKENS[TOK_NOT] = '"!"';
TOK_DEPENDS = 6;  TOKENS[TOK_DEPENDS] = '"depends"';
TOK_ON = 7;       TOKENS[TOK_ON] = '"on"';
TOK_SELECT = 8;   TOKENS[TOK_SELECT] = '"select"';
TOK_IMPLY = 9;    TOKENS[TOK_IMPLY] = '"imply"';
TOK_CONFIG = 10;  TOKENS[TOK_CONFIG] = '"config"';
TOK_DEFAULT = 11; TOKENS[TOK_DEFAULT] = '"default"';
TOK_Y = 12;       TOKENS[TOK_Y] = '"y"';
TOK_N = 13;       TOKENS[TOK_N] = '"n"';
TOK_SOURCE = 14;  TOKENS[TOK_SOURCE] = '"source"';
TOK_BOOL = 15;    TOKENS[TOK_BOOL] = '"bool"';
TOK_IF = 16;      TOKENS[TOK_IF] = '"if"';
TOK_ID = 17;      TOKENS[TOK_ID] = 'identifier';
TOK_EOF = 18;     TOKENS[TOK_EOF] = 'end of file';

class KconfigParserError(Exception):
    def __init__(self, parser, msg, tok=None):
        self.loc = parser.location()
        tok = tok or parser.tok
        if tok != TOK_NONE:
            location = TOKENS.get(tok, None) or ('"%s"' % tok)
            msg = '%s before %s' % (msg, location)
        self.msg = msg

    def __str__(self):
        return "%s: %s" % (self.loc, self.msg)

class KconfigParser:

    @classmethod
    def parse(self, fp, mode=None):
        data = KconfigData(mode or KconfigParser.defconfig)
        parser = KconfigParser(data)
        parser.parse_file(fp)
        return data

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

    def parse_file(self, fp):
        self.abs_fname = os.path.abspath(fp.name)
        self.fname = fp.name
        self.data.previously_included.append(self.abs_fname)
        self.src = fp.read()
        if self.src == '' or self.src[-1] != '\n':
            self.src += '\n'
        self.cursor = 0
        self.line = 1
        self.line_pos = 0
        self.get_token()
        self.parse_config()

    def do_assignment(self, var, val):
        if not var.startswith("CONFIG_"):
            raise Error('assigned variable should start with CONFIG_')
        var = self.data.do_var(var[7:])
        self.data.do_assignment(var, val)

    # file management -----

    def error_path(self):
        inf = self.data.incl_info
        res = ""
        while inf:
            res = ("In file included from %s:%d:\n" % (inf['file'],
                                                       inf['line'])) + res
            inf = inf['parent']
        return res

    def location(self):
        col = 1
        for ch in self.src[self.line_pos:self.pos]:
            if ch == '\t':
                col += 8 - ((col - 1) % 8)
            else:
                col += 1
        return '%s%s:%d:%d' %(self.error_path(), self.fname, self.line, col)

    def do_include(self, include):
        incl_abs_fname = os.path.join(os.path.dirname(self.abs_fname),
                                      include)
        # catch inclusion cycle
        inf = self.data.incl_info
        while inf:
            if incl_abs_fname == os.path.abspath(inf['file']):
                raise KconfigParserError(self, "Inclusion loop for %s"
                                    % include)
            inf = inf['parent']

        # skip multiple include of the same file
        if incl_abs_fname in self.data.previously_included:
            return
        try:
            fp = open(incl_abs_fname, 'rt', encoding='utf-8')
        except IOError as e:
            raise KconfigParserError(self,
                                '%s: %s' % (e.strerror, include))

        inf = self.data.incl_info
        self.data.incl_info = { 'file': self.fname, 'line': self.line,
                'parent': inf }
        KconfigParser(self.data).parse_file(fp)
        self.data.incl_info = inf

    # recursive descent parser -----

    # y_or_n: Y | N
    def parse_y_or_n(self):
        if self.tok == TOK_Y:
            self.get_token()
            return True
        if self.tok == TOK_N:
            self.get_token()
            return False
        raise KconfigParserError(self, 'Expected "y" or "n"')

    # var: ID
    def parse_var(self):
        if self.tok == TOK_ID:
            val = self.val
            self.get_token()
            return self.data.do_var(val)
        else:
            raise KconfigParserError(self, 'Expected identifier')

    # assignment_var: ID (starting with "CONFIG_")
    def parse_assignment_var(self):
        if self.tok == TOK_ID:
            val = self.val
            if not val.startswith("CONFIG_"):
                raise KconfigParserError(self,
                           'Expected identifier starting with "CONFIG_"', TOK_NONE)
            self.get_token()
            return self.data.do_var(val[7:])
        else:
            raise KconfigParserError(self, 'Expected identifier')

    # assignment: var EQUAL y_or_n
    def parse_assignment(self):
        var = self.parse_assignment_var()
        if self.tok != TOK_EQUAL:
            raise KconfigParserError(self, 'Expected "="')
        self.get_token()
        self.data.do_assignment(var, self.parse_y_or_n())

    # primary: NOT primary
    #       | LPAREN expr RPAREN
    #       | var
    def parse_primary(self):
        if self.tok == TOK_NOT:
            self.get_token()
            val = ~self.parse_primary()
        elif self.tok == TOK_LPAREN:
            self.get_token()
            val = self.parse_expr()
            if self.tok != TOK_RPAREN:
                raise KconfigParserError(self, 'Expected ")"')
            self.get_token()
        elif self.tok == TOK_ID:
            val = self.parse_var()
        else:
            raise KconfigParserError(self, 'Expected "!" or "(" or identifier')
        return val

    # disj: primary (OR primary)*
    def parse_disj(self):
        lhs = self.parse_primary()
        while self.tok == TOK_OR:
            self.get_token()
            lhs = lhs | self.parse_primary()
        return lhs

    # expr: disj (AND disj)*
    def parse_expr(self):
        lhs = self.parse_disj()
        while self.tok == TOK_AND:
            self.get_token()
            lhs = lhs & self.parse_disj()
        return lhs

    # condition: IF expr
    #       | empty
    def parse_condition(self):
        if self.tok == TOK_IF:
            self.get_token()
            return self.parse_expr()
        else:
            return None

    # property: DEFAULT y_or_n condition
    #       | DEPENDS ON expr
    #       | SELECT var condition
    #       | BOOL
    def parse_property(self, var):
        if self.tok == TOK_DEFAULT:
            self.get_token()
            val = self.parse_y_or_n()
            cond = self.parse_condition()
            self.data.do_default(var, val, cond)
        elif self.tok == TOK_DEPENDS:
            self.get_token()
            if self.tok != TOK_ON:
                raise KconfigParserError(self, 'Expected "on"')
            self.get_token()
            self.data.do_depends_on(var, self.parse_expr())
        elif self.tok == TOK_SELECT:
            self.get_token()
            symbol = self.parse_var()
            cond = self.parse_condition()
            self.data.do_select(var, symbol, cond)
        elif self.tok == TOK_IMPLY:
            self.get_token()
            symbol = self.parse_var()
            cond = self.parse_condition()
            self.data.do_imply(var, symbol, cond)
        elif self.tok == TOK_BOOL:
            self.get_token()
        else:
            raise KconfigParserError(self, 'Error in recursive descent?')

    # properties: properties property
    #       | /* empty */
    def parse_properties(self, var):
        had_default = False
        while self.tok == TOK_DEFAULT or self.tok == TOK_DEPENDS or \
              self.tok == TOK_SELECT or self.tok == TOK_BOOL or \
              self.tok == TOK_IMPLY:
            self.parse_property(var)

        # for nicer error message
        if self.tok != TOK_SOURCE and self.tok != TOK_CONFIG and \
           self.tok != TOK_ID and self.tok != TOK_EOF:
            raise KconfigParserError(self, 'expected "source", "config", identifier, '
                    + '"default", "depends on", "imply" or "select"')

    # declaration: config var properties
    def parse_declaration(self):
        if self.tok == TOK_CONFIG:
            self.get_token()
            var = self.parse_var()
            self.data.do_declaration(var)
            self.parse_properties(var)
        else:
            raise KconfigParserError(self, 'Error in recursive descent?')

    # clause: SOURCE
    #       | declaration
    #       | assignment
    def parse_clause(self):
        if self.tok == TOK_SOURCE:
            val = self.val
            self.get_token()
            self.do_include(val)
        elif self.tok == TOK_CONFIG:
            self.parse_declaration()
        elif self.tok == TOK_ID:
            self.parse_assignment()
        else:
            raise KconfigParserError(self, 'expected "source", "config" or identifier')

    # config: clause+ EOF
    def parse_config(self):
        while self.tok != TOK_EOF:
            self.parse_clause()
        return self.data

    # scanner -----

    def get_token(self):
        while True:
            self.tok = self.src[self.cursor]
            self.pos = self.cursor
            self.cursor += 1

            self.val = None
            self.tok = self.scan_token()
            if self.tok is not None:
                return

    def check_keyword(self, rest):
        if not self.src.startswith(rest, self.cursor):
            return False
        length = len(rest)
        if self.src[self.cursor + length].isalnum() or self.src[self.cursor + length] == '_':
            return False
        self.cursor += length
        return True

    def scan_token(self):
        if self.tok == '#':
            self.cursor = self.src.find('\n', self.cursor)
            return None
        elif self.tok == '=':
            return TOK_EQUAL
        elif self.tok == '(':
            return TOK_LPAREN
        elif self.tok == ')':
            return TOK_RPAREN
        elif self.tok == '&' and self.src[self.pos+1] == '&':
            self.cursor += 1
            return TOK_AND
        elif self.tok == '|' and self.src[self.pos+1] == '|':
            self.cursor += 1
            return TOK_OR
        elif self.tok == '!':
            return TOK_NOT
        elif self.tok == 'd' and self.check_keyword("epends"):
            return TOK_DEPENDS
        elif self.tok == 'o' and self.check_keyword("n"):
            return TOK_ON
        elif self.tok == 's' and self.check_keyword("elect"):
            return TOK_SELECT
        elif self.tok == 'i' and self.check_keyword("mply"):
            return TOK_IMPLY
        elif self.tok == 'c' and self.check_keyword("onfig"):
            return TOK_CONFIG
        elif self.tok == 'd' and self.check_keyword("efault"):
            return TOK_DEFAULT
        elif self.tok == 'b' and self.check_keyword("ool"):
            return TOK_BOOL
        elif self.tok == 'i' and self.check_keyword("f"):
            return TOK_IF
        elif self.tok == 'y' and self.check_keyword(""):
            return TOK_Y
        elif self.tok == 'n' and self.check_keyword(""):
            return TOK_N
        elif (self.tok == 's' and self.check_keyword("ource")) or \
              self.tok == 'i' and self.check_keyword("nclude"):
            # source FILENAME
            # include FILENAME
            while self.src[self.cursor].isspace():
                self.cursor += 1
            start = self.cursor
            self.cursor = self.src.find('\n', self.cursor)
            self.val = self.src[start:self.cursor]
            return TOK_SOURCE
        elif self.tok.isalnum():
            # identifier
            while self.src[self.cursor].isalnum() or self.src[self.cursor] == '_':
                self.cursor += 1
            self.val = self.src[self.pos:self.cursor]
            return TOK_ID
        elif self.tok == '\n':
            if self.cursor == len(self.src):
                return TOK_EOF
            self.line += 1
            self.line_pos = self.cursor
        elif not self.tok.isspace():
            raise KconfigParserError(self, 'invalid input')

        return None

if __name__ == '__main__':
    argv = sys.argv
    mode = defconfig
    if len(sys.argv) > 1:
        if argv[1] == '--defconfig':
            del argv[1]
        elif argv[1] == '--randconfig':
            random.seed()
            mode = randconfig
            del argv[1]
        elif argv[1] == '--allyesconfig':
            mode = allyesconfig
            del argv[1]
        elif argv[1] == '--allnoconfig':
            mode = allnoconfig
            del argv[1]

    if len(argv) == 1:
        print ("%s: at least one argument is required" % argv[0], file=sys.stderr)
        sys.exit(1)

    if argv[1].startswith('-'):
        print ("%s: invalid option %s" % (argv[0], argv[1]), file=sys.stderr)
        sys.exit(1)

    data = KconfigData(mode)
    parser = KconfigParser(data)
    external_vars = set()
    for arg in argv[3:]:
        m = re.match(r'^(CONFIG_[A-Z0-9_]+)=([yn]?)$', arg)
        if m is not None:
            name, value = m.groups()
            parser.do_assignment(name, value == 'y')
            external_vars.add(name[7:])
        else:
            fp = open(arg, 'rt', encoding='utf-8')
            parser.parse_file(fp)
            fp.close()

    config = data.compute_config()
    for key in sorted(config.keys()):
        if key not in external_vars and config[key]:
            print ('CONFIG_%s=y' % key)

    deps = open(argv[2], 'wt', encoding='utf-8')
    for fname in data.previously_included:
        print ('%s: %s' % (argv[1], fname), file=deps)
    deps.close()
