#!/usr/bin/env python3

import datetime
from decimal import Decimal
from typing import Any, Union

from com.sun.star.sheet import CellFlags
from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA

from .easymain import (log, DATE_OFFSET,
    BaseObject, Color,
    dict_to_property, run_in_thread, set_properties
)
from .easydoc import LODocument
from .easyevents import EventsRangeSelectionListener, LOEvents
from .easyshape import LOShapes, LOShape
from .easydrawpage import LODrawPage
from .easyforms import LOForms


SECONDS_DAY = 60 * 60 * 24
ONLY_VALUES = CellFlags.VALUE + CellFlags.DATETIME + CellFlags.STRING


class LOCellStyle():

    def __init__(self, obj):
        self._obj = obj

    def __str__(self):
        return f'CellStyle: {self.name}'

    @property
    def obj(self):
        return self._obj

    @property
    def name(self):
        return self.obj.Name

    @property
    def properties(self):
        properties = self.obj.PropertySetInfo.Properties
        data = {p.Name: getattr(self.obj, p.Name) for p in properties}
        return data
    @properties.setter
    def properties(self, values):
        set_properties(self.obj, values)


class LOCellStyles():

    def __init__(self, obj, doc):
        self._obj = obj
        self._doc = doc

    def __len__(self):
        return len(self.obj)

    def __getitem__(self, index):
        return LOCellStyle(self.obj[index])

    def __setitem__(self, key, value):
        self.obj[key] = value

    def __delitem__(self, key):
        if not isinstance(key, str):
            key = key.Name
        del self.obj[key]

    def __contains__(self, item):
        return item in self.obj

    @property
    def obj(self):
        return self._obj

    @property
    def names(self):
        return self.obj.ElementNames

    def new(self, name: str):
        obj = self._doc.create_instance('com.sun.star.style.CellStyle')
        self.obj[name] = obj
        return LOCellStyle(obj)


# ~ IsFiltered,
# ~ IsManualPageBreak,
# ~ IsStartOfNewPage
class LOSheetRows():

    def __init__(self, sheet, obj):
        self._sheet = sheet
        self._obj = obj

    def __getitem__(self, index):
        if isinstance(index, int):
            rows = LOSheetRows(self._sheet, self.obj[index])
        else:
            rango = self._sheet[index.start:index.stop,0:]
            rows = LOSheetRows(self._sheet, rango.obj.Rows)
        return rows

    def __len__(self):
        return self.obj.Count

    def __str__(self):
        return self.obj.Name

    @property
    def obj(self):
        return self._obj

    @property
    def visible(self):
        return self._obj.IsVisible
    @visible.setter
    def visible(self, value):
        self._obj.IsVisible = value

    @property
    def color(self):
        return self.obj.CellBackColor
    @color.setter
    def color(self, value):
        self.obj.CellBackColor = value

    @property
    def is_transparent(self):
        return self.obj.IsCellBackgroundTransparent
    @is_transparent.setter
    def is_transparent(self, value):
        self.obj.IsCellBackgroundTransparent = value

    @property
    def height(self):
        return self.obj.Height
    @height.setter
    def height(self, value):
        self.obj.Height = value

    def optimal(self):
        self.obj.OptimalHeight = True
        return

    def insert(self, index, count):
        self.obj.insertByIndex(index, count)
        return

    def remove(self, index, count):
        self.obj.removeByIndex(index, count)
        return


class LOCalcRanges(object):

    def __init__(self, obj):
        self._obj = obj

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def __len__(self):
        return self._obj.Count

    def __iter__(self):
        self._index = 0
        return self

    def __next__(self):
        try:
            r = self.obj[self._index]
            rango = LOCalcRange(r)
        except IndexError:
            raise StopIteration

        self._index += 1
        return rango

    def __contains__(self, item):
        return self._obj.hasByName(item.name)

    def __getitem__(self, index):
        r = self.obj[index]
        rango = LOCalcRange(r)
        return rango

    def __str__(self):
        s = f'Ranges: {self.names}'
        return s

    @property
    def obj(self):
        return self._obj

    @property
    def names(self):
        return self.obj.ElementNames

    @property
    def data(self):
        rows = [r.data for r in self]
        return rows
    @data.setter
    def data(self, values):
        for i, data in enumerate(values):
            self[i].data = data

    @property
    def style(self):
        return ''
    @style.setter
    def style(self, value):
        for r in self:
            r.style = value

    def add(self, rangos: Any):
        if isinstance(rangos, LOCalcRange):
            rangos = (rangos,)
        for r in rangos:
            self.obj.addRangeAddress(r.range_address, False)
        return

    def remove(self, rangos: Any):
        if isinstance(rangos, LOCalcRange):
            rangos = (rangos,)
        for r in rangos:
            self.obj.removeRangeAddress(r.range_address)
        return


