"""
File for classes that handles the actual business logic of
the application. Here is the the place where the actual reading,
parsing and validation of the files happens.
"""
import sys
import re
import fileinput
import copy
import linecache
import click
from .utils import (
    get_leading_whitespace,
    BlankFormatter,
    get_indent,
    add_start_end,
    is_one_line_method,
)
from .base import Builder
import os


class MethodBuilder(Builder):
    already_printed_filepaths = []  # list of already printed files

    def extract_and_set_information(self, filename, start, line, length):
        """
        This is a main abstract method tin the builder base
        to add result into details. Used in Method Builder to
        pull the candidates that are subject to docstrings
        Parameters
        ----------
        str filename: The file's name
        int start: Starting line
        str line: Full line text
        int length: The length of the extracted data
        """
        start_line = linecache.getline(filename, start)
        initial_line = line
        start_leading_space = get_leading_whitespace(
            start_line
        )  # Where function started
        method_string = start_line
        if not is_one_line_method(start_line, self.config.get("keywords")):
            method_string = line
            linesBackwards = method_string.count("\n") - 1
            start_leading_space = get_leading_whitespace(
                linecache.getline(filename, start - linesBackwards)
            )
        line_within_scope = True
        lineno = start + 1
        line = linecache.getline(filename, lineno)
        end_of_file = False
        end = None
        while line_within_scope and not end_of_file:
            current_leading_space = get_leading_whitespace(line)
            if len(current_leading_space) <= len(start_leading_space) and line.strip():
                end = lineno - 1
                break
            method_string += line
            lineno = lineno + 1
            line = linecache.getline(filename, int(lineno))
            end_of_file = True if lineno > length else False

        if not end:
            end = length

        linecache.clearcache()
        return MethodInterface(
            plain=method_string,
            name=self._get_name(initial_line),
            start=start,
            end=end,
            filename=filename,
            arguments=self.extract_arguments(initial_line.strip("\n")),
            config=self.config,
            leading_space=get_leading_whitespace(initial_line),
            placeholders=self.placeholders,
        )

    def validate(self, result):
        """
        An abstract validator method that checks if the method is
        still valid and gives the final decision
        Parameters
        ----------
        MethodInterface result: The Method Interface result
        """
        if not result:
            return False
        name = result.name
        if name not in self.config.get(
            "ignore", []
        ) and not self.is_first_line_documented(result):
            if (
                self.filename not in self.already_printed_filepaths
            ):  # Print file of method to document
                click.echo(
                    "\n\nIn file {} :\n".format(
                        click.style(
                            os.path.join(*self.filename.split(os.sep)[-3:]), fg="red"
                        )
                    )
                )
                self.already_printed_filepaths.append(self.filename)
            confirmed = (
                True
                if self.placeholders
                else click.confirm(
                    "Do you want to document method {}?".format(
                        click.style(name, fg="green")
                    )
                )
            )
            if confirmed:
                return True

        return False

    def extract_arguments(self, line):
        """
        Public extract argument method that calls ArgumentDetails
        class to extract args
        Parameters
        ----------
        """
        args = ArgumentDetails(line, self.config.get("arguments", {}))
        args.extract()
        return args.sanitize()

    def is_first_line_documented(self, result):
        """
        A boolean function that determines weather the first line has
        a docstring or not
        Parameters
        ----------
        MethodInterface result: Is a method interface class that could be
        subject to be taking a docstring
        str line: The line of the found method
        """
        returned = False
        for x in range(result.start, result.end):
            line = linecache.getline(result.filename, x)
            if self.config.get("open") in line:
                returned = True
                break
        linecache.clearcache()
        return returned

    def prompts(self):
        """
        Abstract prompt method in builder to execute prompts over candidates
        """
        for method_interface in self._method_interface_gen():
            method_interface.prompt() if method_interface else None

    def apply(self):
        """
        Over here we are looping over the result of the
        chosen methods to document and applying the changes to the
        files as confirmed
        """
        for method_interface in self._method_interface_gen():
            if not method_interface:
                continue
            fileInput = fileinput.input(method_interface.filename, inplace=True)

            for line in fileInput:
                tmpLine = line
                if self._is_method(line) and ":" not in line:
                    openedP = line.count("(")
                    closedP = line.count(")")
                    pos = 1
                    if openedP == closedP:
                        continue
                    else:
                        while openedP != closedP:
                            tmpLine += fileInput.readline()
                            openedP = tmpLine.count("(")
                            closedP = tmpLine.count(")")
                            pos += 1
                        line = tmpLine

                if self._get_name(line) == method_interface.name:
                    if self.config.get("within_scope"):
                        sys.stdout.write(line + method_interface.result + "\n")
                    else:
                        sys.stdout.write(method_interface.result + "\n" + line)
                else:
                    sys.stdout.write(line)

    def _method_interface_gen(self):
        """
        A generator that yields the method interfaces
        """
        if not self.details:
            yield None

        for filename, func_pack in self.details.items():
            for method_interface in func_pack.values():
                yield method_interface

    def _get_name(self, line):
        """
        Grabs the name of the method from the given line
        Parameters
        ----------
        str line: String line that has the method's name
        """
        for keyword in self.config.get("keywords", []):
            clear_defs = re.sub("{} ".format(keyword), "", line.strip())
            name = re.sub(r"\([^)]*\)\:", "", clear_defs).strip()
            if re.search(r"\(([\s\S]*)\)", name):
                try:
                    name = re.match(r"^[^\(]+", name).group()
                except:
                    pass
            if name:
                return name

    def _is_method(self, line):
        """
        A predicate method that checks if a line is a
        method

        Parameters
        ----------
        str line: Text string of a line in a file
        """
        return line.strip().split(" ")[0] in self.config.get("keywords")


