import os
import re
import hashlib
import json

import colors as C
import toolchain_info

class Block(object):
    @staticmethod
    def get_blocks_from_rst(rst_file, input_text):
        lang_re = re.compile(r"\s*.. code::\s*(\w+)?\s*")
        project_re = re.compile(r"\s*.. code::.*project=(\S+)?")
        main_re = re.compile(r"\s*.. code::.*main=(\S+)?")
        manual_chop_re = re.compile(r"\s*.. code::.*manual_chop?")
        button_re = re.compile(r"\s+(\S+)_button")
        code_config_re = re.compile(r":code-config:`(.*)?`")
        classes_re = re.compile(r"\s*:class:\s*(.+)")
        switches_re = re.compile(r"\s*.. code::.*switches=(\S+)?")
        compiler_switches_re = re.compile(r"Compiler[(](\S+)?[)]")
        gnat_version_re=re.compile(r"\s*.. code::.*gnat=(\S+)?")
        gnatprove_version_re=re.compile(r"\s*.. code::.*gnatprove=(\S+)?")
        gprbuild_version_re=re.compile(r"\s*.. code::.*gprbuild=(\S+)?")

        blocks = []
        lines = input_text.splitlines()

        def first_nonws(line):
            for i, c in enumerate(line):
                if not c.isspace():
                    return i
            return 0

        indents = map(first_nonws, lines)

        classes = []
        compiler_switches = []
        buttons = []
        cb_start = -1
        cb_indent = -1
        lang = ""
        project = None
        main_file = None
        manual_chop = None
        gnat_version = None
        gnatprove_version = None
        gprbuild_version = None
        last_line_number = -1

        def is_empty(line):
            return (not line) or line.isspace()

        def reset_block_info():
            # Reset information for next block
            nonlocal classes, cb_start, cb_indent, lang

            classes, cb_start, cb_indent, lang = [], -1, -1, ""

        def process_block(i, line, indent):
            nonlocal classes, cb_start, cb_indent, lang

            if cb_indent == -1 and not is_empty(line):
                cb_indent = indent

            if indent < cb_indent and not is_empty(line):
                text = ("\n".join(l[cb_indent:] for l in lines[cb_start:i]))
                text = text[1:]     # Remove first newline

                blocks.append(CodeBlock(
                    rst_file,
                    cb_start,
                    i,
                    text,
                    lang,
                    project,
                    main_file,
                    gnat_version,
                    gnatprove_version,
                    gprbuild_version,
                    compiler_switches,
                    classes,
                    manual_chop,
                    buttons
                ))

                reset_block_info()

            m = classes_re.match(line)

            if m:
                classes = [str.strip(l) for l in m.groups()[0].split(",")]
                cb_start = i + 1

        def start_code_block(i, line, indent):
            nonlocal cb_start, lang, project, main_file, manual_chop, \
                     buttons, compiler_switches, \
                     gnat_version, gnatprove_version, gprbuild_version

            cb_start, lang = (
                i + 1,
                lang_re.match(line).groups()[0]
            )
            project = project_re.match(line)
            if project is not None:
                project = project.groups()[0]

            main_file = main_re.match(line)
            if main_file is not None:
                # Retrieve actual main filename
                main_file = main_file.groups()[0]
            if lang == "c":
                manual_chop = True
            else:
                manual_chop = (manual_chop_re.match(line) is not None)
            buttons = button_re.findall(line)

            project_gnat_version = gnat_version_re.match(line)
            project_gnatprove_version = gnatprove_version_re.match(line)
            project_gprbuild_version = gprbuild_version_re.match(line)

            if project_gnat_version is not None:
                gnat_version = ["selected", project_gnat_version.groups()[0]]
            else:
                gnat_version = ["default", toolchain_info.get_toolchain_default_version('gnat')]
            if project_gnatprove_version is not None:
                gnatprove_version = ["selected", project_gnatprove_version.groups()[0]]
            else:
                gnatprove_version = ["default", toolchain_info.get_toolchain_default_version('gnatprove')]
            if project_gprbuild_version is not None:
                gprbuild_version = ["selected", project_gprbuild_version.groups()[0]]
            else:
                gprbuild_version = ["default", toolchain_info.get_toolchain_default_version('gprbuild')]

            all_switches = switches_re.match(line)

            compiler_switches = []
            if all_switches is not None:
                all_switches = all_switches.groups()[0]
                compiler_switches = compiler_switches_re.match(all_switches)
                if compiler_switches is not None:
                    compiler_switches = [str.strip(l)
                        for l in compiler_switches.groups()[0].split(",")]

            # Add default switches
            default_switches = {
                "Builder": [],
                "Compiler": [
                    "-gnata",
                ],
            }

            for category in default_switches:
                for sw in default_switches[category]:
                    if sw not in compiler_switches:
                        compiler_switches.append(sw)


        def start_config_block(i, line, indent):
            blocks.append(ConfigBlock(rst_file,
                **dict(
                    kv.split('=')
                    for kv in code_config_re.findall(line)[0].split(";"))
            ))


        reset_block_info()

        for i, (line, indent) in enumerate(zip(lines, indents)):
            last_line_number = i

            if cb_start != -1:
                process_block(i, line, indent)
            else:
                if line[indent:].startswith(".. code::"):
                    start_code_block(i, line, indent)
                elif line[indent:].startswith(":code-config:"):
                    start_config_block(i, line, indent)

        if cb_start != -1:
            print("{}: code block (start: {}, project: {}) doesn't have explanatory section!".format(
                    C.col("WARNING", C.Colors.YELLOW), cb_start, project))
            process_block(last_line_number + 1, "END", 0)

            # Error: unable to process last code block
            if cb_start != -1:
                print("{}: code block (start: {}, project: {}) hasn't been successfully processed!".format(
                    C.col("ERROR", C.Colors.RED), cb_start, project))
                exit(1)

        return blocks

    def to_json_file(self, json_filename=None):
        block_info = vars(self)

        if json_filename is None:
            json_filename = "block_info.json"
        with open(json_filename, u'w') as f:
            json.dump(block_info, f, indent=4)