class LOCalcRange():
    CELL = 'ScCellObj'

    def __init__(self, obj):
        self._obj = obj
        self._is_cell = obj.ImplementationName == self.CELL

    def __contains__(self, rango):
        if isinstance(rango, LOCalcRange):
            address = rango.range_address
        else:
            address = rango.RangeAddress
        result = self.cursor.queryIntersection(address)
        return bool(result.Count)

    def __getitem__(self, index):
        return LOCalcRange(self.obj[index])

    def __iter__(self):
        self._r = 0
        self._c = 0
        return self

    def __next__(self):
        try:
            rango = self[self._r, self._c]
        except Exception as e:
            raise StopIteration
        self._c += 1
        if self._c == self.obj.Columns.Count:
            self._c = 0
            self._r +=1
        return rango

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def __len__(self):
        ra = self.range_address
        rows = ra.EndRow - ra.StartRow + 1
        return rows

    def __str__(self):
        s = f'Range: {self.name}'
        if self.is_cell:
            s = f'Cell: {self.name}'
        return s

    @property
    def obj(self):
        """Get original object pyUNO"""
        return self._obj

    @property
    def len_columns(self):
        """Get len columns"""
        ra = self.range_address
        cols = ra.EndColumn - ra.StartColumn + 1
        return cols

    @property
    def is_cell(self):
        """True if range is cell"""
        return self._is_cell

    @property
    def is_range(self):
        return not self._is_cell

    @property
    def name(self):
        """Return the range address like string"""
        return self.obj.AbsoluteName

    @property
    def name2(self):
        """Return the range address like string"""
        return self.name.replace('$', '')

    @property
    def address(self):
        """Return com.sun.star.table.CellAddress"""
        return self.obj.CellAddress

    @property
    def range_address(self):
        """Return com.sun.star.table.CellRangeAddress"""
        return self.obj.RangeAddress

    @property
    def sheet(self):
        """Return sheet parent"""
        return LOCalcSheet(self.obj.Spreadsheet)

    @property
    def doc(self):
        """Return doc parent"""
        doc = self.obj.Spreadsheet.DrawPage.Forms.Parent
        return LOCalc(doc)

    @property
    def style(self):
        """Get or set cell style"""
        return self.obj.CellStyle
    @style.setter
    def style(self, value):
        self.obj.CellStyle = value

    @property
    def current_region(self):
        """Return current region"""
        cursor = self.cursor
        cursor.collapseToCurrentRegion()
        rango = self.obj.Spreadsheet[cursor.AbsoluteName]
        return LOCalcRange(rango)

    @property
    def back_color(self):
        """Get or set cell back color"""
        return self._obj.CellBackColor
    @back_color.setter
    def back_color(self, value):
        self._obj.CellBackColor = Color()(value)

    @property
    def type(self):
        """Get type of content.
        See https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1table.html#affea688ab9e00781fa35d8a790d10f0e
        """
        return self.obj.Type

    @property
    def error(self):
        """Get number error in formulas"""
        return self.obj.getError()

    @property
    def string(self):
        """Get or set content like string"""
        return self.obj.String
    @string.setter
    def string(self, value):
        self.obj.setString(value)

    @property
    def float(self):
        """Get or set content like value"""
        return self.obj.Value
    @float.setter
    def float(self, value):
        self.obj.setValue(value)

    @property
    def formula(self):
        """Get or set formula"""
        return self.obj.Formula
    @formula.setter
    def formula(self, value):
        self.obj.setFormula(value)

    @property
    def date(self):
        """Get or set date"""
        value = int(self.obj.Value)
        date = datetime.date.fromordinal(value + DATE_OFFSET)
        return date
    @date.setter
    def date(self, value):
        d = value.toordinal()
        self.float = d - DATE_OFFSET

    @property
    def time(self):
        """Get or set time"""
        seconds = self.obj.Value * SECONDS_DAY
        time_delta = datetime.timedelta(seconds=seconds)
        time = (datetime.datetime.min + time_delta).time()
        return time
    @time.setter
    def time(self, value):
        d = (value.hour * 3600 + value.minute * 60 + value.second) / SECONDS_DAY
        self.float = d

    @property
    def datetime(self):
        """Get or set datetime"""
        return datetime.datetime.combine(self.date, self.time)
    @datetime.setter
    def datetime(self, value):
        d = value.toordinal()
        t = (value - datetime.datetime.fromordinal(d)).seconds / SECONDS_DAY
        self.float = d - DATE_OFFSET + t

    @property
    def value(self):
        """Get or set value, automatically get type data"""
        v = None
        if self.type == VALUE:
            v = self.float
        elif self.type == TEXT:
            v = self.string
        elif self.type == FORMULA:
            v = self.formula
        return v
    @value.setter
    def value(self, value):
        if isinstance(value, str):
            if value[0] in '=':
                self.formula = value
            else:
                self.string = value
        elif isinstance(value, Decimal):
            self.float  = float(value)
        elif isinstance(value, (int, float, bool)):
            self.float = value
        elif isinstance(value, datetime.datetime):
            self.datetime = value
        elif isinstance(value, datetime.date):
            self.date = value
        elif isinstance(value, datetime.time):
            self.time = value

    @property
    def data_array(self):
        """Get or set DataArray"""
        return self.obj.DataArray
    @data_array.setter
    def data_array(self, data):
        self.obj.DataArray = data

    @property
    def formula_array(self):
        """Get or set FormulaArray"""
        return self.obj.FormulaArray
    @formula_array.setter
    def formula_array(self, data):
        self.obj.FormulaArray = data

    @property
    def data(self):
        """Get like data_array, for set, if range is cell, automatically ajust size range.
        """
        return self.data_array
    @data.setter
    def data(self, values):
        if self.is_cell:
            cols = len(values[0])
            rows = len(values)
            if cols == 1 and rows == 1:
                self.data_array = values
            else:
                self.to_size(cols, rows).data = values
        else:
            self.data_array = values

    @property
    def dict(self):
        """Get or set data from to dict"""
        rows = self.data
        k = rows[0]
        data = [dict(zip(k, r)) for r in rows[1:]]
        return data
    @dict.setter
    def dict(self, values):
        data = [tuple(values[0].keys())]
        data += [tuple(d.values()) for d in values]
        self.data = data

    @property
    def next_free(self):
        """Get next free cell"""
        ra = self.current_region.range_address
        col = ra.StartColumn
        row = ra.EndRow + 1
        return LOCalcRange(self.sheet[row, col].obj)

    @property
    def range_data(self):
        """Get range without headers"""
        rango = self.current_region
        ra = rango.range_address
        row1 = ra.StartRow + 1
        row2 = ra.EndRow + 1
        col1 = ra.StartColumn
        col2 = ra.EndColumn + 1
        return LOCalcRange(self.sheet[row1:row2, col1:col2].obj)

    @property
    def array_formula(self):
        return self.obj.ArrayFormula
    @array_formula.setter
    def array_formula(self, value):
        self.obj.ArrayFormula = value

    @property
    def cursor(self):
        cursor = self.obj.Spreadsheet.createCursorByRange(self.obj)
        return cursor

    # ~ To doc
    @property
    def position(self):
        return self.obj.Position

    # ~ To doc
    @property
    def size(self):
        return self.obj.Size

    @property
    def height(self):
        return self.obj.Size.Height

    @property
    def width(self):
        return self.obj.Size.Width

    @property
    def x(self):
        return self.obj.Position.X

    @property
    def y(self):
        return self.obj.Position.Y

    # ~ To doc
    @property
    def possize(self):
        data = {
            'Width': self.size.Width,
            'Height': self.size.Height,
            'X': self.position.X,
            'Y': self.position.Y,
        }
        return data

    @property
    def rows(self):
        return LOSheetRows(self.sheet, self.obj.Rows)

    @property
    def row(self):
        r1 = self.address.Row
        r2 = r1 + 1
        ra = self.current_region.range_address
        c1 = ra.StartColumn
        c2 = ra.EndColumn + 1
        return LOCalcRange(self.sheet[r1:r2, c1:c2].obj)

    def clear(self, what: int=ONLY_VALUES):
        """Clear contents"""
        # ~ http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1CellFlags.html
        self.obj.clearContents(what)
        return

    def offset(self, rows=0, cols=1):
        ra = self.range_address
        col = ra.EndColumn + cols
        row = ra.EndRow + rows
        return LOCalcRange(self.sheet[row, col].obj)

    def to_size(self, cols: int, rows: int):
        cursor = self.cursor
        cursor.collapseToSize(cols, rows)
        rango = self.obj.Spreadsheet[cursor.AbsoluteName]
        return LOCalcRange(rango)


