# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
"""
This provides a set of useful generic types for the gdsMill interface.
"""
import math
import copy
import numpy as np
from openram import debug
from openram import tech
from openram import OPTS
from .utils import round_to_grid
from .vector import vector


class geometry:
    """
    A specific path, shape, or text geometry. Base class for shared
    items.
    """
    def __init__(self, lpp=None):
        """ By default, everything has no size. """
        self.width = 0
        self.height = 0
        if lpp:
            self.lpp = lpp
            self.layerNumber = lpp[0]
            self.layerPurpose = lpp[1]

    def __str__(self):
        """ override print function output """
        debug.error("__str__ must be overridden by all geometry types.", 1)

    def __repr__(self):
        """ override print function output """
        debug.error("__repr__ must be overridden by all geometry types.", 1)

    # def translate_coords(self, coords, mirr, angle, xyShift):
    #     """Calculate coordinates after flip, rotate, and shift"""
    #     coordinate = []
    #     for item in coords:
    #         x = (item[0]*math.cos(angle)-item[1]*mirr*math.sin(angle)+xyShift[0])
    #         y = (item[0]*math.sin(angle)+item[1]*mirr*math.cos(angle)+xyShift[1])
    #         coordinate += [(x, y)]
    #     return coordinate

    def transform_coords(self, coords, offset, mirr, angle):
        """Calculate coordinates after flip, rotate, and shift"""
        coordinate = []
        for item in coords:
            x = item[0] * math.cos(angle) - item[1] * mirr * math.sin(angle) + offset[0]
            y = item[0] * math.sin(angle) + item[1] * mirr * math.cos(angle) + offset[1]
            coordinate += [[x, y]]
        return coordinate

    def normalize(self):
        """ Re-find the LL and UR points after a transform """
        (first, second) = self.boundary
        ll = vector(min(first[0], second[0]),
                    min(first[1], second[1])).snap_to_grid()
        ur = vector(max(first[0], second[0]),
                    max(first[1], second[1])).snap_to_grid()
        self.boundary = [ll, ur]

    def update_boundary(self):
        """ Update the boundary with a new placement. """
        self.compute_boundary(self.offset, self.mirror, self.rotate)

    def compute_boundary(self, offset=vector(0, 0), mirror="", rotate=0):
        """
        Transform with offset, mirror and rotation to get the absolute pin location.
        We must then re-find the ll and ur. The master is the cell instance.
        """
        if OPTS.netlist_only:
            self.boundary = [vector(0, 0), vector(0, 0)]
            return

        (ll, ur) = [vector(0, 0), vector(self.width, self.height)]

        # Mirroring is performed before rotation
        if mirror == "MX":
            ll = ll.scale(1, -1)
            ur = ur.scale(1, -1)
        elif mirror == "MY":
            ll = ll.scale(-1, 1)
            ur = ur.scale(-1, 1)
        elif mirror == "XY":
            ll = ll.scale(-1, -1)
            ur = ur.scale(-1, -1)
        elif mirror == "" or mirror == "R0":
            pass
        else:
            debug.error("Invalid mirroring: {}".format(mirror), -1)

        if rotate == 0:
            pass
        elif rotate == 90:
            ll = ll.rotate_scale(-1, 1)
            ur = ur.rotate_scale(-1, 1)
        elif rotate == 180:
            ll = ll.scale(-1, -1)
            ur = ur.scale(-1, -1)
        elif rotate == 270:
            ll = ll.rotate_scale(1, -1)
            ur = ur.rotate_scale(1, -1)
        else:
            debug.error("Invalid rotation: {}".format(rotate), -1)

        self.boundary = [offset + ll, offset + ur]
        self.normalize()

    def ll(self):
        """ Return the lower left corner """
        return self.boundary[0]

    def ur(self):
        """ Return the upper right corner """
        return self.boundary[1]

    def lr(self):
        """ Return the lower right corner """
        return vector(self.boundary[1].x, self.boundary[0].y)

    def ul(self):
        """ Return the upper left corner """
        return vector(self.boundary[0].x, self.boundary[1].y)

    def uy(self):
        """ Return the upper edge """
        return self.boundary[1].y

    def by(self):
        """ Return the bottom edge """
        return self.boundary[0].y

    def lx(self):
        """ Return the left edge """
        return self.boundary[0].x

    def rx(self):
        """ Return the right edge """
        return self.boundary[1].x

    def cx(self):
        """ Return the center x """
        return 0.5 * (self.boundary[0].x + self.boundary[1].x)

    def cy(self):
        """ Return the center y """
        return 0.5 * (self.boundary[0].y + self.boundary[1].y)

    def center(self):
        """ Return the center coordinate """
        return vector(self.cx(), self.cy())


