# -*- coding: utf8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

from collections import defaultdict

from six import python_2_unicode_compatible, text_type
from six.moves import range

from efc.base.errors import BaseEFCException
from efc.utils import cached_property, col_index_to_str, digit, u

__all__ = ('Operand', 'ErrorOperand', 'ValueErrorOperand', 'WorksheetNotExist',
           'ZeroDivisionErrorOperand', 'SimpleOperand', 'SingleCellOperand',
           'CellSetOperand', 'SimpleSetOperand', 'NamedRangeOperand', 'CellRangeOperand',
           'FunctionNotSupported', 'NotFoundErrorOperand', 'RPNOperand', 'OperandLikeObject', 'OffsetMixin')


class OperandLikeObject(object):
    def __init__(self, ws_name=None, source=None, *args, **kwargs):
        super(OperandLikeObject, self).__init__(*args, **kwargs)
        self.ws_name = ws_name
        self.source = source


@python_2_unicode_compatible
class Operand(OperandLikeObject):
    value = None

    @property
    def digit(self):
        """Digit type"""
        return digit(self.value) if self.value else 0

    @property
    def string(self):
        """String type"""
        if isinstance(self.value, bool):
            return u(self.value).upper()
        elif self.value is not None:
            return u(self.value)
        else:
            return ''

    @property
    def is_blank(self):
        return self.value in (None, '')

    def __int__(self):
        return int(self.value)

    def __float__(self):
        return float(self.value)

    def __str__(self):
        return self.string

    def __trunc__(self):
        return self.__int__()


class ErrorOperand(OperandLikeObject, BaseEFCException):
    code = 300
    msg = 'Unknown error'
    string_value = '#ERROR!'

    def __init__(self, *args, **kwargs):
        self.formula = kwargs.pop('formula', None)
        super(ErrorOperand, self).__init__(*args, **kwargs)

    @property
    def value(self):
        raise self

    @property
    def string(self):
        return self.string_value

    def __getattr__(self, item):
        raise self


class ValueErrorOperand(ErrorOperand):
    code = 301
    msg = 'Cell value error'
    string_value = '#VALUE!'


class WorksheetNotExist(ErrorOperand):
    code = 302
    msg = 'Worksheet does not exist'
    string_value = '#REF!'


class NamedRangeNotExist(ErrorOperand):
    code = 303
    msg = 'Named range "{name}" does not exist'
    string_value = '#NAME?'

    def __init__(self, name, *args, **kwargs):
        super(NamedRangeNotExist, self).__init__(*args, **kwargs)
        self.name = name


class ZeroDivisionErrorOperand(ErrorOperand):
    code = 304
    msg = 'Zero division'
    string_value = '#DIV/0!'


class NotFoundErrorOperand(ErrorOperand):
    code = 305
    msg = 'Result not found'
    string_value = '#VALUE!'


class FunctionNotSupported(ErrorOperand):
    code = 306
    msg = 'Function "{f_name}" not found among available functions'
    string_value = '#NAME?'

    def __init__(self, f_name, *args, **kwargs):
        super(FunctionNotSupported, self).__init__(*args, **kwargs)
        self.f_name = f_name


class SimpleOperand(Operand):
    def __init__(self, value, *args, **kwargs):
        super(SimpleOperand, self).__init__(*args, **kwargs)
        self.value = value


class CellsOperand(OperandLikeObject):
    def address_to_value(self):
        raise NotImplementedError

    @cached_property
    def value(self):
        if self.source.has_worksheet(self.ws_name):
            return self.address_to_value()
        else:
            raise WorksheetNotExist(ws_name=self.ws_name)

    def get_iter(self):
        raise NotImplementedError

    def __iter__(self):
        return self.get_iter()


class OffsetMixin(object):
    def offset(self, row_offset=0, col_offset=0):
        raise NotImplementedError


class SingleCellOperand(CellsOperand, Operand, OffsetMixin):
    def __init__(self, row, column, row_fixed=False, column_fixed=False, *args, **kwargs):
        super(SingleCellOperand, self).__init__(*args, **kwargs)
        self.row = row
        self.column = column
        self.row_fixed = row_fixed
        self.column_fixed = column_fixed

    def address_to_value(self):
        return self.source.cell_to_value(self.row, self.column, self.ws_name)

    def get_iter(self):
        yield self

    @property
    def address(self):
        return "'%s'!%s%d" % (self.ws_name, col_index_to_str(self.column), self.row)

    def offset(self, row_offset=0, col_offset=0):
        new_operand = SingleCellOperand(row=self.row, column=self.column,
                                        row_fixed=self.row_fixed, column_fixed=self.column_fixed,
                                        ws_name=self.ws_name, source=self.source)
        if not new_operand.row_fixed:
            new_operand.row += row_offset
        if not new_operand.column_fixed:
            new_operand.column += col_offset
        return new_operand


