#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2020 Efabless Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# SPDX-License-Identifier: Apache-2.0

#----------------------------------------------------------------------
#
# set_user_id.py ---
#
# Manipulate the magic database, GDS, and verilog source files for the
# user_id_programming block to set the user ID number.
#
# The user ID number is a 32-bit value that is passed to this routine
# as an 8-digit hex number.  If not given as an option, then the script
# will look for the value of the key "project_id" in the info.yaml file
# in the project top level directory
#
# user_id_programming layout map:
# Positions marked (in microns) for value = 0.  For value = 1, move
# the via 0.92um to the left.
#
# Layout grid is 0.46um x 0.34um with half-pitch offset (0.23um, 0.17um)
#
# Signal        Via position (um)
# name		X      Y     
#--------------------------------
# mask_rev[0]   14.49  9.35
# mask_rev[1]	16.33  9.35
# mask_rev[2]	10.35 20.23
# mask_rev[3]	 8.05  9.35
# mask_rev[4]	28.29  9.35
# mask_rev[5]	21.85 25.67
# mask_rev[6]	 8.05 20.23
# mask_rev[7]   20.47  9.35
# mask_rev[8]   17.25 17.85
# mask_rev[9]   25.53 12.07
# mask_rev[10]  22.31 20.23
# mask_rev[11]  13.11  9.35
# mask_rev[12]	23.69 23.29
# mask_rev[13]	24.15 12.07
# mask_rev[14]	13.57 17.85
# mask_rev[15]	23.23  6.97
# mask_rev[16]	24.15 17.85
# mask_rev[17]	 8.51 17.85
# mask_rev[18]	23.69 20.23
# mask_rev[19]	10.81 23.29
# mask_rev[20]	14.95  6.97
# mask_rev[21]	18.17 23.29
# mask_rev[22]	21.39 17.85
# mask_rev[23]	26.45 25.67
# mask_rev[24]	 9.89 17.85
# mask_rev[25]	15.87 17.85
# mask_rev[26]	26.45 17.85
# mask_rev[27]	 8.51  6.97
# mask_rev[28]	10.81  9.35
# mask_rev[29]	27.83 20.23
# mask_rev[30]	16.33 23.29
# mask_rev[31]	 8.05 14.79
#----------------------------------------------------------------------

import os
import sys
import re
import subprocess

def usage():
    print("Usage:")
    print("set_user_id.py [<user_id_value>] [<path_to_project>]")
    print("")
    print("where:")
    print("    <user_id_value>   is a character string of eight hex digits, and")
    print("    <path_to_project> is the path to the project top level directory.")
    print("")
    print("  If <user_id_value> is not given, then it must exist in the info.yaml file.")
    print("  If <path_to_project> is not given, then it is assumed to be the cwd.")
    return 0

