# -*- coding: utf-8 -*-
################################ Begin license #################################
# Copyright (C) Laboratory of Imaging technologies,
#               Faculty of Electrical Engineering,
#               University of Ljubljana.
#
# This file is part of PyXOpto.
#
# PyXOpto is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyXOpto is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PyXOpto. If not, see <https://www.gnu.org/licenses/>.
################################# End license ##################################
from typing import Tuple, List
import numpy as np
from xopto.mcvox.mcdetector.base import Detector
from xopto.mcvox import cltypes, mctypes, mcobject
from xopto.mcvox.mcutil.fiber import MultimodeFiber, FiberLayout
from xopto.mcvox.mcutil import geometry
[docs]class FiberArray(Detector):
[docs]    def cl_type(self, mc: mcobject.McObject) -> cltypes.Structure:
        T = mc.types
        n = self.n
        class ClFiberArray(cltypes.Structure):
            '''
            Structure that that represents a detector in the Monte Carlo
            simulator core.
            Fields
            ------
            transformation: mc_point3f_t*n
                Transforms coordinates from Monte Carlo to the detector / fiber
                coordinates for each fiber.
            core_position: mc_point2f_t*n
                Position of the individual fibers.
            core_r_squared: mc_fp_t*n
                Squared radius of the optical fibers.
            cos_min: mc_fp_t*n
                Cosine of the maximum acceptance angle (in the detector / fiber
                coordinate space).
            offset: mc_int_t
                The offset of the first accumulator in the Monte Carlo
                detector buffer.
            '''
            _fields_ = [
                ('transformation', T.mc_matrix3f_t*n),
                ('core_position', T.mc_point2f_t*n),
                ('core_r_squared', T.mc_fp_t*n),
                ('cos_min', T.mc_fp_t*n),
                ('offset', T.mc_size_t),
            ]
        return ClFiberArray 
[docs]    def cl_declaration(self, mc: mcobject.McObject) -> str:
        '''
        Structure that defines the detector in the Monte Carlo simulator.
        '''
        loc = self.location
        Loc = loc.capitalize()
        n = self.n
        return '\n'.join((
            'struct MC_STRUCT_ATTRIBUTES Mc{}Detector{{'.format(Loc),
            '	mc_matrix3f_t transformation[{}];'.format(n),
            '	mc_point2f_t core_position[{}];'.format(n),
            '	mc_fp_t core_r_squared[{}];'.format(n),
            '	mc_fp_t cos_min[{}];'.format(n),
            '	mc_size_t offset;',
            '};'
        )) 
