import re
import os
import warnings
from functools import partial
from ..exception import FrictionlessException
from ..resource import Resource
from ..dialect import Dialect
from ..package import Package
from ..storage import Storage
from ..parser import Parser
from ..plugin import Plugin
from ..schema import Schema
from ..field import Field
from .. import helpers
from .. import errors


# Plugin


class SpssPlugin(Plugin):
    """Plugin for SPSS

    API      | Usage
    -------- | --------
    Public   | `from frictionless.plugins.spss import SpssPlugin`
    """

    def create_dialect(self, resource, *, descriptor):
        if resource.format in ["sav", "zsav"]:
            return SpssDialect(descriptor)

    def create_parser(self, resource):
        if resource.format in ["sav", "zsav"]:
            return SpssParser(resource)

    def create_storage(self, name, **options):
        if name == "spss":
            return SpssStorage(**options)


# Dialect


class SpssDialect(Dialect):
    """Spss dialect representation

    API      | Usage
    -------- | --------
    Public   | `from frictionless.plugins.spss import SpssDialect`

    Parameters:
        descriptor? (str|dict): descriptor

    Raises:
        FrictionlessException: raise any error that occurs during the process

    """

    pass


# Parser


class SpssParser(Parser):
    """Spss parser implementation.

    API      | Usage
    -------- | --------
    Public   | `from frictionless.plugins.spss import SpssParser`

    """

    needs_loader = False

    # Read

    def read_data_stream_create(self):
        name = os.path.basename(self.resource.path)
        basepath = os.path.dirname(self.resource.path)
        storage = SpssStorage(basepath=basepath)
        resource = storage.read_resource(name)
        self.resource.schema = resource.schema
        with resource:
            yield from resource.data_stream

    # Write

    def write_row_stream_save(self, read_row_stream):
        name = os.path.basename(self.resource.path)
        basepath = os.path.dirname(self.resource.path)
        schema = self.resource.schema
        storage = SpssStorage(basepath=basepath)
        resource = Resource(name=name, data=read_row_stream, schema=schema)
        storage.write_resource(resource, force=True)
        return self.resource.path


# Storage


