from cgi import parse_header
from enum import Enum
from io import BytesIO
from tempfile import TemporaryFile
from typing import Any, Dict, IO, Tuple

from .form_body import FormBody


class ParserState(Enum):
    PART_BOUNDARY = 0
    CONTENT_DISPOSITION = 1
    CONTENT_TYPE = 2
    CONTENT_HEADER = 3
    CONTENT_DATA = 4
    END = 5


class MultipartBody(FormBody):
    @staticmethod
    def from_wsgi(
        wsgi_input: BytesIO, encoding: str = "utf8", boundary: str = ""
    ) -> "MultipartBody":
        assert boundary, (
            "%s.from_wsgi requires boundary parameters." % MultipartBody.__name__
        )
        wsgi_input.seek(0)
        return _parse_multipart_data(wsgi_input.read(), boundary, encoding)


def _parse_multipart_data(
    data: bytes, boundary: str, encoding: str = "utf8"
) -> MultipartBody:
    state = ParserState.PART_BOUNDARY
    prev_byte = None
    cursor = 0
    boundary_length = len(boundary)
    string_buffer = ""
    body = MultipartBody()

    def _append_content_to_body(
        raw_content_disposition: str, _content_type: str, _content_data: bytes
    ) -> None:
        parsed_content_disposition: Tuple[str, Dict[str, str]] = parse_header(
            raw_content_disposition[20:]
        )
        if "filename" in parsed_content_disposition[1]:
            tmp_file = TemporaryFile()
            tmp_file.write(_content_data)
            tmp_file.seek(0)
            body[parsed_content_disposition[1]["name"]] = UploadedFile(
                tmp_file,
                _content_type[14:].lower(),
                parsed_content_disposition[1]["filename"],
            )
        else:
            body[parsed_content_disposition[1]["name"]] = _content_data.decode(encoding)

    content_disposition = ""
    content_type = ""
    content_cursor = 0

    for code in data:
        line_break = code == 0x0A and prev_byte == 0x0D
        new_line_char = code == 0x0A or code == 0x0D

        if not new_line_char:
            string_buffer = string_buffer + chr(code)

        if state is ParserState.PART_BOUNDARY and line_break:
            if string_buffer == "--" + boundary:
                string_buffer = ""
                state = ParserState.CONTENT_DISPOSITION
            else:
                raise IOError(
                    "Could not parse message body, body is malformed or incorrect boundary was passed."
                )

        elif state is ParserState.CONTENT_DISPOSITION and line_break:
            content_disposition = string_buffer
            string_buffer = ""
            state = ParserState.CONTENT_TYPE
            if (
                data[cursor + 1 : cursor + 13].decode(encoding).lower()
                != "content-type"
            ):
                state = ParserState.CONTENT_HEADER
        elif state is ParserState.CONTENT_TYPE and line_break:
            content_type = string_buffer
            state = ParserState.CONTENT_HEADER
            string_buffer = ""
        elif state is ParserState.CONTENT_HEADER and line_break:
            string_buffer = ""
            state = ParserState.CONTENT_DATA
            content_cursor = cursor
        if state is ParserState.CONTENT_DATA:
            if line_break and string_buffer == "--" + boundary:
                content_data = data[content_cursor + 1 : cursor - (boundary_length + 5)]
                _append_content_to_body(content_disposition, content_type, content_data)
                content_type = ""
                state = ParserState.CONTENT_DISPOSITION

            if line_break and string_buffer == "--" + boundary + "--":
                content_data = data[content_cursor + 1 : cursor - (boundary_length + 7)]
                _append_content_to_body(content_disposition, content_type, content_data)
                content_type = ""
                state = ParserState.END

            if line_break:
                string_buffer = ""

        prev_byte = code
        cursor += 1

    return body


class UploadedFile:
    """
    Proxy class for uploaded file
    """

    def __init__(self, file: IO[Any], mimetype: str, filename: str):
        self.file = file
        self.mimetype = mimetype
        self.filename = filename
        self.length = 0
        self._str = ""

    def read(self) -> bytes:
        return self.file.read()

    def seek(self, offset: int) -> int:
        return self.file.seek(offset)

    def close(self) -> None:
        self.file.close()

    def save(self, path: str) -> IO[Any]:
        if self.file.closed:
            raise ValueError(f"Cannot save to file {path} of closed stream.")
        with open(path, "wb") as file:
            self.seek(0)
            file.write(self.read())

        return file

    def __float__(self) -> None:
        raise ValueError(
            f"Cannot convert instance of {TemporaryFile.__name__} to float"
        )

    def __int__(self) -> None:
        raise ValueError(f"Cannot convert instance of {TemporaryFile.__name__} to int")

    def __len__(self) -> int:
        if not self.length:
            bytes = self.read()
            self.seek(0)
            self.length = len(bytes)
        return self.length

    def __bool__(self) -> bool:
        return len(self) > 0

    def __str__(self) -> str:

        if not self._str:
            self._str = self.read().decode()

        return self._str

    def __enter__(self) -> IO[Any]:
        return self.file

    def __exit__(self, *args: Any) -> None:
        self.close()


__all__ = ["MultipartBody", "UploadedFile"]