[docs]    def cl_implementation(self, mc: mcobject.McObject) -> str:
        '''
        Implementation of the detector in the Monte Carlo simulator.
        '''
        loc = self.location
        Loc = loc.capitalize()
        n = self.n
        return '\n'.join((
            'void dbg_print_{}_detector(__mc_detector_mem const Mc{}Detector *detector){{'.format(loc, Loc),
            '	dbg_print("Mc{}Detector - FiberArray fiber array detector:");'.format(Loc),
            '	for(mc_size_t index=0; index < {}; ++index){{'.format(n),
            '		dbg_print_size_t(INDENT "Fiber index:", index);',
            '		dbg_print_matrix3f(INDENT INDENT "transformation:", &detector->transformation[index]);',
            '		dbg_print_point2f(INDENT INDENT "core_position:", &detector->core_position[index]);',
            '		dbg_print_float(INDENT INDENT "core_r_squared (mm2):", detector->core_r_squared[index]*1e6f);',
            '		dbg_print_float(INDENT INDENT "cos_min:", detector->cos_min[index]);',
            '	};',
            '	dbg_print_size_t(INDENT "offset:", detector->offset);',
            '};',
            '',
            'inline void mcsim_{}_detector_deposit('.format(loc),
            '		McSim *mcsim, ',
            '		mc_point3f_t const *pos, mc_point3f_t const *dir, ',
            '		mc_fp_t weight){',
            '',
            '	__global mc_accu_t *address;',
            '',
            '	dbg_print_status(mcsim, "{} FiberArray fiber array detector hit");'.format(loc),
            '',
            '	__mc_detector_mem const Mc{}Detector *detector = '.format(Loc),
            '		mcsim_{}_detector(mcsim);'.format(loc),
            '',
            '',
            '	mc_size_t fiber_index = {}; /* invalid index ... no fiber hit */'.format(n),
            '',
            '	mc_fp_t dx, dy, r_squared;',
            '	mc_point3f_t mc_pos, detector_pos;',
            '',
            '	pragma_unroll_hint({})'.format(n),
            '	for(mc_size_t index=0; index < {}; ++index){{'.format(n),
            '		mc_pos.x = pos->x - detector->core_position[index].x;',
            '		mc_pos.y = pos->y - detector->core_position[index].y;',
            '		mc_pos.z = FP_0;',
            '',
            '		mc_matrix3f_t transformation = detector->transformation[index];',
            '		transform_point3f(&transformation, &mc_pos, &detector_pos);',
            '		dx = detector_pos.x;',
            '		dy = detector_pos.y;',
            '		r_squared = dx*dx + dy*dy;',
            '',
            '		if (r_squared <= detector->core_r_squared[index]){',
            '			/* hit this fiber */',
            '			fiber_index = index;',
            '			break;',
            '		};',
            '	};',
            '',
            '	if (fiber_index >= {})'.format(n),
            '		return;',
            '',
            '	address = mcsim_accumulator_buffer_ex(',
            '		mcsim, detector->offset + fiber_index);',
            '',
            '	/* Transfor direction vector component z into the detector space. */',
            '	mc_fp_t pz = transform_point3f_z(',
            '		&detector->transformation[fiber_index], dir);',
            '	dbg_print_float("Packet direction z:", pz);',
            '	uint32_t ui32w = weight_to_int(weight)*',
            '		(detector->cos_min[fiber_index] <= mc_fabs(pz));',
            '',
            '	if (ui32w > 0){',
            '		dbg_print("{} FiberArray fiber array detector depositing:");'.format(loc),
            '		dbg_print_uint(INDENT "uint weight:", ui32w);',
            '		dbg_print_size_t(INDENT "to fiber index:", fiber_index);',
            '		accumulator_deposit(address, ui32w);',
            '	};',
            '};',
        )) 
    def __init__(self, fibers: List[FiberLayout]):
        '''
        Optical fiber probe detector with an array of optical fibers that
        are optionally tilted(direction parameter). The optical fibers are
        always polished in a way that forms a tight optical contact with
        the surface of the sample.
        Parameters
        ----------
        fiber: List[FiberLayout]
            List of optical fiber configuration that form the fiber array.
        '''
        if isinstance(fibers, FiberArray):
            la = fibers
            fibers = la.fibers
            raw_data = np.copy(la.raw)
            nphotons = la.nphotons
        else:
            nphotons = 0
            raw_data = np.zeros((len(fibers),))
        super().__init__(raw_data, nphotons)
        self._fibers = fibers
[docs]    def update(self, other: 'FiberArray' or dict):
        '''
        Update this detector configuration from the other detector. The
        other detector must be of the same type as this detector or a dict with
        appropriate fields.
        Parameters
        ----------
        other: FiberArray or dict
            This source is updated with the configuration of the other source.
        '''
        if isinstance(other, FiberArray):
            self.fibers = other.fibers
        elif isinstance(other, dict):
            self.fibers = other.get('fibers', self.fibers) 
    def _get_fibers(self) -> List[FiberLayout]:
        return self._fibers
    def _set_fibers(self, fibers: List[FiberLayout]):
        if len(self._fibers) != len(fibers):
            raise ValueError('The number of optical fibers must not change!')
        if type(self._fibers[0]) != type(fibers[0]):
            raise TypeError('The type of optical fibers must not change!')
        self._fibers[:] = fibers
    fibers = property(_get_fibers, _set_fibers, None,
                     'List of optical fibers.')
    def _get_n_fiber(self) -> int:
        return len(self._fibers)
    n = property(_get_n_fiber, None, None,
                 'Number of optical fiber in the array.')
    def __len__(self):
        return len(self._fibers)
    def __iter__(self):
        return iter(self._fibers)