class SetOperand(OperandLikeObject):
    operands_type = None

    def __init__(self, *args, **kwargs):
        super(SetOperand, self).__init__(*args, **kwargs)
        self._items = defaultdict(list)

    def check_type(self, items):
        if isinstance(items, list):
            if any(not isinstance(i, self.operands_type) for i in items):
                raise ValueErrorOperand()
        elif not isinstance(items, self.operands_type):
            raise ValueErrorOperand()

    def add_cell(self, item, row=0):
        self.check_type(item)
        self._items[row].append(item)

    def add_many(self, items, row=0):
        self.check_type(items)
        append = self._items[row].append
        for item in items:
            append(item)

    def add_row(self, items):
        self.check_type(items)
        r = max(self._items) + 1 if self._items else 1
        self.add_many(items, r)

    def get_iter(self):
        for r in sorted(self._items):
            for item in self._items[r]:
                yield item

    def __iter__(self):
        return self.get_iter()

    @cached_property
    def value(self):
        return list(self)


class CellSetOperand(SetOperand):
    operands_type = SingleCellOperand


class SimpleSetOperand(SetOperand):
    operands_type = SimpleOperand


class CellRangeOperand(CellsOperand, OffsetMixin):
    def __init__(self, row1, column1, row2, column2,
                 row1_fixed=False, column1_fixed=False, row2_fixed=False, column2_fixed=False,
                 *args, **kwargs):
        super(CellRangeOperand, self).__init__(*args, **kwargs)
        self.row1 = row1
        self.column1 = column1
        self.row2 = row2
        self.column2 = column2

        self.row1_fixed = row1_fixed
        self.column1_fixed = column1_fixed
        self.row2_fixed = row2_fixed
        self.column2_fixed = column2_fixed

    def get_iter(self):
        column1 = self.source.min_column(self.ws_name) if self.column1 is None else self.column1
        column2 = self.source.max_column(self.ws_name) if self.column2 is None else self.column2
        row1 = self.source.min_row(self.ws_name) if self.row1 is None else self.row1
        row2 = self.source.max_row(self.ws_name) if self.row2 is None else self.row2

        if row1 == row2:
            for c in range(column1, column2 + 1):
                yield SingleCellOperand(row1, c, ws_name=self.ws_name, source=self.source)
        elif column1 == column2:
            for r in range(row1, row2 + 1):
                yield SingleCellOperand(r, column1, ws_name=self.ws_name, source=self.source)
        else:
            for r in range(row1, row2 + 1):
                for c in range(column1, column2 + 1):
                    yield SingleCellOperand(r, c, ws_name=self.ws_name, source=self.source)

    def address_to_value(self):
        return self.get_iter()

    @property
    def address(self):
        return "'%s'!%s%d:%s%d" % (self.ws_name,
                                   col_index_to_str(self.column1), self.row1,
                                   col_index_to_str(self.column2), self.row2)

    def offset(self, row_offset=0, col_offset=0):
        new_operand = CellRangeOperand(row1=self.row1, column1=self.column1,
                                       row2=self.row2, column2=self.column2,
                                       row1_fixed=self.row1_fixed, column1_fixed=self.column1_fixed,
                                       row2_fixed=self.row2_fixed, column2_fixed=self.column2_fixed,
                                       ws_name=self.ws_name, source=self.source)

        if not new_operand.row1_fixed and new_operand.row1 is not None:
            new_operand.row1 += row_offset
        if not new_operand.column1_fixed and new_operand.column1 is not None:
            new_operand.column1 += col_offset
        if not new_operand.row2_fixed and new_operand.row2 is not None:
            new_operand.row2 += row_offset
        if not new_operand.column2_fixed and new_operand.column2 is not None:
            new_operand.column2 += col_offset
        return new_operand


class NamedRangeOperand(CellsOperand):
    def __init__(self, name, *args, **kwargs):
        super(NamedRangeOperand, self).__init__(*args, **kwargs)
        self.name = name

    def address_to_value(self):
        return self.source.named_range_to_cells(self.name, self.ws_name)

    @cached_property
    def value(self):
        if not self.source.has_worksheet(self.ws_name):
            raise WorksheetNotExist(ws_name=self.ws_name)
        elif not self.source.has_named_range(self.name, self.ws_name):
            raise NamedRangeNotExist(self.name, self.ws_name)
        else:
            return self.address_to_value()

    def get_iter(self):
        return iter(self.value)


@python_2_unicode_compatible
class RPNOperand(OperandLikeObject, OffsetMixin):
    def __init__(self, rpn, *args, **kwargs):
        super(RPNOperand, self).__init__(*args, **kwargs)
        self.rpn = rpn
        self._result = None

    @cached_property
    def evaluated_value(self):
        v = self.rpn.calc(ws_name=self.ws_name, source=self.source)
        if isinstance(v, RPNOperand):
            v = v.evaluated_value
        return v

    def __getattr__(self, item):
        return getattr(self.evaluated_value, item)

    def offset(self, row_offset=0, col_offset=0):
        return RPNOperand(rpn=self.rpn.offset(row_offset, col_offset), ws_name=self.ws_name, source=self.source)

    def __int__(self):
        return int(self.evaluated_value)

    def __float__(self):
        return float(self.evaluated_value)

    def __str__(self):
        return text_type(self.evaluated_value)

    def __trunc__(self):
        return self.__int__()
