import os
import nixio as nix
import numpy as np
from enum import Enum
import datetime as dt

from numpy.core.defchararray import not_equal
from nixview.constants import io_chunksize as chunksize


class ItemDescriptor():
    def __init__(self, name=None, id=None, type=None, value=None, definition=None, block_id=None, entity_type=None, shape=None, metadata=None, data_type=None, source_id=None, created_at=None, updated_at=None) -> None:
        super().__init__()
        self.name = name
        self.type = type
        self.id = id
        self.block_id= block_id
        self.definition = definition
        self.value = value
        self.entity_type = entity_type
        self.data_type = data_type
        self.shape = shape
        self.metadata_id = metadata
        self.source_id = source_id
        self.created_at = None
        self.updated_at = None
        
    def to_html(self):
        descr = "<html><h4>%s: %s</h4>" % (self.type, self.name)
        descr += "<ol style='list-style-type:none'>"
        descr += "<li><small><b>id:</b> %s</small></li>" % (self.id)
        descr += "<li><small><b>entity type:</b> %s</small></li>" % (self.entity_type.value)
        descr += "<li><small><b>data type:</b> %s</small></li>" % (str(self.data_type))
        descr += "<li><small><b>data shape:</b> %s</small></li>" % (str(self.shape))
        descr += "<hr>"
        descr += "<p><small><b>definition:</b> %s</small></p>" % (self.definition)
        descr += "<hr>"
        descr += "<li><small><b>metadata id:</b> %s</small></li>" % (self.metadata_id)
        descr += "<li><small><b>source id:</b> %s</small></li>" % (self.source_id)
        descr += "<hr>"
        descr += "<li><small><b>created at:</b> %s</small></li>" % (str(dt.datetime.fromtimestamp(self.created_at)) if self.created_at else "")
        descr += "<li><small><b>updated at:</b> %s</small></li>" % (str(dt.datetime.fromtimestamp(self.updated_at)) if self.updated_at else "")
        descr += "</ol>"
        
        descr += "</html>"
        return descr


class FileDescriptor():
    def __init__(self, filename, format, version, created_at, updated_at, size) -> None:
        super().__init__()
        self.name = filename
        self.size = None
        self.format = format
        self.version = version
        self.created_at = created_at
        self.updated_at = updated_at
        self.size = size
        self.block_count = 0
        self.data_array_count = 0
        self.tag_count = 0
        self.group_count = 0
        self.data_frame_count = 0
    
    def toHtml(self):
        def namAndPath(filename):
            parts = filename.split(os.sep)
            name = parts[-1]
            path = ""
            if len(parts) > 1:
                path = os.sep.join(parts[:-1])
            return name, path

        name, path = namAndPath(self.name)
        descr = "<html><h4>%s</h4>" % name
        descr += "<ol style='list-style-type:none'>"
        descr += "<li><small><b>location:</b> %s</small></li>" % (path if len(path) > 1 else ".")
        descr += "<li><small><b>format:</b> %s</small></li>" % (self.format)
        descr += "<li><small><b>nix format version:</b> %s</small></li>" % (str(self.version))
        descr += "<li><small><b>file size:</b> %.2f MB</small></li>" % (self.size)
        descr += "<hr>"
        descr += "<li>File contents</li>"
        descr += "<li><small><b>blocks:</b> %i</small></li>" % self.block_count
        descr += "<li><small><b>groups:</b> %i</small></li>" % self.group_count
        descr += "<li><small><b>data arrays:</b> %i</small></li>" % self.data_array_count
        descr += "<li><small><b>data frames:</b> %i</small></li>" % self.data_frame_count
        descr += "<li><small><b>tags:</b> %i</small></li>" % self.tag_count
        descr += "<hr>"
        descr += "<li><small><b>created at:</b> %s</small></li>" % (str(dt.datetime.fromtimestamp(self.created_at)))
        descr += "<li><small><b>updated at:</b> %s</small></li>" % (str(dt.datetime.fromtimestamp(self.updated_at)))
        descr += "</ol>"
        descr += "</html>"
        return descr


class NodeType(Enum):
     Root = "root"
     Section = "Section"
     Block = "Block"
     DataArray = "Data Array"
     DataFrame = "Data Frame"
     Property = "Property"
     Dimension = "Dimension"
     Source = "Source"
     Tag = "Tag"
     MultiTag = "Multi Tag"
     Group = "Group"
     Feature="Feature"