class instance(geometry):
    """
    An instance of an instance/module with a specified location and
    rotation
    """
    def __init__(self, name, mod, offset=[0, 0], mirror="R0", rotate=0):
        """Initializes an instance to represent a module"""
        super().__init__()
        debug.check(mirror not in ["R90", "R180", "R270"],
                    "Please use rotation and not mirroring during instantiation.")

        self.name = name
        self.mod = mod
        self.gds = mod.gds
        self.rotate = rotate
        self.offset = vector(offset).snap_to_grid()
        self.mirror = mirror
        if OPTS.netlist_only:
            self.width = 0
            self.height = 0
        else:
            if mirror in ["R90", "R270"] or rotate in [90, 270]:
                self.width = round_to_grid(mod.height)
                self.height = round_to_grid(mod.width)
            else:
                self.width = round_to_grid(mod.width)
                self.height = round_to_grid(mod.height)
        self.compute_boundary(offset, mirror, rotate)

        debug.info(4, "creating instance: " + self.name)

    def get_blockages(self, lpp, top=False):
        """ Retrieve blockages of all modules in this instance.
        Apply the transform of the instance placement to give absolute blockages."""
        angle = math.radians(float(self.rotate))
        mirr = 1
        if self.mirror == "R90":
            angle += math.radians(90.0)
        elif self.mirror == "R180":
            angle += math.radians(180.0)
        elif self.mirror == "R270":
            angle += math.radians(270.0)
        elif self.mirror == "MX":
            mirr = -1
        elif self.mirror == "MY":
            mirr = -1
            angle += math.radians(180.0)
        elif self.mirror == "XY":
            mirr = 1
            angle += math.radians(180.0)

        new_blockages = []
        if self.mod.is_library_cell:
            # Writes library cell blockages as shapes instead of a large metal blockage
            blockages = []
            blockages = self.mod.gds.getBlockages(lpp)
            for b in blockages:
                new_blockages.append(self.transform_coords(b, self.offset, mirr, angle))
        else:
            blockages = self.mod.get_blockages(lpp)
            for b in blockages:
                new_blockages.append(self.transform_coords(b, self.offset, mirr, angle))
        return new_blockages

    def gds_write_file(self, new_layout):
        """Recursively writes all the sub-modules in this instance"""
        debug.info(4, "writing instance: " + self.name)
        # make sure to write out my module/structure
        # (it will only be written the first time though)
        self.mod.gds_write_file(self.gds)
        # now write an instance of my module/structure
        new_layout.addInstance(self.gds,
                               self.mod.cell_name,
                               offsetInMicrons=self.offset,
                               mirror=self.mirror,
                               rotate=self.rotate)

    def place(self, offset, mirror="R0", rotate=0):
        """ This updates the placement of an instance. """
        # Update the placement of an already added instance
        self.offset = vector(offset).snap_to_grid()
        self.mirror = mirror
        self.rotate = rotate
        self.update_boundary()
        debug.info(3, "placing instance {}".format(self))

    def get_pin(self, name, index=-1):
        """ Return an absolute pin that is offset and transformed based on
        this instance location. Index will return one of several pins."""

        if index == -1:
            pin = copy.deepcopy(self.mod.get_pin(name))
            pin.transform(self.offset, self.mirror, self.rotate)
            return pin
        else:
            pins = copy.deepcopy(self.mod.get_pin(name))
            pins.transform(self.offset, self.mirror, self.rotate)
            return pin[index]

    def get_num_pins(self, name):
        """ Return the number of pins of a given name """
        return len(self.mod.get_pins(name))

    def get_pins(self, name):
        """ Return an absolute pin that is offset and transformed based on
        this instance location. """

        pin = copy.deepcopy(self.mod.get_pins(name))

        new_pins = []
        for p in pin:
            p.transform(self.offset, self.mirror, self.rotate)
            new_pins.append(p)
        return new_pins

    def calculate_transform(self, node):
        #set up the rotation matrix
        angle = math.radians(float(node.rotate))
        mRotate = np.array([[math.cos(angle), -math.sin(angle), 0.0],
                            [math.sin(angle), math.cos(angle), 0.0],
                            [0.0, 0.0, 1.0]])

        #set up translation matrix
        translateX = float(node.offset[0])
        translateY = float(node.offset[1])
        mTranslate = np.array([[1.0, 0.0, translateX],
                                [0.0, 1.0, translateY],
                                [0.0, 0.0, 1.0]])

        #set up the scale matrix (handles mirror X)
        scaleX = 1.0
        if(node.mirror == 'MX'):
            scaleY = -1.0
        else:
            scaleY = 1.0
        mScale = np.array([[scaleX, 0.0, 0.0],
                            [0.0, scaleY, 0.0],
                            [0.0, 0.0, 1.0]])

        return (mRotate, mScale, mTranslate)

    def apply_transform(self, mtransforms, uVector, vVector, origin):
        origin = np.dot(mtransforms[0], origin)    # rotate
        uVector = np.dot(mtransforms[0], uVector)  # rotate
        vVector = np.dot(mtransforms[0], vVector)  # rotate
        origin = np.dot(mtransforms[1], origin)    # scale
        uVector = np.dot(mtransforms[1], uVector)  # scale
        vVector = np.dot(mtransforms[1], vVector)  # scale
        origin = np.dot(mtransforms[2], origin)

        return(uVector, vVector, origin)

    def apply_path_transform(self, path):
        uVector = np.array([[1.0], [0.0], [0.0]])
        vVector = np.array([[0.0], [1.0], [0.0]])
        origin = np.array([[0.0], [0.0], [1.0]])

        while(path):
            instance = path.pop(-1)
            mtransforms = self.calculate_transform(instance)
            (uVector, vVector, origin) = self.apply_transform(mtransforms, uVector, vVector, origin)

        return (uVector, vVector, origin)

    def reverse_transformation_bitcell(self, cell_name):
        path = [] # path currently follwed in bitcell search
        cell_paths = [] # saved paths to bitcells
        origin_offsets = [] # cell to bank offset
        Q_offsets = [] # Q to cell offet
        Q_bar_offsets = [] # Q_bar to cell offset
        bl_offsets = [] # bl to cell offset
        br_offsets = [] # br to cell offset
        bl_meta = [] # bl offset metadata (row,col,name)
        br_meta  = [] # br offset metadata (row,col,name)

        def walk_subtree(node):
            path.append(node)

            if node.mod.name == cell_name:
                cell_paths.append(copy.copy(path))

                # get the row and col names from the path
                row = int(path[-1].name.split('_')[-2][1:])
                col = int(path[-1].name.split('_')[-1][1:])

                cell_bl_meta = []
                cell_br_meta = []

                normalized_storage_nets = node.mod.get_normalized_storage_nets_offset()
                (normalized_bl_offsets, normalized_br_offsets, bl_names, br_names) = node.mod.get_normalized_bitline_offset()

                for offset in range(len(normalized_bl_offsets)):
                    for port in range(len(bl_names)):
                        cell_bl_meta.append([bl_names[offset], row, col, port])

                for offset in range(len(normalized_br_offsets)):
                    for port in range(len(br_names)):
                        cell_br_meta.append([br_names[offset], row, col, port])

                if normalized_storage_nets == []:
                    debug.error("normalized storage nets should not be empty! Check if the GDS labels Q and Q_bar are correctly set on M1 of the cell",1)
                Q_x = normalized_storage_nets[0][0]
                Q_y = normalized_storage_nets[0][1]

                Q_bar_x = normalized_storage_nets[1][0]
                Q_bar_y = normalized_storage_nets[1][1]

                if node.mirror == 'MX':
                    Q_y = -1 * Q_y
                    Q_bar_y = -1 * Q_bar_y

                    for pair in range(len(normalized_bl_offsets)):
                        normalized_bl_offsets[pair] = (normalized_bl_offsets[pair][0],
                                                       -1 * normalized_bl_offsets[pair][1])

                    for pair in range(len(normalized_br_offsets)):
                        normalized_br_offsets[pair] = (normalized_br_offsets[pair][0],
                                                       -1 * normalized_br_offsets[pair][1])

                Q_offsets.append([Q_x, Q_y])
                Q_bar_offsets.append([Q_bar_x, Q_bar_y])

                bl_offsets.append(normalized_bl_offsets)
                br_offsets.append(normalized_br_offsets)

                bl_meta.append(cell_bl_meta)
                br_meta.append(cell_br_meta)

            elif node.mod.insts is not []:
                for instance in node.mod.insts:
                    walk_subtree(instance)
            path.pop(-1)

        walk_subtree(self)
        for path in cell_paths:
            vector_spaces = self.apply_path_transform(path)
            origin = vector_spaces[2]
            origin_offsets.append([origin[0], origin[1]])

        return(origin_offsets, Q_offsets, Q_bar_offsets, bl_offsets, br_offsets, bl_meta, br_meta)

    def __str__(self):
        """ override print function output """
        return "( inst: " + self.name + " @" + str(self.offset) + " mod=" + self.mod.cell_name + " " + self.mirror + " R=" + str(self.rotate) + ")"

    def __repr__(self):
        """ override print function output """
        return "( inst: " + self.name + " @" + str(self.offset) + " mod=" + self.mod.cell_name + " " + self.mirror + " R=" + str(self.rotate) + ")"


