import codecs
import json
import sys
from collections import OrderedDict
from decimal import Decimal

from flattentool.input import FORMATS as INPUT_FORMATS
from flattentool.json_input import JSONParser
from flattentool.lib import parse_sheet_configuration
from flattentool.output import FORMATS as OUTPUT_FORMATS
from flattentool.output import FORMATS_SUFFIX, LINE_TERMINATORS
from flattentool.schema import SchemaParser
from flattentool.xml_output import toxml


def create_template(
    schema,
    output_name=None,
    output_format="all",
    main_sheet_name="main",
    rollup=False,
    root_id=None,
    use_titles=False,
    disable_local_refs=False,
    truncation_length=3,
    no_deprecated_fields=False,
    line_terminator="CRLF",
    convert_wkt=False,
    **_,
):
    """
    Creates template file(s) from given inputs
    This function is built to deal with commandline input and arguments
    but to also be called from elsewhere in future

    """

    if line_terminator not in LINE_TERMINATORS.keys():
        raise Exception(f"{line_terminator} is not a valid line terminator")

    convert_flags = {"wkt": convert_wkt}

    parser = SchemaParser(
        schema_filename=schema,
        rollup=rollup,
        root_id=root_id,
        use_titles=use_titles,
        disable_local_refs=disable_local_refs,
        truncation_length=truncation_length,
        exclude_deprecated_fields=no_deprecated_fields,
        convert_flags=convert_flags,
    )
    parser.parse()

    def spreadsheet_output(spreadsheet_output_class, name):
        spreadsheet_output = spreadsheet_output_class(
            parser=parser,
            main_sheet_name=main_sheet_name,
            output_name=name,
            line_terminator=LINE_TERMINATORS[line_terminator],
        )
        spreadsheet_output.write_sheets()

    if output_format == "all":
        if not output_name:
            output_name = "template"
        for format_name, spreadsheet_output_class in OUTPUT_FORMATS.items():
            spreadsheet_output(
                spreadsheet_output_class, output_name + FORMATS_SUFFIX[format_name]
            )

    elif output_format in OUTPUT_FORMATS.keys():  # in dictionary of allowed formats
        if not output_name:
            output_name = "template" + FORMATS_SUFFIX[output_format]
        spreadsheet_output(OUTPUT_FORMATS[output_format], output_name)

    else:
        raise Exception("The requested format is not available")


def flatten(
    input_name,
    schema=None,
    output_name=None,
    output_format="all",
    main_sheet_name="main",
    root_list_path="main",
    root_is_list=False,
    sheet_prefix="",
    filter_field=None,
    filter_value=None,
    preserve_fields=None,
    rollup=False,
    root_id=None,
    use_titles=False,
    xml=False,
    id_name="id",
    disable_local_refs=False,
    remove_empty_schema_columns=False,
    truncation_length=3,
    line_terminator="CRLF",
    convert_wkt=False,
    **_,
):
    """
    Flatten a nested structure (JSON) to a flat structure (spreadsheet - csv or xlsx).

    """

    if (filter_field is None and filter_value is not None) or (
        filter_field is not None and filter_value is None
    ):
        raise Exception("You must use filter_field and filter_value together")

    if line_terminator not in LINE_TERMINATORS.keys():
        raise Exception(f"{line_terminator} is not a valid line terminator")

    convert_flags = {"wkt": convert_wkt}

    if schema:
        schema_parser = SchemaParser(
            schema_filename=schema,
            rollup=rollup,
            root_id=root_id,
            use_titles=use_titles,
            disable_local_refs=disable_local_refs,
            truncation_length=truncation_length,
            convert_flags=convert_flags,
        )
        schema_parser.parse()
    else:
        schema_parser = None

    # context manager to clean up ZODB database when it exits
    with JSONParser(
        json_filename=input_name,
        root_list_path=None if root_is_list else root_list_path,
        schema_parser=schema_parser,
        rollup=rollup,
        root_id=root_id,
        use_titles=use_titles,
        xml=xml,
        id_name=id_name,
        filter_field=filter_field,
        filter_value=filter_value,
        preserve_fields=preserve_fields,
        remove_empty_schema_columns=remove_empty_schema_columns,
        truncation_length=truncation_length,
        persist=True,
        convert_flags=convert_flags,
    ) as parser:

        def spreadsheet_output(spreadsheet_output_class, name):
            spreadsheet_output = spreadsheet_output_class(
                parser=parser,
                main_sheet_name=main_sheet_name,
                output_name=name,
                sheet_prefix=sheet_prefix,
                line_terminator=LINE_TERMINATORS[line_terminator],
            )
            spreadsheet_output.write_sheets()

        if output_format == "all":
            if not output_name:
                output_name = "flattened"
            for format_name, spreadsheet_output_class in OUTPUT_FORMATS.items():
                spreadsheet_output(
                    spreadsheet_output_class, output_name + FORMATS_SUFFIX[format_name]
                )

        elif output_format in OUTPUT_FORMATS.keys():  # in dictionary of allowed formats
            if not output_name:
                output_name = "flattened" + FORMATS_SUFFIX[output_format]
            spreadsheet_output(OUTPUT_FORMATS[output_format], output_name)

        else:
            raise Exception("The requested format is not available")