class CodeBlock(Block):
    @staticmethod
    def from_json_file(json_filename=None):

        if json_filename is None:
            json_filename = "block_info.json"

        if os.path.isfile(json_filename):
            with open(json_filename, u'r') as f:
                block_info_json = json.load(f)
                return CodeBlock(**block_info_json)

        return None

    def __init__(self, rst_file, line_start, line_end, text, language, project,
                 main_file,
                 gnat_version,gnatprove_version,gprbuild_version,
                 compiler_switches, classes, manual_chop, buttons,
                 active=None,no_check=None,syntax_only=None,run_it=None,
                 compile_it=None,prove_it=None,
                 source_files=None,
                 project_filename=None,spark_project_filename=None,
                 project_main_file=None,
                 text_hash=None,text_hash_short=None):
        self.rst_file = rst_file
        self.line_start = line_start
        self.line_end = line_end
        self.text = text
        self.language = language
        self.project = project
        self.main_file = main_file
        self.gnat_version = gnat_version
        self.gnatprove_version = gnatprove_version
        self.gprbuild_version = gprbuild_version
        self.compiler_switches = compiler_switches
        self.classes = classes
        self.manual_chop = manual_chop
        self.buttons = buttons
        self.active = active if active is not None else True

        self.no_check = no_check if no_check is not None else \
            any(sphinx_class in ["ada-nocheck", "c-nocheck"]
                for sphinx_class in self.classes)

        self.syntax_only = syntax_only if syntax_only is not None else \
            'ada-syntax-only' in self.classes

        self.run_it = run_it if run_it is not None else \
            (('ada-run' in self.classes
              or 'ada-run-expect-failure' in self.classes
              or 'run' in self.buttons)
              and not 'ada-norun' in self.classes)
        self.compile_it = compile_it if compile_it is not None else \
            self.run_it or \
            (('ada-compile' in self.classes and self.language == 'ada')
             or ('c-compile' in self.classes and self.language == 'c')
             or 'compile' in self.buttons)

        prove_buttons = ["prove", "prove_flow", "prove_flow_report_all",
                         "prove_report_all"]
        prove_classes = ["ada-prove", "ada-prove-flow", "ada-prove-flow-report-all",
                         "ada-prove-report-all"]

        self.prove_it = prove_it if prove_it is not None else \
            (any(b in prove_classes for b in self.classes)
             or any(b in prove_buttons for b in self.buttons))

        self.source_files = source_files if source_files is not None else \
            list()
        self.project_filename = project_filename
        self.spark_project_filename = spark_project_filename
        self.project_main_file = project_main_file

        # Hash of source-code
        str_text = str(self.text).encode("utf-8")
        self.text_hash: str = text_hash if text_hash is not None else \
            hashlib.sha512(str_text).hexdigest()
        self.text_hash_short: str = text_hash_short if text_hash_short is not None else \
            hashlib.md5(str_text).hexdigest()


class ConfigBlock(Block):
    def __init__(self, **opts):
        self._opts = opts
        for k, v in opts.items():
            setattr(self, k, False if v == "False" else True)

    def update(self, other_config):
        self.__init__(**other_config._opts)
