#!/usr/bin/env python

# 
#  Copyright 2013 Simone Campagna
# 
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
# 
#      http://www.apache.org/licenses/LICENSE-2.0
# 
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
#

__author__ = 'Simone Campagna'
__copyright__ = 'Copyright (c) 2013 Simone Campagna'
__license__ = 'Apache License Version 2.0'
__version__ = '1.0'

import os
import sys
import time

from .record import Record, MetaRecord
from .litebs_environment import USERNAME

class DTType(int):
    INF = -1
    INF_LABEL = 'INF'

    @classmethod
    def fromstring(cls, init):
        return cls(cls._string2int(init))

    @classmethod
    def _string2int(cls, init):
        raise NotImplemented()

    def __repr__(self):
       return "{0}({1!r})".format(self.__class__.__name__, str(self))

class Time(DTType):
    INF = -1
    INF_LABEL = 'INF'
    TIME_FORMAT = "%Y%m%d %H:%M:%S"
    TIME_CONVERTER = time.localtime
    #TIME_CONVERTER = time.gmtime
    def __new__(cls, init=None):
        if init is None:
            init = time.time()
        elif isinstance(init, str):
            init = cls._string2int(init)
        return DTType.__new__(cls, init)

    @classmethod
    def quantized(cls, time_quantum, t):
        if time_quantum > 0:
            qt = (int(t + time_quantum - 1) // time_quantum ) * time_quantum
            return cls(qt)
        else:
            return cls(t)
    @classmethod
    def _string2int(cls, init):
        if init == cls.INF_LABEL:
            init = -1
        else:
            init = time.mktime(time.strptime(init, cls.TIME_FORMAT))
        return init

    def __str__(self):
        if self == 0:
            return ''
        elif self < 0:
            return self.INF_LABEL
        else:
            return time.strftime(self.TIME_FORMAT, self.TIME_CONVERTER(self))


class Duration(DTType):
    INF = -1
    INF_LABEL = "INF"
    def __new__(cls, init=None):
        if init is None:
            init = -1
        elif isinstance(init, str):
            if init == cls.INF_LABEL:
                init = -1
            else:
                init = int(cls._string2int(init))
        return DTType.__new__(cls, init)

    @classmethod
    def _string2int(cls, init):
        if init == cls.INF_LABEL:
            init = -1
        else:
            l = init.split('+', 1)
            if len(l) == 2:
                n_days = int(l[0].strip())
                s_rest = l[1].strip()
            else:
                n_days = 0
                s_rest = l[0].strip()
            l = s_rest.split(':', 2)
            if len(l) == 3:
                n_hours = int(l[0].strip())
                n_minutes = int(l[1].strip())
                n_seconds = int(l[2].strip())
            elif len(l) == 2:
                n_hours = 0
                n_minutes = int(l[0].strip())
                n_seconds = int(l[1].strip())
            elif len(l) == 1:
                n_hours = 0
                n_minutes = 0
                n_seconds = int(l[0].strip())
            init = n_seconds + 60 * (n_minutes + 60 * (n_hours + 24 * n_days))
        return init

    @classmethod
    def fromstring(cls, init):
        return cls(cls._string2int(init))

    def __str__(self):
        if self == 0:
            return '0'
        elif self < 0:
            return self.INF_LABEL
        else:
            i_self = int(self)
            n_microseconds = (self - i_self) * 1000000
            n_days, i_rest = divmod(i_self, 86400)
            n_hours, i_rest = divmod(i_rest, 3600)
            n_minutes, n_seconds = divmod(i_rest, 60)
            if n_microseconds:
                ms = '.{0:6d}'.format(n_microseconds)
            else:
                ms = ''
            if n_days or n_hours:
                l_hms = [n_hours, n_minutes, n_seconds]
            else:
                l_hms = [n_minutes, n_seconds]
            hms = ':'.join('{0:02d}'.format(n) for n in l_hms)
            if n_days:
                fmt = "{0}+{1}{2}"
            else:
                fmt = "{1}{2}"
            return fmt.format(n_days, hms, ms)

class Bool(int):
    TRUE = 1
    FALSE = 0
    def __new__(cls, init=None):
        if isinstance(init, str):
            if init == "True":
                init = True
            elif init == "False":
                init = False
            else:
                raise ValueError("invalid value {0!r} for {1} type".format(init, cls.__name__))
        return int.__new__(cls, init)

    def __str__(self):
        if self:
            return 'True'
        else:
            return 'False'

class NodesRequest(str):
    def validate(self):
        for node_request in self.split():
            node_request.validate()

    def split(self, node_request_class=None):
        if node_request_class is None:
            node_request_class = NodeRequest
        return [node_request_class.fromstring(nr) for nr in super(NodesRequest, self).split('+')]

    @classmethod
    def join(cls, node_requests):
        return cls('+'.join(node_request.tostring() for node_request in node_requests))

class MetaRequest(type):
    def __new__(cls, class_name, class_bases, class_dict):
        t = super().__new__(cls, class_name, class_bases, class_dict)
        t.ANY_SYMBOLS = {t.ANY_SYMBOL}
        t.ANY_SYMBOLS.update(t.ANY_ALTERNATE_SYMBOLS) 
        t.ALL_SYMBOLS = {t.ALL_SYMBOL}
        t.ALL_SYMBOLS.update(t.ALL_ALTERNATE_SYMBOLS) 
        return t
class Request(int, metaclass=MetaRequest):
    ANY = -1
    ANY_SYMBOL = 'ANY'
    ANY_ALTERNATE_SYMBOLS = {'*'}
    ALL = -2
    ALL_SYMBOL = 'ALL'
    ALL_ALTERNATE_SYMBOLS = {'@'}
    def __new__(cls, init):
        if isinstance(init, str):
            return cls.fromstring(init)
        else:
            return int.__new__(cls, init)

    @classmethod
    def fromstring(cls, init):
        if init in cls.ANY_SYMBOLS:
            return int.__new__(cls, cls.ANY)
        elif init in cls.ALL_SYMBOLS:
            return int.__new__(cls, cls.ALL)
        else:
            return int.__new__(cls, init)
   
    def __str__(self):
        #if self == self.ANY:
        if self == self.ALL:
            return self.ALL_SYMBOL
        elif self < 0:
            return self.ANY_SYMBOL
        else:
            return super(Request, self).__str__()
        
class NodeRequest(Record):
    FIELDS = (
	('nodes',		dict(type=Request, default=Request.ANY)),
	('node_name',		dict(type=str, default='')),
	('procs',		dict(type=Request, default=Request.ANY)),
    )

    def tostring(self):
        if self.node_name:
            return "{0}:{1}".format(self.node_name, self.procs)
        else:
            return "{0}:{1}".format(self.nodes, self.procs)

    @classmethod
    def fromstring(cls, opt_str):
        np = [e.strip() for e in opt_str.split(":", 1)]
        nodes = Request.ANY
        procs = Request.ANY
        node_name = ''
        try:
            nodes = Request(np[0])
        except ValueError:
            handle_error(trace=True)
            nodes = 1
            node_name = np[0]
            #raise ValueError("option {0}: invalid nodes value {1!r}".format(opt_str, np[0]))
        if len(np) == 2:
            for kv in np[1].split(','):
                kvl = kv.split('=', 1)
                if len(kvl) == 1:
                    try:
                        procs = Request(kv)
                    except ValueError:
                        raise ValueError("option {0}: invalid procs value {1!r}".format(opt_str, np[1]))
                else:
                    k, v = kvl
                    if k == 'procs':
                        try:
                            procs = Request(v)
                        except ValueError:
                            raise ValueError("option {0}: invalid procs value {1!r}".format(opt_str, v))
                    elif k == 'name':
                        node_name = v
        else:
            procs = Request.ANY
        if node_name:
            nodes = 1
        return cls(nodes=nodes, node_name=node_name, procs=procs)

    def is_floating(self):
        if (not self.node_name) and nodes < 0:
            return True
        elif procs < 0:
            return True
        else:
            return False

class JobRequest(NodeRequest):
    pass

class ReservationRequest(NodeRequest):
    pass

class Node(Record):
    STATUS_OPEN = 'Open'
    STATUS_CLOSED = 'Closed'
    FIELDS = (
	('node_id',		dict(type=int, sql_primary_key=True)),
	('node_name',		dict(type=str, sql_unique_key=True)),
	('status',		dict(type=str, default=STATUS_OPEN)),
	('ssh_interface',	dict(type=str)),
	('mpi_interface',	dict(type=str)),
	#('running',		dict(type=Bool, default=Bool.TRUE)),
	('procs',		dict(type=int)),
	('busy_procs',		dict(type=int, default=0)),
    )

    @property
    def free_procs(self):
        return self.procs - self.busy_procs

class Queue(Record):
    STATUS_OPEN = 'Open'
    STATUS_CLOSED = 'Closed'
    FIELDS = (
	('queue_id',		dict(type=int, sql_primary_key=True)),
	('queue_name',		dict(type=str, sql_unique_key=True)),
	('node_group_name',	dict(type=str, default='ALL')),
	('priority',		dict(type=Duration, default=Duration(0))),
	('status',		dict(type=str, default=STATUS_OPEN)),
	('max_duration',	dict(type=int, default=-1)),
	('max_tot_nodes',	dict(type=int, default=-1)),
	('max_tot_procs',	dict(type=int, default=-1)),
	('max_job_nodes',	dict(type=int, default=-1)),
	('max_job_procs',	dict(type=int, default=-1)),
	('max_running_jobs',	dict(type=int, default=-1)),
	('max_queued_jobs',	dict(type=int, default=-1)),
    )

class NodeGroup(Record):
    STATUS_OPEN = 'Open'
    STATUS_CLOSED = 'Closed'
    FIELDS = (
	('node_group_id',	dict(type=int, sql_primary_key=True)),
	('node_group_name',	dict(type=str, sql_unique_key=True)),
	('status',		dict(type=str, default=STATUS_OPEN)),
    )
    PROPERTIES = (
	'nodes',
    )
    def __init__(self, *l_args, **kw_args):
        super(NodeGroup, self).__init__(*l_args, **kw_args)
        self._node_group_bindings = ()

    def bind(self, node_group_bindings):
        self._node_group_bindings = tuple(node_group_bindings)

    @property
    def node_list(self):
        return tuple(node_group_binding.node_name for node_group_binding in self._node_group_bindings)
    
    @property
    def nodes(self):
        return ', '.join(self.node_list)
    
class NodeGroupBinding(Record):
    FIELDS = (
	('node_group_binding_id',	dict(type=int, sql_primary_key=True)),
	('node_group_name',		dict(type=str)),
	('node_name',			dict(type=str)),
    )

class _JRRecord(Record):
    NODE_REQUEST_CLASS = NodeRequest
    BINDING_CLASS = None
    PROPERTIES = (
	'tot_request',
	'nodes',
    )
    def __init__(self, *l_args, **kw_args):
        super(_JRRecord, self).__init__(*l_args, **kw_args)
        self._bindings = ()

    @property
    def node_requests(self):
        node_requests_p = []
        for node_request in self.nodes_request.split(self.NODE_REQUEST_CLASS):
            prio = 0
            if node_request.node_name:
                prio += 1000
            elif node_request.nodes > 0:
                prio += 100
            elif node_request.procs > 0:
                prio += 10
            node_requests_p.append((node_request, prio))
        node_requests_p.sort(key=lambda x: -x[-1])
        node_requests = tuple(node_request for node_request, prio in node_requests_p)
        return node_requests

    @property
    def tot_request(self):
        tot_nodes, tot_procs = 0, 0
        flt_nodes, flt_procs = False, False
        for node_request in self.node_requests:
            nodes, procs = node_request.nodes, node_request.procs
            if nodes < 0:
                flt_nodes = True
            if procs < 0:
                flt_procs = True
            tot_nodes += nodes
            tot_procs += nodes * procs
        if flt_nodes:
            tot_nodes = -1
        if flt_procs:
            tot_procs = -1
        return self.NODE_REQUEST_CLASS(nodes=tot_nodes, procs=tot_procs)
    
    def bind(self, bindings):
        self._bindings = tuple(bindings)

    @property
    def node_list(self):
        return tuple((binding.node_name, binding.procs) for binding in self._bindings)
    
    @property
    def nodes(self):
        return '+'.join('{0}:{1}'.format(node_name, procs) for node_name, procs in self.node_list)
    
    #@property
    #def node_list(self):
    #    return tuple(binding.node_name for binding in self._bindings)
    
    #@property
    #def nodes(self):
    #    return ', '.join(self.node_list)
    
class ReservationBinding(Record):
    FIELDS = (
	('reservation_binding_id',	dict(type=int, sql_primary_key=True)),
	('reservation_id',		dict(type=int, default=0)),
	('node_name',			dict(type=str)),
	('procs',			dict(type=int)),
    )

class StrList(str):
    SEPARATOR = ','
    def split(self):
        return [e.strip() for e in super(StrList, self).split(self.SEPARATOR)]
    def join(self, lst):
        return self.SEPARATOR.join(lst)

class Reservation(_JRRecord):
    STATUS_WAITING = 'W'
    STATUS_ACTIVE = 'A'
    STATUS_COMPLETED = 'C'
    ALL_STATUS_SET = {STATUS_WAITING, STATUS_ACTIVE, STATUS_COMPLETED}
    FIELDS = (
	('reservation_id',		dict(type=int, sql_primary_key=True)),
	('status',			dict(type=str, default=STATUS_WAITING)),
	('start_time',			dict(type=Time)),
	('duration',			dict(type=Duration, default=Duration.INF)),
	('complete_time',		dict(type=Time, default=0)),
	('nodes_request',		dict(type=NodesRequest)),
	('username',			dict(type=str, default=USERNAME)),
	('users',			dict(type=StrList, default=USERNAME)),
    )
#    PROPERTIES = (
#	'nodes',
#	'tot_request',
#    )
    NODE_REQUEST_CLASS = ReservationRequest
    BINDING_CLASS = ReservationBinding

    def __contains__(self, t):
        if self.duration < 0:
            return self.start_time <= t
        else:
            return self.start_time <= t < self.start_time + self.duration

    def validate(self, now=None):
        if now is None:
            now = Time()
        if self.start_time > 0 and self.start_time < now:
            raise ValueError("start_time {0} is in the past".format(self.start_time))
        if self.duration > 0 and self.start_time + self.duration < self.start_time:
         raise ValueError("start_time {0} is greater than end_time {1}".format(self.start_time, self.start_time + self.duration))

        self.nodes_request.validate()

    def set_held(self, t):
        self.status = self.STATUS_HELD
        self.queue_time = t

class NodeAllocation(Record):
    FIELDS = (
	('start_time',			dict(type=Time)),
	('duration',			dict(type=Duration, default=Duration.INF)),
	('procs',			dict(type=int)),
    )

    def end_time(self):
        if self.duration < 0:
            return Time.INF
        elif self.start_time < 0:
            return Time.INF
        elif self.start_time == 0:
            return self.start_time
        else:
            return Time(self.start_time + self.duration)
            
class Cluster(Record):
    FIELDS = (
	('cluster_id',				dict(type=int, sql_primary_key=True)),
	('cluster_name',			dict(type=str, default='localhost')),
	('default_queue',			dict(type=str, default='')),
	('default_account',			dict(type=str, default='')),
    )

class AcceptedOptions(Record):
    FIELDS = (
	('demo_mode',				dict(type=Bool, default=Bool.FALSE)),
	('demo_job_timeout_frequency',		dict(type=float, default=0.1)),
    )

class Option(Record):
    RECORD_ID_NAME = 'name'
    FIELDS = (
	('name',				dict(type=str, sql_unique_key=True)),
	('value',				dict(type=str)),
    )

class Config(Record):
    FIELDS = (
	('config_id',				dict(type=int, sql_primary_key=True)),
	('max_cycles',				dict(type=int, default=-1)),
	('time_interval',			dict(type=float, default=1.00)),
	('grace_interval',			dict(type=Duration, default=Duration(60))),
	('trace_errors',			dict(type=Bool, default=Bool.TRUE)),
	('verbose_level',			dict(type=int, default=1)),
	('starting_timeout',			dict(type=int, default=30)),
	('exiting_timeout',			dict(type=int, default=300)),	# warning: this can be dangerous! It will free nodes before the job is ended
	('interactive_timeout',			dict(type=int, default=-1)),
	('interactive_interval',		dict(type=float, default=1.0)),
	('max_queued_jobs',			dict(type=int, default=-1)),
	('max_running_jobs',			dict(type=int, default=-1)),
	('backfilling',				dict(type=int, default=Bool.TRUE)),
	('max_top_dogs',			dict(type=int, default=5)),
	('archive_jobs_older_than',		dict(type=Duration, default=Duration(86400))),
	('archive_reservations_older_than',	dict(type=Duration, default=Duration(86400))),
	('archiviation_time_interval',		dict(type=Duration, default=Duration(3600))),
	('statistics_time_interval',		dict(type=Duration, default=Duration("00:05:00"))),
	('db_cache_timeout',			dict(type=Duration, default=Duration(600))),
	('time_quantum',			dict(type=Duration, default=60)),
    )

class Directive(Record):
    DIRECTIVE_SHUTDOWN = 'shutdown'
    DIRECTIVE_SET = {DIRECTIVE_SHUTDOWN}
    FIELDS = (
	('directive_id',	dict(type=int, sql_primary_key=True)),
	('time',		dict(type=Time)),
	('directive',		dict(type=str)),
	('executed',		dict(type=Bool, default=Bool.FALSE)),
    )


class MetaPermission(MetaRecord):
    def __new__(cls, class_name, class_bases, class_dict):
        t = MetaRecord.__new__(cls, class_name, class_bases, class_dict)
        subfeatures = {}
        for feature in t.FEATURES:
            subfeatures[feature] = t.subfeatures(feature)
        t.SUBFEATURES = subfeatures
        return t

class UserGroup(Record):
    FIELDS = (
	('user_group_id',	dict(type=int, sql_primary_key=True)),
	('user_group_name',	dict(type=str)),
	('username',		dict(type=str)),
    )

class Permission(Record, metaclass=MetaPermission):
    TYPE_USER = 'user'
    TYPE_USER_GROUP = 'user_group'
    DEFAULT_NAME = 'ALL'
    FEATURE_MONITORING = 'monitoring'
    FEATURE_SUBMISSION = 'submission'
    FEATURE_RESERVATION = 'reservation'
    FEATURE_ADMINISTRATION = 'administration'
    FEATURE_DEFAULT = {
	FEATURE_MONITORING:	True,
	FEATURE_SUBMISSION:	True,
	FEATURE_RESERVATION:	False,
	FEATURE_ADMINISTRATION:	False,
    }
    FEATURE_REQUIREMENTS = {
	FEATURE_ADMINISTRATION:	{FEATURE_MONITORING, FEATURE_RESERVATION},
	FEATURE_RESERVATION:	{FEATURE_MONITORING},
	FEATURE_SUBMISSION:	{FEATURE_MONITORING},
	FEATURE_MONITORING:	{},
    }
    FEATURES = (FEATURE_ADMINISTRATION, FEATURE_RESERVATION, FEATURE_SUBMISSION, FEATURE_MONITORING)
    SUBFEATURES = {} # completed my metaclass
    FIELDS = (
	('permission_id',	dict(type=int, sql_primary_key=True)),
	('type',		dict(type=str)),
	('name',		dict(type=str, default=DEFAULT_NAME)),
	('feature',		dict(type=str)),
	('allow',		dict(type=Bool)),
    )

    @classmethod
    def subfeatures(cls, feature):
        features = set()
        features.add(feature)
        for subfeature in cls.FEATURE_REQUIREMENTS[feature]:
            if not subfeature in features:
                features.update(cls.subfeatures(subfeature))
        return features

class Account(Record):
    FIELDS = (
	('account_id',		dict(type=int, sql_primary_key=True)),
	('account_name',	dict(type=str)),
	('budget',		dict(type=Duration, default=Duration.INF)),
	('consumed_budget',	dict(type=Duration, default=Duration(0))),
    )

    @property
    def residual_budget(self):
        if self.budget == Duration.INF:
            return Duration.INF
        elif self.consumed_budget < 0 or self.consumed_budget >= self.budget:
            return Duration(0)
        else:
            return Duration(self.budget - self.consumed_budget)

class AccountAuth(Record):
    TYPE_USER = 'user'
    TYPE_USER_GROUP = 'user_group'
    DEFAULT_NAME = 'ALL'
    FIELDS = (
	('account_auth_id',	dict(type=int, sql_primary_key=True)),
	('account_name',	dict(type=str)),
	('auth_type',		dict(type=str)),
	('name',		dict(type=str)),
    )

class UserAccount(Record):
    FIELDS = (
	('user_account_id',	dict(type=int, sql_primary_key=True)),
	('username',		dict(type=str)),
	('consumed_budget',	dict(type=Duration, default=Duration(0))),
	('num_run_jobs',	dict(type=int, default=0)),
    )
    PROPERTIES = (
	'mean_job_duration',
    )

    @property
    def mean_job_duration(self):
        if self.num_run_jobs > 0:
            return Duration(int(round(self.consumed_budget / float(self.num_run_jobs))))
        else:
            return Duration(0)

class NodeSelection(Record):
    FIELDS = (
	('node_name',		dict(type=str)),
	('procs',		dict(type=int, default=-1)),
    )

    @classmethod
    def fromstring(cls, s):
        np = [e.strip() for e in s.split(":", 1)]
        node_name = np[0]
        if len(np) == 2:
            try:
                procs = int(np[1])
            except ValueError:
                raise ValueError("NodeSelection {0}: invalid procs value {1!r}".format(s, np[1]))
        else:
            procs = -1
        return cls(node_name=node_name, procs=procs)

class JobBinding(Record):
    FIELDS = (
	('job_binding_id',	dict(type=int, sql_primary_key=True)),
	('job_id',		dict(type=int)),
	('node_name',		dict(type=str)),
	('in_use',		dict(type=int, default=+1)),
	('procs',		dict(type=int)),
    )


class Job(_JRRecord):
    STATUS_PENDING = 'P'
    STATUS_HELD = 'H'
    STATUS_STARTING = 'S'
    STATUS_RUNNING = 'R'
    STATUS_EXITING = 'E'
    STATUS_COMPLETED = 'C'
    STATUS_DELETED = 'D'
    
    RUNNING_STATUS_SET = {STATUS_STARTING, STATUS_RUNNING, STATUS_EXITING}
    QUEUED_STATUS_SET = RUNNING_STATUS_SET.union({STATUS_HELD, STATUS_PENDING})
    COMPLETED_STATUS_SET = {STATUS_COMPLETED, STATUS_DELETED}
    PENDING_STATUS_SET = {STATUS_PENDING, STATUS_HELD}
    ALL_STATUS_SET = {STATUS_HELD, STATUS_PENDING, STATUS_STARTING, STATUS_RUNNING, STATUS_EXITING, STATUS_COMPLETED, STATUS_DELETED}

    MAX_JOB_NAME = 16

    FIELDS = (
	('job_id',		dict(type=int, sql_primary_key=True)),
	('job_name',		dict(type=str, default='')),
	('queue_name',		dict(type=str, default='')),
	('account_name',	dict(type=str, default='')),
	('reservation_id',	dict(type=int, default=0)),
	('dependency',		dict(type=str, default='')),
	('min_start_time',	dict(type=Time, default=0)),
	('priority',		dict(type=int, default=0)),
	('interactive',		dict(type=Bool, default=Bool.FALSE)),
	('command',		dict(type=str)),
	('work_dir',		dict(type=str, default='')),
	('input_filename',	dict(type=str, default='')),
	('output_filename',	dict(type=str, default='')),
	('error_filename',	dict(type=str, default='')),
	('duration',		dict(type=Duration, default=Duration.INF)),
	('username',		dict(type=str, default=USERNAME)),
	('nodes_request',	dict(type=NodesRequest)),
	('queue_time',		dict(type=Time, default=0)),
	('start_time',		dict(type=Time, default=0)),
	('complete_time',	dict(type=Time, default=0)),
	('consumed_budget',	dict(type=Duration, default=Duration(0))),
	('process_id',		dict(type=int, default=-1)),
	('master_node',		dict(type=str, default='')),
	('master_node_ssh',	dict(type=str, default='')),
	('master_node_mpi',	dict(type=str, default='')),
	('starter_process_id',	dict(type=int, default=0)),
	('status',		dict(type=str, default=STATUS_PENDING)),
	('exit_code',		dict(type=int, default=-1)),
	('info',		dict(type=str, default="")),
    )
    PROPERTIES = (
	'run_time',
	'queuing_time',
	'change_time',
	'required_budget',
    ) + _JRRecord.PROPERTIES

    NODE_REQUEST_CLASS = JobRequest
    BINDING_CLASS = JobBinding

    @property
    def job_type(self):
        if self.interactive:
            return 'I'
        else:
            return 'B'

    @property
    def required_budget(self):
        if self.duration < 0:
            return Duration.INF
        tot_request = self.tot_request
        if tot_request.procs < 0:
            return Duration.INF
        return Duration(self.duration * tot_request.procs)

    def get_run_time(self, t=None):
        if self.start_time > 0:
            if self.complete_time > 0:
                rt = self.complete_time - self.start_time
            else:
                if t is None:
                    t = Time()
                rt = t - self.start_time
        else:
            rt = 0
        return Duration(int(round(rt)))

    run_time = property(get_run_time)

    def get_queuing_time(self, t=None):
        if self.start_time > 0:
            return 0
        else:
            if t is None:
                t = Time()
            rt = t - self.queue_time
            return Duration(int(round(rt)))

    queuing_time = property(get_queuing_time)

    def set_total_priority(self, system_priority, t=None):
        self._total_priority = self.get_queuing_time(t) + self.priority + system_priority

    @property
    def change_time(self):
        if self.complete_time > 0:
            return self.complete_time
        elif self.start_time > 0:
            return self.start_time
        else:
            return self.queue_time

    @property
    def formatted_job_name(self):
        return self.job_name[:self.MAX_JOB_NAME]

    def validate(self):
        try:
            self.nodes_request.validate()
        except ValueError as e:
            raise ValueError("Record {0}, field_name {1}=={2}".format(self.__class__.__name__, 'nodes_request', e))

    #def check_permissions(self):
    #    for path in self.work_dir, os.path.dirname(self.output_filename), os.path.dirname(self.error_filename):
    #        if not os.access(path, os.W_OK):
    #            raise SubmitError("Cannot write in directory {0!r}".format(path))

    def preprocess(self):
        if self.work_dir == '':
            self.work_dir = os.getcwd()
        if self.input_filename == '':
            self.input_filename = "/dev/null"
        if self.job_name == '':
            self.job_name = "job_{job_id}"
        if self.output_filename == '':
            self.output_filename = "{job_name}.{job_id}.eo"
        if self.error_filename == '':
            self.error_filename = self.output_filename
        self.dependency = self.dependency.strip()
        self.output_filename = os.path.abspath(os.path.realpath(self.output_filename))
        self.error_filename = os.path.abspath(os.path.realpath(self.error_filename))

    def postprocess(self):
        keys = set()
        job_name = self._format(self.job_name)
        if job_name != self.job_name:
            self.job_name = job_name
            keys.add('job_name')
        output_filename = self._format(self.output_filename)
        if output_filename != self.output_filename:
            self.output_filename = output_filename
            keys.add('output_filename')
        error_filename = self._format(self.error_filename)
        if error_filename != self.error_filename:
            self.error_filename = error_filename
            keys.add('error_filename')
        return keys

    #@property
    #def node_list(self):
    #    return tuple((binding.node_name, binding.procs) for binding in self._bindings)
    
    #@property
    #def nodes(self):
    #    return '+'.join('{0}:{1}'.format(node_name, procs) for node_name, procs in self.node_list)
    
    @property
    def node_selection(self):
        return ', '.join("{0}:{1}".format(node_name, procs) for node_name, procs in getattr(self, '_node_selection', []))

    def _format(self, s):
        return s.format(job_name=self.job_name, job_id=self.job_id)

    def get_job_name(self):
        return self._format(self.job_name)

    def get_input_filename(self):
        return self._format(self.input_filename)

    def get_output_filename(self):
        return self._format(self.output_filename)

    def get_error_filename(self):
        return self._format(self.error_filename)

    def get_work_dir(self):
        return self._format(self.work_dir)

    @property
    def node_requests(self):
        return self.nodes_request.split()

    @property
    def job_request(self):
        job_nodes, job_procs = 0, 0
        flt_nodes, flt_procs = False, False
        for node_request in self.node_requests:
            nodes, procs = node_request.nodes, node_request.procs
            if nodes < 0:
                flt_nodes = True
            if procs < 0:
                flt_procs = True
            job_nodes += nodes
            job_procs += procs
        if flt_nodes:
            job_nodes = -1
        if flt_procs:
            job_procs = -1
        return JobRequest(nodes=job_nodes, procs=job_procs)

    def set_held(self, t):
        self.status = self.STATUS_HELD
        self.queue_time = t

    def set_pending(self, t):
        self.status = self.STATUS_PENDING
        self.queue_time = t

    def set_queuing_time(self, t):
        self.queue_time = t

    def set_starting(self, t):
        self.status = self.STATUS_STARTING
        self.start_time = t

    def set_running(self, t):
        self.status = self.STATUS_RUNNING
        #self.start_time = time.time()

    def set_exiting(self, t):
        self.status = self.STATUS_EXITING
        self.complete_time = t

    def set_completed(self, t, status=STATUS_COMPLETED):
        assert status in {self.STATUS_COMPLETED, self.STATUS_DELETED}
        self.status = status
        self.complete_time == t
        elapsed = self.complete_time - self.start_time
        request = self.tot_request
        self.consumed_budget = Duration(elapsed * request.procs)

    def set_deleted(self, t):
        self.status = self.STATUS_DELETED
        if self.complete_time == 0:
            self.complete_time = t

class ArchivedJob(Job):
    pass

class ArchivedJobBinding(JobBinding):
    pass

class ArchivedReservation(Reservation):
    pass

class ArchivedReservationBinding(ReservationBinding):
    pass

#class JobSpool(Record):
#    FIELDS = (
#	('job_id',		dict(type=int, sql_primary_key=True)),
#	('start_time',		dict(type=Time, default=Time(0))),
#	('complete_time',	dict(type=Time, default=Time(0))),
#	('exit_code',		dict(type=int, default=-1)),
#    )
#
#class JobEnv(Record):
#    FIELDS = (
#	('var_name',		dict(type=str)),
#	('var_value',		dict(type=str, default="")),
#    )

if __name__ == "__main__":
    for s in "1:10", "-1:8", "9:-1", "-1:-1", "3:name=node03,procs=8", "4:name=node02,3", "node00:27", "node00:name=node09,18":
        print(repr(s), NodeRequest.fromstring(s))
    s = "node05:-1+node04:-1"
    nodes_request = NodesRequest(s)
    print()
    print(nodes_request.split())
    print()
    for nr in s.split('+'):
        print(repr(nr), NodeRequest.fromstring(nr))
    print()

    for t_init in (None, "20121231 10:30:23", "INF", Time.INF):
        t = Time(t_init)
        print(t_init, t, repr(t))
        rt = Time(str(t))
        assert rt == t, "{0} != {1}, {0!r} != {1!r}".format(rt, t)
    print()
    for d_init in (None, 12, 72, 4501, 86399, 86400, 86401):
        d = Duration(d_init)
        print(d_init, d, repr(d))
        rd = Duration(str(d))
        assert rd == d, "{0} != {1}, {0!r} != {1!r}".format(rd, d)
