#This file is part of Tryton.  The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from string import Template
import time
from itertools import izip
from ..model import ModelView, ModelSQL, fields
from ..tools import datetime_strftime
from ..pyson import Eval, And
from ..transaction import Transaction
from ..pool import Pool
from ..config import CONFIG
from ..backend import TableHandler

__all__ = [
    'SequenceType', 'Sequence', 'SequenceStrict',
    ]

sql_sequence = CONFIG.options['db_type'] == 'postgresql'


class SequenceType(ModelSQL, ModelView):
    "Sequence type"
    __name__ = 'ir.sequence.type'

    name = fields.Char('Sequence Name', required=True, translate=True)
    code = fields.Char('Sequence Code', required=True)


class Sequence(ModelSQL, ModelView):
    "Sequence"
    __name__ = 'ir.sequence'

    _strict = False
    name = fields.Char('Sequence Name', required=True, translate=True)
    code = fields.Selection('code_get', 'Sequence Code', required=True,
        states={
            'readonly': Eval('context', {}).contains('code'),
            })
    active = fields.Boolean('Active')
    prefix = fields.Char('Prefix')
    suffix = fields.Char('Suffix')
    type = fields.Selection([
        ('incremental', 'Incremental'),
        ('decimal timestamp', 'Decimal Timestamp'),
        ('hexadecimal timestamp', 'Hexadecimal Timestamp'),
        ], 'Type')
    number_next_internal = fields.Integer('Next Number',
        states={
            'invisible': ~Eval('type').in_(['incremental']),
            'required': And(Eval('type').in_(['incremental']),
                not sql_sequence),
            }, depends=['type'])
    number_next = fields.Function(number_next_internal, 'get_number_next',
        'set_number_next')
    number_increment = fields.Integer('Increment Number',
        states={
            'invisible': ~Eval('type').in_(['incremental']),
            'required': Eval('type').in_(['incremental']),
            }, depends=['type'])
    padding = fields.Integer('Number padding',
        states={
            'invisible': ~Eval('type').in_(['incremental']),
            'required': Eval('type').in_(['incremental']),
            }, depends=['type'])
    timestamp_rounding = fields.Float('Timestamp Rounding', required=True,
        states={
            'invisible': ~Eval('type').in_(
                ['decimal timestamp', 'hexadecimal timestamp']),
            }, depends=['type'])
    timestamp_offset = fields.Float('Timestamp Offset', required=True,
        states={
            'invisible': ~Eval('type').in_(
                ['decimal timestamp', 'hexadecimal timestamp']),
            }, depends=['type'])
    last_timestamp = fields.Integer('Last Timestamp',
        states={
            'invisible': ~Eval('type').in_(
                ['decimal timestamp', 'hexadecimal timestamp']),
            'required': Eval('type').in_(
                ['decimal timestamp', 'hexadecimal timestamp']),
            }, depends=['type'])

    @classmethod
    def __setup__(cls):
        super(Sequence, cls).__setup__()
        cls._sql_constraints += [
            ('check_timestamp_rounding', 'CHECK(timestamp_rounding > 0)',
                'Timestamp rounding should be greater than 0'),
            ]
        cls._error_messages.update({
                'missing': 'Missing sequence.',
                'invalid_prefix': ('Invalid prefix "%(prefix)s" on sequence '
                    '"%(sequence)s".'),
                'invalid_suffix': ('Invalid suffix "%(suffix)s" on sequence '
                    '"%(sequence)s".'),
                'future_last_timestamp': ('Last Timestamp cannot be in the '
                    'future on sequence "%s".'),
                })

    @classmethod
    def __register__(cls, module_name):
        cursor = Transaction().cursor
        table = TableHandler(cursor, cls, module_name)

        # Migration from 2.0 rename number_next into number_next_internal
        table.column_rename('number_next', 'number_next_internal')

        super(Sequence, cls).__register__(module_name)

        # Migration from 2.0 create sql_sequence
        if sql_sequence and not cls._strict:
            sequences = cls.search([])
            for sequence in sequences:
                if sequence.type != 'incremental':
                    continue
                if not TableHandler.sequence_exist(cursor,
                        sequence._sql_sequence_name):
                    sequence.create_sql_sequence(sequence.number_next_internal)

    @staticmethod
    def default_active():
        return True

    @staticmethod
    def default_type():
        return 'incremental'

    @staticmethod
    def default_number_increment():
        return 1

    @staticmethod
    def default_number_next():
        return 1

    @staticmethod
    def default_padding():
        return 0

    @staticmethod
    def default_timestamp_rounding():
        return 1.0

    @staticmethod
    def default_timestamp_offset():
        return 946681200.0  # Offset for 2000-01-01

    @staticmethod
    def default_last_timestamp():
        return 0.0

    @staticmethod
    def default_code():
        return Transaction().context.get('code')

    def get_number_next(self, name):
        cursor = Transaction().cursor
        sql_name = self._sql_sequence_name
        if sql_sequence and not self._strict:
            cursor.execute('SELECT '
                'CASE WHEN NOT is_called THEN last_value '
                    'ELSE last_value + increment_by '
                'END FROM "%s"' % sql_name)
            return cursor.fetchone()[0]
        else:
            return self.number_next_internal

    @classmethod
    def set_number_next(cls, sequences, name, value):
        super(Sequence, cls).write(sequences, {
                'number_next_internal': value,
                })

    @classmethod
    def create(cls, vlist):
        sequences = super(Sequence, cls).create(vlist)
        for sequence, values in izip(sequences, vlist):
            if sql_sequence and not cls._strict:
                sequence.update_sql_sequence(values.get('number_next',
                        cls.default_number_next()))
        return sequences

    @classmethod
    def write(cls, sequences, values):
        result = super(Sequence, cls).write(sequences, values)
        if sql_sequence and not cls._strict:
            for sequence in sequences:
                sequence.update_sql_sequence(values.get('number_next'))
        return result

    @classmethod
    def delete(cls, sequences):
        if sql_sequence and not cls._strict:
            for sequence in sequences:
                sequence.delete_sql_sequence()
        return super(Sequence, cls).delete(sequences)

    @classmethod
    def code_get(cls):
        pool = Pool()
        SequenceType = pool.get('ir.sequence.type')
        sequence_types = SequenceType.search([])
        return [(x.code, x.name) for x in sequence_types]

    @classmethod
    def validate(cls, sequences):
        super(Sequence, cls).validate(sequences)
        cls.check_prefix_suffix(sequences)
        cls.check_last_timestamp(sequences)

    @classmethod
    def check_prefix_suffix(cls, sequences):
        "Check prefix and suffix"

        for sequence in sequences:
            for fix, error_message in ((sequence.prefix, 'invalid_prefix'),
                    (sequence.suffix, 'invalid_suffix')):
                try:
                    cls._process(sequence.prefix)
                    cls._process(sequence.suffix)
                except Exception:
                    cls.raise_user_error(error_message, {
                            'prefix': fix,
                            'sequence': sequence.rec_name,
                            })

    @classmethod
    def check_last_timestamp(cls, sequences):
        "Check last_timestamp"

        for sequence in sequences:
            next_timestamp = cls._timestamp(sequence)
            if sequence.last_timestamp > next_timestamp:
                cls.raise_user_error('future_last_timestamp', (
                        sequence.rec_name,))

    @property
    def _sql_sequence_name(self):
        'Return SQL sequence name'
        return '%s_%s' % (self._table, self.id)

    def create_sql_sequence(self, number_next=None):
        'Create the SQL sequence'
        cursor = Transaction().cursor
        if self.type != 'incremental':
            return
        if number_next is None:
            number_next = self.number_next
        cursor.execute('CREATE SEQUENCE "' + self._sql_sequence_name
            + '" INCREMENT BY %s START WITH %s',
            (self.number_increment, number_next))

    def update_sql_sequence(self, number_next=None):
        'Update the SQL sequence'
        cursor = Transaction().cursor
        exist = TableHandler.sequence_exist(cursor, self._sql_sequence_name)
        if self.type != 'incremental':
            if exist:
                self.delete_sql_sequence()
            return
        if not exist:
            self.create_sql_sequence(number_next)
            return
        if number_next is None:
            number_next = self.number_next
        cursor.execute('ALTER SEQUENCE "' + self._sql_sequence_name
            + '" INCREMENT BY %s RESTART WITH %s',
            (self.number_increment, number_next))

    def delete_sql_sequence(self):
        'Delete the SQL sequence'
        cursor = Transaction().cursor
        if self.type != 'incremental':
            return
        cursor.execute('DROP SEQUENCE "%s"'
            % self._sql_sequence_name)

    @staticmethod
    def _process(string, date=None):
        pool = Pool()
        Date = pool.get('ir.date')
        if not date:
            date = Date.today()
        year = datetime_strftime(date, '%Y')
        month = datetime_strftime(date, '%m')
        day = datetime_strftime(date, '%d')
        return Template(string or '').substitute(
                year=year,
                month=month,
                day=day,
                )

    @staticmethod
    def _timestamp(sequence):
        return int((time.time() - sequence.timestamp_offset)
                / sequence.timestamp_rounding)

    @classmethod
    def _get_sequence(cls, sequence):
        if sequence.type == 'incremental':
            if sql_sequence and not cls._strict:
                cursor = Transaction().cursor
                cursor.execute('SELECT nextval(\'"%s"\')'
                    % sequence._sql_sequence_name)
                number_next, = cursor.fetchone()
            else:
                #Pre-fetch number_next
                number_next = sequence.number_next_internal

                with Transaction().set_user(0):
                    cls.write([sequence], {
                            'number_next_internal': (number_next
                                + sequence.number_increment),
                            })
            return '%%0%sd' % sequence.padding % number_next
        elif sequence.type in ('decimal timestamp', 'hexadecimal timestamp'):
            timestamp = sequence.last_timestamp
            while timestamp == sequence.last_timestamp:
                timestamp = cls._timestamp(sequence)
            with Transaction().set_user(0):
                cls.write([sequence], {
                    'last_timestamp': timestamp,
                    })
            if sequence.type == 'decimal timestamp':
                return '%d' % timestamp
            else:
                return hex(timestamp)[2:].upper()
        return ''

    @classmethod
    def get_id(cls, domain):
        '''
        Return sequence value for the domain
        '''
        if isinstance(domain, cls):
            domain = domain.id
        if isinstance(domain, (int, long)):
            domain = [('id', '=', domain)]

        # bypass rules on sequences
        with Transaction().set_context(user=False):
            with Transaction().set_user(0):
                try:
                    sequence, = cls.search(domain, limit=1)
                except TypeError:
                    cls.raise_user_error('missing')
            date = Transaction().context.get('date')
            return '%s%s%s' % (
                cls._process(sequence.prefix, date=date),
                cls._get_sequence(sequence),
                cls._process(sequence.suffix, date=date),
                )

    @classmethod
    def get(cls, code):
        return cls.get_id([('code', '=', code)])


class SequenceStrict(Sequence):
    "Sequence Strict"
    __name__ = 'ir.sequence.strict'
    _table = 'ir_sequence_strict'  # Needed to override Sequence._table
    _strict = True

    @classmethod
    def get_id(cls, clause):
        Transaction().cursor.lock(cls._table)
        return super(SequenceStrict, cls).get_id(clause)
