# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""
Text encoding utilities, text file I/O

Functions 'get_coding', 'decode', 'encode'  come from Eric4
source code (Utilities/__init___.py) Copyright © 2003-2009 Detlev Offenbach
"""

from __future__ import annotations

import os
import re
from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF32

# ------------------------------------------------------------------------------
#  Functions for encoding and decoding *text data* itself, usually originating
#  from or destined for the *contents* of a file.
# ------------------------------------------------------------------------------

# Codecs for working with files and text.
CODING_RE = re.compile(r"coding[:=]\s*([-\w_.]+)")
CODECS = [
    "utf-8",
    "iso8859-1",
    "iso8859-15",
    "ascii",
    "koi8-r",
    "cp1251",
    "koi8-u",
    "iso8859-2",
    "iso8859-3",
    "iso8859-4",
    "iso8859-5",
    "iso8859-6",
    "iso8859-7",
    "iso8859-8",
    "iso8859-9",
    "iso8859-10",
    "iso8859-13",
    "iso8859-14",
    "latin-1",
    "utf-16",
]


def get_coding(text: str) -> str | None:
    """
    Function to get the coding of a text.
    @param text text to inspect (string)
    @return coding string
    """
    for line in text.splitlines()[:2]:
        try:
            result = CODING_RE.search(str(line))
        except UnicodeDecodeError:
            # This could fail because str assume the text
            # is utf8-like and we don't know the encoding to give
            # it to str
            pass
        else:
            if result:
                codec = result.group(1)
                # sometimes we find a false encoding that can
                # result in errors
                if codec in CODECS:
                    return codec


def decode(text: bytes) -> tuple[str, str] | tuple[str, str] | tuple[str, str]:
    """
    Function to decode a text.
    @param text text to decode (bytes)
    @return decoded text and encoding
    """
    try:
        if text.startswith(BOM_UTF8):
            # UTF-8 with BOM
            return str(text[len(BOM_UTF8) :], "utf-8"), "utf-8-bom"
        elif text.startswith(BOM_UTF16):
            # UTF-16 with BOM
            return str(text[len(BOM_UTF16) :], "utf-16"), "utf-16"
        elif text.startswith(BOM_UTF32):
            # UTF-32 with BOM
            return str(text[len(BOM_UTF32) :], "utf-32"), "utf-32"
        coding = get_coding(text)
        if coding:
            return str(text, coding), coding
    except (UnicodeError, LookupError):
        pass
    # Assume UTF-8
    try:
        return str(text, "utf-8"), "utf-8-guessed"
    except (UnicodeError, LookupError):
        pass
    # Assume Latin-1 (behaviour before 3.7.1)
    return str(text, "latin-1"), "latin-1-guessed"


def encode(text: str, orig_coding: str) -> tuple[bytes, str] | tuple[bytes, str]:
    """
    Function to encode a text.
    @param text text to encode (string)
    @param orig_coding type of the original coding (string)
    @return encoded text and encoding
    """
    if orig_coding == "utf-8-bom":
        return BOM_UTF8 + text.encode("utf-8"), "utf-8-bom"

    # Try saving with original encoding
    if orig_coding:
        try:
            return text.encode(orig_coding), orig_coding
        except (UnicodeError, LookupError):
            pass

    # Try declared coding spec
    coding = get_coding(text)
    if coding:
        try:
            return text.encode(coding), coding
        except (UnicodeError, LookupError):
            raise RuntimeError("Incorrect encoding (%s)" % coding)
    if (
        orig_coding
        and orig_coding.endswith("-default")
        or orig_coding.endswith("-guessed")
    ):
        coding = orig_coding.replace("-default", "")
        coding = orig_coding.replace("-guessed", "")
        try:
            return text.encode(coding), coding
        except (UnicodeError, LookupError):
            pass

    # Try saving as ASCII
    try:
        return text.encode("ascii"), "ascii"
    except UnicodeError:
        pass

    # Save as UTF-8 without BOM
    return text.encode("utf-8"), "utf-8"


def write(text: str, filename: str, encoding: str = "utf-8", mode: str = "wb") -> str:
    """
    Write 'text' to file ('filename') assuming 'encoding'
    Return (eventually new) encoding
    """
    text, encoding = encode(text, encoding)
    with open(filename, mode) as textfile:
        textfile.write(text)
    return encoding


def writelines(
    lines: list[str], filename: str, encoding: str = "utf-8", mode: str = "wb"
) -> str:
    """
    Write 'lines' to file ('filename') assuming 'encoding'
    Return (eventually new) encoding
    """
    return write(os.linesep.join(lines), filename, encoding, mode)


def read(filename: str, encoding: str = "utf-8") -> tuple[str, str]:
    """
    Read text from file ('filename')
    Return text and encoding
    """
    with open(filename, "rb") as file:
        text, encoding = decode(file.read())
    return text, encoding


def readlines(filename: str, encoding: str = "utf-8") -> tuple[list[str], str]:
    """
    Read lines from file ('filename')
    Return lines and encoding
    """
    text, encoding = read(filename, encoding)
    return text.split(os.linesep), encoding
