# -*- coding: utf-8 -*-
import collections
import datetime
import io
import json
import os
import re
import traceback

import six
try:
    import configobj
except ImportError:
    configobj = None
try:
    import json5
except ImportError:
    json5 = None
try:
    import toml
except ImportError:
    toml = None
try:
    import xmltodict
except ImportError:
    xmltodict = None
try:
    import yaml
except ImportError:
    yaml = None


__all__ = ['AnyMarkupError', 'parse', 'parse_file', 'serialize', 'serialize_file']
__version__ = '0.8.1'


fmt_to_exts = {'ini': ['ini'],
               'json': ['json'],
               'json5': ['json5'],
               'toml': ['toml'],
               'xml': ['xml'],
               'yaml': ['yaml', 'yml']}
fmt_to_lib = {'ini': (configobj, 'configobj'),
              'json': (json, 'json'),
              'json5': (json5, 'json5'),
              'toml': (toml, 'toml'),
              'xml': (xmltodict, 'xmltodict'),
              'yaml': (yaml, 'PyYAML')}


def _is_utf8(enc_str):
    return enc_str.lower() in ['utf8', 'utf-8']


class AnyMarkupError(Exception):
    def __init__(self, cause, original_tb=''):
        """Wrapper for all errors that occur during anymarkup calls.

        Args:
            cause: either a reraised exception or a string with cause
        """
        super(AnyMarkupError, self).__init__()
        self.cause = cause
        self.original_tb = original_tb

    def __str__(self):
        cause = str(self.cause)
        if isinstance(self.cause, Exception):
            cause = 'caught {0}: {1}'.format(type(self.cause), cause)
        msg = 'AnyMarkupError: {0}'.format(cause)
        if self.original_tb:
            msg += '\nOriginal traceback:\n{0}'.format(self.original_tb)
        return msg


def parse(inp, format=None, encoding='utf-8', force_types=True, interpolate=True):
    """Parse input from file-like object, unicode string or byte string.

    Args:
        inp: file-like object, unicode string or byte string with the markup
        format: explicitly override the guessed `inp` markup format
        encoding: `inp` encoding, defaults to utf-8
        force_types:
            if `True`, integers, floats, booleans and none/null
                are recognized and returned as proper types instead of strings;
            if `False`, everything is converted to strings
            if `None`, backend return value is used
        interpolate: turn on interpolation for INI files (defaults to True)
    Returns:
        parsed input (dict or list) containing unicode values
    Raises:
        AnyMarkupError if a problem occurs while parsing or inp
    """
    proper_inp = inp
    if hasattr(inp, 'read'):
        proper_inp = inp.read()
    # if proper_inp is unicode, encode it
    if isinstance(proper_inp, six.text_type):
        proper_inp = proper_inp.encode(encoding)

    # try to guess markup type
    fname = None
    if hasattr(inp, 'name'):
        fname = inp.name
    fmt = _get_format(format, fname, proper_inp)

    # make it look like file-like bytes-yielding object
    proper_inp = six.BytesIO(proper_inp)

    try:
        res = _do_parse(proper_inp, fmt, encoding, force_types, interpolate)
    except Exception as e:
        # I wish there was only Python 3 and I could just use "raise ... from e"
        raise AnyMarkupError(e, traceback.format_exc())
    if res is None:
        res = {}

    return res


def parse_file(path, format=None, encoding='utf-8', force_types=True, interpolate=True):
    """A convenience wrapper of parse, which accepts path of file to parse.

    Args:
        path: path to file to parse
        format: explicitly override the guessed `inp` markup format
        encoding: file encoding, defaults to utf-8
        force_types:
            if `True`, integers, floats, booleans and none/null
                are recognized and returned as proper types instead of strings;
            if `False`, everything is converted to strings
            if `None`, backend return value is used
        interpolate: turn on interpolation for INI files (defaults to True)
    Returns:
        parsed `inp` (dict or list) containing unicode values
    Raises:
        AnyMarkupError if a problem occurs while parsing
    """
    try:
        with open(path, 'rb') as f:
            return parse(f, format, encoding, force_types, interpolate)
    except EnvironmentError as e:
        raise AnyMarkupError(e, traceback.format_exc())


def serialize(struct, format, target=None, encoding='utf-8'):
    """Serialize given structure and return it as encoded string or write it to file-like object.

    Args:
        struct: structure (dict or list) with unicode members to serialize; note that list
            can only be serialized to json
        format: specify markup format to serialize structure as
        target: binary-opened file-like object to serialize to; if None (default),
            the result will be returned instead of writing to `target`
        encoding: encoding to use when serializing, defaults to utf-8
    Returns:
        bytestring with serialized structure if `target` is None; return value of
        `target.write` otherwise
    Raises:
        AnyMarkupError if a problem occurs while serializing
    """
    # raise if "unicode-opened"
    if hasattr(target, 'encoding') and target.encoding:
        raise AnyMarkupError('Input file must be opened in binary mode')

    fname = None
    if hasattr(target, 'name'):
        fname = target.name

    fmt = _get_format(format, fname)

    try:
        serialized = _do_serialize(struct, fmt, encoding)
        if target is None:
            return serialized
        else:
            return target.write(serialized)
    except Exception as e:
        raise AnyMarkupError(e, traceback.format_exc())


def serialize_file(struct, path, format=None, encoding='utf-8'):
    """A convenience wrapper of serialize, which accepts path of file to serialize to.

    Args:
        struct: structure (dict or list) with unicode members to serialize; note that list
            can only be serialized to json
        path: path of the file to serialize to
        format: override markup format to serialize structure as (taken from filename
            by default)
        encoding: encoding to use when serializing, defaults to utf-8
    Returns:
        number of bytes written
    Raises:
        AnyMarkupError if a problem occurs while serializing
    """
    try:
        with open(path, 'wb') as f:
            return serialize(struct, format, f, encoding)
    except EnvironmentError as e:
        raise AnyMarkupError(e, traceback.format_exc())


def _check_lib_installed(fmt, action):
    if fmt_to_lib[fmt][0] is None:
        raise ImportError('Can\'t {action} {fmt}: {name} not installed'.
                          format(action=action, fmt=fmt, name=fmt_to_lib[fmt][1]))


def _do_parse(inp, fmt, encoding, force_types, interpolate):
    """Actually parse input.

    Args:
        inp: bytes yielding file-like object
        fmt: format to use for parsing
        encoding: encoding of `inp`
        force_types:
            if `True`, integers, floats, booleans and none/null
                are recognized and returned as proper types instead of strings;
            if `False`, everything is converted to strings
            if `None`, backend return value is used