import logging
import os
from pathlib import Path

from ligo.dsl.DefaultParamsLoader import DefaultParamsLoader
from ligo.dsl.definition_parsers.DefinitionParserOutput import DefinitionParserOutput
from ligo.dsl.symbol_table.SymbolTable import SymbolTable
from ligo.dsl.symbol_table.SymbolType import SymbolType
from ligo.environment.EnvironmentSettings import EnvironmentSettings
from ligo.util.Logger import log
from ligo.util.ParameterValidator import ParameterValidator
from ligo.util.PathBuilder import PathBuilder
from ligo.util.ReflectionHandler import ReflectionHandler
from ligo.workflows.instructions.Instruction import Instruction
from scripts.DocumentatonFormat import DocumentationFormat
from scripts.specification_util import write_class_docs


class InstructionParser:

    keyword = "instructions"

    @staticmethod
    def parse(definition_output: DefinitionParserOutput, path):

        specification = definition_output.specification
        symbol_table = definition_output.symbol_table

        if InstructionParser.keyword in specification:
            if len(specification[InstructionParser.keyword].keys()) > 1:
                logging.warning(f"InstructionParser: multiple instructions were listed in the specification (under keys "
                                f"{str(list(specification[InstructionParser.keyword].keys()))[1:-1]}). "
                                "These instructions are independent and results from one instruction are not available to others. "
                                "If this is the intended behavior, please ignore this warning. If the output of one instruction is needed for the "
                                "other, please use separate YAML specifications and separate runs to perform the analysis.")

            for key in specification[InstructionParser.keyword]:
                assert type(specification[InstructionParser.keyword][key]) == dict, f"InstructionParser: instructions are incorrectly defined. Please make sure each instruction is defined under a unique keyword, like this:\n" \
                                                                                    f"instructions:\n" \
                                                                                    f"  my_instruction:\n" \
                                                                                    f"    type: InstructionType\n" \
                                                                                    f"    ... other parameters\n\n" \
                                                                                    f"Found keyword '{key}' containing '{specification[InstructionParser.keyword][key]}', which is a {type(specification[InstructionParser.keyword][key]).__name__}, " \
                                                                                    f"expected a dictionary instead."

                specification[InstructionParser.keyword][key], symbol_table = \
                    InstructionParser.parse_instruction(key, specification[InstructionParser.keyword][key], symbol_table, path)
        else:
            specification[InstructionParser.keyword] = {}

        return symbol_table, specification[InstructionParser.keyword]

    @staticmethod
    @log
    def parse_instruction(key: str, instruction: dict, symbol_table: SymbolTable, path) -> tuple:

        ParameterValidator.assert_keys_present(list(instruction.keys()), ["type"], InstructionParser.__name__, key)
        valid_instructions = [cls[:-6] for cls in ReflectionHandler.discover_classes_by_partial_name("Parser", "dsl/instruction_parsers/")]
        ParameterValidator.assert_in_valid_list(instruction["type"], valid_instructions, "InstructionParser", "type")

        default_params = DefaultParamsLoader.load("instructions/", instruction["type"])
        instruction = {**default_params, **instruction}
        parser = ReflectionHandler.get_class_by_name("{}Parser".format(instruction["type"]), "instruction_parsers/")()
        instruction_object = parser.parse(key, instruction, symbol_table, path)

        symbol_table.add(key, SymbolType.INSTRUCTION, instruction_object)
        return instruction, symbol_table

    @staticmethod
    def generate_docs(path: Path):
        inst_path = PathBuilder.build(path / "instructions")
        instructions = sorted(ReflectionHandler.all_nonabstract_subclasses(Instruction, "Instruction", subdirectory='instructions/'), key=lambda x: x.__name__)

        inst_paths = {}

        for instruction in instructions:
            instruction_name = instruction.__name__[:-11]
            if hasattr(InstructionParser, f"make_{instruction_name.lower()}_docs"):
                fn = getattr(InstructionParser, f"make_{instruction_name.lower()}_docs")
                file_path = fn(inst_path)
            else:
                file_path = InstructionParser.make_docs(instruction, instruction_name, inst_path)

            inst_paths[instruction_name] = file_path

        inst_file_path = inst_path / "instructions.rst"
        with inst_file_path.open('w') as file:
            for key, item in inst_paths.items():
                lines = f"{key}\n---------------------------\n\n.. include:: {os.path.relpath(item, EnvironmentSettings.source_docs_path)}\n\n"
                file.writelines(lines)

    @staticmethod
    def make_docs(instruction, name, path: Path):
        file_path = path / f"{name}.rst"

        with file_path.open("w") as file:
            doc_format = DocumentationFormat(instruction, "", DocumentationFormat.LEVELS[1])
            write_class_docs(doc_format, file)
        return file_path