class DataView():

    def __init__(self, item_descriptor, file_handler) -> None:
        super().__init__()
        self._item_descriptor = item_descriptor
        self._file_handler = file_handler
        self._buffer = None
        self._full_shape = item_descriptor.shape
        self._offset = np.zeros(len(self._full_shape), dtype=int)
        self._count = None
        self._max_dim = None
        self.init_buffer()
        self.request_more()

    def request_more(self):
        if self.fully_loaded:
            return
        data = self._file_handler.request_data(self._item_descriptor, self._offset, self._count)
        if data is not None and self._buffer is None:
            self._buffer = data
            self._offset = data.shape
        else:
            print("need to fetch more!")
        
    def init_buffer(self):
        buffer_shape = np.zeros(len(self._full_shape), dtype=int)
        max_dim_count = chunksize
        max_dim = np.argmax(self._full_shape)
        for i, d in enumerate(self._full_shape):
            if i != max_dim:
                buffer_shape[i] = self._full_shape[i]
                max_dim_count /= self._full_shape[i]
        buffer_shape[max_dim] = max_dim_count
        self._count = buffer_shape
        self._max_dim = max_dim
    
    
    @property
    def fully_loaded(self):
        return self._buffer is not None and self._full_shape == self._buffer.shape
    
    def __str__(self) -> str:
        r = self._item_descriptor.name + " " + str(self._item_descriptor.entity_type)
        r += "buffer size: "  + str(self._buffer.shape) if self._buffer is not None else "" + "\n"
        r += "chunk size:" + str(self._count)
        r += "is fully loaded: " + str(self.fully_loaded)
        return r
    
class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]


class EntityBuffer():

    def __init__(self) -> None:
        super().__init__()
        self._buffer = {}
    
    def put(self, entity):
        if not hasattr(entity, "id"):
            return
        id = entity.id
        if id not in self._buffer.keys():
            self._buffer[id] = entity
    
    def has(self, id):
        return id in self._buffer.keys()
    
    def get(self, id):
        if self.has(id):
            return self._buffer[id]
        else:
            return None
    
    def clear(self):
        self._buffer.clear()