class path(geometry):
    """Represents a Path"""

    def __init__(self, lpp, coordinates, path_width):
        """Initializes a path for the specified layer"""
        super().__init__(lpp)
        self.name = "path"
        self.coordinates = map(lambda x: [x[0], x[1]], coordinates)
        self.coordinates = vector(self.coordinates).snap_to_grid()
        self.path_width = path_width

        # FIXME figure out the width/height. This type of path is not
        # supported right now. It might not work in gdsMill.
        assert(0)

    def gds_write_file(self, new_layout):
        """Writes the path to GDS"""
        debug.info(4, "writing path (" + str(self.layerNumber) + "): " + self.coordinates)
        new_layout.addPath(layerNumber=self.layerNumber,
                           purposeNumber=self.layerPurpose,
                           coordinates=self.coordinates,
                           width=self.path_width)

    def get_blockages(self, layer):
        """ Fail since we don't support paths yet. """
        assert(0)

    def __str__(self):
        """ override print function output """
        return "path: layer=" + self.layerNumber + " purpose=" + str(self.layerPurpose) + " w=" + self.width

    def __repr__(self):
        """ override print function output """
        return "( path: layer=" + self.layerNumber + " purpose=" + str(self.layerPurpose) + " w=" + self.width + " coords=" + str(self.coordinates) + " )"


