# -*- coding: utf-8 -*-
"""
Created on Wed Mar  6 19:23:56 2024

@author: bertot
"""

import numpy as np
import math
# from gdshelpers.geometry.chip import Cell
from gdshelpers.parts.waveguide import Waveguide
from gdshelpers.parts.port import Port
from gdshelpers.parts.coupler import GratingCoupler


# This function generates a focusing grating coupler, horizontal and with circular grating lines
# LIST OF PARAMETERS (measurements and coordinates always in microns):
    # cell: the main cell to which the layers have to be added
    # x0 and y0: coordinates of the starting point of the grating (where the input waveguide starts/ends)
    # WG_width: width of the input waveguide
    # grating_angle: aperture angle of the grating (usually 50°)
    # grating_period: period (LAMBDA) of the grating
    # grating_DC: duty cycle (usually 0.5)
    # n_gratings: number of grating lines (usually 25)
    # taper_length: length of the taper between input waveguide and grating lines (usually 12.5 um)
    # dist: additional width on each side of the box around the waveguides
    # layers: number of the layers for the box for etching, the portion for shallow etching and the waveguides (usually 99)
def generate_focusing_grating_circular(cell, x0, y0, flip, WG_width, grating_angle, grating_period, grating_DC, n_gratings, taper_length, dist, layerbox, layer_shallowetch, layerWG):
    grating_total_length = taper_length + grating_period*n_gratings
    angle_shallow_etch = grating_angle + math.radians(10)
    if flip == 0:
        angle = np.pi
        Xstart_shallowetch = x0-taper_length-grating_period*n_gratings/2
        shallow_etch1 = Waveguide.make_at_port(Port(origin=(Xstart_shallowetch,y0), angle=np.pi/2, width=grating_period*n_gratings))
        shallow_etch1.add_bend(angle=-angle_shallow_etch/2, radius=taper_length+grating_period*n_gratings/2)
        shallow_etch11 = Waveguide.make_at_port(Port(origin=(Xstart_shallowetch,y0), angle=np.pi/2, width=grating_period*n_gratings))
        shallow_etch11.add_bend(angle=-grating_angle/2, radius=taper_length+grating_period*n_gratings/2)
        shallow_etch2 = Waveguide.make_at_port(Port(origin=(Xstart_shallowetch,y0), angle=-np.pi/2, width=grating_period*n_gratings))
        shallow_etch2.add_bend(angle=angle_shallow_etch/2, radius=taper_length+grating_period*n_gratings/2)
        shallow_etch22 = Waveguide.make_at_port(Port(origin=(Xstart_shallowetch,y0), angle=-np.pi/2, width=grating_period*n_gratings))
        shallow_etch22.add_bend(angle=grating_angle/2, radius=taper_length+grating_period*n_gratings/2)
    else:
        angle = 0
        Xstart_shallowetch = x0+taper_length+grating_period*n_gratings/2 # +grating_total_length/2
        shallow_etch1 = Waveguide.make_at_port(Port(origin=(Xstart_shallowetch,y0), angle=np.pi/2, width=grating_period*n_gratings))
        shallow_etch1.add_bend(angle=angle_shallow_etch/2, radius=taper_length+grating_period*n_gratings/2)
        shallow_etch11 = Waveguide.make_at_port(Port(origin=(Xstart_shallowetch,y0), angle=np.pi/2, width=grating_period*n_gratings))
        shallow_etch11.add_bend(angle=grating_angle/2, radius=taper_length+grating_period*n_gratings/2)
        shallow_etch2 = Waveguide.make_at_port(Port(origin=(Xstart_shallowetch,y0), angle=-np.pi/2, width=grating_period*n_gratings))
        shallow_etch2.add_bend(angle=-angle_shallow_etch/2, radius=taper_length+grating_period*n_gratings/2)
        shallow_etch22 = Waveguide.make_at_port(Port(origin=(Xstart_shallowetch,y0), angle=-np.pi/2, width=grating_period*n_gratings))
        shallow_etch22.add_bend(angle=-grating_angle/2, radius=taper_length+grating_period*n_gratings/2)
    coupler_params = {'width': WG_width,'angle': angle,'full_opening_angle': grating_angle,'grating_period': grating_period,
                      'grating_ff': grating_DC,'n_gratings': n_gratings,'taper_length': taper_length}
    coupler = GratingCoupler.make_traditional_coupler(origin=(x0, y0), **coupler_params)
    coupler_shapely = coupler.get_shapely_object()
    buffered_coupler_shapely = coupler_shapely.buffer(dist)
    cell.add_to_layer(layerWG, coupler)
    cell.add_to_layer(98, shallow_etch11, shallow_etch22)
    cell.add_to_layer(layerbox, buffered_coupler_shapely)
    cell.add_to_layer(layer_shallowetch, shallow_etch1, shallow_etch2)