class FileHandler(metaclass=Singleton):
    
    def __init__(self) -> None:
        super().__init__()
        self._filename = None
        self._nix_file = None
        self._file_requests = []
        self._entity_buffer = EntityBuffer()
        self._file_descriptor = None
        self._file_version = None
    
    def open(self, filename):
        self.close()
      
        if not os.path.exists(filename):
            return False, "File %s could not be found!" % filename
        try:
            self._nix_file = nix.File.open(filename, nix.FileMode.ReadOnly)
            self._filename = filename
            self._file_descriptor = FileDescriptor(self.filename, self._nix_file.format, self._nix_file.version, 
                                                   self._nix_file.created_at, self._nix_file.updated_at, os.path.getsize(self.filename)/1e+6)
            self.file_descriptor.block_count = len(self._nix_file.blocks)
            for b in self._nix_file.blocks:
                self.file_descriptor.data_array_count += len(b.data_arrays)
                self.file_descriptor.group_count += len(b.groups)
                self.file_descriptor.tag_count += len(b.tags)
                self.file_descriptor.tag_count += len(b.multi_tags)
                if hasattr(b, "data_frames"): 
                    self.file_descriptor.data_frame_count += len(b.data_frames)
            self._file_version = self._nix_file.version
            return True, "Successfully opened file %s." % filename.split(os.sep)[-1]
        except RuntimeError as e:
            return False, "Failed to open file %s! \n Error message is: %s" % (filename, e)
        except OSError as e:
            return False, "Failed to open file %s! \n Error message is: %s\n Probably no nix file?!" % (filename, e)

    def close(self):
        if self._nix_file is not None and self._nix_file.is_open():
            self._nix_file.close()
            self._nix_file = None
            self._file_requests = []
            self._entity_buffer.clear()
            self._file_descriptor = None
            self._file_version = None

    @property
    def file_descriptor(self):
        return self._file_descriptor

    @property
    def is_valid(self):
        return self._nix_file is not None and self._nix_file.is_open()
    
    @property
    def filename(self):
        return self._filename

    def request_data(self, entity_descriptor, offset=None, count=None):
        entity = self._entity_buffer.get(entity_descriptor.id)
        if entity is None:
            print("need to do something else")
        for i, (o, c) in enumerate(zip(offset, count)):
            if o + c > entity.shape[i]:
                count[i] = entity.shape[i] - o
        seg = tuple([slice(o, o + c) for o, c in zip(offset, count)])
        print(seg)
        return entity[seg]

    def request_section_descriptor(self, id):
        fs = self._entity_buffer.get(id)
        if fs is None:
            found_section = self._nix_file.find_sections(lambda s: s.id == id)
            fs = found_section[0] if len(found_section) > 0 else None
        if fs is None:
            return None
        else:
            item = ItemDescriptor(fs.name, fs.id, fs.type, definition=fs.definition, entity_type=NodeType.Section)
            return item
    
    def request_metadata(self, root_id=None, depth=1):
        """[summary]

        Args:
            root_id ([type], optional): [description]. Defaults to None.
            depth (int, optional): [description]. Defaults to 1.
        """
        def get_subsections(section):
            sub_sections = []
            for s in section.sections:
                self._entity_buffer.put(s)
                sub_sections.append(ItemDescriptor(s.name, s.id, s.type, definition=s.definition, entity_type=NodeType.Section))
            return sub_sections
        
        def get_properties(section):
            props = []
            for p in section.props:
                value = ""
                if self._file_version < (1,1,1):
                    vals = p.values 
                    if len(vals) > 1:
                        value += "["
                        value += ",".join(map(str, [v.value for v in vals]))
                        value += "]"
                    else:
                        value = str(vals[0].value)    
                else:
                    vals = p.values
                    value += "["
                    value += ",".join(map(str, [v.value for v in vals]))
                    value += "]"
                if p.unit is not None:
                    value += " " + p.unit
                props.append(ItemDescriptor(p.name, p.id, value=value, entity_type=NodeType.Property))
            return props

        sections = []
        properties = []
        if root_id is None:
            sections = get_subsections(self._nix_file)
        else:
            fs = self._entity_buffer.get(root_id)
            if fs is None:
                found_section = self._nix_file.find_sections(lambda s: s.id == root_id)
                fs = found_section[0] if len(found_section) > 0 else None
            if fs is None:
                return sections, properties
            sections.extend(get_subsections(fs))
            properties.extend(get_properties(fs))
        return sections, properties

    def _entity_info(self, entities, block_id, entity_type):
        infos = []
        for e in entities:
            self._entity_buffer.put(e)
            itd = ItemDescriptor(e.name, e.id, e.type, definition=e.definition, entity_type=entity_type, block_id=block_id)
            section = e.metadata if hasattr(e, "metadata") else None
            itd.metadata_id = section.id if section is not None else None
            itd.data_type = e.data_type if hasattr(e, "data_type") else None
            itd.created_at = e.created_at if hasattr(e, "created_at") else None
            itd.updated_at = e.updated_at if hasattr(e, "updated") else None
            itd.shape = e.shape if hasattr(e, "shape") else None
            src = e.source if hasattr(e, "source") else None
            itd.source_id = src.id if src is not None else None
            infos.append(itd)
            if entity_type == NodeType.DataArray:
                itd.value = "%s %s entries" % (str(e.shape), e.dtype)
            elif entity_type == NodeType.Tag:
                point_or_segment = "segment" if e.extent else "point"
                start = str(e.position)
                end = ("to " + str(tuple(np.array(e.position) + np.array(e.extent)))) if e.extent else ""
                itd.value = "tags %s %s %s" %(point_or_segment, start, end) 
            # TODO set the value to something meaningful for the various entity types
        return infos

    def request_blocks(self):
        return self._entity_info(self._nix_file.blocks, None, NodeType.Block)

    def get_block(self, id):
        b = b = self._entity_buffer.get(id)
        if not b:
            b = self._nix_file.blocks[id]
        return b
    
    def request_data_arrays(self, block_id):
        b = self.get_block(block_id)
        return self._entity_info(b.data_arrays, block_id, NodeType.DataArray)

    def request_tags(self, block_id):
        b = self.get_block(block_id)
        tags = self._entity_info(b.tags, block_id, NodeType.Tag)
        tags.extend(self._entity_info(b.multi_tags, block_id, NodeType.MultiTag))
        return tags

    def request_references(self, block_id, tag_id, is_mtag):
        b = self.get_block(block_id)
        t = self._entity_buffer.get(tag_id)
        if t is None:
            if is_mtag:
                t = b.multi_tags[tag_id]
            else:
                t = b.tags[tag_id]
        return self._entity_info(t.references, block_id, NodeType.DataArray)

    def request_features(self, block_id, tag_id, is_mtag):
        b = self.get_block(block_id)
        t = self._entity_buffer.get(tag_id)
        if t is None:
            if is_mtag:
                t = b.multi_tags[tag_id]
            else:
                t = b.tags[tag_id]
        feats = []
        for f in t.features:
            itd = ItemDescriptor(f.data.name, f.id, f.link_type, definition=f.data.definition, block_id=block_id, entity_type=NodeType.Feature)
            feats.append(itd)
        return feats

    def request_dimensions(self, block_id, array_id):
        da = self._entity_buffer.get(array_id)
        if da is None:
            b = self.get_block(block_id)
            da = b.data_arrays[array_id]
        dimensions = []
        for i, d in enumerate(da.dimensions):
            dim_name = "%i. dim: %s" % (i+1, d.label if hasattr(d, "label") else "")
            dim_type= "%s %s" % (d.dimension_type, "dimension")
            dimensions.append(ItemDescriptor(dim_name, type=dim_type, entity_type=NodeType.Dimension, block_id=block_id))
        return dimensions

    def request_data_frames(self, block_id):
        if self._nix_file.version[1] >= 2:
            b = self.get_block(block_id)
            return self._entity_info(b.data_frames, block_id, NodeType.DataFrame)
        return []

    def request_groups(self, block_id):
        b = self.get_block(block_id)
        return self._entity_info(b.groups, block_id, NodeType.Group)

    def request_sources(self, block_id, parent_source_id=None):
        def get_subsources(src):
            sub_sources = []
            for s in src.sources:
                self._entity_buffer.put(s)
                sub_sources.append(ItemDescriptor(s.name, s.id, s.type, definition=s.definition, entity_type=NodeType.Source))
            return sub_sources
        b = self.get_block(block_id)
        if parent_source_id is None:
            return self._entity_info(b.sources, block_id, NodeType.Source)
        else:
            srcs = b.find_sources(lambda s: s.id == parent_source_id)
            sources = []
            for src in srcs:
                sources.extend(get_subsources(src))
            return sources