#
# Copyright 2017-present CIG, Inc.
#
# 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.
#
import base64
import binascii
import json
import pprint
import random
import os
import structlog

from enum import Enum
from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue, succeed

from voltha.core.logical_device_agent import mac_str_to_tuple
from voltha.protos.bbf_fiber_types_pb2 import *
from voltha.protos.common_pb2 import OperStatus, AdminState
from voltha.protos.device_pb2 import Device
from voltha.protos.device_pb2 import Port
from voltha.protos.logical_device_pb2 import *
from voltha.protos.openflow_13_pb2 import *

from voltha.protos.olt_common_pb2 import *
from voltha.protos.olt_d_pb2 import *
from voltha.protos.olt_pon_pb2 import *
from voltha.protos.olt_switch_pb2 import *
#from voltha.adapters.cig_olt.protos.olt_common_pb2 import *
#from voltha.adapters.cig_olt.protos.olt_d_pb2 import *
#from voltha.adapters.cig_olt.protos.olt_pon_pb2 import *
#from voltha.adapters.cig_olt.protos.olt_switch_pb2 import *

from cig_olt_zmq import *
from cig_olt_xpon import *

log = structlog.get_logger()

class NniPort(object):
    """
    A class similar to the 'Port' class in the VOLTHA
    
    TODO: Merge this with the Port class or cleanup where possible
          so we do not duplicate fields/properties/methods
    """

    def __init__(self, nni_port, parent_id):
        self.log = structlog.get_logger(device_id=parent_id)
        self._parent_id = parent_id
        self._port_no = nni_port.port_no
        self._mac_address = nni_port.mac_address
        if self._mac_address is None:
            self._mac_address = "11:22:33:44:55:66"

        if nni_port.port_type == OLT_NNI_PORT_TYPE_10G:
            self._ofp_capabilities = OFPPF_10GB_FD | OFPPF_FIBER
            self._current_speed = OFPPF_10GB_FD
            self._max_speed = OFPPF_10GB_FD
        elif nni_port.port_type == OLT_NNI_PORT_TYPE_40G:
            self._ofp_capabilities = OFPPF_40GB_FD | OFPPF_FIBER
            self._current_speed = OFPPF_40GB_FD
            self._max_speed = OFPPF_40GB_FD
        elif nni_port.port_type == OLT_NNI_PORT_TYPE_100G or nni_port.port_type == OLT_NNI_PORT_TYPE_80G:
            self._ofp_capabilities = OFPPF_100GB_FD | OFPPF_FIBER
            self._current_speed = OFPPF_100GB_FD
            self._max_speed = OFPPF_100GB_FD
        else:
            return error

        self._ofp_state = OFPPS_LIVE
        
        self.startup = None
        self._port = None
        self._logical_port = None

        self._admin_state = AdminState.ENABLED
        self._oper_status = OperStatus.ACTIVE
        log.info('Creating NNI Port {}'.format(self._port_no))

    def get_port(self):
        """
        Get the VOLTHA PORT object for this port
        :return: VOLTHA Port object
        """
        self.log.info('get nni gort', self._port_no)
        if self._port is None:
            self._port = Port(port_no=self._port_no,    \
                              label='NNI port',         \
                              type=Port.ETHERNET_NNI,   \
                              admin_state=self._admin_state,    \
                              oper_status=self._oper_status)
        return self._port
    
    def get_logical_port(self):
        """
        Get the VOLTHA logical port for this port
        :return: VOLTHA logical port or None if not supported
        """
        if self._logical_port is None:
            openflow_port = ofp_port(port_no=self._port_no, \
                                     hw_addr=mac_str_to_tuple(self._mac_address),   \
                                     name='nni' + str(self._port_no),   \
                                     config=0,  \
                                     state=self._ofp_state, \
                                     curr=self._ofp_capabilities,   \
                                     advertised=self._ofp_capabilities, \
                                     peer=self._ofp_capabilities,   \
                                     curr_speed=self._current_speed,    \
                                     max_speed=self._max_speed)
    
            self._logical_port = LogicalPort(id='nni{}'.format(self._port_no),  \
                                             ofp_port=openflow_port,    \
                                             device_id=self._parent_id, \
                                             device_port_no=self._port_no,  \
                                             root_port=True)
        return self._logical_port

    def delete(self):
        """
        Parent device is being deleted. Do not change any config but
        stop all polling
        """
        self.log.info('Deleting')
        #self.state = DELETING
        #self.cancel_deferred()

