from __future__ import print_function
import sys
import os
import json
import argparse
from multiprocessing import freeze_support
from .langserver import LangServer
from .jsonrpc import JSONRPC2Connection, ReadWriter, path_from_uri
from .parse_fortran import fortran_file, process_file
from ._version import __version__


def error_exit(error_str):
    print("ERROR: {0}".format(error_str))
    sys.exit(-1)


def main():
    #
    freeze_support()
    parser = argparse.ArgumentParser()
    parser.description = "FORTRAN Language Server ({0})".format(__version__)
    parser.add_argument(
        "--version", action="store_true", help="Print server version number and exit"
    )
    parser.add_argument(
        "--nthreads",
        type=int,
        default=4,
        help="Number of threads to use during workspace initialization (default: 4)",
    )
    parser.add_argument(
        "--notify_init",
        action="store_true",
        help="Send notification message when workspace initialization is complete",
    )
    parser.add_argument(
        "--symbol_skip_mem",
        action="store_true",
        help="Do not include type members in document symbol results",
    )
    parser.add_argument(
        "--incremental_sync",
        "--incrmental_sync",
        action="store_true",
        help="Use incremental document synchronization (beta)",
    )
    parser.add_argument(
        "--autocomplete_no_prefix",
        action="store_true",
        help="Do not filter autocomplete results by variable prefix",
    )
    parser.add_argument(
        "--autocomplete_no_snippets",
        action="store_true",
        help="Do not use snippets with place holders in autocomplete results",
    )
    parser.add_argument(
        "--autocomplete_name_only",
        action="store_true",
        help="Complete only the name of procedures and not the parameters",
    )
    parser.add_argument(
        "--lowercase_intrinsics",
        action="store_true",
        help="Use lowercase for intrinsics and keywords in autocomplete requests",
    )
    parser.add_argument(
        "--use_signature_help",
        action="store_true",
        help="Use signature help instead of subroutine/function snippets",
    )
    parser.add_argument(
        "--variable_hover",
        action="store_true",
        help=(
            "Show hover information for variables (default: subroutines/functions only)"
        ),
    )
    parser.add_argument(
        "--hover_signature",
        action="store_true",
        help=(
            "Show signature information in hover for argument (also enables"
            " '--variable_hover')"
        ),
    )
    parser.add_argument(
        "--hover_language",
        type=str,
        default=None,
        help=(
            "Language used for responses to hover requests (for editor syntax"
            " highlighting)"
        ),
    )
    parser.add_argument(
        "--preserve_keyword_order",
        action="store_true",
        help=(
            "Display variable keywords information in original order (default: sort to"
            " consistent ordering)"
        ),
    )
    parser.add_argument(
        "--enable_code_actions",
        action="store_true",
        help="Enable experimental code actions (default: false)",
    )
    parser.add_argument(
        "--max_line_length",
        type=int,
        default=-1,
        help="Maximum line length (default: none)",
    )
    parser.add_argument(
        "--max_comment_line_length",
        type=int,
        default=-1,
        help="Maximum comment line length (default: none)",
    )
    parser.add_argument(
        "--disable_diagnostics", action="store_true", help="Disable diagnostics"
    )
    parser.add_argument(
        "--debug_log",
        action="store_true",
        help="Generate debug log in project root folder",
    )
    group = parser.add_argument_group("DEBUG", "Options for debugging language server")
    group.add_argument(
        "--debug_parser",
        action="store_true",
        help="Test source code parser on specified file",
    )
    group.add_argument(
        "--debug_diagnostics",
        action="store_true",
        help="Test diagnostic notifications for specified file",
    )
    group.add_argument(
        "--debug_symbols",
        action="store_true",
        help="Test symbol request for specified file",
    )
    group.add_argument(
        "--debug_workspace_symbols", type=str, help="Test workspace/symbol request"
    )
    group.add_argument(
        "--debug_completion",
        action="store_true",
        help="Test completion request for specified file and position",
    )
    group.add_argument(
        "--debug_signature",
        action="store_true",
        help="Test signatureHelp request for specified file and position",
    )
    group.add_argument(
        "--debug_definition",
        action="store_true",
        help="Test definition request for specified file and position",
    )
    group.add_argument(
        "--debug_hover",
        action="store_true",
        help="Test hover request for specified file and position",
    )
    group.add_argument(
        "--debug_implementation",
        action="store_true",
        help="Test implementation request for specified file and position",
    )
    group.add_argument(
        "--debug_references",
        action="store_true",
        help="Test references request for specified file and position",
    )
    group.add_argument(
        "--debug_rename",
        type=str,
        help="Test rename request for specified file and position",
    )
    group.add_argument(
        "--debug_actions",
        action="store_true",
        help="Test codeAction request for specified file and position",
    )
    group.add_argument(
        "--debug_filepath", type=str, help="File path for language server tests"
    )
    group.add_argument(
        "--debug_rootpath", type=str, help="Root path for language server tests"
    )
    group.add_argument(
        "--debug_line",
        type=int,
        help="Line position for language server tests (1-indexed)",
    )
    group.add_argument(
        "--debug_char",
        type=int,
        help="Character position for language server tests (1-indexed)",
    )
    group.add_argument(
        "--debug_full_result",
        action="store_true",
        help="Print full result object instead of condensed version",
    )
    args = parser.parse_args()
    if args.version:
        print("{0}".format(__version__))
        sys.exit(0)
    debug_server = (
        args.debug_diagnostics
        or args.debug_symbols
        or args.debug_completion
        or args.debug_signature
        or args.debug_definition
        or args.debug_hover
        or args.debug_implementation
        or args.debug_references
        or (args.debug_rename is not None)
        or args.debug_actions
        or (args.debug_rootpath is not None)
        or (args.debug_workspace_symbols is not None)
    )
    #
    settings = {
        "nthreads": args.nthreads,
        "notify_init": args.notify_init,
        "symbol_include_mem": (not args.symbol_skip_mem),
        "sync_type": 2 if args.incremental_sync else 1,
        "autocomplete_no_prefix": args.autocomplete_no_prefix,
        "autocomplete_no_snippets": args.autocomplete_no_snippets,
        "autocomplete_name_only": args.autocomplete_name_only,
        "lowercase_intrinsics": args.lowercase_intrinsics,
        "use_signature_help": args.use_signature_help,
        "variable_hover": (args.variable_hover or args.hover_signature),
        "hover_signature": args.hover_signature,
        "sort_keywords": (not args.preserve_keyword_order),
        "enable_code_actions": (args.enable_code_actions or args.debug_actions),
        "max_line_length": args.max_line_length,
        "max_comment_line_length": args.max_comment_line_length,
        "disable_diagnostics": args.disable_diagnostics,
    }
    if args.hover_language is not None:
        settings["hover_language"] = args.hover_language
    #
    if args.debug_parser:
        if args.debug_filepath is None:
            error_exit("'debug_filepath' not specified for parsing test")
        file_exists = os.path.isfile(args.debug_filepath)
        if file_exists is False:
            error_exit("Specified 'debug_filepath' does not exist")
        # Get preprocessor definitions from config file
        pp_suffixes = None
        pp_defs = {}
        include_dirs = []
        if args.debug_rootpath:
            config_path = os.path.join(args.debug_rootpath, ".fortls")
            config_exists = os.path.isfile(config_path)
            if config_exists:
                try:
                    with open(config_path, "r") as fhandle:
                        config_dict = json.load(fhandle)
                        pp_suffixes = config_dict.get("pp_suffixes", None)
                        pp_defs = config_dict.get("pp_defs", {})
                        include_dirs = config_dict.get("include_dirs", [])
                        if isinstance(pp_defs, list):
                            pp_defs = {key: "" for key in pp_defs}
                except:
                    print("Error while parsing '.fortls' settings file")
                # Make relative include paths absolute
                for (i, include_dir) in enumerate(include_dirs):
                    if not os.path.isabs(include_dir):
                        include_dirs[i] = os.path.abspath(
                            os.path.join(args.debug_rootpath, include_dir)
                        )
        #
        print("\nTesting parser")
        print('  File = "{0}"'.format(args.debug_filepath))
        file_obj = fortran_file(args.debug_filepath, pp_suffixes)
        err_str = file_obj.load_from_disk()
        if err_str is not None:
            error_exit("Reading file failed: {0}".format(err_str))
        print("  Detected format: {0}".format("fixed" if file_obj.fixed else "free"))
        print("\n=========\nParser Output\n=========\n")
        _, file_ext = os.path.splitext(os.path.basename(args.debug_filepath))
        preproc_file = False
        if pp_suffixes is not None:
            preproc_file = file_ext in pp_suffixes
        else:
            preproc_file = file_ext == file_ext.upper()
        if preproc_file:
            file_ast = process_file(
                file_obj, True, debug=True, pp_defs=pp_defs, include_dirs=include_dirs
            )
        else:
            file_ast = process_file(file_obj, True, debug=True)
        print("\n=========\nObject Tree\n=========\n")
        for obj in file_ast.get_scopes():
            print("{0}: {1}".format(obj.get_type(), obj.FQSN))
            print_children(obj)
        print("\n=========\nExportable Objects\n=========\n")
        for _, obj in file_ast.global_dict.items():
            print("{0}: {1}".format(obj.get_type(), obj.FQSN))
    #
    elif debug_server:
        prb, pwb = os.pipe()
        tmpin = os.fdopen(prb, "rb")
        tmpout = os.fdopen(pwb, "wb")
        s = LangServer(
            conn=JSONRPC2Connection(ReadWriter(tmpin, tmpout)),
            debug_log=args.debug_log,
            settings=settings,
        )
        #
        if args.debug_rootpath:
            dir_exists = os.path.isdir(args.debug_rootpath)
            if dir_exists is False:
                error_exit(
                    "Specified 'debug_rootpath' does not exist or is not a directory"
                )
            print('\nTesting "initialize" request:')
            print('  Root = "{0}"'.format(args.debug_rootpath))
            s.serve_initialize({"params": {"rootPath": args.debug_rootpath}})
            if len(s.post_messages) == 0:
                print("  Succesful!")
            else:
                print("  Succesful with errors:")
                for message in s.post_messages:
                    print("    {0}".format(message[1]))
            # Print module directories
            print("\n  Source directories:")
            for source_dir in s.source_dirs:
                print("    {0}".format(source_dir))
        #
        if args.debug_diagnostics:
            print('\nTesting "textDocument/publishDiagnostics" notification:')
            check_request_params(args, loc_needed=False)
            s.serve_onSave({"params": {"textDocument": {"uri": args.debug_filepath}}})
            diag_results, _ = s.get_diagnostics(args.debug_filepath)
            if diag_results is not None:
                if args.debug_full_result:
                    print(json.dumps(diag_results, indent=2))
                else:
                    sev_map = ["ERROR", "WARNING", "INFO"]
                    if len(diag_results) == 0:
                        print("\nNo errors or warnings")
                    else:
                        print("\nReported errors or warnings:")
                    for diag in diag_results:
                        sline = diag["range"]["start"]["line"]
                        message = diag["message"]
                        sev = sev_map[diag["severity"] - 1]
                        print('  {0:5d}:{1}  "{2}"'.format(sline, sev, message))
        #
        if args.debug_symbols:
            print('\nTesting "textDocument/documentSymbol" request:')
            check_request_params(args, loc_needed=False)
            s.serve_onSave({"params": {"textDocument": {"uri": args.debug_filepath}}})
            symbol_results = s.serve_document_symbols(
                {"params": {"textDocument": {"uri": args.debug_filepath}}}
            )
            if args.debug_full_result:
                print(json.dumps(symbol_results, indent=2))
            else:
                for symbol in symbol_results:
                    sline = symbol["location"]["range"]["start"]["line"]
                    if "containerName" in symbol:
                        parent = symbol["containerName"]
                    else:
                        parent = "null"
                    print(
                        "  line {2:5d}  symbol -> {1:3d}:{0:30} parent = {3}".format(
                            symbol["name"], symbol["kind"], sline, parent
                        )
                    )
        #
        if args.debug_workspace_symbols is not None:
            print('\nTesting "workspace/symbol" request:')
            if args.debug_rootpath is None:
                error_exit("'debug_rootpath' not specified for debug request")
            symbol_results = s.serve_workspace_symbol(
                {"params": {"query": args.debug_workspace_symbols}}
            )
            if args.debug_full_result:
                print(json.dumps(symbol_results, indent=2))
            else:
                for symbol in symbol_results:
                    path = path_from_uri(symbol["location"]["uri"])
                    sline = symbol["location"]["range"]["start"]["line"]
                    if "containerName" in symbol:
                        parent = symbol["containerName"]
                    else:
                        parent = "null"
                    print(
                        "  {2}::{3:d}  symbol -> {1:3d}:{0:30} parent = {4}".format(
                            symbol["name"],
                            symbol["kind"],
                            os.path.relpath(path, args.debug_rootpath),
                            sline,
                            parent,
                        )
                    )
        #
        if args.debug_completion:
            print('\nTesting "textDocument/completion" request:')
            check_request_params(args)
            s.serve_onSave({"params": {"textDocument": {"uri": args.debug_filepath}}})
            completion_results = s.serve_autocomplete(
                {
                    "params": {
                        "textDocument": {"uri": args.debug_filepath},
                        "position": {
                            "line": args.debug_line - 1,
                            "character": args.debug_char - 1,
                        },
                    }
                }
            )
            if completion_results is None:
                print("  No results!")
            else:
                print("  Results:")
                if args.debug_full_result:
                    print(json.dumps(completion_results, indent=2))
                else:
                    for obj in completion_results:
                        print(
                            "    {0}: {1} -> {2}".format(
                                obj["kind"], obj["label"], obj["detail"]
                            )
                        )
        #
        if args.debug_signature:
            print('\nTesting "textDocument/signatureHelp" request:')
            check_request_params(args)
            s.serve_onSave({"params": {"textDocument": {"uri": args.debug_filepath}}})
            signature_results = s.serve_signature(
                {
                    "params": {
                        "textDocument": {"uri": args.debug_filepath},
                        "position": {
                            "line": args.debug_line - 1,
                            "character": args.debug_char - 1,
                        },
                    }
                }
            )
            if signature_results is None:
                print("  No Results!")
            else:
                print("  Results:")
                if args.debug_full_result:
                    print(json.dumps(signature_results, indent=2))
                else:
                    active_param = signature_results.get("activeParameter", 0)
                    print("    Active param = {0}".format(active_param))
                    active_signature = signature_results.get("activeSignature", 0)
                    print("    Active sig   = {0}".format(active_signature))
                    for i, signature in enumerate(signature_results["signatures"]):
                        print("    {0}".format(signature["label"]))
                        for j, obj in enumerate(signature["parameters"]):
                            if (i == active_signature) and (j == active_param):
                                active_mark = "*"
                            else:
                                active_mark = " "
                            arg_desc = obj.get("documentation")
                            if arg_desc is not None:
                                print(
                                    "{2}     {0} :: {1}".format(
                                        arg_desc, obj["label"], active_mark
                                    )
                                )
                            else:
                                print("{1}     {0}".format(obj["label"], active_mark))
        #
        if args.debug_definition or args.debug_implementation:
            if args.debug_definition:
                print('\nTesting "textDocument/definition" request:')
            elif args.debug_implementation:
                print('\nTesting "textDocument/implementation" request:')
            check_request_params(args)
            s.serve_onSave({"params": {"textDocument": {"uri": args.debug_filepath}}})
            if args.debug_definition:
                definition_results = s.serve_definition(
                    {
                        "params": {
                            "textDocument": {"uri": args.debug_filepath},
                            "position": {
                                "line": args.debug_line - 1,
                                "character": args.debug_char - 1,
                            },
                        }
                    }
                )
            elif args.debug_implementation:
                definition_results = s.serve_implementation(
                    {
                        "params": {
                            "textDocument": {"uri": args.debug_filepath},
                            "position": {
                                "line": args.debug_line - 1,
                                "character": args.debug_char - 1,
                            },
                        }
                    }
                )
            print("  Result:")
            if definition_results is None:
                print("    No result found!")
            else:
                if args.debug_full_result:
                    print(json.dumps(definition_results, indent=2))
                else:
                    print('    URI  = "{0}"'.format(definition_results["uri"]))
                    print(
                        "    Line = {0}".format(
                            definition_results["range"]["start"]["line"] + 1
                        )
                    )
                    print(
                        "    Char = {0}".format(
                            definition_results["range"]["start"]["character"] + 1
                        )
                    )
        #
        if args.debug_hover:
            print('\nTesting "textDocument/hover" request:')
            check_request_params(args)
            s.serve_onSave({"params": {"textDocument": {"uri": args.debug_filepath}}})
            hover_results = s.serve_hover(
                {
                    "params": {
                        "textDocument": {"uri": args.debug_filepath},
                        "position": {
                            "line": args.debug_line - 1,
                            "character": args.debug_char - 1,
                        },
                    }
                }
            )
            print("  Result:")
            if hover_results is None:
                print("    No result found!")
            else:
                if args.debug_full_result:
                    print(json.dumps(hover_results, indent=2))
                else:
                    contents = hover_results["contents"]
                    print("=======")
                    if isinstance(contents, dict):
                        print(contents["value"])
                    else:
                        print(contents)
                    print("=======")
        #
        if args.debug_references:
            print('\nTesting "textDocument/references" request:')
            check_request_params(args)
            s.serve_onSave({"params": {"textDocument": {"uri": args.debug_filepath}}})
            ref_results = s.serve_references(
                {
                    "params": {
                        "textDocument": {"uri": args.debug_filepath},
                        "position": {
                            "line": args.debug_line - 1,
                            "character": args.debug_char - 1,
                        },
                    }
                }
            )
            print("  Result:")
            if ref_results is None:
                print("    No result found!")
            else:
                if args.debug_full_result:
                    print(json.dumps(ref_results, indent=2))
                else:
                    print("=======")
                    for result in ref_results:
                        print(
                            "  {0}  ({1}, {2})".format(
                                result["uri"],
                                result["range"]["start"]["line"] + 1,
                                result["range"]["start"]["character"] + 1,
                            )
                        )
                    print("=======")
        #
        if args.debug_rename is not None:
            print('\nTesting "textDocument/rename" request:')
            check_request_params(args)
            s.serve_onSave({"params": {"textDocument": {"uri": args.debug_filepath}}})
            ref_results = s.serve_rename(
                {
                    "params": {
                        "textDocument": {"uri": args.debug_filepath},
                        "position": {
                            "line": args.debug_line - 1,
                            "character": args.debug_char - 1,
                        },
                        "newName": args.debug_rename,
                    }
                }
            )
            print("  Result:")
            if ref_results is None:
                print("    No changes found!")
            else:
                if args.debug_full_result:
                    print(json.dumps(ref_results, indent=2))
                else:
                    print("=======")
                    for uri, result in ref_results["changes"].items():
                        path = path_from_uri(uri)
                        print('File: "{0}"'.format(path))
                        file_obj = s.workspace.get(path)
                        if file_obj is not None:
                            file_contents = file_obj.contents_split
                            for change in result:
                                start_line = change["range"]["start"]["line"]
                                end_line = change["range"]["end"]["line"]
                                start_col = change["range"]["start"]["character"]
                                end_col = change["range"]["end"]["character"]
                                print("  {0}, {1}".format(start_line + 1, end_line + 1))
                                new_contents = []
                                for i in range(start_line, end_line + 1):
                                    line = file_contents[i]
                                    print("  - {0}".format(line))
                                    if i == start_line:
                                        new_contents.append(
                                            line[:start_col] + change["newText"]
                                        )
                                    if i == end_line:
                                        new_contents[-1] += line[end_col:]
                                for line in new_contents:
                                    print("  + {0}".format(line))
                                print()
                        else:
                            print('Unknown file: "{0}"'.format(path))
                    print("=======")
        #
        if args.debug_actions:
            import pprint

            pp = pprint.PrettyPrinter(indent=2, width=120)
            print('\nTesting "textDocument/getActions" request:')
            check_request_params(args)
            s.serve_onSave({"params": {"textDocument": {"uri": args.debug_filepath}}})
            action_results = s.serve_codeActions(
                {
                    "params": {
                        "textDocument": {"uri": args.debug_filepath},
                        "range": {
                            "start": {
                                "line": args.debug_line - 1,
                                "character": args.debug_char - 1,
                            },
                            "end": {
                                "line": args.debug_line - 1,
                                "character": args.debug_char - 1,
                            },
                        },
                    }
                }
            )
            if args.debug_full_result:
                print(json.dumps(action_results, indent=2))
            else:
                for result in action_results:
                    print(
                        "Kind = '{0}', Title = '{1}'".format(
                            result["kind"], result["title"]
                        )
                    )
                    for editUri, editChange in result["edit"]["changes"].items():
                        print("\nChange: URI = '{0}'".format(editUri))
                        pp.pprint(editChange)
                    print()
        tmpout.close()
        tmpin.close()
    #
    else:
        stdin, stdout = _binary_stdio()
        s = LangServer(
            conn=JSONRPC2Connection(ReadWriter(stdin, stdout)),
            debug_log=args.debug_log,
            settings=settings,
        )
        s.run()