class MethodFormatter:

    formatted_string = "{open}{break_after_open}{method_docstring}{break_after_docstring}{empty_line}{argument_format}{break_before_close}{close}"
    fmt = BlankFormatter()

    def format(self):
        """
        Public formatting method that executes a pattern of methods to
        complete the process
        """
        self.pre()
        self.build_docstrings()
        self.build_arguments()
        self.result = self.fmt.format(self.formatted_string, **self.method_format)
        self.add_indentation()
        self.polish()

    def wrap_strings(self, words):
        """
        Compact how many words should be in a line
        Parameters
        ----------
        str words: docstring given
        """
        subs = []
        n = self.config.get("words_per_line")
        for i in range(0, len(words), n):
            subs.append(" ".join(words[i : i + n]))
        return "\n".join(subs)

    def pre(self):
        """
        In the formatter, this method sets up the object that
        will be used in a formatted way,. Also translates configs
        into consumable values
        """
        method_format = copy.deepcopy(self.config)
        method_format["indent"] = (
            get_indent(method_format["indent"]) if method_format["indent"] else "    "
        )
        method_format["indent_content"] = (
            get_indent(method_format["indent"])
            if get_indent(method_format["indent_content"])
            else ""
        )
        method_format["break_after_open"] = (
            "\n" if method_format["break_after_open"] else ""
        )
        method_format["break_after_docstring"] = (
            "\n" if method_format["break_after_docstring"] else ""
        )
        method_format["break_before_close"] = (
            "\n" if method_format["break_before_close"] else ""
        )
        method_format["empty_line"] = "\n"

        argument_format = copy.deepcopy(self.config.get("arguments"))
        argument_format["inline"] = "" if argument_format["inline"] else "\n"

        self.method_format = method_format
        self.argument_format = argument_format

    def build_docstrings(self):
        """
        Mainly adds docstrings of the method after cleaning up text
        into reasonable chunks
        """
        text = self.method_docstring or "Missing Docstring!"
        self.method_format["method_docstring"] = self.wrap_strings(text.split(" "))

    def build_arguments(self):
        """
        Main function for wrapping up argument docstrings
        """
        if not self.arguments:  # if len(self.arguments) == 0
            self.method_format["argument_format"] = ""
            self.method_format["break_before_close"] = ""
            self.method_format["empty_line"] = ""
            return

        config = self.config.get("arguments")
        formatted_args = "{prefix} {type} {name}: {doc}"

        title = self.argument_format.get("title")
        if title:
            underline = "-" * len(title)
            self.argument_format["title"] = (
                "{}\n{}\n".format(title, underline)
                if config.get("underline")
                else "{}\n".format(title)
            )

        result = []

        if self.arguments:  # if len(self.arguments) > 0
            for argument_details in self.arg_docstring:
                argument_details["prefix"] = self.argument_format.get("prefix")
                result.append(
                    self.fmt.format(formatted_args, **argument_details).strip()
                )

        self.argument_format["body"] = "\n".join(result)
        self.method_format["argument_format"] = self.fmt.format(
            "{title}{body}", **self.argument_format
        )

    def add_indentation(self):
        """
        Translates indent params to actual indents
        """
        temp = self.result.split("\n")
        space = self.method_format.get("indent")
        indent_content = self.method_format.get("indent_content")
        if indent_content:
            content = temp[1:-1]
            content = [indent_content + docline for docline in temp][1:-1]
            temp[1:-1] = content
        self.result = "\n".join([space + docline for docline in temp])

    def confirm(self, polished):
        """
        Pop up editor function to finally confirm if the documented
        format is accepted
        Parameters
        ----------
        str polished: complete polished string before popping up
        """
        polished = add_start_end(polished)
        method_split = self.plain.split("\n")
        if self.config.get("within_scope"):
            # Check if method comes in an unusual format
            keywords = self.config.get("keywords")
            firstLine = method_split[0]
            pos = 1
            while not is_one_line_method(firstLine, keywords):
                firstLine += method_split[pos]
                pos += 1
            method_split.insert(pos, polished)
        else:
            method_split.insert(0, polished)

        try:
            result = "\n".join(method_split)
            message = click.edit(
                "## CONFIRM: MODIFY DOCSTRING BETWEEN START AND END LINES ONLY\n\n"
                + result
            )
            message = "\n".join(message.split("\n")[2:])
        except:
            print("Quitting the program in the editor terminates the process. Thanks")
            sys.exit()

        final = []
        start = False
        end = False

        for x in message.split("\n"):
            stripped = x.strip()
            if stripped == "## END":
                end = True
            if start and not end:
                final.append(x)
            if stripped == "## START":
                start = True

        self.result = "\n".join(final)

    def polish(self):
        """
        Editor wrapper to confirm result
        """
        docstring = self.result.split("\n")
        polished = "\n".join([self.leading_space + docline for docline in docstring])
        if self.placeholders:
            self.result = polished
        else:
            self.confirm(polished)