# This function generates vertical MMI with 1 input and 2 outputs
# LIST OF PARAMETERS (measurements and coordinates always in microns):
    # cell: the main cell to which the layers have to be added
    # x0 and y0: coordinates of the starting point of the MMI (where the input waveguide ends)
    # WG_width: width of the input waveguide
    # flip: parameters deciding the rotation of the MMI. flip = 0 means MMI is created upwards, viceversa for flip = 1
    # dist: additional width on each side of the box around the waveguides
    # layers: number of the layers for the box for etching and the waveguides (usually 99)
def generate_MMI_1x2(cell, x0, y0, WG_width, flip, dist, layerbox, layerWG):
    L_taper = 8
    W_taper_end = 1
    L_MMI = 7.8
    W_MMI = 3
    tapers_distance = 1.75
    if flip == 0:
        start_MMI = (x0,y0+L_taper)
        start_taperOUT1 = (x0+tapers_distance/2,y0+L_taper+L_MMI)
        start_taperOUT2 = (x0-tapers_distance/2,y0+L_taper+L_MMI)
        final_center = (x0,y0+L_taper*2+L_MMI)
        angle = np.pi/2
    else:
        start_MMI = (x0,y0-L_taper)
        start_taperOUT1 = (x0+tapers_distance/2,y0-L_taper-L_MMI)
        start_taperOUT2 = (x0-tapers_distance/2,y0-L_taper-L_MMI)
        final_center = (x0,y0-L_taper*2-L_MMI)
        angle = -np.pi/2
    MMI_taperIN = Waveguide.make_at_port(Port(origin=(x0,y0), angle=angle, width=WG_width))
    MMI_taperIN.add_straight_segment(length=L_taper, final_width=W_taper_end)
    MMI = Waveguide.make_at_port(Port(origin=start_MMI, angle=angle, width=W_MMI))
    MMI.add_straight_segment(length=L_MMI)
    MMI_taperOUT1 = Waveguide.make_at_port(Port(origin=start_taperOUT1, angle=angle, width=W_taper_end))
    MMI_taperOUT1.add_straight_segment(length=L_taper, final_width=WG_width)
    MMI_taperOUT2 = Waveguide.make_at_port(Port(origin=start_taperOUT2, angle=angle, width=W_taper_end))
    MMI_taperOUT2.add_straight_segment(length=L_taper, final_width=WG_width)
    cell.add_to_layer(layerWG, MMI_taperIN, MMI, MMI_taperOUT1, MMI_taperOUT2)
    box_MMI = Waveguide.make_at_port(Port(origin=(x0,y0), angle=angle, width=WG_width+2*dist))
    box_MMI.add_straight_segment(length=L_taper, final_width=W_MMI+2*dist)
    box_MMI.add_straight_segment(length=L_MMI)
    box_MMI.add_straight_segment(length=L_taper, final_width=tapers_distance+WG_width+2*dist)
    cell.add_to_layer(layerbox, box_MMI)
    return final_center     