class PonPort(object):
    """
    A class similar to the 'Port' class in the VOLTHA
    
    TODO: Merge this with the Port class or cleanup where possible
          so we do not duplicate fields/properties/methods
    """
    MAX_ONUS_SUPPORTED = 1024
    DEFAULT_ENABLED = False

    #def __init__(self, port_no, parent_id):
    def __init__(self, port_no, parent):
        # TODO: Weed out those properties supported by common 'Port' object
        self.log = structlog.get_logger(device_id=parent.device_id)
        self._parent_id = parent.device_id
        self._parent = parent
        self._pon_id = port_no
        self._port_no = port_no
        self._name = 'xpon 0/{}'.format(port_no)
        self._label = 'pon-{}'.format(port_no)
        self.startup = None
        self._port = None

        self._discovery_tick = 20.0
        self._no_onu_discover_tick = self._discovery_tick / 2
        self._sync_tick = 20.0
        self._in_sync = False
        self._expedite_sync = False
        self._expedite_count = 0


        
        self._admin_state = AdminState.ENABLED
        self._oper_status = OperStatus.ACTIVE

        #self.onus = {} 
        #self._onus = {}         # serial_number-base64 -> ONU  (allowed list)
        self._onu_by_id = {}    # onu-id -> ONU

        self._deferred = None                     # General purpose
        self._discovery_deferred = None           # Specifically for ONU discovery
        self._sync_deferred = None                # For sync of PON config to hardware

        self._active_los_alarms = set()           # ONU-ID

        # xPON configuration

        self._xpon_name = None
        self._enabled = False
        self._downstream_fec_enable = False
        self._upstream_fec_enable = False
        self._deployment_range = 25000
        self._authentication_method = 'serial-number'


        log.info('Creating pon Port {}'.format(self._port_no))  

    def __del__(self):
        #self.stop()
        pass
    
    def __str__(self):
        return "PonPort-{}: Admin: {}, Oper: {}, OLT: {}".format(self._label,
                                                                 self._admin_state,
                                                                 self._oper_status,
                                                                 self._parent)
    def get_port(self):
        """
        Get the VOLTHA PORT object for this port
        :return: VOLTHA Port object
        """
        self.log.info('get pon gort', self._port_no)
        if self._port is None:
            self._port = Port(    \
                port_no=100 + self._port_no,    \
                #port_no=self._port_no,    \
                label=self._label,        \
                type=Port.PON_OLT,        \
                admin_state=self._admin_state,    \
                oper_status=self._oper_status     \
            )

        return self._port

    @property
    def port_number(self):
        return self._port_no

    @property
    def name(self):
        return self._name

    @property
    def pon_id(self):
        return self._pon_id

    @property
    def olt(self):
        return self._parent

    def onu(self, onu_id):
        return self._onu_by_id.get(onu_id)
        
    #def onu_add(self, onu_id, sn):
    def onu_add(self, onu_info):

        sn = onu_info['serial-number']
        onu_id = onu_info['onu-id']
        self.log.info('Add ONU: {}'.format(sn))

        if onu_id not in self._onu_by_id:
            # Newly found and not enabled ONU, enable it now if not at max

            if len(self._onu_by_id) < self.MAX_ONUS_SUPPORTED:
                # TODO: For now, always allow any ONU to be activated

                #self.on_new_onu_discovered(onu)
                #self.onus[sn] = str(onu_id)
                self._onu_by_id[onu_id] = Onu(onu_info)
                olt = self.olt
                onu = self.onu(onu_id)
                if (olt.work_mode == OLT_MODE_AUTO) and (onu._ranging_status == 1)  and (onu._config_status == 0):
                    #add onu to oltd
                    self.onu_add_msg_send(onu)
                    onu._status_machine = 'activating'
                    
                elif (olt.work_mode == OLT_MODE_CONFIG) and (onu._config_status == 1):
                    #add onu to oltd
                    self.onu_add_msg_send(onu)
                    onu._status_machine = 'activating'
                else:
                    pass
            else:
                self.log.warning('Maximum number of ONUs already provisioned')
        else:
            # ONU has been enabled
            pass


    def onu_update(self, onu_info):

        if onu_info['serial-number'] is not None:
            sn = onu_info['serial-number']
            self.log.info('update ONU: {}'.format(sn))
            
        onu_id = onu_info['onu-id']
        onu = self.onu(onu_id)
        if onu is None:
            return
            
        olt = self.olt
        if olt.work_mode == OLT_MODE_AUTO:
            return
        
        if onu_info['ranging-status'] is not None:
            onu._ranging_status = onu_info['ranging-status']
                    
        if onu_info['config-status'] is not None:
            if (onu_info['config-status'] == 1) and (onu._config_status == 0):
                #add onu to oltd
                self.onu_add_msg_send(onu)
                onu._status_machine = 'activating'
                onu._config_status = onu_info['config-status']
                    
            if (onu_info['config-status'] == 0) and (onu._config_status == 1):
                self.onu_delete_msg_send(onu)
                del self._onu_by_id[onu_id]
                    
                       
        if onu_info['channel-id'] is not None:
            onu._channel_id = onu_info['channel-id']

        if onu_info['status_machine'] is not None:
            onu._status_machine = onu_info['status_machine']

        if onu_info['upstream-fec'] is not None:
            if onu._status_machine == 'activating' or onu._status_machine == 'activation_successful':
                if onu._upstream_fec != onu_info['upstream-fec']:
                    onu._upstream_fec = onu_info['upstream-fec']
                    self.onu_update_msg_send(onu)
            else :
                onu._upstream_fec = onu_info['upstream-fec']
                

    def onu_del(self, onu_id):
        self.log.info('Delete ONU: {}'.format(onu_id))

        if onu_id in self._onu_by_id:
            onu = self.onu(onu_id)
            if onu is None:
                return
            if onu._status_machine != 'init':
                self.onu_delete_msg_send(onu)
                
            del self._onu_by_id[onu_id]

        else:
            # ONU has been delete
            pass


    def onu_exist_check(self, onu_id):
        return onu_id in self._onu_by_id

    def onu_add_msg_send(self, onu):
        try:
            msg_hdr = OltMsgCommonHdr(
                type=OLT_D_ADD_ONU,
                src_appId=OLT_APPID_VOLTHA,
                sync=0
            )
            data=msg_hdr.SerializeToString()
            self._parent.zmq_client_async.async_send(data,1)

            olt = self.olt
            if olt.work_mode == OLT_MODE_AUTO:
                onu_add = OltDAddOnu(
                    pon_port=onu._pon_id,
                    onu_id=onu.onu_id,
                    sn=onu._serial_number_string,
                    registration_id= "",
                    fec_upstream= onu._upstream_fec,
                    authentication_method=SERIAL_NUMBER
                )
            else:
                channel_partition_name = onu._channel_partition
                cp = olt.channel_partitions.get(channel_partition_name)
                
                if cp['authentication-method'] == 'serial-number':
                    auth_method = SERIAL_NUMBER
                elif cp['authentication-method'] == 'loid':
                    auth_method = LOID
                elif cp['authentication-method'] == 'registration-id':
                    auth_method = REGISTRATION_ID
                elif cp['authentication-method'] == 'omci':
                    auth_method = OMCI
                elif cp['authentication-method'] == 'dot1x':
                    auth_method = DOT1X
                else :
                    auth_method = SERIAL_NUMBER

                #self.log.info('onu_add_msg_send cp', cp) 
                #self.log.info('onu_add_msg_send auth_method', auth_method)
                #ani = olt.ont_anis.get(onu._vani_name)
                #if ani is not None:
                    #ups_fec = ani['upstream-fec']
                #else:
                    #ups_fec = 1
                
                onu_add = OltDAddOnu(
                    pon_port=onu._pon_id,
                    onu_id=onu.onu_id,
                    sn=onu._serial_number_string,
                    registration_id= onu._registration_id,
                    #fec_upstream=ups_fec,
                    fec_upstream=onu._upstream_fec,
                    authentication_method=auth_method
                )
            data=onu_add.SerializeToString()
            self._parent.zmq_client_async.async_send(data,0)
        
        except Exception as e:
            self.log.exception('Exception during add onu', e=e)
            
    def onu_delete_msg_send(self, onu):
        try:
            msg_hdr = OltMsgCommonHdr(
                type=OLT_D_DELETE_ONU,
                src_appId=OLT_APPID_VOLTHA,
                sync=0
            )
            data=msg_hdr.SerializeToString()
            self._parent.zmq_client_async.async_send(data,1)
        
            onu_delete = OltDDeleteOnu(
                pon_port=onu._pon_id,
                onu_id=onu.onu_id,
                sn=onu._serial_number_string
            )
            data=onu_delete.SerializeToString()
            self._parent.zmq_client_async.async_send(data,0)
        
        except Exception as e:
            self.log.exception('Exception during delete onu', e=e)


    def onu_update_msg_send(self, onu):
        try:
            msg_hdr = OltMsgCommonHdr(
                type=OLT_D_UPDATE_ONU,
                src_appId=OLT_APPID_VOLTHA,
                sync=0
            )
            data=msg_hdr.SerializeToString()
            self._parent.zmq_client_async.async_send(data,1)

            olt = self.olt
            channel_partition_name = onu._channel_partition
            cp = olt.channel_partitions.get(channel_partition_name)
            
            if cp['authentication-method'] == 'serial-number':
                auth_method = SERIAL_NUMBER
            elif cp['authentication-method'] == 'loid':
                auth_method = LOID
            elif cp['authentication-method'] == 'registration-id':
                auth_method = REGISTRATION_ID
            elif cp['authentication-method'] == 'omci':
                auth_method = OMCI
            elif cp['authentication-method'] == 'dot1x':
                auth_method = DOT1X
            else :
                auth_method = SERIAL_NUMBER

            onu_update = OltDUpdateOnu(
                pon_port=onu._pon_id,
                onu_id=onu.onu_id,
                sn=onu._serial_number_string,
                registration_id= onu._registration_id,
                fec_upstream=onu._upstream_fec,
                authentication_method=auth_method
            )
            data=onu_update.SerializeToString()
            self._parent.zmq_client_async.async_send(data,0)
        
        except Exception as e:
            self.log.exception('Exception during update onu', e=e)


    def delete(self):
        """
        Parent device is being deleted. Do not change any config but
        stop all polling
        """
        self.log.info('Deleting')
        #self.state = DELETING
        #self.cancel_deferred()
        for onu in self._onu_by_id.itervalues():