if __name__ == '__main__':

    # Coordinate pairs in microns for the zero position on each bit
    mask_rev = (
	(14.49,  9.35), (16.33,  9.35), (10.35, 20.23), ( 8.05,  9.35),
	(28.29,  9.35), (21.85, 25.67), ( 8.05, 20.23), (20.47,  9.35),
	(17.25, 17.85), (25.53, 12.07), (22.31, 20.23), (13.11,  9.35),
	(23.69, 23.29), (24.15, 12.07), (13.57, 17.85), (23.23,  6.97),
	(24.15, 17.85), ( 8.51, 17.85), (23.69, 20.23), (10.81, 23.29),
	(14.95,  6.97), (18.17, 23.29), (21.39, 17.85), (26.45, 25.67),
	( 9.89, 17.85), (15.87, 17.85), (26.45, 17.85), ( 8.51,  6.97),
	(10.81,  9.35), (27.83, 20.23), (16.33, 23.29), ( 8.05, 14.79));

    optionlist = []
    arguments = []

    debugmode = False
    reportmode = False

    for option in sys.argv[1:]:
        if option.find('-', 0) == 0:
            optionlist.append(option)
        else:
            arguments.append(option)

    if len(arguments) > 2:
        print("Wrong number of arguments given to set_user_id.py.")
        usage()
        sys.exit(0)

    if '-debug' in optionlist:
        debugmode = True
    if '-report' in optionlist:
        reportmode = True

    user_id_value = None
    user_project_path = None

    if len(arguments) > 0:
        user_id_value = arguments[0]

        # Convert to binary
        try:
            user_id_int = int('0x' + user_id_value, 0)
            user_id_bits = '{0:032b}'.format(user_id_int)
        except:
            user_project_path = arguments[0]

    if len(arguments) == 0:
        user_project_path = os.getcwd()
    elif len(arguments) == 2:
        user_project_path = arguments[1]
    elif user_project_path == None:
        user_project_path = arguments[0]
    else:
        user_project_path = os.getcwd()

    if not os.path.isdir(user_project_path):
        print('Error:  Project path "' + user_project_path + '" does not exist or is not readable.')
        sys.exit(1)

    # Check for valid directories

    if not user_id_value:
        if os.path.isfile(user_project_path + '/info.yaml'):
            with open(user_project_path + '/info.yaml', 'r') as ifile:
                infolines = ifile.read().splitlines()
                for line in infolines:
                    kvpair = line.split(':')
                    if len(kvpair) == 2:
                        key = kvpair[0].strip()
                        value = kvpair[1].strip()
                        if key == 'project_id':
                            user_id_value = value.strip('"\'')
                            break

            if not user_id_value:
                print('Error:  No project_id key:value pair found in project info.yaml.')
                sys.exit(1)

            try:
                user_id_int = int('0x' + user_id_value, 0)
                user_id_bits = '{0:032b}'.format(user_id_int)
            except:
                print('Error:  Cannot parse user ID "' + user_id_value + '" as an 8-digit hex number.')
                sys.exit(1)

        else:
            print('Error:  No info.yaml file and no user ID argument given.')
            sys.exit(1)

    if reportmode:
        print(str(user_id_int))
        sys.exit(0)

    print('Setting project user ID to: ' + user_id_value)

    magpath = user_project_path + '/mag'
    gdspath = user_project_path + '/gds'
    vpath = user_project_path + '/verilog'
    errors = 0 

    if not os.path.isdir(gdspath):
        print('No directory ' + gdspath + ' found (path to GDS).')
        sys.exit(1)

    if not os.path.isdir(vpath):
        print('No directory ' + vpath + ' found (path to verilog).')
        sys.exit(1)

    if not os.path.isdir(magpath):
        print('No directory ' + magpath + ' found (path to magic databases).')
        sys.exit(1)

    print('Step 1:  Modify GDS of the user_id_programming subcell')

    # Bytes leading up to via position are:
    viarec = "00 06 0d 02 00 43 00 06 0e 02 00 2c 00 2c 10 03 "
    viabytes = bytes.fromhex(viarec)

    # Check for either GDS file being gzipped
    gdsbakgz = gdspath + '/user_id_prog_zero.gds.gz'
    gdsfilegz = gdspath + '/user_id_programming.gds.gz'

    if os.path.isfile(gdsbakgz):
        subprocess.run(['gunzip', gdsbakgz],
		stdout = subprocess.DEVNULL,
		stderr = subprocess.DEVNULL)
        zero_zipped = True
    else:
        zero_zipped = False

    if os.path.isfile(gdsfilegz):
        subprocess.run(['gunzip', gdsfilegz],
		stdout = subprocess.DEVNULL,
		stderr = subprocess.DEVNULL)
        file_zipped = True
    else:
        file_zipped = False

    # Read the GDS file.  If a backup was made of the zero-value
    # program, then use it.

    gdsbak = gdspath + '/user_id_prog_zero.gds'
    gdsfile = gdspath + '/user_id_programming.gds'

    if os.path.isfile(gdsbak):
        with open(gdsbak, 'rb') as ifile:
            gdsdata = ifile.read()
    else:
        with open(gdsfile, 'rb') as ifile:
            gdsdata = ifile.read()

    for i in range(0,32):
        # Ignore any zero bits.
        if user_id_bits[i] == '0':
            continue

        coords = mask_rev[i]
        xum = coords[0]
        yum = coords[1]

        # Contact is 0.17 x 0.17, so add and subtract 0.085 to get
        # the corner positions.

        xllum = xum - 0.085
        yllum = yum - 0.085
        xurum = xum + 0.085
        yurum = yum + 0.085
 
        # Get the 4-byte hex values for the corner coordinates
        xllnm = round(xllum * 1000)
        yllnm = round(yllum * 1000)
        xllhex = '{0:08x}'.format(xllnm)
        yllhex = '{0:08x}'.format(yllnm)
        xurnm = round(xurum * 1000)
        yurnm = round(yurum * 1000)
        xurhex = '{0:08x}'.format(xurnm)
        yurhex = '{0:08x}'.format(yurnm)

        # Magic's GDS output for vias always starts at the lower left
        # corner and goes counterclockwise, repeating the first point.
        viaoldposdata = viarec + xllhex + yllhex + xurhex + yllhex
        viaoldposdata += xurhex + yurhex + xllhex + yurhex + xllhex + yllhex
            
        # For "one" bits, the X position is moved 0.92 microns to the left
        newxllum = xllum - 0.92
        newxurum = xurum - 0.92

        # Get the 4-byte hex values for the new corner coordinates
        newxllnm = round(newxllum * 1000)
        newxllhex = '{0:08x}'.format(newxllnm)
        newxurnm = round(newxurum * 1000)
        newxurhex = '{0:08x}'.format(newxurnm)

        vianewposdata = viarec + newxllhex + yllhex + newxurhex + yllhex
        vianewposdata += newxurhex + yurhex + newxllhex + yurhex + newxllhex + yllhex

        # Diagnostic
        if debugmode:
            print('Bit ' + str(i) + ':')
            print('Via position ({0:3.2f}, {1:3.2f}) to ({2:3.2f}, {3:3.2f})'.format(xllum, yllum, xurum, yurum))
            print('Old hex string = ' + viaoldposdata)
            print('New hex string = ' + vianewposdata)

        # Convert hex strings to byte arrays
        viaoldbytedata = bytearray.fromhex(viaoldposdata)
        vianewbytedata = bytearray.fromhex(vianewposdata)

        # Replace the old data with the new
        if viaoldbytedata not in gdsdata:
            print('Error: via not found for bit position ' + str(i))
            errors += 1 
        else:
            gdsdata = gdsdata.replace(viaoldbytedata, vianewbytedata)

    if errors == 0:
        # Keep a copy of the original 
        if not os.path.isfile(gdsbak):
            if file_zipped:
                if os.path.isfile(gdsfilegz):
                    os.rename(gdsfilegz, gdsbakgz)
                else:
                    os.rename(gdsfile, gdsbak)
                    subprocess.run(['gzip', gdsbak, '-n', '--best'],
				stdout = subprocess.DEVNULL,
				stderr = subprocess.DEVNULL)
            else:
                os.rename(gdsfile, gdsbak)

        with open(gdsfile, 'wb') as ofile:
            ofile.write(gdsdata)
        if file_zipped:
            subprocess.run(['gzip', gdsfile, '-n', '--best'],
			stdout = subprocess.DEVNULL,
			stderr = subprocess.DEVNULL)

        print('Done!')
            
    else:
        print('There were errors in processing.  No file written.')
        print('Ending process.')
        sys.exit(1)

    print('Step 2:  Add user project ID parameter to verilog.')

    changed = False
    with open(vpath + '/rtl/caravel.v', 'r') as ifile:
        vlines = ifile.read().splitlines()
        outlines = []
        for line in vlines:
            oline = re.sub("parameter USER_PROJECT_ID = 32'h[0-9A-F]+;",
			"parameter USER_PROJECT_ID = 32'h" + user_id_value + ";",
			line)
            if oline != line:
                changed = True
            outlines.append(oline)

    if changed:
        with open(vpath + '/rtl/caravel.v', 'w') as ofile:
            for line in outlines:
                print(line, file=ofile)
            print('Done!')
    else:
        print('Error:  No substitutions done on verilog/rtl/caravel.v.')
        print('Ending process.')
        sys.exit(1)

    print('Step 3:  Add user project ID text to top level layout.')

    with open(magpath + '/user_id_textblock.mag', 'r') as ifile:
        maglines = ifile.read().splitlines()
        outlines = []
        digit = 0
        wasseen = {}
        for line in maglines:
            if 'alphaX_' in line:
                dchar = user_id_value[7 - digit].upper()
                oline = re.sub('alpha_[0-9A-F]', 'alpha_' + dchar, line)
                # Add path reference if cell was not previously found in the file
                if dchar not in wasseen:
                    if 'hexdigits' not in oline:
                        oline += ' hexdigits'
                outlines.append(oline)
                wasseen[dchar] = True
                digit += 1
            else:
                outlines.append(line)

    if digit == 8:
        with open(magpath + '/user_id_textblock.mag', 'w') as ofile:
            for line in outlines:
                print(line, file=ofile)
        print('Done!')
    elif digit == 0:
        print('Error:  No digits were replaced in the layout.')
    else:
        print('Error:  Only ' + str(digit) + ' digits were replaced in the layout.')

    sys.exit(0)