import numpy as np
import math, csv

class Node:
    def __init__(self, attr):
        self.attribute, self.children, self.answer = attr, [], ""

def read_data(filename):
    with open(filename, 'r') as f:
        data = list(csv.reader(f))
    return data[0], data[1:]

def subtables(data, col, delete):
    values = np.unique(data[:, col])
    split = {}
    for val in values:
        subset = data[data[:, col] == val]
        if delete: subset = np.delete(subset, col, 1)
        split[val] = subset
    return values, split

def entropy(S):
    vals, counts = np.unique(S, return_counts=True)
    probs = counts / S.size
    return -np.sum(probs * np.log2(probs))

def gain_ratio(data, col):
    total_entropy = entropy(data[:, -1])
    values, split = subtables(data, col, False)
    total = len(data)
    split_info, weighted_entropy = 0, 0
    for v in values:
        subset = split[v]
        ratio = len(subset) / total
        weighted_entropy += ratio * entropy(subset[:, -1])
        split_info += ratio * math.log(ratio, 2)
    return (total_entropy - weighted_entropy) / -split_info if split_info != 0 else 0

def create_node(data, metadata):
    if len(np.unique(data[:, -1])) == 1:
        leaf = Node("")
        leaf.answer = data[0, -1]
        return leaf
    gains = [gain_ratio(data, i) for i in range(data.shape[1] - 1)]
    best = int(np.argmax(gains))
    node = Node(metadata[best])
    values, split = subtables(data, best, True)
    new_meta = np.delete(metadata, best)
    for v in values:
        node.children.append((v, create_node(split[v], new_meta)))
    return node

def print_tree(node, level=0):
    indent = " " * level
    if node.answer:
        print(f"{indent}{node.answer}")
    else:
        print(f"{indent}{node.attribute}")
        for val, child in node.children:
            print(f"{indent}({val}) ->", end=" ")
            print_tree(child, level + 2)
