# -*- coding: utf-8 -*-

# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""This module has a QRenderer to export QDesign to a GDS file."""
# pylint: disable=too-many-lines

import math
import os
from copy import deepcopy
from operator import itemgetter
from typing import TYPE_CHECKING, Tuple, Union, Optional
from typing import Dict as DictType
from collections import defaultdict

import gdstk
import geopandas
import numpy as np
import pandas as pd
import shapely
from scipy.spatial import distance
from shapely.geometry import LineString

from qiskit_metal import draw
from qiskit_metal.renderers.renderer_base import QRenderer
from qiskit_metal.renderers.renderer_gds.make_cheese import Cheesing
from qiskit_metal.toolbox_metal.parsing import is_true

from ... import Dict
from .. import config

if not config.is_building_docs():
    from qiskit_metal.toolbox_python.utility_functions import (
        can_write_to_path,
        get_range_of_vertex_to_not_fillet,
    )

if TYPE_CHECKING:
    # For linting typechecking, import modules that can't be loaded here under normal conditions.
    # For example, I can't import QDesign, because it requires QRenderer first. We have the
    # chicken and egg issue.
    from qiskit_metal.designs import QDesign


class QGDSRenderer(QRenderer):
    """Extends QRenderer to export GDS formatted files. The methods which a
    user will need for GDS export should be found within this class.

    All chips within design should be exported to one gds file.
    For the "subtraction box":
    1. If user wants to export the entire design, AND if the base class of
    QDesign._chips[chip_name]['size'] has dict following below example:
    {'center_x': 0.0, 'center_y': 0.0, 'size_x': 9, 'size_y': 6}
    then this box will be used for every layer within a chip.

    2. If user wants to export entire design, BUT there is no information in
    QDesign._chips[chip_name]['size'], then the renderer will calculate the
    size of all of the components and use that size for the "subtraction box"
    for every layer within a chip.

    3. If user wants to export a list of explicit components, the bounding
    box will be calculated by size of QComponents in the QGeometry table.
    Then be scaled by bounding_box_scale_x and bounding_box_scale_y.

    4. Note: When using the Junction table, the cell for Junction should
    be "x-axis" aligned and then GDS rotates based on LineString given
    in Junction table.


    datatype:
        * 10 Polygon
        * 11 Flexpath

    Default Options:
        * short_segments_to_not_fillet: 'True'
        * check_short_segments_by_scaling_fillet: '2.0'
        * gds_unit: '1'
        * ground_plane: 'True'
        * negative_mask: Dict(main=[])
        * corners: 'natural'
        * tolerance: '0.00001'
        * precision: '0.000000001'
        * width_LineString: '10um'
        * path_filename: '../resources/Fake_Junctions.GDS'
        * junction_pad_overlap: '5um'
        * max_points: '199'
        * fabricate: 'False'
        * cheese: Dict
            * datatype: '100'
            * shape: '0'
            * cheese_0_x: '50um'
            * cheese_0_y: '50um'
            * cheese_1_radius: '100um'
            * delta_x='100um',
            * delta_y='100um',
            * edge_nocheese='200um',
            * view_in_file: Dict(main={1: True})
        * no_cheese: Dict
            * datatype: '99'
            * buffer: '25um'
            * cap_style: '2'
            * join_style: '2'
            * view_in_file: Dict(main={1: True})
        * bounding_box_scale_x: '1.2'
        * bounding_box_scale_y: '1.2'
    """

    # Default options, over-written by passing ``options` dict to
    # render_options.
    # Type: Dict[str, str]
    default_options = Dict(
        # Before converting LINESTRING to FlexPath for GDS, check for fillet
        # errors for LINESTRINGS in QGeometry in QGeometry due to short
        # segments.  If true, break up the LINESTRING so any segment which is
        # shorter than the scaled-fillet by "fillet_scale_factor" will be
        # separated so the short segment will not be fillet'ed.
        short_segments_to_not_fillet="True",
        check_short_segments_by_scaling_fillet="2.0",
        # DO NOT MODIFY `gds_unit`. Gets overwritten by ``set_units``.
        # gdstk unit is 1 meter.  gds_units appear to ONLY be used during
        # write_gds().  Note that gds_unit will be overwritten from the design
        # units, during init().
        # Moving to Gdstk, the default unit is 1 µm (10⁻⁶ m), but we overwrite this.
        # WARNING: this cannot be changed since it is only used during the
        # init once.
        gds_unit="1",  # 1m (one meter, if you want to set 1 um, set 1e-6)
        # Implement creating a ground plane which is scaled from largest
        # bounding box, then QGeometry which is marked as subtract will be
        # removed from ground_plane.  Then the balance of QGeometry will be
        # placed placed in same layer as ground_plane.
        ground_plane="True",
        # By default, export_to_gds() will create a positive_mask for every
        # chip and layer.  Within the Dict, there needs to be an entry for each
        # chip.  Each chip has a list of layers that should export as a
        # negative mask.  If layer number in list, the mask will be negative
        # for that layer.  If user wants to export to a negative_mask for all
        # layers, every layer_number MUST be in list.
        negative_mask=Dict(main=[]),
        # For the gds file, Show/Don't Show intermediate steps?
        # If false, show the intermediate steps in the exported gds file.
        # If true, show the geometries on either neg_datatype_fabricate or pos_datatype_fabricate.
        #   Example:  # denotes the layer number
        #           delete for negative mask: TOP_main_#_NoCheese_99, TOP_main_#_one_hole
        #           delete for positive mask: TOP_main_#_NoCheese_99, TOP_main_#_one_hole,
        #                                       ground_main_#
        fabricate="False",
        # corners: ("natural", "miter", "bevel", "round", "smooth", callable)
        # Type of joins. A callable must receive 6 arguments
        # (vertex and direction vector from both segments being joined,
        # the center and width of the path)
        # and return a list of vertices that make the join.
        # A list can be used to define the join for each parallel path.
        corners="natural",
        # tolerance > precision
        # Precision used for gds lib, boolean operations and FlexPath should
        # likely be kept the same.  They can be different, but increases odds
        # of weird artifacts or misalignment.  Some of this occurs regardless
        # (might be related to offset of a curve when done as a
        # boolean vs. rendered), but they are <<1nm, which isn't even picked
        # up by any fab equipment (so can be ignored).  Numerical errors start
        # to pop up if set precision too fine,
        # but 1nm seems to be the finest precision we use anyhow.
        # FOR NOW SPECIFY IN METERS.
        tolerance="0.00001",  # 10.0 um
        # With input from fab people, any of the weird artifacts
        # (like unwanted gaps) that are less than 1nm in size can be ignored.
        # They don't even show up in the fabricated masks.
        # So, the precision of e-9 (so 1 nm) should be good as a default.
        # FOR NOW SPECIFY IN METERS.
        precision="0.000000001",  # 1.0 nm
        # Since Qiskit Metal GUI, does not require a width for LineString, GDS,
        # will provide a default value.
        width_LineString="10um",
        # The file is expected to be in GDS format.  The cell will be placed
        # into gds Metal output without being edited. The name of the cell can
        # be placed as options for a component, i.e. placing within a qubit.
        # During export, the cell will NOT be edited, just imported.
        path_filename="../resources/Fake_Junctions.GDS",
        # For junction table, when cell from default_options.path_filename does
        # not fit into linestring, QGDSRender will create two pads and add to
        # junction to fill the location of lineString.  The junction_pad_overlap
        # is from the junction cell to the newly created pads.
        junction_pad_overlap="5um",
        # Vertex limit for FlexPath
        # max_points (integer) – If the number of points in the polygonal path
        # boundary is greater than max_points, it will be fractured in smaller
        # polygons with at most max_points each. If max_points is zero,
        # no fracture will occur. GDSII used 199 as the default because the historical
        # max value of vertices for a poly/path was 199 (fabrication equipment
        # restrictions).  The hard max limit that a GDSII file can
        # handle is 8191. Gdstk allows arbitary numbers as does most modern software.
        max_points="199",
        # Cheesing is denoted by each chip and layer.
        cheese=Dict(
            # Cheesing is NOT completed
            datatype="100",
            # Expect to mostly cheese a square, but allow for expansion.
            # 0 is rectangle, 1 is circle
            shape="0",
            # rectangle
            cheese_0_x="25um",
            cheese_0_y="25um",
            # circle
            cheese_1_radius="100um",
            # Identify which layers to view in gds output file, for each chip
            view_in_file=Dict(main={1: True}),
            # delta spacing between holes
            delta_x="100um",
            delta_y="100um",
            # Keep a buffer around the perimeter of chip, that will
            # not need cheesing.
            edge_nocheese="200um",
        ),
        # Think of this as a keep-out region for cheesing.
        no_cheese=Dict(
            # For every layer, if there is a ground plane, do cheesing and
            # place the output on the datatype number (sub-layer number)
            datatype="99",
            buffer="25um",
            # The styles of caps are specified by integer values:
            # 1 (round), 2 (flat), 3 (square).
            cap_style="2",
            # The styles of joins between offset segments are specified by
            # integer values:
            # 1 (round), 2 (mitre), and 3 (bevel).
            join_style="2",
            # Identify which layers to view in gds output file, for each chip
            view_in_file=Dict(main={1: True}),
        ),
        # (float): Scale box of components to render.
        # Should be greater than 1.0.  For benefit of the GUI, keep this the
        # last entry in the dict.  GUI shows a note regarding bound_box.
        bounding_box_scale_x="1.2",
        bounding_box_scale_y="1.2",
    )
    """Default options"""

    name = "gds"
    """Name"""

    # When additional columns are added to QGeometry,
    # this is the example to populate it.
    # e.g. element_extensions = dict(
    #         base=dict(color=str, klayer=int),
    #         path=dict(thickness=float, material=str, perfectE=bool),
    #         poly=dict(thickness=float, material=str), )
    # """element extensions dictionary from base class:
    #    element_extensions = dict() """

    # Add columns to junction table during QGDSRenderer.load()
    # element_extensions  is now being populated as part of load().
    # Determined from element_table_data.

    # Dict structure MUST be same as  element_extensions!!!!!!
    # This dict will be used to update QDesign during init of renderer.
    # Keeping this as a cls dict so could be edited before renderer is
    # instantiated.  To update component.options junction table.

    element_table_data = dict(
        # Cell_name must exist in gds file with: path_filename
        junction=dict(cell_name="my_other_junction"))
    """Element table data"""

    def __init__(
        self,
        design: "QDesign",
        initiate=True,
        render_template: Dict = None,
        render_options: Dict = None,
    ):
        """Create a QRenderer for GDS interface: export and import.

        Args:
            design (QDesign): Use QGeometry within QDesign  to obtain elements
                            for GDS file.
            initiate (bool, optional): True to initiate the renderer.
                                        Defaults to True.
            render_template (Dict, optional): Typically used by GUI for
                                template options for GDS.  Defaults to None.
            render_options (Dict, optional): Used to override all options.
                                            Defaults to None.
        """

        super().__init__(
            design=design,
            initiate=initiate,
            render_template=render_template,
            render_options=render_options,
        )

        self.lib: Union[gdstk.Library, None] = None
        self.new_gds_library()

        self.dict_bounds: DictType[str, Dict] = Dict()

        # Updated each time export_to_gds() is called.
        self.chip_info: DictType[str, dict] = dict()

        # check the scale
        self._check_bounding_box_scale()

        # if imported, hold the path to file name, otherwise None.
        self.imported_junction_gds: Union[str, None] = None  # track only once

        QGDSRenderer.load()

    def _initiate_renderer(self):
        """Not used by the gds renderer at this time. Only returns True."""
        return True

    def _close_renderer(self):
        """Not used by the gds renderer at this time. Only returns True."""
        return True

    def render_design(self):
        """Export the design to GDS."""
        self.export_to_gds(file_name=self.design.name, highlight_qcomponents=[])

    def _check_bounding_box_scale(self):
        """Some error checking for bounding_box_scale_x and
        bounding_box_scale_y numbers."""
        bounding_box_scale_x = self.parse_value(
            self.options.bounding_box_scale_x)
        bounding_box_scale_y = self.parse_value(
            self.options.bounding_box_scale_y)

        if bounding_box_scale_x < 1:
            self.options["bounding_box_scale_x"] = (
                QGDSRenderer.default_options.bounding_box_scale_x)
            self.logger.warning(
                "Expected float and number greater than or equal to"
                " 1.0 for bounding_box_scale_x. User"
                f"provided bounding_box_scale_x = {bounding_box_scale_x}"
                ", using default_options.bounding_box_scale_x.")

        if bounding_box_scale_y < 1:
            self.options["bounding_box_scale_y"] = (
                QGDSRenderer.default_options.bounding_box_scale_y)
            self.logger.warning(
                "Expected float and number greater than or equal to 1.0 for "
                "bounding_box_scale_y.  User provided "
                f"bounding_box_scale_y = {bounding_box_scale_y}, "
                "using default_options.bounding_box_scale_y.")

    def _can_write_to_path(self, file: str) -> int:
        """Check if can write file.

        Args:
            file (str): Has the path and/or just the file name.

        Returns:
            int: 1 if access is allowed. Else returns 0, if access not given.
        """
        status, directory_name = can_write_to_path(file)  # pylint: disable=possibly-used-before-assignment
        if status:
            return 1

        self.logger.warning("Not able to write to directory."
                            f'File:"{file}" not written.'
                            f' Checked directory:"{directory_name}".')
        return 0

    def _update_units(self):
        """Update the options in the units.

        Warning: DOES NOT CHANGE THE CURRENT LIB
        """
        self.options["gds_unit"] = 1.0 / self.design.parse_value("1 meter")

    def _separate_subtract_shapes(self, chip_name: str, table_name: str,
                                  table: geopandas.GeoSeries) -> None:
        """For each chip and table, separate them by subtract being either True
        or False. Names of chip and table should be same as the QGeometry
        tables.

        Args:
            chip_name (str): Name of "chip".  Example is "main".
            table_name (str): Name for "table".  Example is "poly", and "path".
            table (geopandas.GeoSeries): Table with similar qgeometries.
        """
        # pylint: disable=singleton-comparison
        subtract_true = table[table["subtract"] == True]

        subtract_false = table[table["subtract"] == False]

        setattr(self, f"{chip_name}_{table_name}_subtract_true", subtract_true)
        setattr(self, f"{chip_name}_{table_name}_subtract_false",
                subtract_false)

    @staticmethod
    def _get_bounds(
        gs_table: geopandas.GeoDataFrame,) -> Tuple[float, float, float, float]:
        """Get the bounds for all of the elements in gs_table.

        Args:
            gs_table (geopandas.GeoDataFrame): A GeoPandas GeoDataFrame used to describe
                                        components in a design.

        Returns:
            Tuple[float, float, float, float]: The bounds of all of the
            elements in this table. [minx, miny, maxx, maxy]
        """
        if len(gs_table) == 0:
            return (0, 0, 0, 0)

        return gs_table.total_bounds

    @staticmethod
    def _inclusive_bound(all_bounds: list) -> tuple:
        """Given a list of tuples which describe corners of a box, i.e. (minx,
        miny, maxx, maxy). This method will find the box, which will include
        all boxes.  In another words, the smallest minx and miny; and the
        largest maxx and maxy.

        Args:
            all_bounds (list): List of bounds. Each tuple corresponds to a box.

        Returns:
            tuple: Describe a box which includes the area of each box
            in all_bounds.
        """

        # If given an empty list.
        if len(all_bounds) == 0:
            return (0.0, 0.0, 0.0, 0.0)

        inclusive_tuple = (
            min(all_bounds, key=itemgetter(0))[0],
            min(all_bounds, key=itemgetter(1))[1],
            max(all_bounds, key=itemgetter(2))[2],
            max(all_bounds, key=itemgetter(3))[3],
        )
        return inclusive_tuple

    @staticmethod
    def _midpoint_xy(x_1: float, y_1: float, x_2: float,
                     y_2: float) -> Tuple[float, float]:
        """Calculate the center of a line segment with endpoints (x1,y1) and
        (x2,y2).

        Args:
            x1 (float): x of endpoint (x1,y1)
            y1 (float): y of endpoint (x1,y1)
            x2 (float): x of endpoint (x2,y2)
            y2 (float): y of endpoint (x2,y2)

        Returns:
            Tuple[float, float]:
            1st float: x for midpoint
            2nd float: y for midpoint
        """
        midx = (x_1 + x_2) / 2
        midy = (y_1 + y_2) / 2

        return midx, midy

    def _scale_max_bounds(self, chip_name: str,
                          all_bounds: list) -> Tuple[tuple, tuple]:
        """Given the list of tuples to represent all of the bounds for path,
        poly, etc. This will return the scaled using self.bounding_box_scale_x
        and self.bounding_box_scale_y, and the max bounds of the tuples
        provided.

        Args:
            chip_name (str): Name of chip.
            all_bounds (list): Each tuple=(minx, miny, maxx, maxy) in list
                            represents bounding box for poly, path, etc.

        Returns:
            tuple[tuple, tuple]:
            first tuple: A scaled bounding box which includes all paths, polys, etc.;
            second tuple: A  bounding box which includes all paths, polys, etc.
        """
        # If given an empty list.
        if len(all_bounds) == 0:
            return (0.0, 0.0, 0.0, 0.0), (0.0, 0.0, 0.0, 0.0)

        # Get an inclusive bounding box to contain all of the tuples provided.
        minx, miny, maxx, maxy = self._inclusive_bound(all_bounds)

        # Center of inclusive bounding box
        center_x = (minx + maxx) / 2
        center_y = (miny + maxy) / 2

        scaled_width = (maxx - minx) * self.parse_value(
            self.options.bounding_box_scale_x)
        scaled_height = (maxy - miny) * self.parse_value(
            self.options.bounding_box_scale_y)

        # Scaled inclusive bounding box by self.options.bounding_box_scale_x
        #  and self.options.bounding_box_scale_y.
        scaled_box = (
            center_x - (0.5 * scaled_width),
            center_y - (0.5 * scaled_height),
            center_x + (0.5 * scaled_width),
            center_y + (0.5 * scaled_height),
        )

        self.dict_bounds[chip_name]["scaled_box"] = scaled_box
        self.dict_bounds[chip_name]["inclusive_box"] = (minx, miny, maxx, maxy)

        return scaled_box, (minx, miny, maxx, maxy)

    def _check_qcomps(self,
                      highlight_qcomponents: list = None) -> Tuple[list, int]:
        """Confirm the list doesn't have names of components repeated. Confirm
        that the name of component exists in QDesign.

        Args:
            highlight_qcomponents (list, optional): List of strings which
                                        denote the name of QComponents to render.
                                        Empty list means to render entire design.
                                        Defaults to [].

        Returns:
            Tuple[list, int]:
            list: Unique list of QComponents to render.
            int: 0 if all ended well. Otherwise,
            1 if QComponent name not in design.
        """
        if highlight_qcomponents is None:
            highlight_qcomponents = []

        # Remove identical QComponent names.
        unique_qcomponents = list(set(highlight_qcomponents))

        # Confirm all QComponent are in design.
        for qcomp in unique_qcomponents:
            if qcomp not in self.design.name_to_id:
                self.logger.warning(
                    f"The component={qcomp} in highlight_qcomponents not"
                    " in QDesign. The GDS data not generated.")
                return unique_qcomponents, 1

        # For Subtraction bounding box.
        # If list passed to export is the whole chip, then want to use the
        # bounding box from design planar.  If list is subset of chip, then
        # calculate a custom bounding box and scale it.

        # pylint: disable=protected-access
        if len(unique_qcomponents) == len(self.design._components):
            # Since user wants all of the chip to be rendered, use the
            # design.planar bounding box.
            unique_qcomponents[:] = []

        return unique_qcomponents, 0

    def _create_qgeometry_for_gds(self,
                                  highlight_qcomponents: list = None) -> int:
        """Using self.design, this method does the following:

        1. Gather the QGeometries to be used to write to file.
           Duplicate names in highlight_qcomponents will be removed without
           warning.

        2. Populate self.dict_bounds, for each chip, contains the maximum bound
           for all elements to render.

        3. Calculate scaled bounding box to emulate size of chip using
           self.bounding_box_scale(x and y) and place into
           self.dict_bounds[chip_name]['for_subtract'].

        4. Gather Geometries to export to GDS format.

        Args:
            highlight_qcomponents (list): List of strings which denote the name
                            of QComponents to render.
                            If empty, render all components in design.
                            If QComponent names are duplicated,
                            duplicates will be ignored.

        Returns:
            int: 0 if all ended well.
            Otherwise, 1 if QComponent name(s) not in design.
        """
        if highlight_qcomponents is None:
            highlight_qcomponents = []
        unique_qcomponents, status = self._check_qcomps(highlight_qcomponents)
        if status == 1:
            return 1
        self.dict_bounds.clear()

        for chip_name, _ in self.chip_info.items():
            # put the QGeometry into GDS format.
            # There can be more than one chip in QGeometry.
            # They all export to one gds file.
            self.chip_info[chip_name]["all_subtract"] = []
            self.chip_info[chip_name]["all_no_subtract"] = []

            self.dict_bounds[chip_name] = Dict()
            self.dict_bounds[chip_name]["gather"] = []
            self.dict_bounds[chip_name]["for_subtract"] = tuple()
            all_table_subtracts = []
            all_table_no_subtracts = []

            for table_name in self.design.qgeometry.get_element_types():
                # Get table for chip and table_name, and reduce
                # to keep just the list of unique_qcomponents.
                table = self._get_table(table_name, unique_qcomponents,
                                        chip_name)

                if table_name == "junction":
                    self.chip_info[chip_name]["junction"] = deepcopy(table)
                else:
                    # For every chip, and layer, separate the "subtract"
                    # and "no_subtract" elements and gather bounds.
                    # self.dict_bounds[chip_name] = list_bounds
                    self._gather_subtract_elements_and_bounds(
                        chip_name,
                        table_name,
                        table,
                        all_table_subtracts,
                        all_table_no_subtracts,
                    )

            # If list of QComponents provided, use the
            # bounding_box_scale(x and y), otherwise use self._chips.
            scaled_max_bound, max_bound = self._scale_max_bounds(
                chip_name, self.dict_bounds[chip_name]["gather"])
            if highlight_qcomponents:
                self.dict_bounds[chip_name]["for_subtract"] = scaled_max_bound
            else:
                chip_box, status = self.design.get_x_y_for_chip(chip_name)
                if status == 0:
                    self.dict_bounds[chip_name]["for_subtract"] = chip_box
                else:
                    self.dict_bounds[chip_name]["for_subtract"] = max_bound
                    self.logger.warning(
                        f"design.get_x_y_for_chip() did NOT return a good "
                        f"code for chip={chip_name},for ground subtraction-box"
                        f" using the size calculated from QGeometry, "
                        f"({max_bound}) will be used. ")
            if is_true(self.options.ground_plane):
                self._handle_ground_plane(chip_name, all_table_subtracts,
                                          all_table_no_subtracts)

        return 0

    def _handle_ground_plane(self, chip_name: str, all_table_subtracts: list,
                             all_table_no_subtracts: list):
        """Place all the subtract geometries for one chip into
        self.chip_info[chip_name]['all_subtract_true'].

        For LINESTRING within table that has a value for fillet, check if any
        segment is shorter than fillet radius.  If so, then break the
        LINESTRING so that shorter segments do not get fillet'ed and longer
        segments get fillet'ed.  Add the multiple LINESTRINGS back to table.
        Also remove "bad" LINESTRING from table.

        Then use _qgeometry_to_gds() to convert the QGeometry elements to gdstk
        elements.  The gdstk elements are placed in
        self.chip_info[chip_name]['q_subtract_true'].

        Args:
            chip_name (str): Chip_name that is being processed.
            all_table_subtracts (list): Add to self.chip_info by layer number.
            all_table_no_subtracts (list): Add to self.chip_info by layer number.
        """

        fix_short_segments = self.parse_value(
            self.options.short_segments_to_not_fillet)
        all_layers = self.design.qgeometry.get_all_unique_layers(chip_name)

        for chip_layer in all_layers:
            copy_subtract = []
            copy_no_subtract = []
            copy_subtract = deepcopy(all_table_subtracts)
            copy_no_subtract = deepcopy(all_table_no_subtracts)

            for item in copy_subtract:
                item.drop(item.index[item["layer"] != chip_layer], inplace=True)

            for item_no in copy_no_subtract:
                item_no.drop(item_no.index[item_no["layer"] != chip_layer],
                             inplace=True)

            self.chip_info[chip_name][chip_layer]["all_subtract_true"] = (
                geopandas.GeoDataFrame(
                    pd.concat(copy_subtract, ignore_index=False)))

            self.chip_info[chip_name][chip_layer]["all_subtract_false"] = (
                geopandas.GeoDataFrame(
                    pd.concat(copy_no_subtract, ignore_index=False)))

            self.chip_info[chip_name][chip_layer][
                "all_subtract_true"].reset_index(inplace=True)
            self.chip_info[chip_name][chip_layer][
                "all_subtract_false"].reset_index(inplace=True)

            if is_true(fix_short_segments):
                self._fix_short_segments_within_table(chip_name, chip_layer,
                                                      "all_subtract_true")
                self._fix_short_segments_within_table(chip_name, chip_layer,
                                                      "all_subtract_false")

            self.chip_info[chip_name][chip_layer][
                "q_subtract_true"] = self.chip_info[chip_name][chip_layer][
                    "all_subtract_true"].apply(self._qgeometry_to_gds, axis=1)

            self.chip_info[chip_name][chip_layer][
                "q_subtract_false"] = self.chip_info[chip_name][chip_layer][
                    "all_subtract_false"].apply(self._qgeometry_to_gds, axis=1)

    # Handling Fillet issues.

    def _fix_short_segments_within_table(self, chip_name: str, chip_layer: int,
                                         all_sub_true_or_false: str):
        """Update self.chip_info geopandas.GeoDataFrame.

        Will iterate through the rows to examine the LineString.
        Then determine if there is a segment that is shorter than the criteria
        based on default_options. If so, then remove the row, and append
        shorter LineString with no fillet, within the dataframe.

        Args:
            chip_name (str): The name of chip.
            chip_layer (int): The layer within the chip to be evaluated.
            all_sub_true_or_false (str): To be used within self.chip_info:
                                'all_subtract_true' or 'all_subtract_false'.
        """
        # pylint: disable=too-many-locals
        data_frame = self.chip_info[chip_name][chip_layer][
            all_sub_true_or_false]
        df_fillet = data_frame[-data_frame["fillet"].isnull()]

        if not df_fillet.empty:
            # Don't edit the table when iterating through the rows.
            # Save info in dict and then edit the table.
            edit_index = dict()
            for index, row in df_fillet.iterrows():
                # print(
                #     f'With parse_value: {self.parse_value(row.fillet)}, '
                #     f'row.fillet: {row.fillet}')
                status, all_shapelys = self._check_length(
                    row.geometry, row.fillet)
                if status > 0:
                    edit_index[index] = all_shapelys

            df_copy = self.chip_info[chip_name][chip_layer][
                all_sub_true_or_false].copy(deep=True)
            for del_key, the_shapes in edit_index.items():
                # copy row "index" into a new data-frame "status" times.
                # Then replace the LONG shapely with all_shapelys.
                # For any entries in edit_index, edit table here.
                orig_row = df_copy.loc[del_key].copy(deep=True)
                df_copy = df_copy.drop(index=del_key)

                for dummy_new_row, short_shape in the_shapes.items():
                    orig_row["geometry"] = short_shape["line"]
                    orig_row["fillet"] = short_shape["fillet"]
                    # Keep ignore_index=False, otherwise,
                    # the other del_key will not be found.
                    df_copy = df_copy.append(orig_row, ignore_index=False)

            self.chip_info[chip_name][chip_layer][
                all_sub_true_or_false] = df_copy.copy(deep=True)

    def _check_length(self, a_shapely: shapely.geometry.LineString,
                      a_fillet: float) -> Tuple[int, Dict]:
        """Determine if a_shapely has short segments based on scaled fillet
        value.

        Use check_short_segments_by_scaling_fillet to determine the criteria
        for flagging a segment.  Return Tuple with flagged segments.

        The "status" returned in int:
            * -1: Method needs to update the return code.
            * 0: No issues, no short segments found
            * int: The number of shapelys returned. New shapelys, should
                    replace the ones provided in a_shapely

        The "shorter_lines" returned in dict:
        key: Using the index values from list(a_shapely.coords)
        value: dict() for each new, shorter, LineString

        The dict()
        key: fillet, value: can be float from before, or undefined to
                            denote no fillet.
        key: line, value: shorter LineString

        Args:
            a_shapely (shapely.geometry.LineString): A shapely object that
                                                    needs to be evaluated.
            a_fillet (float): From component developer.

        Returns:
            Tuple[int, Dict]:
            int: Number of short segments that should not have fillet.
            Dict: The key is an index into a_shapely. The value is a dict with
            fillet and shorter LineString.
        """
        # pylint: disable=too-many-locals
        # pylint: disable=too-many-arguments
        # pylint: disable=too-many-statements
        # pylint: disable=too-many-branches

        # Holds all of the index of when a segment is too short.
        idx_bad_fillet = list()
        status = -1  # Initialize to meaningless value.
        coords = list(a_shapely.coords)
        len_coords = len(coords)

        all_idx_bad_fillet = dict()

        self._identify_vertex_not_to_fillet(coords, a_fillet,
                                            all_idx_bad_fillet)

        shorter_lines = dict()

        idx_bad_fillet = sorted(all_idx_bad_fillet["reduced_idx"])
        status = len(idx_bad_fillet)

        if status:
            midpoints = all_idx_bad_fillet["midpoints"]
            no_fillet_vertices = list()
            fillet_vertices = list()

            # Gather the no-fillet segments
            for idx, (start, stop) in enumerate(idx_bad_fillet):
                no_fillet_vertices.clear()
                if idx == 0 and start == 0:
                    # The first segment.
                    if stop == len_coords - 1:
                        # Every vertex should not be fillet'd
                        no_fillet_vertices = coords[start:len_coords]
                        shorter_lines[stop] = dict({
                            "line": LineString(no_fillet_vertices),
                            "fillet": float("NaN"),
                        })
                    else:
                        no_fillet_vertices = coords[start:stop + 1]
                        no_fillet_vertices.append(midpoints[stop])
                        shorter_lines[stop] = dict({
                            "line": LineString(no_fillet_vertices),
                            "fillet": float("NaN"),
                        })
                elif idx == status - 1 and stop == len_coords - 1:
                    # The last segment
                    no_fillet_vertices = coords[start:stop + 1]
                    no_fillet_vertices.insert(0, midpoints[start - 1])
                    shorter_lines[stop] = dict({
                        "line": LineString(no_fillet_vertices),
                        "fillet": float("NaN")
                    })
                else:
                    # Segment in between first and last segment.
                    no_fillet_vertices = coords[start:stop + 1]
                    no_fillet_vertices.insert(0, midpoints[start - 1])
                    no_fillet_vertices.append(midpoints[stop])
                    shorter_lines[stop] = dict({
                        "line": LineString(no_fillet_vertices),
                        "fillet": float("NaN")
                    })
            # Gather the fillet segments.
            at_vertex = 0
            for idx, (start, stop) in enumerate(idx_bad_fillet):
                fillet_vertices.clear()
                if idx == 0 and start == 0:
                    pass  # just update at_vertex
                if idx == 0 and start == 1:
                    init_tuple = coords[0]
                    fillet_vertices = [init_tuple, midpoints[start - 1]]
                    shorter_lines[start] = dict({
                        "line": LineString(fillet_vertices),
                        "fillet": a_fillet
                    })
                if idx == 0 and start > 1:
                    fillet_vertices = coords[0:start]
                    fillet_vertices.append(midpoints[start - 1])
                    shorter_lines[start] = dict({
                        "line": LineString(fillet_vertices),
                        "fillet": a_fillet
                    })
                    if idx == status - 1 and stop != len_coords - 1:
                        # Extra segment after the last no-fillet.
                        fillet_vertices.clear()
                        fillet_vertices = coords[stop + 1:len_coords]
                        fillet_vertices.insert(0, midpoints[stop])
                        shorter_lines[len_coords] = dict({
                            "line": LineString(fillet_vertices),
                            "fillet": a_fillet
                        })
                elif idx == status - 1 and start == 0 and stop != len_coords - 1:
                    # At last tuple, and and start at first index,
                    # and the stop is not last index of coords.
                    fillet_vertices = coords[stop + 1:len_coords]
                    fillet_vertices.insert(0, midpoints[stop])
                    shorter_lines[start] = dict({
                        "line": LineString(fillet_vertices),
                        "fillet": a_fillet
                    })
                elif idx == status - 1 and stop != len_coords - 1:
                    # At last tuple, and the stop is not last index of coords.
                    fillet_vertices = coords[at_vertex + 1:start]
                    fillet_vertices.insert(0, midpoints[at_vertex])
                    fillet_vertices.append(midpoints[start - 1])
                    shorter_lines[start] = dict({
                        "line": LineString(fillet_vertices),
                        "fillet": a_fillet
                    })
                    # Extra segment after the last no-fillet.
                    fillet_vertices.clear()
                    fillet_vertices = coords[stop + 1:len_coords]
                    fillet_vertices.insert(0, midpoints[stop])
                    shorter_lines[len_coords] = dict({
                        "line": LineString(fillet_vertices),
                        "fillet": a_fillet
                    })
                else:
                    if (start - at_vertex) > 1:
                        fillet_vertices = coords[at_vertex + 1:start]
                        fillet_vertices.insert(0, midpoints[at_vertex])
                        fillet_vertices.append(midpoints[start - 1])
                        shorter_lines[start] = dict({
                            "line": LineString(fillet_vertices),
                            "fillet": a_fillet
                        })
                at_vertex = stop  # Need to update for every loop.
        else:
            # No short segments.
            shorter_lines[len_coords - 1] = a_shapely
        return status, shorter_lines

    def _identify_vertex_not_to_fillet(self, coords: list, a_fillet: float,
                                       all_idx_bad_fillet: dict):
        """Use coords to denote segments that are too short.  In particular,
        when fillet'd, they will cause the appearance of incorrect fillet when
        graphed.

        Args:
            coords (list): User provide a list of tuples.
                    The tuple is (x,y) location for a vertex.
                    The list represents a LineString.
            a_fillet (float): The value provided by component developer.
            all_idx_bad_fillet (dict): An empty dict which will be
                                        populated by this method.

        Dictionary:
            Key 'reduced_idx' will hold list of tuples.
                    The tuples correspond to index for list named "coords".
            Key 'midpoints' will hold list of tuples.
                    The index of a tuple corresponds to two index within coords.
            For example, a index in midpoints is x,
            that corresponds midpoint of segment x-1 to x.
        """

        # Depreciated since there is no longer a scale factor
        # given to QCheckLength.
        # fillet_scale_factor = self.parse_value(
        #     self.options.check_short_segments_by_scaling_fillet)

        # precision = float(self.parse_value(self.options.precision))

        # For now, DO NOT allow the user of GDS to provide the precision.
        # user_precision = int(np.abs(np.log10(precision)))

        all_idx_bad_fillet["reduced_idx"] = get_range_of_vertex_to_not_fillet(
            coords,
            a_fillet,
            self.design.template_options.PRECISION,
            add_endpoints=True)
        midpoints = [
            self._midpoint_xy(coords[idx - 1][0], coords[idx - 1][1],
                              vertex2[0], vertex2[1])
            for idx, vertex2 in enumerate(coords)
            if idx > 0
        ]
        all_idx_bad_fillet["midpoints"] = midpoints

    # Move data around to be useful for GDS

    def _gather_subtract_elements_and_bounds(
        self,
        chip_name: str,
        table_name: str,
        table: geopandas.GeoDataFrame,
        all_subtracts: list,
        all_no_subtracts: list,
    ):
        """For every chip, and layer, separate the "subtract" and "no_subtract"
        elements and gather bounds for all the elements in qgeometries. Use
        format: f'{chip_name}_{table_name}s'.

        Args:
            chip_name (str): Name of chip.  Example is 'main'.
            table_name (str): There are multiple tables in QGeometry table.
                                Example: 'path' and 'poly'.
            table (geopandas.GeoDataFrame): Actual table for the name.
            all_subtracts (list): Pass by reference so method can update
                                    this list.
            all_no_subtracts (list): Pass by reference so method can update
                                    this list.
        """

        # Determine bound box and return scalar larger than size.
        bounds = tuple(self._get_bounds(table))

        # Add the bounds of each table to list.
        self.dict_bounds[chip_name]["gather"].append(bounds)

        if is_true(self.options.ground_plane):
            self._separate_subtract_shapes(chip_name, table_name, table)

            all_subtracts.append(
                getattr(self, f"{chip_name}_{table_name}_subtract_true"))
            all_no_subtracts.append(
                getattr(self, f"{chip_name}_{table_name}_subtract_false"))

        # Done because ground plane option may be false.
        # This is not used anywhere currently.
        # Keep this deprecated code.
        # polys use gdstk.Polygon;    paths use gdstk.LineString

        # q_geometries = table.apply(self._qgeometry_to_gds, axis=1)
        # setattr(self, f'{chip_name}_{table_name}s', q_geometries)

    def _get_table(self, table_name: str, unique_qcomponents: list,
                   chip_name: str) -> geopandas.GeoDataFrame:
        """If unique_qcomponents list is empty, get table using table_name from
        QGeometry tables for all elements with table_name.  Otherwise, return a
        table with fewer elements, for just the qcomponents within the
        unique_qcomponents list.

        Args:
            table_name (str): Can be "path", "poly", etc. from the
                            QGeometry tables.
            unique_qcomponents (list): User requested list of qcomponent
                            names to export to GDS file.

        Returns:
            geopandas.GeoDataFrame: Table of elements within the QGeometry.
        """

        # self.design.qgeometry.tables is a dict. key=table_name, value=geopandas.GeoDataFrame
        if len(unique_qcomponents) == 0:
            table = self.design.qgeometry.tables[table_name]
        else:
            table = self.design.qgeometry.tables[table_name]
            # Convert string QComponent.name  to QComponent.id
            highlight_id = [
                self.design.name_to_id[a_qcomponent]
                for a_qcomponent in unique_qcomponents
            ]

            # Remove QComponents which are not requested.
            table = table[table["component"].isin(highlight_id)]

        table = table[table["chip"] == chip_name]

        return table

    # To export the data.

    def new_gds_library(self) -> gdstk.Library:
        """Creates a new GDS Library. Deletes the old. Create a new GDS library
        file. It can contains multiple cells.

        Returns:
            gdstk.Library: GDS library which can contain multiple cells.
        """

        self._update_units()

        if self.lib:
            # Clear old cells from the library
            for c in list(self.lib.cells):
                self.lib.remove(c)

        # Create a new GDS library file. It can contains multiple cells.
        self.lib = gdstk.Library(
            unit=float(self.parse_value(self.options.gds_unit)),
            precision=float(self.parse_value(self.options.precision)),
        )

        return self.lib

    def _check_cheese(self, chip: str, layer: int) -> int:
        """Examine the option for cheese_view_in_file.

        Args:
            chip (str): User defined chip name.
            layer (int): Layer used in chip.

        Returns:
            int: Observation of option based on chip and layer information.

            * 0 This is the initialization state.
            * 1 The layer is in the chip and cheese is True.
            * 2 The layer is in the chip and cheese is False.
            * 3 The chip is not in dict, so can't give answer.
            * 4 The layer is not in the chip, so can't give answer.
        """
        code = 0

        cheese_option = self.parse_value(self.options.cheese.view_in_file)

        if chip in cheese_option:
            if layer in cheese_option[chip]:
                if is_true(cheese_option[chip][layer]):
                    code = 1
                else:
                    code = 2
            else:
                code = 4
        else:
            code = 3

        return code

    def _check_no_cheese(self, chip: str, layer: int) -> int:
        """Examine the option for no_cheese_view_in_file.

        Args:
            chip (str): User defined chip name.
            layer (int): Layer used in chip.

        Returns:
            int: Observation of option based on chip and layer information.

            * 0 This is the initialization state.
            * 1 The layer is in the chip and viewing no-cheese is True.
            * 2 The layer is in the chip and viewing no-cheese is False.
            * 3 The chip is not in dict, so can't give answer.
            * 4 The layer is not in the chip, so can't give answer.
        """

        code = 0

        no_cheese_option = self.parse_value(self.options.no_cheese.view_in_file)

        if chip in no_cheese_option:
            if layer in no_cheese_option[chip]:
                if is_true(no_cheese_option[chip][layer]):
                    code = 1
                else:
                    code = 2
            else:
                code = 4
        else:
            code = 3

        return code

    def _check_either_cheese(self, chip: str, layer: int) -> int:
        """Use methods to check two options and give review of values for
        no_cheese_view_in_file and cheese_view_in_file.

        Args:
            chip (str): User defined chip name.
            layer (int): Layer used in chip.

        Returns:
            int: Observation of options based on chip and layer information.

            * 0 This is the initialization state.
            * 1 Show the layer in both cheese and no cheese
            * 2 Show the layer in just the cheese
            * 3 Show the no-cheese, but not the cheese
            * 4 Do NOT show the layer in neither cheese
            * 5 The chip is not in the default option.
            * 6 The layer is not in the chip dict.
        """
        # pylint: disable=too-many-return-statements
        code = 0
        no_cheese_code = self._check_no_cheese(chip, layer)
        cheese_code = self._check_cheese(chip, layer)

        if no_cheese_code == 0 or cheese_code == 0:
            self.logger.warning("Not able to get no_cheese_view_in_file or "
                                "cheese_view_in_file from self.options.")
            code = 0
            return code
        if no_cheese_code == 1 and cheese_code == 1:
            code = 1
            return code
        if no_cheese_code == 2 and cheese_code == 1:
            code = 2
            return code
        if no_cheese_code == 1 and cheese_code == 2:
            code = 3
            return code
        if no_cheese_code == 2 and cheese_code == 2:
            code = 4
            return code
        if no_cheese_code == 3 or cheese_code == 3:
            code = 5
            self.logger.warning(
                f"Chip={chip} is not either in no_cheese_view_in_file "
                f"or cheese_view_in_file from self.options.")
            return code
        if no_cheese_code == 4 or cheese_code == 4:
            code = 6
            self.logger.warning(
                f"layer={layer} is not in chip={chip} either in "
                f"no_cheese_view_in_file or cheese_view_in_file from self.options."
            )
            return code

        return code

    def _populate_cheese(self):
        """Iterate through each chip, then layer to determine the cheesing
        geometry."""

        # lib = self.lib
        cheese_sub_layer = int(self.parse_value(self.options.cheese.datatype))
        nocheese_sub_layer = int(
            self.parse_value(self.options.no_cheese.datatype))

        for chip_name in self.chip_info:
            layers_in_chip = self.design.qgeometry.get_all_unique_layers(
                chip_name)

            for chip_layer in layers_in_chip:
                code = self._check_cheese(chip_name, chip_layer)
                if code == 1:
                    chip_box, status = self.design.get_x_y_for_chip(chip_name)
                    if status == 0:
                        minx, miny, maxx, maxy = chip_box

                        self._cheese_based_on_shape(
                            minx,
                            miny,
                            maxx,
                            maxy,
                            chip_name,
                            chip_layer,
                            cheese_sub_layer,
                            nocheese_sub_layer,
                        )

    def _cheese_based_on_shape(
        self,
        minx: float,
        miny: float,
        maxx: float,
        maxy: float,
        chip_name: str,
        chip_layer: int,
        cheese_sub_layer: int,
        nocheese_sub_layer: int,
    ):
        """Instantiate class to do cheesing.

        Args:
            minx (float): Chip minimum x location.
            miny (float): Chip minimum y location.
            maxx (float): Chip maximum x location.
            maxy (float): chip maximum y location.
            chip_name (str): User defined chip name.
            layer (int): Layer number for calculating the cheese.
            cheese_sub_layer (int):  User defined datatype, considered a
                    sub-layer number for where to place the cheese output.
            nocheese_sub_layer (int): User defined datatype, considered a
                    sub-layer number for where to place the NO_cheese output.
        """
        # pylint: disable=too-many-locals
        # pylint: disable=too-many-arguments

        max_points = int(self.parse_value(self.options.max_points))
        cheese_shape = int(self.parse_value(self.options.cheese.shape))
        all_nocheese = self.chip_info[chip_name][chip_layer]["no_cheese"]
        all_nocheese_gds = self.chip_info[chip_name][chip_layer][
            "no_cheese_gds"]
        delta_x = float(self.parse_value(self.options.cheese.delta_x))
        delta_y = float(self.parse_value(self.options.cheese.delta_y))
        edge_nocheese = float(
            self.parse_value(self.options.cheese.edge_nocheese))
        precision = float(self.parse_value(self.options.precision))
        is_neg_mask = self._is_negative_mask(chip_name, chip_layer)
        fab = is_true(self.options.fabricate)

        if cheese_shape == 0:
            cheese_x = float(self.parse_value(self.options.cheese.cheese_0_x))
            cheese_y = float(self.parse_value(self.options.cheese.cheese_0_y))
            a_cheese = Cheesing(
                all_nocheese,
                all_nocheese_gds,
                self.lib,
                minx,
                miny,
                maxx,
                maxy,
                chip_name,
                edge_nocheese,
                chip_layer,
                is_neg_mask,
                cheese_sub_layer,
                nocheese_sub_layer,
                fab,
                self.logger,
                max_points,
                precision,
                cheese_shape=cheese_shape,
                shape_0_x=cheese_x,
                shape_0_y=cheese_y,
                delta_x=delta_x,
                delta_y=delta_y,
            )
        elif cheese_shape == 1:
            cheese_radius = float(
                self.parse_value(self.options.cheese.cheese_1_radius))
            a_cheese = Cheesing(
                all_nocheese,
                all_nocheese_gds,
                self.lib,
                minx,
                miny,
                maxx,
                maxy,
                chip_name,
                edge_nocheese,
                chip_layer,
                is_neg_mask,
                cheese_sub_layer,
                nocheese_sub_layer,
                fab,
                self.logger,
                max_points,
                precision,
                cheese_shape=cheese_shape,
                shape_1_radius=cheese_radius,
                delta_x=delta_x,
                delta_y=delta_y,
            )
        else:
            self.logger.warning(
                f"The cheese_shape={cheese_shape} is unknown in QGDSRenderer.")
            a_cheese = None

        if a_cheese is not None:
            _dummy_a_lib = a_cheese.apply_cheesing()

    def _populate_no_cheese(self):
        """Iterate through every chip and layer.  If options choose to have
        either cheese or no-cheese, a MultiPolygon is placed
        self.chip_info[chip_name][chip_layer]['no_cheese'].

        If user selects to view the no-cheese, the method placed the
        cell with no-cheese at
        f'TOP_{chip_name}_{chip_layer}_NoCheese_{sub_layer}'.  The sub_layer
        is data_type and denoted in the options.
        """

        # pylint: disable=too-many-nested-blocks

        no_cheese_buffer = float(self.parse_value(
            self.options.no_cheese.buffer))
        sub_layer = int(self.parse_value(self.options.no_cheese.datatype))
        lib = self.lib
        fab = is_true(self.options.fabricate)

        for chip_name, _ in self.chip_info.items():
            layers_in_chip = self.design.qgeometry.get_all_unique_layers(
                chip_name)

            for chip_layer in layers_in_chip:
                code = self._check_either_cheese(chip_name, chip_layer)

                if code in (1, 2, 3):
                    if (len(self.chip_info[chip_name][chip_layer]
                            ["all_subtract_true"]) != 0):
                        sub_df = self.chip_info[chip_name][chip_layer][
                            "all_subtract_true"]
                        no_cheese_multipolygon = self._cheese_buffer_maker(
                            sub_df, chip_name, no_cheese_buffer)
                        if no_cheese_multipolygon is not None:
                            self.chip_info[chip_name][chip_layer][
                                "no_cheese"] = (no_cheese_multipolygon)
                            all_nocheese_gds = self._multipolygon_to_gds(
                                no_cheese_multipolygon,
                                chip_layer,
                                sub_layer,
                                no_cheese_buffer,
                            )
                            self.chip_info[chip_name][chip_layer][
                                "no_cheese_gds"] = (all_nocheese_gds)

                            if (self._check_no_cheese(chip_name, chip_layer)
                                    == 1 and not fab):
                                no_cheese_subtract_cell_name = (
                                    f"TOP_{chip_name}_{chip_layer}_NoCheese_{sub_layer}"
                                )
                                no_cheese_cell = lib.new_cell(
                                    no_cheese_subtract_cell_name)
                                for poly_el in all_nocheese_gds:
                                    if isinstance(poly_el, list):
                                        for sub_poly in poly_el:
                                            no_cheese_cell.add(sub_poly)
                                    else:
                                        no_cheese_cell.add(poly_el)

                                chip_only_top_name = f"TOP_{chip_name}"
                                chip_only_top = next(
                                    (c for c in lib.cells
                                     if c.name == chip_only_top_name),
                                    None,
                                )
                                if (chip_only_top and
                                        no_cheese_cell.bounding_box()
                                        is not None):
                                    chip_only_top.add(
                                        gdstk.Reference(no_cheese_cell))
                                else:
                                    lib.remove(no_cheese_cell)

    def _cheese_buffer_maker(
        self, sub_df: geopandas.GeoDataFrame, chip_name: str,
        no_cheese_buffer: float
    ) -> Union[None, shapely.geometry.multipolygon.MultiPolygon]:
        """For each layer in each chip, and if it has a ground plane
        (subtract==True), determine the no-cheese buffer and return a shapely
        object. Before the buffer is created for no-cheese, the LineStrings and
        Polygons are all combined.

        Args:
            sub_df (geopandas.GeoDataFrame): The subset of QGeometry tables
                for each chip, and layer, and only if the layer has a ground plane.
            chip_name (str): Name of chip.
            no_cheese_buffer (float): Will be used for fillet and
                size of buffer.

        Returns:
            Union[None, shapely.geometry.multipolygon.MultiPolygon]: The
            shapely which combines the polygons and linestrings and creates
            buffer as specified through default_options.
        """
        # pylint: disable=too-many-locals
        style_cap = int(self.parse_value(self.options.no_cheese.cap_style))
        style_join = int(self.parse_value(self.options.no_cheese.join_style))

        poly_sub_df = sub_df[sub_df.geometry.apply(
            lambda x: isinstance(x, shapely.geometry.polygon.Polygon))]
        poly_sub_geo = poly_sub_df["geometry"].tolist()

        path_sub_df = sub_df[sub_df.geometry.apply(
            lambda x: isinstance(x, shapely.geometry.linestring.LineString))]
        path_sub_geo = path_sub_df["geometry"].tolist()
        path_sub_width = path_sub_df["width"].tolist()
        # for n in range(len(path_sub_geo)):
        for index, _ in enumerate(path_sub_geo):
            path_sub_geo[index] = path_sub_geo[index].buffer(
                path_sub_width[index] / 2,
                cap_style=style_cap,
                join_style=style_join)

        #  Need to add buffer_size, cap style, and join style to default options
        combo_list = path_sub_geo + poly_sub_geo
        combo_shapely = draw.union(combo_list)

        if not combo_shapely.is_empty:
            # Can return either Multipolygon or just one polygon.
            combo_shapely = combo_shapely.buffer(no_cheese_buffer,
                                                 cap_style=style_cap,
                                                 join_style=style_join)
            if isinstance(combo_shapely, shapely.geometry.polygon.Polygon):
                combo_shapely = shapely.geometry.MultiPolygon([combo_shapely])

            # Check if the buffer went past the chip size.
            chip_box, status = self.design.get_x_y_for_chip(chip_name)
            if status == 0:
                minx, miny, maxx, maxy = chip_box
                c_minx, c_miny, c_maxx, c_maxy = combo_shapely.bounds
                if c_minx < minx or c_miny < miny or c_maxx > maxx or c_maxy > maxy:
                    self.logger.warning(
                        f"The bounding box for no-cheese is outside of chip size.\n"
                        f"Bounding box for chip is {chip_box}.\n"
                        f"Bounding box with no_cheese buffer is {combo_shapely.bounds}."
                    )
            else:
                self.logger.warning(
                    f"design.get_x_y_for_chip() did NOT return a good code for chip={chip_name},"
                    f"for _cheese_buffer_maker.  The chip boundary will not be tested."
                )

            # The type of combo_shapely will be
            # <class 'shapely.geometry.multipolygon.MultiPolygon'>
            return combo_shapely
        return None  # Need explicitly to avoid lint warnings.

    def _get_rectangle_points(self, chip_name: str) -> Tuple[list, list]:
        """There can be more than one chip in QGeometry. All chips export to
        one gds file. Each chip uses its own subtract rectangle.

        Args:
            chip_name (str): Name of chip to render.

        Returns:
            Tuple[list, list]: The subtract-rectangle for the chip_name.
        """
        layers_in_chip = self.design.qgeometry.get_all_unique_layers(chip_name)

        minx, miny, maxx, maxy = self.dict_bounds[chip_name]["for_subtract"]
        rectangle_points = [(minx, miny), (maxx, miny), (maxx, maxy),
                            (minx, maxy)]

        return layers_in_chip, rectangle_points

    def _populate_poly_path_for_export(self):
        """Using the geometries for each table name in QGeometry, populate
        self.lib to eventually write to a GDS file.

        For every layer within a chip, use the same "subtraction box" for the
        elements that have subtract as true.  Every layer within a chip will
        have cell named:  f'TOP_{chip_name}_{chip_layer}'.

        Args:
            file_name (str): The path and file name to write the gds file.
                             Name needs to include desired extension,
                             i.e. "a_path_and_name.gds".
        """

        precision = float(self.parse_value(self.options.precision))

        lib = self.new_gds_library()

        if is_true(self.options.ground_plane):
            all_chips_top_name = "TOP"
            all_chips_top = lib.new_cell(all_chips_top_name)
            for chip_name, _ in self.chip_info.items():
                chip_only_top_name = f"TOP_{chip_name}"
                chip_only_top = lib.new_cell(chip_only_top_name)

                layers_in_chip, rectangle_points = self._get_rectangle_points(
                    chip_name)

                for chip_layer in layers_in_chip:
                    self._handle_photo_resist(
                        lib,
                        chip_only_top,
                        chip_name,
                        chip_layer,
                        rectangle_points,
                        precision,
                    )

                # If junction table, import the cell and cell to chip_only_top
                if "junction" in self.chip_info[chip_name]:
                    self._import_junctions_to_one_cell(chip_name, lib,
                                                       chip_only_top,
                                                       layers_in_chip)

                # put all chips into TOP
                if chip_only_top.bounding_box() is not None:
                    all_chips_top.add(gdstk.Reference(chip_only_top))
                else:
                    lib.remove(chip_only_top)

    def _handle_photo_resist(
        self,
        lib: gdstk.Library,
        chip_only_top: gdstk.Cell,
        chip_name: str,
        chip_layer: int,
        rectangle_points: list,
        precision: float,
    ):
        """Handle the positive vs negative mask.

        Args:
            lib (gdstk.Library): The GDS library to export.
            chip_only_top (gdstk.Cell): The GDS  cell for top.
            chip_name (str): Name of chip to render.
            chip_layer (int): Layer of the chip to render.
            rectangle_points (list): The rectangle to denote the ground
                                    for each layer.
            precision (float): Used for GDS python export.
        """

        self.chip_info[chip_name]["subtract_poly"] = gdstk.Polygon(
            rectangle_points, layer=chip_layer, datatype=10)

        ground_cell_name = f"TOP_{chip_name}_{chip_layer}"
        ground_cell = lib.new_cell(ground_cell_name)

        if self._is_negative_mask(chip_name, chip_layer):
            self._negative_mask(
                lib,
                chip_only_top,
                ground_cell,
                chip_name,
                chip_layer,
                precision,
            )
        else:
            self._positive_mask(
                lib,
                chip_only_top,
                ground_cell,
                chip_name,
                chip_layer,
                precision,
            )

    def _is_negative_mask(self, chip: str, layer: int) -> bool:
        """Check options to see if negative mask is requested for the
        chip and layer.

        Args:
            chip (str): Chip name to search for in options.
            layer (int): Layer to search for within chip.  Determine if this
                         layer should have negative mask.

        Returns:
            bool: If there should be a negative mask for this chip and layer.
        """
        if chip in self.options.negative_mask.keys():
            if layer in self.options.negative_mask[chip]:
                return True

        return False

    def _negative_mask(
        self,
        lib: gdstk.Library,
        chip_only_top: gdstk.Cell,
        ground_cell: gdstk.Cell,
        chip_name: str,
        chip_layer: int,
        precision: float,
    ):
        """Apply logic for negative_mask.

        Args:
            lib (gdstk.Library): GDS library.
            chip_only_top (gdstk.Cell): The chip's top cell.
            ground_cell (gdstk.Cell): The ground cell for this layer.
            chip_name (str): Name of chip to render.
            chip_layer (int): Layer of the chip to render.
            precision (float):  GDS geometry precision.
        """
        if len(self.chip_info[chip_name][chip_layer]["q_subtract_true"]) != 0:
            # Difference for True-False.
            diff_geometry = gdstk.boolean(
                self.chip_info[chip_name][chip_layer]["q_subtract_true"],
                self.chip_info[chip_name][chip_layer]["q_subtract_false"],
                "not",
                layer=chip_layer,
                precision=precision,
            )

            if diff_geometry is None:
                self.design.logger.warning(
                    "There is no table named diff_geometry to write.")
            else:
                ground_cell.add(*diff_geometry)

        QGDSRenderer._add_groundcell_to_chip_only_top(lib, chip_only_top,
                                                      ground_cell)

    def _positive_mask(
        self,
        lib: gdstk.Library,
        chip_only_top: gdstk.Cell,
        ground_cell: gdstk.Cell,
        chip_name: str,
        chip_layer: int,
        precision: float,
    ):
        """Apply logic for positive mask for a chip to render.

        Args:
            lib (gdstk.Library): GDS library.
            chip_only_top (gdstk.Cell): The chip's top cell.
            ground_cell (gdstk.Cell): The ground cell for this layer.
            chip_name (str): Chip name.
            chip_layer (int): Layer index.
            precision (float): GDS geometry precision.
        """
        if len(self.chip_info[chip_name][chip_layer]["q_subtract_true"]) != 0:
            diff_geometry = gdstk.boolean(
                [self.chip_info[chip_name]["subtract_poly"]],
                self.chip_info[chip_name][chip_layer]["q_subtract_true"],
                "not",
                layer=chip_layer,
                datatype=0,
                precision=precision,
            )

            if diff_geometry is None:
                self.design.logger.warning(
                    "There is no table named diff_geometry to write.")
            else:
                ground_plane_cell_name = f"ground_{chip_name}_{chip_layer}"
                ground_plane_cell = lib.new_cell(ground_plane_cell_name)
                ground_plane_cell.add(*diff_geometry)
                ground_cell.add(gdstk.Reference(ground_plane_cell))

        self._handle_q_subtract_false(chip_name, chip_layer, ground_cell)
        QGDSRenderer._add_groundcell_to_chip_only_top(lib, chip_only_top,
                                                      ground_cell)

    def _handle_q_subtract_false(self, chip_name: str, chip_layer: int,
                                 ground_cell: gdstk.Cell):
        """For each layer, add the subtract=false components to ground.

        Args:
            chip_name (str): Name of chip to render.
            chip_layer (int): Name of layer to render.
            ground_cell (gdstk.Cell): The cell in lib to add to.
                                            Cell created for each layer.
        """
        if self.chip_info[chip_name][chip_layer]["q_subtract_false"] is None:
            self.logger.warning(f"There is no table named "
                                f"self.chip_info[{chip_name}][q_subtract_false]"
                                f" to write.")
        else:
            if len(self.chip_info[chip_name][chip_layer]
                   ["q_subtract_false"]) != 0:
                ground_cell.add(
                    *self.chip_info[chip_name][chip_layer]["q_subtract_false"])

    @classmethod
    def _add_groundcell_to_chip_only_top(cls, lib: gdstk.Library,
                                         chip_only_top: gdstk.Cell,
                                         ground_cell: gdstk.Cell) -> None:
        """Add the ground cell to the top of cell for chip.

        Args:
            lib (gdstk.GdsLibrary): Holds all of the chips to export to gds.
            chip_only_top (gdstk.Cell): Cell which for a single chip.
            ground_cell (gdstk.Cell): The ground cell to add to
                                    chip_only_top. Cell created for each layer.
        """
        # put all cells into TOP_chipname, if not empty.
        # When checking for bounding box, gdstk will return None if empty.

        if ground_cell.bounding_box() is not None:
            chip_only_top.add(gdstk.Reference(ground_cell))
        else:
            lib.remove(ground_cell)

    def _get_linestring_characteristics(
            self, row: pd.Series) -> Tuple[Tuple, float, float]:
        """Given a row in the Junction table, give the characteristics of
        LineString in row.geometry.

        Args:
            row (pd.Pandas): A row from Junction table of QGeometry.

        Returns:
            Tuple:
            * 1st entry is Tuple[float,float]: The midpoint of Linestring from
            row.geometry in format (x,y).
            * 2nd entry is float: The angle in degrees of Linestring from
            row.geometry.
            * 3rd entry is float: Is the magnitude of Linestring from
            row.geometry.
        """
        precision = float(self.parse_value(self.options.precision))
        for_rounding = int(np.abs(np.log10(precision)))

        [(minx, miny), (maxx, maxy)] = row.geometry.coords[:]

        center = QGDSRenderer._midpoint_xy(minx, miny, maxx, maxy)
        rotation = math.degrees(math.atan2((maxy - miny), (maxx - minx)))
        magnitude = np.round(
            distance.euclidean(row.geometry.coords[0], row.geometry.coords[1]),
            for_rounding,
        )

        return center, rotation, magnitude

    def _give_rotation_center_twopads(
        self, row: pd.Series, a_cell_bounding_box: np.ndarray
    ) -> Tuple[
            float,
            Tuple[float, float],
            Union[gdstk.Polygon, None],
            Union[gdstk.Polygon, None],
    ]:
        """Calculate the angle for rotation, center of LineString in
        row.geometry, and if needed create two pads to connect the junction to
        qubit.

        Args:
            row (pd.Pandas): A row from Junction table of QGeometry.
            a_cell_bounding_box (numpy.ndarray): Give the bounding box of cell
                used in row.gds_cell_name.

        Returns:
            Tuple:
            * 1st entry is float: The angle in degrees of Linestring from
            row.geometry.
            * 2nd entry is Tuple[float,float]: The midpoint of Linestring
            from row.geometry in format (x,y).
            * 3rd entry is gdstk.Rectangle: None if Magnitude of
            LineString is smaller than width of cell from row.gds_cell_name.
            Otherwise the rectangle for pad on LEFT of row.gds_cell_name.
            * 4th entry is gdstk.Rectangle: None if Magnitude of
            LineString is smaller than width of cell from row.gds_cell_name.
            Otherwise the rectangle for pad on RIGHT of row.gds_cell_name.
        """
        # pylint: disable=too-many-locals
        junction_pad_overlap = float(
            self.parse_value(self.options.junction_pad_overlap))

        pad_height = row.width
        center, rotation, magnitude = self._get_linestring_characteristics(row)
        [(jj_minx, jj_miny), (jj_maxx, jj_maxy)] = a_cell_bounding_box[0:2]

        pad_left = None
        pad_right = None
        jj_x_width = abs(jj_maxx - jj_minx)
        jj_y_height = abs(jj_maxy - jj_miny)

        # jj_center_x = (jj_x_width / 2) + jj_minx
        jj_center_y = (jj_y_height / 2) + jj_miny

        pad_height = row.width

        if pad_height < jj_y_height:
            # pylint: disable=protected-access
            text_id = self.design._components[row.component]._name
            self.logger.warning(
                f"In junction table, component={text_id} with name={row.name} "
                f"has width={pad_height} smaller than cell dimension={jj_y_height}."
            )

        if jj_x_width < magnitude:
            pad_x_size_minus_overlap = (magnitude - jj_x_width) / 2
            pad_miny = jj_center_y - (pad_height / 2)
            pad_left = gdstk.Polygon(
                [
                    (jj_minx - pad_x_size_minus_overlap, pad_miny),
                    (jj_minx + junction_pad_overlap, pad_miny),
                    (jj_minx + junction_pad_overlap, pad_miny + pad_height),
                    (jj_minx - pad_x_size_minus_overlap, pad_miny + pad_height),
                ],
                layer=int(row.layer),
                datatype=10,
            )
            pad_right = gdstk.Polygon(
                [
                    (jj_maxx - junction_pad_overlap, pad_miny),
                    (jj_maxx + pad_x_size_minus_overlap, pad_miny),
                    (jj_maxx + pad_x_size_minus_overlap, pad_miny + pad_height),
                    (jj_maxx - junction_pad_overlap, pad_miny + pad_height),
                ],
                layer=int(row.layer),
                datatype=10,
            )

        return rotation, center, pad_left, pad_right

    ############

    def import_junction_gds_file(self,
                                 lib: gdstk.Library,
                                 directory_name: Optional[str] = None) -> bool:
        """Import the file which contains all junctions for design.

        If the file has already been imported, just return True.

        When the design has junctions on multiple chips,
        we only need to import the file once to get all of the junction cells.

        Args:
            lib: Main GDS library used by this renderer.
            directory_name: Directory path where the junction GDS file is expected.

        Returns:
            True if the file was imported (or was already imported), False otherwise.
        """

        if self.imported_junction_gds is None:  # auto get dir name
            if self.options.path_filename is None or self.options.path_filename == '':
                self.logger.warning(
                    "No path_filename specified for junction GDS import.")
                return False
            else:
                _, directory_name = can_write_to_path(
                    self.options.path_filename)

        # TODO: Validate this works as intended with new gdstk.read_gds
        # already imported once in this renderer
        if self.imported_junction_gds is not None:
            return True

        if not os.path.isfile(self.options.path_filename):
            message_str = (
                f'Not able to find file:"{self.options.path_filename}".  '
                f"Not used to replace junction."
                f' Checked directory:"{directory_name}".')
            self.logger.warning(message_str)
            return False

        # read external library (with its own unit/precision)
        imported_lib = gdstk.read_gds(self.options.path_filename)
        imported_cells = getattr(imported_lib, "cells", None)

        if not imported_cells:
            message_str = (
                f'Imported file "{self.options.path_filename}" contains no cells.  '
                "Not used to replace junction.")
            self.logger.warning(message_str)
            return False

        # replicate old gdspy `units="convert"` behavior:
        # # rescale imported cells so their physical size matches this renderer's lib.
        # # coord_target = coord_imported * (imported_lib.unit / lib.unit)
        # scale = imported_lib.unit / lib.unit

        # # only do work if there is a real difference
        # if not math.isclose(scale, 1.0, rel_tol=1e-12, abs_tol=0.0):
        #     self.logger.info(
        #         "Rescaling imported junction library from unit=%g to unit=%g "
        #         "(scale factor=%g).",
        #         imported_lib.unit,
        #         lib.unit,
        #         scale,
        #     )
        #     for cell in imported_cells:
        #         # scale in-place; affects polygons, paths, labels, references
        #         cell.scale(scale, scale)

        # merge imported cells into main library, like old lib.read_gds did
        for cell in imported_cells:
            lib.add(cell)

        self.imported_junction_gds = self.options.path_filename
        return True

    def _import_junctions_to_one_cell(
        self,
        chip_name: str,
        lib: gdstk.Library,
        chip_only_top: gdstk.Cell,
        layers_in_chip: list,
    ):
        """Given lib, import the gds file from default options.  Based on the
        cell name in QGeometry table, import the cell from the gds file and
        place it in hierarchy of chip_only_top. In addition, the linestring
        should be two vertexes, and denotes two things.
        1. The midpoint of
        segment is the the center of cell.

        2. The angle made by second tuple - fist tuple  for delta y/ delta x
        is used to rotate the cell.

        Args:
            chip_name (str): The name of chip.
            lib (gdstk.Library): The library used to export the entire QDesign.
            chip_only_top (gdstk.Cell): The cell used for
                                                just chip_name.
            layers_in_chip (list):  List of all layers in chip.
        """
        # pylint: disable=too-many-locals
        # pylint: disable=too-many-nested-blocks

        # Make sure the file exists, before trying to read it.
        _, directory_name = can_write_to_path(self.options.path_filename)
        layers_in_junction_table = set(
            self.chip_info[chip_name]["junction"]["layer"])

        if self.import_junction_gds_file(lib=lib,
                                         directory_name=directory_name):
            for iter_layer in layers_in_chip:
                if self._is_negative_mask(chip_name, iter_layer):
                    # Want to export negative mask
                    # Gather the pads into hold_all_pads_cell for same layer.
                    if iter_layer in layers_in_junction_table:
                        chip_only_top_layer_name = f"TOP_{chip_name}_{iter_layer}"
                        top_layer_cell = next(
                            (c for c in lib.cells
                             if c.name == chip_only_top_layer_name),
                            None,
                        )
                        if top_layer_cell:
                            hold_all_pads_cell = lib.new_cell(
                                f"r_l_hold_all_pads_{iter_layer}")
                            top_layer_cell.add(
                                gdstk.Reference(hold_all_pads_cell))
                            hold_all_jj_cell = lib.new_cell(
                                f"all_jj_imported_{iter_layer}")

                            self._add_negative_extension_to_jj(
                                chip_name,
                                iter_layer,
                                lib,
                                chip_only_top,
                                top_layer_cell,
                                hold_all_pads_cell,
                                hold_all_jj_cell,
                            )
                else:
                    # By default, make a positive mask.
                    for row in self.chip_info[chip_name]["junction"].itertuples(
                    ):
                        chip_layer = int(row.layer)
                        ground_cell_name = f"TOP_{chip_name}_{chip_layer}"
                        if (ground_cell_name in [c.name for c in lib.cells] and
                                chip_layer == iter_layer):
                            chip_layer_cell = next(
                                (c for c in lib.cells
                                 if c.name == ground_cell_name),
                                None,
                            )
                            if row.gds_cell_name in [c.name for c in lib.cells]:
                                self._add_positive_extension_to_jj(
                                    lib, row, chip_layer_cell)
                            else:
                                self.logger.warning(
                                    f"Cell={row.gds_cell_name} from junction"
                                    f"table not found in {self.options.path_filename}."
                                )

    def _add_negative_extension_to_jj(
        self,
        chip_name: str,
        jj_layer: int,
        lib: gdstk.Library,
        chip_only_top: gdstk.Cell,
        chip_only_top_layer: gdstk.Cell,
        hold_all_pads_cell: gdstk.Cell,
        hold_all_jj_cell: gdstk.Cell,
    ) -> None:
        """Gather extension pads & junctions for negative mask usage.
        Manipulate existing geometries for the layer that the junctions need
         to be added.  Since boolean subtraction is computationally intensive,
         the method will gather the pads for a layer, and do the boolean just
         once. Then add the junctions to difference.

        Args:
            chip_name (str): Chip name.
            jj_layer (int): Layer index for junction.
            lib (gdstk.Library): GDS library.
            chip_only_top (gdstk.Cell): Chip top cell.
            chip_only_top_layer (gdstk.Cell): Chip/layer cell.
            hold_all_pads_cell (gdstk.Cell): Cell collecting extension pads.
            hold_all_jj_cell (gdstk.Cell): Cell collecting junction references.
        """
        boolean_by_layer = self.chip_info[chip_name]["junction"][
            "layer"] == jj_layer
        for row in self.chip_info[chip_name]["junction"][
                boolean_by_layer].itertuples():
            if row.gds_cell_name in [c.name for c in lib.cells]:
                self._gather_negative_extension_for_jj(lib, row,
                                                       hold_all_pads_cell,
                                                       hold_all_jj_cell)
            else:
                self.logger.warning(
                    f"Junction cell={row.gds_cell_name} not "
                    f"n {self.options.path_filename} for negative "
                    "mask usage.")

        diff_r_l_pads_name = f"r_l_pads_diff_{jj_layer}"
        diff_pad_cell_layer = lib.new_cell(diff_r_l_pads_name)
        chip_only_top.add(gdstk.Reference(diff_pad_cell_layer))

        precision = float(self.parse_value(self.options.precision))

        if chip_only_top_layer.bounding_box() is not None:
            boolean_result = gdstk.boolean(
                chip_only_top_layer.get_polygons(),
                hold_all_pads_cell.get_polygons(),
                "not",
                precision=precision,
            )
            if boolean_result:
                for poly in boolean_result:
                    diff_pad_cell_layer.add(poly)

        if hold_all_jj_cell.bounding_box() is not None:
            diff_pad_cell_layer.add(gdstk.Reference(hold_all_jj_cell))

        self._clean_hierarchy(
            lib,
            chip_only_top,
            chip_only_top_layer,
            diff_pad_cell_layer,
            hold_all_pads_cell,
        )

    @classmethod
    def _clean_hierarchy(
        cls,
        lib: gdstk.Library,
        chip_only_top: gdstk.Cell,
        chip_only_top_layer: gdstk.Cell,
        diff_pad_cell_layer: gdstk.Cell,
        hold_all_pads_cell: gdstk.Cell,
    ) -> None:
        """Delete cell that doesn't have pad nor jjs.  Then use same
        name for correct cell.  Also, get rid of cell that had the pads
        since subtraction happened and we don't need it any more.

        Args:
            lib (gdstk.Library): [The library used to export the entire QDesign.
            chip_only_top (gdstk.Cell): [description]
            chip_only_top_layer (gdstk.Cell): Cell under chip,
                                        with specific layer.
            diff_pad_cell_layer (gdstk.Cell): Holds result of top_layer - pads + jjs.
            hold_all_pads_cell (gdstk.Cell): Collect all the jj's with movement.
        """
        hold_name = chip_only_top_layer.name
        lib.remove(chip_only_top_layer)
        lib.rename_cell(diff_pad_cell_layer, hold_name)

        if diff_pad_cell_layer.bounding_box() is not None:
            chip_only_top.add(gdstk.Reference(diff_pad_cell_layer))
        else:
            lib.remove(diff_pad_cell_layer)

        for ref in list(hold_all_pads_cell.references):
            lib.remove(ref.ref_cell)
        lib.remove(hold_all_pads_cell)

    def _gather_negative_extension_for_jj(
        self,
        lib: gdstk.Library,
        row: pd.Series,
        hold_all_pads_cell: gdstk.Cell,
        hold_all_jj_cell: gdstk.Cell,
    ) -> None:
        """Gather the pads and jjs and put them in separate cells.  The
        the pads can be boolean'd 'not' just once. After boolean for pads, then
        the jjs will be added to result.  The boolean is very
        time intensive, so just want to do it once.

        Args:
            lib (gdstk.Library): GDS library.
            row (pd.Series): Row from junction table.
            hold_all_pads_cell (gdstk.Cell): Collecting extension pads.
            hold_all_jj_cell (gdstk.Cell): Collecting junction references.
        """

        a_cell = next((c for c in lib.cells if c.name == row.gds_cell_name),
                      None)
        if a_cell is None:
            return
        bounding_box = a_cell.bounding_box()
        if bounding_box is None:
            return

        rotation, center, pad_left, pad_right = self._give_rotation_center_twopads(
            row, bounding_box)
        jj_pad_r_l_name = (f"{row.gds_cell_name}_QComponent_is_{row.component}"
                           f"_Name_is_{row.name}_name_is_{row.name}")
        temp_cell = lib.new_cell(jj_pad_r_l_name)
        if pad_left is not None:
            temp_cell.add(pad_left)
        if pad_right is not None:
            temp_cell.add(pad_right)

        hold_all_jj_cell.add(
            gdstk.Reference(a_cell, origin=center, rotation=rotation))
        hold_all_pads_cell.add(
            gdstk.Reference(temp_cell, origin=center, rotation=rotation))

    def _add_positive_extension_to_jj(
        self,
        lib: gdstk.Library,
        row: pd.Series,
        chip_layer_cell: gdstk.Cell,
    ):
        """Get the extension pads, then add or subtract to extracted cell based on
        positive or negative mask.

        Args:
            lib (gdstk.Library): The library used to export the entire QDesign.
            row (pd.core.frame.Pandas): Each row is from the qgeometry
                                            junction table.
            chip_layer_cell (gdstk.Cell): The cell used for
                                            chip_name and layer_num.
        """
        a_cell = next((c for c in lib.cells if c.name == row.gds_cell_name),
                      None)
        if a_cell is None:
            return
        bounding_box = a_cell.bounding_box()
        if bounding_box is None:
            return

        rotation, center, pad_left, pad_right = self._give_rotation_center_twopads(
            row, bounding_box)
        jj_pad_r_l_name = (
            f"pads_{row.gds_cell_name}_QComponent_is_{row.component}_name_is_{row.name}"
        )
        temp_cell = lib.new_cell(jj_pad_r_l_name)

        if pad_left is not None:
            temp_cell.add(pad_left)
        if pad_right is not None:
            temp_cell.add(pad_right)

        chip_layer_cell.add(
            gdstk.Reference(a_cell, origin=center, rotation=rotation))
        if temp_cell.bounding_box() is not None:
            chip_layer_cell.add(
                gdstk.Reference(temp_cell, origin=center, rotation=rotation))
        else:
            lib.remove(temp_cell)

    def export_to_gds(self,
                      file_name: str,
                      highlight_qcomponents: list = None) -> int:
        """Use the design which was used to initialize this class. The
        QGeometry element types of both "path" and "poly", will be used, to
        convert QGeometry to GDS formatted file.

        Args:
            file_name (str): File name which can also include directory path.
                             If the file exists, it will be overwritten.
            highlight_qcomponents (list): List of strings which denote
                                        the name of QComponents to render.
                                        If empty, render all components in design.

        Returns:
            int: 0=file_name can not be written, otherwise 1=file_name has been written
        """
        if highlight_qcomponents is None:
            highlight_qcomponents = []

        if not self._can_write_to_path(file_name):
            return 0

        # There can be more than one chip in QGeometry.
        # They all export to one gds file.
        # Each chip will hold the rectangle for subtract for each layer so:
        # chip_info[chip_name][subtract_box][(min_x,min_y,max_x,max_y)]
        # chip_info[chip_name][layer_number][all_subtract_elements]
        # chip_info[chip_name][layer_number][all_no_subtract_elements]
        self.chip_info.clear()
        self.chip_info.update(self._get_chip_names())

        # if imported, hold the path to file name, otherwise None.
        self.imported_junction_gds = None

        max_points = int(self.parse_value(self.options.max_points))

        if self._create_qgeometry_for_gds(highlight_qcomponents) == 0:
            # Create self.lib and populate path and poly.
            self._populate_poly_path_for_export()

            # Add no-cheese MultiPolygon to
            # self.chip_info[chip_name][chip_layer]['no_cheese'],
            # if self.options requests the layer.
            self._populate_no_cheese()

            # Use self.options  to decide what to put for export
            # into self.chip_info[chip_name][chip_layer]['cheese'].
            # Not finished.
            self._populate_cheese()

            # Export the file to disk from self.lib
            self.lib.write_gds(file_name, max_points)

            return 1

        return 0

    def _multipolygon_to_gds(
            self,
            multi_poly: shapely.geometry.multipolygon.MultiPolygon,
            layer: int,
            data_type: int,
            no_cheese_buffer: float,  # pylint: disable=unused-argument
    ) -> list:
        """Convert a shapely MultiPolygon to corresponding gdstk.

        Args:
            multi_poly (shapely.geometry.multipolygon.MultiPolygon): The
                                shapely geometry of no-cheese boundary.
            layer (int): The layer of the input multipolygon.
            data_type (int): Used as a "sub-layer" to place the no-cheese
                                gdstk output.
            no_cheese_buffer (float): Used for both fillet and buffer size.

        Returns:
            list: Each entry is converted to GDSII.
        """
        # pylint: disable=too-many-locals

        precision = float(self.parse_value(self.options.precision))

        all_polys = list(multi_poly.geoms)
        all_gds = list()

        for poly in all_polys:
            exterior_coords = list(poly.exterior.coords)
            exterior_poly = gdstk.Polygon(exterior_coords,
                                          layer=layer,
                                          datatype=data_type)

            if poly.interiors:
                holes = []
                for hole in poly.interiors:
                    hole_coords = list(hole.coords)
                    holes.append(
                        gdstk.Polygon(hole_coords,
                                      layer=layer,
                                      datatype=data_type))

                diff_result = gdstk.boolean(
                    [exterior_poly],
                    holes,
                    "not",
                    layer=layer,
                    datatype=data_type,
                    precision=precision,
                )
                # Poly fracturing leading to a funny shape. Leave this out of gds output for now.
                # a_poly.fillet(no_cheese_buffer,
                #               points_per_2pi=128,
                #               precision=precision)
                all_gds.append(diff_result)
            else:
                # Poly fracturing leading to a funny shape. Leave this out of gds output for now.
                # exterior_poly.fillet(no_cheese_buffer,
                #                      points_per_2pi=128,
                #                      precision=precision)
                all_gds.append(exterior_poly)

        return all_gds

    def _qgeometry_to_gds(
        self, qgeometry_element: pd.Series
    ) -> Union[gdstk.Polygon, gdstk.FlexPath, None]:
        """Convert the design.qgeometry table to format used by GDS renderer.
        Convert the class to a series of GDSII elements.

        Args:
            qgeometry_element (pd.Series): Expect a shapely object.

        Returns:
            Union['gdstk.Polygon' or 'gdstk.FlexPath' or None]: Convert the
            class to a series of GDSII format on the input pd.Series.


        *NOTE:*
        GDS:
            points (array-like[N][2]) – Coordinates of the vertices of
                                        the polygon.
            layer (integer) – The GDSII layer number for this
                                        qgeometry_element.
            datatype (integer) – The GDSII datatype for this qgeometry_element
                                (between 0 and 255).
                                datatype=10 or 11 means only that they are
                                from a Polygon vs. LineString.
                                This can be changed.

        See:
            https://heitzmann.github.io/gdstk/geometry/gdstk.Polygon.html
        """

        corners = self.options.corners
        precision = self.parse_value(self.options.precision)
        tolerance = self.parse_value(self.options.tolerance)

        geom = qgeometry_element.geometry
        layer_num = int(qgeometry_element.layer)

        if isinstance(geom, shapely.geometry.Polygon):
            exterior_coords = list(geom.exterior.coords)
            exterior_poly = gdstk.Polygon(
                exterior_coords,
                layer=layer_num,
                datatype=10,
            )

            # Handle holes
            if geom.interiors:
                holes = []
                for hole in geom.interiors:
                    hole_coords = list(hole.coords)
                    holes.append(
                        gdstk.Polygon(hole_coords, layer=layer_num,
                                      datatype=10))
                result = gdstk.boolean(
                    [exterior_poly],
                    holes,
                    "not",
                    layer=layer_num,
                    datatype=10,
                    precision=float(precision),
                )
                if len(result) != 1:
                    self.logger.warning(
                        "Some elements are not inside the geometry; these are invalid holes."
                    )
                return result[0]
            return exterior_poly

        if isinstance(geom, shapely.geometry.LineString):
            if not math.isnan(qgeometry_element.width):
                use_width = qgeometry_element.width
            else:
                use_width = self.parse_value(self.options.width_LineString)
                qcomponent_id = self.parse_value(qgeometry_element.component)
                name = self.parse_value(qgeometry_element["name"])
                layer_num = self.parse_value(qgeometry_element.layer)
                width = self.parse_value(qgeometry_element.width)
                self.logger.warning(
                    f"Since width:{width} for a Path is not a number, "
                    f"it will be exported using width_LineString:"
                    f" {use_width}.  The component_id is:{qcomponent_id},"
                    f" name is:{name}, layer is: {layer_num}")

            if "fillet" in qgeometry_element:
                if (math.isnan(qgeometry_element.fillet) or
                        qgeometry_element.fillet <= 0 or
                        qgeometry_element.fillet
                        < 0.5 * qgeometry_element.width):
                    to_return = gdstk.FlexPath(
                        list(geom.coords),
                        use_width,
                        layer=layer_num,
                        datatype=11,
                        tolerance=tolerance,
                    )

                else:
                    to_return = gdstk.FlexPath(
                        list(geom.coords),
                        use_width,
                        layer=layer_num,
                        datatype=11,
                        joins=corners,
                        bend_radius=qgeometry_element.fillet,
                        tolerance=tolerance,
                    )

                return to_return

            # Could be junction table with a linestring.
            # Look for gds_path_filename in column.
            self.logger.warning(f"Linestring did not have fillet in column. "
                                f"The qgeometry_element was not drawn.\n"
                                f"The qgeometry_element within table is:\n"
                                f"{qgeometry_element}")
            return None  # Need explicitly to avoid lint warnings.

        self.logger.warning(
            f"Unexpected shapely object geometry."
            f"The variable qgeometry_element is {type(geom)}, "
            f"method can currently handle Polygon and FlexPath.")
        return None

    def _get_chip_names(self) -> Dict:
        """Returns a dict of unique chip names for ALL tables within QGeometry.
        In another words, for every "path" table, "poly" table ... etc, this
        method will search for unique chip names and return a dict of unique
        chip names from QGeometry table.

        Returns:
            Dict: dict with key of chip names and value of empty
            dict to hold things for renderers.
        """
        chip_names = Dict()
        for table_name in self.design.qgeometry.get_element_types():
            table = self.design.qgeometry.tables[table_name]
            names = table["chip"].unique().tolist()
            chip_names += names
        unique_list = list(set(chip_names))

        unique_dict = Dict()
        for chip in unique_list:
            unique_dict[chip] = Dict()
        return unique_dict

    def show_imported_junction_gds(self):
        r"""If junction gds file has been imported, show the cells in it.
        To use, you may need to first set:
        >>> gds.options.path_filename = "../resources/Fake_Junctions.GDS"
        This function will read the file again, and print out the cells in it.
        It will also render the cells as an SVG inline if run in a Jupyter notebook.
        """
        if self.imported_junction_gds is not None:
            imported_lib = gdstk.read_gds(self.imported_junction_gds)
            print("imported_lib name:", imported_lib.name)
            print("imported_lib unit:", imported_lib.unit)
            print("imported_lib precision:", imported_lib.precision)
            for cell in imported_lib.cells:
                self.debug_print_cell(cell, show=True)
        else:
            self.logger.info(
                "No junction gds file has been imported. \n"
                "You can set it with `gds.options.path_filename = ...`")

    @staticmethod
    def debug_print_cell(cell, show=False):
        """Compact pretty-print summary of a gdstk Cell."""

        if show:
            cell.write_svg("test_output.svg")
            from IPython.display import SVG, display
            display(SVG("test_output.svg"))

        print(f"\n=== {cell.name} ===")

        # Bounding box
        try:
            bbox = cell.bounding_box()
        except Exception:
            bbox = None
        print(f"bbox: {bbox}")

        # Paths
        print("paths:", end=" ")
        if cell.paths:
            print(f"{len(cell.paths)}")
            for p in cell.paths:
                print(f"  - {p} (pts={len(p.points)})")
        else:
            print("0")

        # Polygons
        print("polygons:", end=" ")
        if cell.polygons:
            print(f"{len(cell.polygons)}")
            for poly in cell.polygons:
                print(f"  - poly(n={len(poly.points)})")
        else:
            print("0")

        # References
        print("refs:", end=" ")
        if cell.references:
            print(f"{len(cell.references)}")
            for r in cell.references:
                print(f"  - {r}")
        else:
            print("0")

        # Properties
        print("props:", end=" ")
        if cell.properties:
            print(f"{len(cell.properties)}")
            for obj, props in cell.properties.items():
                print(f"  - {obj}: {props}")
        else:
            print("0")

        print("=== end ===\n")

    @staticmethod
    def debug_summarize_gds_library(lib: gdstk.Library, show=False) -> None:
        """Print a concise summary report for a gdstk.Library.

        The summary includes:

        - Library metadata (name, unit, precision, number of cells).
        - Cell names and whether they have geometry.
        - Per (layer, datatype) statistics:
            - number of polygons
            - number of paths

        Args:
            lib: Loaded gdstk.Library instance.

        Example:
            .. code-block:: python

                import gdstk

                lib = gdstk.read_gds("awesome_design.gds")
                summarize_gds_library(lib)
        """
        if show:
            write_library_overview_svg(lib, "test_output.svg")
            from IPython.display import SVG, display
            display(SVG("test_output.svg"))

        # Basic library info
        print(f"\n=== GDS LIBRARY SUMMARY ===")
        print(f"name:      {getattr(lib, 'name', None)}")
        print(f"unit:      {lib.unit}")
        print(f"precision: {lib.precision}")
        print(f"cells:     {len(lib.cells)}")

        # Cell list
        print("\nCELLS:")
        for cell in lib.cells:
            has_geom = bool(cell.polygons or cell.paths or cell.references)
            bbox = cell.bounding_box()
            bbox_str = "None" if bbox is None else f"{bbox}"
            print(f"  - {cell.name:30s} geom={has_geom} bbox={bbox_str}")

        # Collect per (layer, datatype) stats
        layer_stats: DictType[Tuple[int, int],
                              DictType[str, int]] = defaultdict(lambda: {
                                  "polygons": 0,
                                  "paths": 0
                              })

        for cell in lib.cells:
            # Polygons
            for poly in cell.polygons:
                key = (poly.layer, poly.datatype)
                layer_stats[key]["polygons"] += 1

            # Paths (FlexPath / RobustPath)
            for path in cell.paths:
                # FlexPath / RobustPath can have multiple parallel elements
                # with potentially multiple layers; treat each layer separately.
                layers = getattr(path, "layers", [getattr(path, "layer", 0)])
                datatypes = getattr(path, "datatypes",
                                    [getattr(path, "datatype", 0)])

                # Normalize to lists of same length
                if not isinstance(layers, (list, tuple)):
                    layers = [layers]
                if not isinstance(datatypes, (list, tuple)):
                    datatypes = [datatypes]

                # Pair up safely
                for lyr, dt in zip(layers, datatypes):
                    key = (int(lyr), int(dt))
                    layer_stats[key]["paths"] += 1

        # If lib.layers_and_datatypes() is available, we can cross-check:
        try:
            lad = lib.layers_and_datatypes()
        except AttributeError:
            lad = None

        print("\nLAYER / DATATYPE USAGE:")
        if not layer_stats:
            print("  (no geometry found)")
        else:
            print("  layer  dtype   polys   paths")
            print("  -----  -----   -----   -----")
            for (layer, dtype), counts in sorted(layer_stats.items()):
                print(f"  {layer:5d}  {dtype:5d}   "
                      f"{counts['polygons']:5d}   {counts['paths']:5d}")

        if lad is not None:
            # Show the raw set from lib.layers_and_datatypes() for comparison
            print("\nlib.layers_and_datatypes():")
            for layer, dtype in sorted(lad):
                print(f"  (layer={layer}, datatype={dtype})")

        print("\n=== END SUMMARY ===\n")


def write_library_overview_svg(lib: gdstk.Library,
                               filename: str = "library_overview.svg",
                               background: str | None = "white",
                               scale: float = 10.0) -> None:
    """Write a single SVG that references all cells in a library.

    Args:
        lib: GDS library to export.
        filename: Output SVG filename.
        background: SVG background color, or None for transparent.
        scale: Coordinate scaling factor applied when writing SVG.
    """
    top = gdstk.Cell("LIB_OVERVIEW")
    for cell in lib.top_level():  # for cell in lib.cells:
        top.add(gdstk.Reference(cell))
    top.write_svg(filename, background=background, scaling=scale)