class LOCalcSheet(BaseObject):

    def __init__(self, obj):
        super().__init__(obj)

    def __getitem__(self, index):
        return LOCalcRange(self.obj[index])

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def __str__(self):
        return f'Sheet: {self.name}'

    @property
    def doc(self):
        return LOCalc(self.obj.DrawPage.Forms.Parent)

    @property
    def name(self):
        return self._obj.Name
    @name.setter
    def name(self, value):
        self._obj.Name = value

    @property
    def code_name(self):
        return self._obj.CodeName
    @code_name.setter
    def code_name(self, value):
        self._obj.CodeName = value

    @property
    def visible(self):
        return self._obj.IsVisible
    @visible.setter
    def visible(self, value):
        self._obj.IsVisible = value

    @property
    def color(self):
        return self._obj.TabColor
    @color.setter
    def color(self, value):
        self._obj.TabColor = Color()(value)

    @property
    def used_area(self):
        cursor = self.create_cursor()
        cursor.gotoEndOfUsedArea(True)
        return self[cursor.AbsoluteName]

    @property
    def is_protected(self):
        return self._obj.isProtected()

    @property
    def password(self):
        return ''
    @password.setter
    def password(self, value):
        self.obj.protect(value)

    @property
    def draw_page(self):
        return LODrawPage(self.obj.DrawPage)
    @property
    def dp(self):
        return self.draw_page
    @property
    def shapes(self):
        return self.draw_page

    @property
    def forms(self):
        return LOForms(self.obj.DrawPage)

    @property
    def events(self):
        return LOEvents(self.obj.Events)

    @property
    def height(self):
        return self.obj.Size.Height

    @property
    def width(self):
        return self.obj.Size.Width

    def protect(self, value):
        self.obj.protect(value)
        return

    def unprotect(self, value):
        try:
            self.obj.unprotect(value)
            return True
        except:
            pass
        return False

    def move(self, pos: int=-1):
        index = pos
        if pos < 0:
            index = len(self.doc)
        self.doc.move(self.name, index)
        return

    def remove(self):
        self.doc.remove(self.name)
        return

    def copy(self, new_name: str='', pos: int=-1):
        index = pos
        if pos < 0:
            index = len(self.doc)
        new_sheet = self.doc.copy_sheet(self.name, new_name, index)
        return new_sheet

    def copy_to(self, doc: Any, target: str='', pos: int=-1):
        """Copy sheet to other document.
        """
        index = pos
        if pos < 0:
            index = len(doc)

        new_name = target or self.name
        sheet = doc.copy_from(self.doc, self.name, new_name, index)
        return sheet

    def activate(self):
        """Activate sheet"""
        self.doc.activate(self.obj)
        return

    def create_cursor(self, rango: Any=None):
        if rango is None:
            cursor = self.obj.createCursor()
        else:
            obj = rango
            if hasattr(rango, 'obj'):
                obj = rango.obj
            cursor = self.obj.createCursorByRange(obj)
        return cursor