def check_request_params(args, loc_needed=True):
    if args.debug_filepath is None:
        error_exit("'debug_filepath' not specified for debug request")
    file_exists = os.path.isfile(args.debug_filepath)
    if file_exists is False:
        error_exit("Specified 'debug_filepath' does not exist")
    print('  File = "{0}"'.format(args.debug_filepath))
    if loc_needed:
        if args.debug_line is None:
            error_exit("'debug_line' not specified for debug request")
        print("  Line = {0}".format(args.debug_line))
        if args.debug_char is None:
            error_exit("'debug_char' not specified for debug request")
        print("  Char = {0}\n".format(args.debug_char))


def print_children(obj, indent=""):
    for child in obj.get_children():
        print("  {0}{1}: {2}".format(indent, child.get_type(), child.FQSN))
        print_children(child, indent + "  ")


def _binary_stdio():
    """Construct binary stdio streams (not text mode).
    This seems to be different for Window/Unix Python2/3, so going by:
        https://stackoverflow.com/questions/2850893/reading-binary-data-from-stdin
    """
    PY3K = sys.version_info >= (3, 0)

    if PY3K:
        stdin, stdout = sys.stdin.buffer, sys.stdout.buffer
    else:
        # Python 2 on Windows opens sys.stdin in text mode, and
        # binary data that read from it becomes corrupted on \r\n
        if sys.platform == "win32":
            # set sys.stdin to binary mode
            import os
            import msvcrt

            msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
            msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
        stdin, stdout = sys.stdin, sys.stdout

    return stdin, stdout
