import csv
import re
import string
import sys

from common import FrissError, friss_stderr, Node
import railtype
import utils

dont_parse = {"Drawn", "Coded", "Comments"}
verbose = 0

class FrissDictReader:

    def __init__(self, csv_file):
        with open(csv_file, newline='', encoding="utf-8") as f:
            reader = csv.reader(f)
            self.header = next(reader)
            self.rows = [row for row in reader]

    def __iter__(self):
        for row_i, row in enumerate(self.rows):
            yield RowDict(self.header, row, row_i+2)


class RowDict:

    def __init__(self, header, row, row_index):
        self.header = header
        self.header_pos = {name: i for i, name in enumerate(self.header)}
        self.row = row
        self.row_index = row_index
        self.col_index = 0

    def __getitem__(self, key):
        i = self.header_pos[key]
        self.col_index = i
        return self.row[i]

    def abc(self, num):
        """Integer to uppercase character."""
        chars = string.ascii_uppercase
        if num == 0:
            return chars[0]
        quot = num
        out = ""
        while (quot):
            (quot, rem) = divmod(quot, len(chars))
            out += chars[rem]
        return out

    def items(self):
        for k in self.header:
            yield (k, self[k])

    @property
    def pos(self):
        return "{}{}".format(self.abc(self.col_index), self.row_index)


def blocks():
    return railtype.blocks


def db(csv_file):
    db = railtype.RailtypeDb()
    reader = FrissDictReader(csv_file)
    # Parse lines
    sort = 0
    used_labels = set()
    for drow in reader:
        if not drow["label"]:
            continue
        label = drow["label"]
        if label in used_labels:
            raise FrissError("Multiple definition of label {}".format(label))
        if utils.xor(utils.empty(drow["lc_rural"]),
                     utils.empty(drow["lc_city"])):
            raise FrissError(
                "Must define either both or neither level crossing.")
        used_labels.add(label)
        kwargs = {"sort": sort}
        for key, value in drow.items():
            try:
                parsed = parse_cell(type_=key, value=value)
                if parsed is None:  # Ignore values without parser
                    continue
                if key in railtype.Railtype.prop_strings:
                    if parsed.startswith('_'):
                        kwargs[key] = kwargs[parsed[1:]]
                    elif parsed in db.strings:
                        kwargs[key] = db.strings[parsed]
                    else:
                        str_id = "STR_CSV_{}_{}".format(label, key).upper()
                        db.strings[parsed] = str_id
                        kwargs[key] = str_id
                else:
                    kwargs[key] = parsed
            except FrissError:
                friss_stderr("Couldn't parse cell {}".format(drow.pos), "ERR")
                raise
        db.add_railtype(**kwargs)
        sort += 1
    return db


def parse_cell(type_, value):
    if type_ in railtype.Railtype.prop_strings:
        return parse_string(value)
    if type_ == "extra":
        return {parse_string(line) for line in value.splitlines()}
    if type_ in {"available", "color", "maint_cost", "pur_cost",
                 "signal_group", "speed"}:
        if utils.empty(value):
            if type_ == "color":
                return None
            elif type_ == "signal_group":
                return ""
        p = parse_int(value)
        if type_ == "color" and p > 255:
            raise FrissError("Integer must be between 0-255")
        if type_ == "signal_group" and not (6 >= p >= 1):
            raise FrissError("Signal group must be between 1-6")
        if p > 65525:
            raise FrissError("Integer must be between 0-65525")
        return p
    if type_ == "label":
        return parse_label(value)
    if type_ == "introduces":
        if utils.empty(value):
            return ""
        return [parse_label(v.strip()) for v in value.split(',')]
    if type_ in {"ballast", "catenary", "lc_city", "lc_rural", "signals",
                 "sleepers"}:
        if utils.empty(value):
            return ""
        if type_ in {"lc_city", "lc_rural"}:
            return parse_transition_tree(value, allow_probability=True)
        else:
            return parse_transition_tree(value, allow_probability=False)
    global dont_parse
    if verbose and type_ not in dont_parse:
        msg = ('Ignoring cells with header "{}", because no parser '
               'found, and header not in ignore list.').format(type_)
        friss_stderr(msg, "WARN")
        dont_parse.add(type_)
    return None


def parse_int(str_, min_=None, max_=None):
    try:
        if str_.lower().startswith("0x"):
            val = int(str_[2:], base=16)
        else:
            val = int(str_)
        if val < 0:
            raise ValueError
    except ValueError:
        raise FrissError("Not a valid non-negative integer.")
    return val


def parse_label(str_):
    m = re.match(r"^[A-Z0-9]{4}$", str_.strip())
    if not m:
        raise FrissError("Bad label")
    return m.group()


def parse_string(str_):
    if utils.empty(str_):
        return ""
    return str_.strip()


def parse_transition_tree(str_, allow_probability=False):
    lines = str_.splitlines()
    if not re.match(r"\S+", lines[0]):
        raise FrissError("Bad root node.")
    current_node = root = Node(name=lines[0])
    node_stack = [root]
    prob_stack = [[]]
    depth = 1
    for line in lines[1:]:
        ws = re.match(r"\s*", line)
        new_depth = ws.span()[1] - ws.span()[0]
        if new_depth < 1 or new_depth > depth+1:
            raise FrissError('Bad depth: "{}"'.format(line))
        if new_depth > depth:
            node_stack.append(current_node)
            prob_stack.append([])
        elif new_depth < depth:
            for _ in range(depth - new_depth):
                node_stack.pop()
                prob_stack.pop()
        assert node_stack and prob_stack
        rest = line[ws.span()[1] : ]
        m = re.match(r"^(\S+)\s+(\d+)\s+(\d+)(?:\s+(\d+))?\s*$", rest)
        if not m:
            raise FrissError('Bad transition pattern: "{}"'.format(line))
        if not m.group(4):
            prob = 100
        else:
            prob = parse_int(m.group(4))
        if prob != 100 and not allow_probability:
            raise FrissError("Probability not allowed for this feature.")
        prob_stack[-1].append(prob)
        if prob == 0 or prob % 25 != 0 or sum(prob_stack[-1]) > 100:
            raise FrissError(
                "Total probability must be between 25-100 and multiple of 25.")
        (start_y, end_y) = (parse_int(m.group(2)), parse_int(m.group(3)))
        if start_y > end_y:
            raise FrissError(
                'End year must come after beginning: "{}"'.format(line))
        current_node = Node(name=m.group(1))
        node_stack[-1].add_child(current_node, start_y, end_y, prob)
        depth = new_depth
    return root