class SpssStorage(Storage):
    """SPSS storage implementation

    API      | Usage
    -------- | --------
    Public   | `from frictionless.plugins.spss import SpssStorage`

    Parameters:
        basepath? (str): A path to a dir for reading/writing SAV files.
            Defaults to current dir.
    """

    def __init__(self, *, basepath=None):

        # Set attributes
        basepath = basepath or os.getcwd()
        if not os.path.isdir(basepath):
            note = f'Path "{basepath}" is not a directory, or doesn\'t exist'
            raise FrictionlessException(errors.StorageError(note=note))
        self.__basepath = basepath

        # Silent warnings
        sav = helpers.import_from_plugin("savReaderWriter", plugin="spss")
        warnings.filterwarnings("ignore", category=sav.SPSSIOWarning)

    def __iter__(self):
        names = []
        for path in os.listdir(self.__basepath):
            name = self.__read_convert_name(path)
            if name is not None:
                names.append(name)
        return iter(names)

    # Read

    def read_resource(self, name):
        sav = helpers.import_from_plugin("savReaderWriter", plugin="spss")
        path = self.__write_convert_name(name)
        if not os.path.isfile(path):
            note = f'Resource "{name}" does not exist'
            raise FrictionlessException(errors.StorageError(note=note))
        with sav.SavHeaderReader(path, ioUtf8=True) as reader:
            spss_schema = reader.all()
            schema = self.__read_convert_schema(spss_schema)
            data = partial(self.__read_convert_data, name, schema)
            resource = Resource(name=name, schema=schema, data=data)
            return resource

    def read_package(self):
        package = Package()
        for name in self:
            resource = self.read_resource(name)
            package.resources.append(resource)
        return package

    def __read_convert_name(self, path):
        if path.endswith((".sav", ".zsav")):
            return os.path.splitext(path)[0]

    def __read_convert_schema(self, spss_schema):
        schema = Schema()
        for name in spss_schema.varNames:
            type = self.__read_convert_type(spss_schema.formats[name])
            field = Field(name=name, type=type)
            title = spss_schema.varLabels[name]
            if title:
                field.title = title
            schema.fields.append(field)
        return schema

    def __read_convert_data(self, name, schema):
        sav = helpers.import_from_plugin("savReaderWriter", plugin="spss")
        path = self.__write_convert_name(name)
        yield schema.field_names
        with sav.SavReader(path, ioUtf8=True) as reader:
            for item in reader:
                cells = []
                for index, field in enumerate(schema.fields):
                    value = item[index]
                    if value is not None:
                        if field.type == "integer":
                            value = int(float(value))
                        elif field.type in ["datetime", "date", "time"]:
                            format = FORMAT_READ[field.type]
                            value = reader.spss2strDate(value, format, None)
                    cells.append(value)
                yield cells

    def __read_convert_type(self, spss_type=None):

        # Mapping
        mapping = [
            ("string", re.compile(r"\bA\d+")),
            ("number", re.compile(r"\bF\d+\.\d+")),  # Basic decimal number
            ("number", re.compile(r"\b[E|N]\d+\.?\d*")),  # Exponent or N format number
            (
                "integer",
                re.compile(r"\bF\d+"),
            ),  # Integer (must come after Basic decimal in list)
            ("date", re.compile(r"\b[A|E|J|S]?DATE\d+")),  # Various date formats
            ("datetime", re.compile(r"\bDATETIME\d+")),
            ("time", re.compile(r"\bTIME\d+")),
            ("number", re.compile(r"\bDOLLAR\d+")),
            ("number", re.compile(r"\bPCT\d+")),  # Percentage format
        ]

        # Return type
        if spss_type:
            for type, pattern in mapping:
                if pattern.match(spss_type):
                    return type
            return "string"

        # Return mapping
        return mapping

    # Write

    def write_resource(self, resource, *, force=False):
        package = Package(resources=[resource])
        return self.write_package(package, force=force)

    def write_package(self, package, *, force=False):
        existent_names = list(self)

        # Check existence
        for resource in package.resources:
            if resource.name in existent_names:
                if not force:
                    note = f'Resource "{resource.name}" already exists'
                    raise FrictionlessException(errors.StorageError(note=note))
                self.delete_resource(resource.name)

        # Save resources
        for resource in package.resources:
            if not resource.schema:
                resource.infer()
            self.__write_convert_data(resource)

    def __write_convert_name(self, name):
        path = os.path.normpath(os.path.join(self.__basepath, f"{name}.sav"))
        if not path.startswith(os.path.normpath(self.__basepath)):
            note = f'Resource name "{name}" is not valid.'
            raise FrictionlessException(errors.StorageError(note=note))
        return path

    def __write_convert_schema(self, resource):
        spss_schema = {"varNames": [], "varLabels": {}, "varTypes": {}, "formats": {}}
        mapping = self.__write_convert_type()

        # Add fields
        sizes = {}
        for field in resource.schema.fields:
            spss_schema["varNames"].append(field.name)
            if field.title:
                spss_schema["varLabels"][field.name] = field.title
            spss_type = mapping.get(field.type)
            if spss_type:
                spss_schema["varTypes"][field.name] = spss_type[0]
                spss_schema["formats"][field.name] = spss_type[1]
            else:
                sizes[field.name] = 0

        # Set string sizes
        with resource:
            for row in resource.row_stream:
                for name in sizes.keys():
                    cell = row[name]
                    field = resource.schema.get_field(name)
                    cell, notes = field.write_cell(cell)
                    size = len(cell.encode("utf-8"))
                    if size > sizes[name]:
                        sizes[name] = size
            for name, size in sizes.items():
                spss_schema["varTypes"][name] = size

        return spss_schema

    def __write_convert_data(self, resource):
        mapping = self.__write_convert_type()
        sav = helpers.import_from_plugin("savReaderWriter", plugin="spss")
        path = self.__write_convert_name(resource.name)
        spss_schema = self.__write_convert_schema(resource)
        with sav.SavWriter(path, ioUtf8=True, **spss_schema) as writer:
            with resource:
                for row in resource.row_stream:
                    result = []
                    for field in resource.schema.fields:
                        cell = row[field.name]
                        if field.type in ["datetime", "date", "time"]:
                            format = FORMAT_WRITE[field.type]
                            cell = cell.strftime(format).encode()
                            cell = writer.spssDateTime(cell, format)
                        elif field.type not in mapping:
                            cell, notes = field.write_cell(cell)
                            cell = cell.encode("utf-8")
                        result.append(cell)
                    writer.writerow(result)

    def __write_convert_type(self, type=None):

        # Mapping
        mapping = {
            "integer": [0, "F10"],
            "number": [0, "F10.2"],
            "datetime": [0, "DATETIME20"],
            "date": [0, "DATE10"],
            "time": [0, "TIME8"],
            "year": [0, "F10"],
        }

        # Return type
        if type:
            return mapping.get(type)

        # Return mapping
        return mapping

    # Delete

    def delete_resource(self, name, *, ignore=False):
        return self.delete_package([name], ignore=ignore)

    def delete_package(self, names, *, ignore=False):
        for name in names:

            # Check existent
            if name not in self:
                if not ignore:
                    note = f'Resource "{name}" does not exist'
                    raise FrictionlessException(errors.StorageError(note=note))
                continue

            # Delete file
            path = self.__write_convert_name(name)
            os.remove(path)


# Internal

FORMAT_READ = {
    "date": "%Y-%m-%d",
    "datetime": "%Y-%m-%d %H:%M:%S",
    "time": "%H:%M:%S.%f",
}

FORMAT_WRITE = {
    "date": "%Y-%m-%d",
    "datetime": "%Y-%m-%d %H:%M:%S",
    "time": "%H:%M:%S.%f",
}