class LOCalcSheetsCodeName(BaseObject):
    """Access by code name sheet"""

    def __init__(self, obj):
        super().__init__(obj)
        self._sheets = obj.Sheets

    def __getitem__(self, index):
        """Index access"""
        for sheet in self._sheets:
            if sheet.CodeName == index:
                return LOCalcSheet(sheet)
        raise IndexError

    def __iter__(self):
        self._i = 0
        return self

    def __next__(self):
        """Interation sheets"""
        try:
            sheet = LOCalcSheet(self._sheets[self._i])
        except Exception as e:
            raise StopIteration
        self._i += 1
        return sheet

    def __contains__(self, item):
        """Contains"""
        for sheet in self._sheets:
            if sheet.CodeName == item:
                return True
        return False


class LOCalc(LODocument):
    """Classe for Calc module"""
    TYPE_RANGES = ('ScCellObj', 'ScCellRangeObj')
    RANGES = 'ScCellRangesObj'
    SHAPE = 'com.sun.star.drawing.SvxShapeCollection'
    _type = 'calc'

    def __init__(self, obj):
        super().__init__(obj)
        self._sheets = obj.Sheets
        self._listener_range_selection = None

    def __getitem__(self, index):
        """Index access"""
        return LOCalcSheet(self._sheets[index])

    def __setitem__(self, key: str, value: Any):
        """Insert new sheet"""
        self._sheets[key] = value

    def __len__(self):
        """Count sheets"""
        return self._sheets.Count

    def __contains__(self, item):
        """Contains"""
        return item in self._sheets

    def __iter__(self):
        self._i = 0
        return self

    def __next__(self):
        """Interation sheets"""
        try:
            sheet = LOCalcSheet(self._sheets[self._i])
        except Exception as e:
            raise StopIteration
        self._i += 1
        return sheet

    def __str__(self):
        return f'Calc: {self.title}'

    @property
    def sheets(self):
        """Acces by code name sheet"""
        return LOCalcSheetsCodeName(self.obj)

    @property
    def headers(self):
        """Get true if is visible columns/rows headers"""
        return self._cc.ColumnRowHeaders
    @headers.setter
    def headers(self, value):
        """Set visible columns/rows headers"""
        self._cc.ColumnRowHeaders = value

    @property
    def tabs(self):
        """Get true if is visible tab sheets"""
        return self._cc.SheetTabs
    @tabs.setter
    def tabs(self, value):
        """Set visible tab sheets"""
        self._cc.SheetTabs = value

    @property
    def selection(self):
        """Get current seleccion"""
        sel = None
        selection = self.obj.CurrentSelection
        type_obj = selection.ImplementationName

        if type_obj in self.TYPE_RANGES:
            sel = LOCalcRange(selection)
        elif type_obj == self.RANGES:
            sel = LOCalcRanges(selection)
        elif type_obj == self.SHAPE:
            if len(selection) == 1:
                sel = LOShape(selection[0])
            else:
                sel = LOShapes(selection)
        else:
            log.debug(type_obj)
        return sel

    @property
    def active(self):
        """Get active sheet"""
        return LOCalcSheet(self._cc.ActiveSheet)

    @property
    def names(self):
        """Get all sheet names"""
        names = self.obj.Sheets.ElementNames
        return names

    @property
    def events(self):
        """Get class events"""
        return LOEvents(self.obj.Events)

    def ranges(self):
        """Create ranges container"""
        obj = self._create_instance('com.sun.star.sheet.SheetCellRanges')
        return LOCalcRanges(obj)

    def new_sheet(self):
        """Create new instance sheet"""
        s = self._create_instance('com.sun.star.sheet.Spreadsheet')
        return s

    def select(self, rango: Any):
        """Select range"""
        obj = rango
        if hasattr(rango, 'obj'):
            obj = rango.obj
        self._cc.select(obj)
        return

    @run_in_thread
    def start_range_selection(self, controllers: Any, args: dict={}):
        """Start select range selection by user

        `See Api RangeSelectionArguments <https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1sheet_1_1RangeSelectionArguments.html>`_
        """
        if args:
            args['CloseOnMouseRelease'] = args.get('CloseOnMouseRelease', True)
        else:
            args = dict(
                Title = 'Please select a range',
                CloseOnMouseRelease = True)
        properties = dict_to_property(args)

        self._listener_range_selection = EventsRangeSelectionListener(controllers(self))
        self._cc.addRangeSelectionListener(self._listener_range_selection)
        self._cc.startRangeSelection(properties)
        return

    def remove_range_selection_listener(self):
        """Remove range listener"""
        if not self._listener_range_selection is None:
            self._cc.removeRangeSelectionListener(self._listener_range_selection)
        return

    def activate(self, sheet: Union[str, LOCalcSheet]):
        """Activate sheet

        :param sheet: Sheet to activate
        :type sheet: str, pyUno or LOCalcSheet
        """
        obj = sheet
        if isinstance(sheet, LOCalcSheet):
            obj = sheet.obj
        elif isinstance(sheet, str):
            obj = self._sheets[sheet]
        self._cc.setActiveSheet(obj)
        return

    def insert(self, name: Union[str, list, tuple]):
        """Insert new sheet

        :param name: Name new sheet, or iterable with names.
        :type name: str, list or tuple
        :return: New last instance sheet.
        :rtype: LOCalcSheet
        """
        names = name
        if isinstance(name, str):
            names = (name,)
        for n in names:
            self._sheets[n] = self._create_instance('com.sun.star.sheet.Spreadsheet')
        return LOCalcSheet(self._sheets[n])

    def move(self, sheet: Union[str, LOCalcSheet], pos: int=-1):
        """Move sheet name to position

        :param sheet: Instance or Name sheet to move
        :type sheet: LOCalcSheet or str
        :param pos: New position, if pos=-1 move to end
        :type pos: int
        """
        name = sheet
        index = pos
        if pos < 0:
            index = len(self)
        if isinstance(sheet, LOCalcSheet):
            name = sheet.name
        self._sheets.moveByName(name, index)
        return

    def remove(self, sheet: Union[str, LOCalcSheet]):
        """Remove sheet by name

        :param sheet: Instance or Name sheet to move
        :type sheet: LOCalcSheet or str
        """
        name = sheet
        if isinstance(sheet, LOCalcSheet):
            name = sheet.name
        self._sheets.removeByName(name)
        return

    def _get_new_name_sheet(self, name):
        i = 1
        new_name = f'{name}_{i}'
        while new_name in self:
            i += 1
            new_name = f'{name}_{i}'
        return new_name

    def copy_sheet(self, sheet: Union[str, LOCalcSheet], new_name: str='', pos: int=-1):
        """Copy sheet
        """
        name = sheet
        if isinstance(sheet, LOCalcSheet):
            name = sheet.name
        index = pos
        if pos < 0:
            index = len(self)
        if not new_name:
            new_name = self._get_new_name_sheet(name)
        self._sheets.copyByName(name, new_name, index)
        return LOCalcSheet(self._sheets[new_name])

    def copy_from(self, doc: Any, source: Any=None, target: Any=None, pos: int=-1):
        """Copy sheet from document
        """
        index = pos
        if pos < 0:
            index = len(self)

        names = source
        if not source:
            names = doc.names
        elif isinstance(source, str):
            names = (source,)
        elif isinstance(source, LOCalcSheet):
            names = (source.name,)

        new_names = target
        if not target:
            new_names = names
        elif isinstance(target, str):
            new_names = (target,)

        for i, name in enumerate(names):
            self._sheets.importSheet(doc.obj, name, index + i)
            self[index + i].name = new_names[i]

        return LOCalcSheet(self._sheets[index])

    def sort(self, reverse=False):
        """Sort sheets by name

        :param reverse: For order in reverse
        :type reverse: bool
        """
        names = sorted(self.names, reverse=reverse)
        for i, n in enumerate(names):
            self.move(n, i)
        return

    def get_ranges(self, address: str):
        """Get the same range address in each sheet.
        """
        ranges = self.ranges()
        ranges.add([sheet[address] for sheet in self])
        return ranges

    @property
    def cs(self):
        return self.cell_styles
    @property
    def cell_styles(self):
        obj = self.obj.StyleFamilies['CellStyles']
        return LOCellStyles(obj, self)