# From http://bugs.python.org/issue16535
class NumberStr(float):
    def __init__(self, o):
        # We don't call the parent here, since we're deliberately altering it's functionality
        # pylint: disable=W0231
        self.o = o

    def __repr__(self):
        return str(self.o)

    # This is needed for this trick to work in python 3.4
    def __float__(self):
        return self


def decimal_default(o):
    if isinstance(o, Decimal):
        if int(o) == o:
            return int(o)
        else:
            return NumberStr(o)
    raise TypeError(repr(o) + " is not JSON serializable")


def unflatten(
    input_name,
    base_json=None,
    input_format=None,
    output_name=None,
    root_list_path=None,
    root_is_list=False,
    encoding="utf8",
    timezone_name="UTC",
    root_id=None,
    schema="",
    convert_titles=False,
    cell_source_map=None,
    heading_source_map=None,
    id_name=None,
    xml=False,
    vertical_orientation=False,
    metatab_name=None,
    metatab_only=False,
    metatab_schema="",
    metatab_vertical_orientation=False,
    xml_schemas=None,
    default_configuration="",
    disable_local_refs=False,
    xml_comment=None,
    truncation_length=3,
    convert_wkt=False,
    **_,
):
    """
    Unflatten a flat structure (spreadsheet - csv or xlsx) into a nested structure (JSON).

    """

    if input_format is None:
        raise Exception("You must specify an input format (may autodetect in future")
    elif input_format not in INPUT_FORMATS:
        raise Exception("The requested format is not available")
    if metatab_name and base_json:
        raise Exception("Not allowed to use base_json with metatab")

    convert_flags = {"wkt": convert_wkt}

    if root_is_list:
        base = None
    elif base_json:
        with open(base_json) as fp:
            base = json.load(fp, object_pairs_hook=OrderedDict)
    else:
        base = OrderedDict()

    base_configuration = parse_sheet_configuration(
        [item.strip() for item in default_configuration.split(",")]
    )

    cell_source_map_data = OrderedDict()
    heading_source_map_data = OrderedDict()

    if metatab_name and not root_is_list:
        spreadsheet_input_class = INPUT_FORMATS[input_format]
        spreadsheet_input = spreadsheet_input_class(
            input_name=input_name,
            timezone_name=timezone_name,
            root_list_path="meta",
            include_sheets=[metatab_name],
            convert_titles=convert_titles,
            vertical_orientation=metatab_vertical_orientation,
            id_name=id_name,
            xml=xml,
            use_configuration=False,
            convert_flags=convert_flags,
        )
        if metatab_schema:
            parser = SchemaParser(
                schema_filename=metatab_schema,
                disable_local_refs=disable_local_refs,
                convert_flags=convert_flags,
            )
            parser.parse()
            spreadsheet_input.parser = parser
        spreadsheet_input.encoding = encoding
        spreadsheet_input.read_sheets()
        (
            result,
            cell_source_map_data_meta,
            heading_source_map_data_meta,
        ) = spreadsheet_input.fancy_unflatten(
            with_cell_source_map=cell_source_map,
            with_heading_source_map=heading_source_map,
        )
        for key, value in (cell_source_map_data_meta or {}).items():
            ## strip off meta/0/ from start of source map as actually data is at top level
            cell_source_map_data[key[7:]] = value
        for key, value in (heading_source_map_data_meta or {}).items():
            ## strip off meta/ from start of source map as actually data is at top level
            heading_source_map_data[key[5:]] = value

        # update individual keys from base configuration
        base_configuration.update(
            spreadsheet_input.sheet_configuration.get(metatab_name, {})
        )

        if result:
            base.update(result[0])

    if root_list_path is None:
        root_list_path = base_configuration.get("RootListPath", "main")
    if id_name is None:
        id_name = base_configuration.get("IDName", "id")

    if not metatab_only or root_is_list:
        spreadsheet_input_class = INPUT_FORMATS[input_format]
        spreadsheet_input = spreadsheet_input_class(
            input_name=input_name,
            timezone_name=timezone_name,
            root_list_path=root_list_path,
            root_is_list=root_is_list,
            root_id=root_id,
            convert_titles=convert_titles,
            exclude_sheets=[metatab_name],
            vertical_orientation=vertical_orientation,
            id_name=id_name,
            xml=xml,
            base_configuration=base_configuration,
            convert_flags=convert_flags,
        )
        if schema:
            parser = SchemaParser(
                schema_filename=schema,
                rollup=True,
                root_id=root_id,
                disable_local_refs=disable_local_refs,
                truncation_length=truncation_length,
                convert_flags=convert_flags,
            )
            parser.parse()
            spreadsheet_input.parser = parser
        spreadsheet_input.encoding = encoding
        spreadsheet_input.read_sheets()
        (
            result,
            cell_source_map_data_main,
            heading_source_map_data_main,
        ) = spreadsheet_input.fancy_unflatten(
            with_cell_source_map=cell_source_map,
            with_heading_source_map=heading_source_map,
        )
        cell_source_map_data.update(cell_source_map_data_main or {})
        heading_source_map_data.update(heading_source_map_data_main or {})
        if root_is_list:
            base = list(result)
        else:
            base[root_list_path] = list(result)

    if xml:
        xml_root_tag = base_configuration.get("XMLRootTag", "iati-activities")
        xml_output = toxml(
            base,
            xml_root_tag,
            xml_schemas=xml_schemas,
            root_list_path=root_list_path,
            xml_comment=xml_comment,
        )
        if output_name is None:
            sys.stdout.buffer.write(xml_output)
        else:
            with codecs.open(output_name, "wb") as fp:
                fp.write(xml_output)
    else:
        if output_name is None:
            print(
                json.dumps(base, indent=4, default=decimal_default, ensure_ascii=False)
            )
        else:
            with codecs.open(output_name, "w", encoding="utf-8") as fp:
                json.dump(
                    base, fp, indent=4, default=decimal_default, ensure_ascii=False
                )
    if cell_source_map:
        with codecs.open(cell_source_map, "w", encoding="utf-8") as fp:
            json.dump(
                cell_source_map_data,
                fp,
                indent=4,
                default=decimal_default,
                ensure_ascii=False,
            )
    if heading_source_map:
        with codecs.open(heading_source_map, "w", encoding="utf-8") as fp:
            json.dump(
                heading_source_map_data,
                fp,
                indent=4,
                default=decimal_default,
                ensure_ascii=False,
            )