class label(geometry):
    """Represents a text label"""

    def __init__(self, text, lpp, offset, zoom=None):
        """Initializes a text label for specified layer"""
        super().__init__(lpp)
        self.name = "label"
        self.text = text
        self.offset = vector(offset).snap_to_grid()

        if not zoom:
            try:
                self.zoom = tech.GDS["zoom"]
            except:
                self.zoom = None
        else:
            self.zoom = zoom

        self.size = 0

        debug.info(4, "creating label " + self.text + " " + str(self.layerNumber) + " " + str(self.offset))

    def gds_write_file(self, new_layout):
        """Writes the text label to GDS"""
        debug.info(4, "writing label (" + str(self.layerNumber) + "): " + self.text)
        new_layout.addText(text=self.text,
                           layerNumber=self.layerNumber,
                           purposeNumber=self.layerPurpose,
                           offsetInMicrons=self.offset,
                           magnification=self.zoom,
                           rotate=None)

    def get_blockages(self, layer):
        """ Returns an empty list since text cannot be blockages. """
        return []

    def __str__(self):
        """ override print function output """
        return "label: " + self.text + " layer=" + str(self.layerNumber) + " purpose=" + str(self.layerPurpose)

    def __repr__(self):
        """ override print function output """
        return "( label: " + self.text + " @" + str(self.offset) + " layer=" + str(self.layerNumber) + " purpose=" + str(self.layerPurpose) + " )"


class rectangle(geometry):
    """Represents a rectangular shape"""

    def __init__(self, lpp, offset, width, height):
        """Initializes a rectangular shape for specified layer"""
        super().__init__(lpp)
        self.name = "rect"
        self.offset = vector(offset).snap_to_grid()
        self.size = vector(width, height).snap_to_grid()
        self.width = round_to_grid(self.size.x)
        self.height = round_to_grid(self.size.y)
        self.compute_boundary(offset, "", 0)

        debug.info(4, "creating rectangle (" + str(self.layerNumber) + "): "
                   + str(self.width) + "x" + str(self.height) + " @ " + str(self.offset))

    def get_blockages(self, layer):
        """ Returns a list of one rectangle if it is on this layer"""
        if self.layerNumber == layer:
            return [[self.offset,
                     vector(self.offset.x + self.width,
                            self.offset.y + self.height)]]
        else:
            return []

    def gds_write_file(self, new_layout):
        """Writes the rectangular shape to GDS"""
        debug.info(4, "writing rectangle (" + str(self.layerNumber) + "):"
                   + str(self.width) + "x" + str(self.height) + " @ " + str(self.offset))
        new_layout.addBox(layerNumber=self.layerNumber,
                          purposeNumber=self.layerPurpose,
                          offsetInMicrons=self.offset,
                          width=self.width,
                          height=self.height,
                          center=False)

    def __str__(self):
        """ override print function output """
        return self.__repr__()

    def __repr__(self):
        """ override print function output """
        return "( rect: @" + str(self.offset) + " WxH=" + str(self.width) + "x" + str(self.height) + " layer=" + str(self.layerNumber) + " purpose=" + str(self.layerPurpose) + " )"