# This function generates vertical X-coupler with 2 inputs and 2 outputs
# LIST OF PARAMETERS (measurements and coordinates always in microns):
    # cell: the main cell to which the layers have to be added
    # x0 and y0: coordinates of the starting point of the MMI (where the input waveguide ends)
    # WG_width: width of the input waveguide
    # flip: parameters deciding the rotation of the X-coupler. flip = 0 means X-coupler is built upwards, viceversa for flip = 1
    # dist: additional width on each side of the box around the waveguides
    # layers: number of the layers for the box for etching and the waveguides (usually 99)    
def generate_Xcoupler(cell, x0, y0, WG_width, flip, dist, layerbox, layerWG):
    L_taper = 8
    W_taper_end = 1
    L_MMI = 28.1
    W_MMI = 5
    tapers_distance = 1.66
    if flip == 0:
        start_MMI_taperIN1 = (x0+tapers_distance/2,y0)
        start_MMI_taperIN2 = (x0-tapers_distance/2,y0)
        start_MMI = (x0,y0+L_taper)
        start_taperOUT1 = (x0+tapers_distance/2,y0+L_taper+L_MMI)
        start_taperOUT2 = (x0-tapers_distance/2,y0+L_taper+L_MMI)
        final_center = (x0,y0+L_taper*2+L_MMI)
        angle = np.pi/2
    else:
        start_MMI_taperIN1 = (x0-tapers_distance/2,y0)
        start_MMI_taperIN2 = (x0+tapers_distance/2,y0)
        start_MMI = (x0,y0-L_taper)
        start_taperOUT1 = (x0+tapers_distance/2,y0-L_taper-L_MMI)
        start_taperOUT2 = (x0-tapers_distance/2,y0-L_taper-L_MMI)
        final_center = (x0,y0-L_taper*2-L_MMI)
        angle = -np.pi/2
    MMI_taperIN1 = Waveguide.make_at_port(Port(origin=start_MMI_taperIN1, angle=angle, width=WG_width))
    MMI_taperIN1.add_straight_segment(length=L_taper, final_width=W_taper_end)
    MMI_taperIN2 = Waveguide.make_at_port(Port(origin=start_MMI_taperIN2, angle=angle, width=WG_width))
    MMI_taperIN2.add_straight_segment(length=L_taper, final_width=W_taper_end)
    MMI = Waveguide.make_at_port(Port(origin=start_MMI, angle=angle, width=W_MMI))
    MMI.add_straight_segment(length=L_MMI)
    MMI_taperOUT1 = Waveguide.make_at_port(Port(origin=start_taperOUT1, angle=angle, width=W_taper_end))
    MMI_taperOUT1.add_straight_segment(length=L_taper, final_width=WG_width)
    MMI_taperOUT2 = Waveguide.make_at_port(Port(origin=start_taperOUT2, angle=angle, width=W_taper_end))
    MMI_taperOUT2.add_straight_segment(length=L_taper, final_width=WG_width)
    cell.add_to_layer(layerWG, MMI_taperIN1,MMI_taperIN2,  MMI, MMI_taperOUT1, MMI_taperOUT2)
    box_MMI = Waveguide.make_at_port(Port(origin=(x0,y0), angle=angle, width=tapers_distance+WG_width+2*dist))
    box_MMI.add_straight_segment(length=L_taper, final_width=W_MMI+2*dist)
    box_MMI.add_straight_segment(length=L_MMI)
    box_MMI.add_straight_segment(length=L_taper, final_width=tapers_distance+WG_width+2*dist)
    cell.add_to_layer(layerbox, box_MMI)
    return final_center
  

# This function generates a ridge-slot converter, both vertical or horizontal
# LIST OF PARAMETERS (measurements and coordinates always in microns):
    # cell: the main cell to which the layers have to be added
    # x0 and y0: coordinates of the starting point of the MMI (where the input waveguide ends)
    # W_stripWG: width of the ridge waveguide
    # W_slotWG: width of the slot waveguide
    # rotation: parameters deciding the rotation of the converter, values from 0 to 3. The angle is calculated as 90*rotation
    # dist: additional width on each side of the box around the waveguides
    # layers: number of the layers for the box for etching and the waveguides (usually 99)     