class MethodInterface(MethodFormatter):
    def __init__(
        self,
        plain,
        name,
        start,
        end,
        filename,
        arguments,
        config,
        leading_space,
        placeholders,
    ):
        self.plain = plain
        self.name = name
        self.start = start
        self.end = end
        self.filename = filename
        self.arguments = arguments
        self.method_docstring = ""
        self.arg_docstring = []
        self.config = config
        self.leading_space = leading_space
        self.placeholders = placeholders

    def prompt(self):
        """
        Wrapper method for prompts and calls for prompting args and
        methods then formats them
        """
        self._prompt_docstring()
        self._prompt_args()
        self.format()

    def _prompt_docstring(self):
        """
        Simple prompt for a method's docstring
        """
        if self.placeholders:
            self.method_docstring = "<docstring>"
        else:
            echo_name = click.style(self.name, fg="green")
            self.method_docstring = click.prompt(
                "\n({}) Method docstring ".format(echo_name)
            )

    def _prompt_args(self):
        """
        Wrapper for prompting arguments
        """

        def _echo_arg_style(argument):
            """
            Just a small wrapper for echoing args
            Parameters
            ----------
            str argument: argument name
            """
            return click.style("{}".format(argument), fg="red")

        for arg in self.arguments:
            doc_placeholder = "<arg docstring>"
            arg_doc = (
                click.prompt("\n({}) Argument docstring ".format(_echo_arg_style(arg)))
                if not self.placeholders
                else doc_placeholder
            )
            show_arg_type = self.config.get("arguments", {}).get("add_type", False)
            if show_arg_type:
                arg_placeholder = "<type>"
                arg_type = (
                    click.prompt("({}) Argument type ".format(_echo_arg_style(arg)))
                    if not self.placeholders
                    else arg_placeholder
                )
            self.arg_docstring.append(dict(type=arg_type, doc=arg_doc, name=arg))


class ArgumentDetails(object):
    def __init__(self, line, config):
        self.line = line
        self.config = config
        self.args = []

    def extract(self):
        """
        Retrieves arguments from a line
        """
        try:
            ignore = self.config.get("ignore")
            args = re.search(r"\(([\s\S]*)\)", self.line).group(1).split(",")
            self.args = filter(
                lambda x: x not in ignore,
                filter(
                    None,
                    [arg.replace("\n", "").replace("\t", "").strip() for arg in args],
                ),
            )
        except:
            pass

    def sanitize(self):
        """
        Sanitizes arguments to validate all arguments are correct
        """
        return map(lambda arg: re.findall(r"[a-zA-Z0-9_]+", arg)[0], self.args)