[docs]    def check(self):
        '''
        Check if the configuration has errors and raise exceptions if so.
        '''
        for fiber_cfg in self._fibers:
            for other_cfg in self._fibers:
                if other_cfg != fiber_cfg:
                    d = np.linalg.norm(fiber_cfg.position - other_cfg.position)
                    if d < max(fiber_cfg.fiber.dcladding, other_cfg.fiber.dcladding):
                        raise ValueError('Some of the fibers in the '
                                         'detector array overlap!')
        return True 
[docs]    def fiber_position(self, index: int) -> Tuple[float, float]:
        '''
        Returns the position of the fiber center as a tuple (x, y).
        Parameters
        ----------
        index: int
            Fiber index from 0 to n -1.
        Returns
        -------
        position: (float, float)
            The position of the fiber center as a tuple (x, y).
        '''
        n = len(self._fibers)
        if index >= n or index < -n:
            raise IndexError('The fiber index is out of valid range!')
        return tuple(self._fibers[index].position[:2]) 
    def __getitem__(self, what):
        return self._fibers[what]
    def __setitem__(self, what, value: FiberLayout or List[FiberLayout]):
        self._fibers[what] = value
    def _get_normalized(self) -> np.ndarray:
        return self.raw*(1.0/max(self.nphotons, 1.0))
    normalized = property(_get_normalized, None, None, 'Normalized.')
    reflectance = property(_get_normalized, None, None, 'Reflectance.')
    transmittance = property(_get_normalized, None, None, 'Transmittance.')
[docs]    def cl_pack(self, mc: mcobject.McObject,
                target : cltypes.Structure = None) -> cltypes.Structure:
        '''
        Fills the structure (target) with the data required by the
        Monte Carlo simulator.
        See the :py:meth:`FiberArray.cl_type` method for a detailed list
        of fields.
        Parameters
        ----------
        mc: mcobject.McObject
            Monte Carlo simulator instance.
        target: cltypes.Structure
            Ctypes structure that is filled with the source data.
        Returns
        -------
        target: cltypes.Structure
            Filled structure received as an input argument or a new
            instance if the input argument target is None.
        '''
        if target is None:
            target_type = self.cl_type(mc)
            target = target_type()
        for index, fiber_cfg in enumerate(self._fibers):
            adir = fiber_cfg.direction[0], fiber_cfg.direction[1], \
                   
abs(fiber_cfg.direction[2])
            T = geometry.transform_base(adir, (0.0, 0.0, 1.0))
            cos_min = (1.0 - (fiber_cfg.fiber.na/fiber_cfg.fiber.ncore)**2)**0.5
            core_r_squared = 0.25*fiber_cfg.fiber.dcore**2
            target.transformation[index].fromarray(T)
            target.core_position[index].fromarray(fiber_cfg.position)
            target.core_r_squared[index] = core_r_squared
            target.cos_min[index] = cos_min
        allocation = mc.cl_allocate_rw_accumulator_buffer(self, self.shape)
        target.offset = allocation.offset
        return target 
[docs]    def todict(self):
        '''
        Save the accumulator configuration without the accumulator data to
        a dictionary. Use the :meth:`FiberArray.fromdict` method to create a new
        accumulator instance from the returned data.
        Returns
        -------
        data: dict
            Accumulator configuration as a dictionary.
        '''
        return {
            'type':'FiberArray',
            'fibers': [item.todict() for item in self._fibers],
        } 
[docs]    @staticmethod
    def fromdict(data):
        '''
        Create an accumulator instance from a dictionary.
        Parameters
        ----------
        data: dict
            Dictionary created by the :py:meth:`FiberArray.todict` method.
        '''
        detector_type = data.pop('type')
        if detector_type != 'FiberArray':
            raise TypeError(
                'Expected a "FiberArray" type bot got "{}"!'.format(
                    detector_type))
        fibers = data.pop('fibers')
        fibers = [FiberLayout.fromdict(item) for item in fibers]
        return FiberArray(fibers, **data) 
    def __str__(self):
        return 'FiberArray(fibers={})'.format(self._fibers)
    def __repr__(self):
        return '{} #{}'.format(self.__str__(), id(self))