def ridge_slot_WG_converter(cell, xo, yo, W_stripWG, W_slotWG, W_slot, rotation, dist, layerbox, layerWG):
    L_taper = 2.5
    W_taper_end = 0.62
    L_TOT_MMI = 6
    L_MMI = 1.38
    L_taper_OUT = L_TOT_MMI - L_MMI
    W_MMI = 1.24
    if rotation == 0:
        start_MMI = (xo+L_taper,yo)
        start_taperOUT = (start_MMI[0]+L_MMI,yo)
        final_center = (xo+L_taper+L_TOT_MMI,yo)
    elif rotation == 1:
        start_MMI = (xo,yo+L_taper)
        start_taperOUT = (xo,start_MMI[1]+L_MMI)
        final_center = (xo,yo+L_taper+L_TOT_MMI)
    elif rotation == 2:
        start_MMI = (xo-L_taper,yo)
        start_taperOUT = (start_MMI[0]-L_MMI,yo)
        final_center = (xo-L_taper-L_TOT_MMI,yo)
    else:
        start_MMI = (xo,yo-L_taper)
        start_taperOUT = (xo,start_MMI[1]-L_MMI)
        final_center = (xo,yo-L_taper-L_TOT_MMI)
    taperIN = Waveguide.make_at_port(Port(origin=(xo,yo), angle=rotation*np.pi/2, width=W_stripWG))
    taperIN.add_straight_segment(length=L_taper, final_width=W_taper_end)
    MMI = Waveguide.make_at_port(Port(origin=start_MMI, angle=rotation*np.pi/2, width=W_MMI))
    MMI.add_straight_segment(length=L_MMI)
    taperOUT = Waveguide.make_at_port(Port(origin=start_taperOUT, angle=rotation*np.pi/2, width=[(W_MMI-W_slot)/2, W_slot, (W_MMI-W_slot)/2]))
    taperOUT.add_straight_segment(length=L_taper_OUT, final_width=[W_slotWG, W_slot, W_slotWG])
    cell.add_to_layer(layerWG, taperIN, MMI, taperOUT)
    box_converter = Waveguide.make_at_port(Port(origin=(xo,yo), angle=rotation*np.pi/2, width=W_stripWG+2*dist))
    box_converter.add_straight_segment(length=L_taper+L_MMI+L_taper_OUT, final_width=2*W_slotWG+W_slot+2*dist)
    cell.add_to_layer(layerbox, box_converter)
    return final_center
 

# This function generates a heater. You can modify it to adapt it to your own design
# LIST OF PARAMETERS (measurements and coordinates always in microns):
    # cell: the main cell to which the layers have to be added
    # x0 and y0: coordinates of the starting point of the MMI (where the input waveguide ends)
    # radius_bend: radius of curvature of ridge waveguide (useful if the heater goes on top of a bent portion of the waveguides)
    # layer_heater: number of the layer for the heater      
def generate_heater(cell, x0, y0, radius_bend, layer_heater):
    origin_heater = (x0-10,y0+520)
    heater = Waveguide.make_at_port(Port(origin=origin_heater, angle=3*np.pi/2, width=150))
    heater.add_straight_segment(length=150)
    heater.add_straight_segment(length=80, final_width=30)
    heater.add_straight_segment(length=180)
    heater.add_bend(angle=np.pi/2, radius=40, final_width=10)
    heater.add_straight_segment(length=200)
    heater.add_bend(angle=-np.pi, radius=20, final_width=1)
    heater.add_straight_segment(length=200)
    heater.add_bend(angle=np.pi, radius=radius_bend)
    heater.add_straight_segment(length=200)
    heater.add_bend(angle=-np.pi, radius=20, final_width=10)
    heater.add_straight_segment(length=380)
    heater.add_bend(angle=-np.pi/2, radius=40, final_width=30)
    heater.add_straight_segment(length=320)
    heater.add_straight_segment(length=80, final_width=150)
    heater.add_straight_segment(length=150)
    cell.add_to_layer(layer_heater, heater)

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

